This commit is contained in:
alirehmani
2024-05-10 17:50:17 +05:00
parent e75f8e9059
commit bd1c3692c5
153 changed files with 0 additions and 12612 deletions

View File

@@ -1,6 +0,0 @@
VITE_API_URL_LOCAL=https://localhost:5001
VITE_API_URL_SERVER=https://localhost
VITE_WORKER_URL_LOCAL=https://localhost:5002
VITE_WORKER_URL_SERVER=https://localhost:444
ALCHEMY_ID=Bao7OirVe4bmYiDbPh0l8cs5gYb5D4_9
WALLET_CONNECT_PROJECT_ID=363bf09c10fec2293b21ee199b2ce8d5

View File

@@ -1,2 +0,0 @@
node_modules
dist

View File

@@ -1,98 +0,0 @@
{
"root": true,
"extends": [
"plugin:@typescript-eslint/recommended",
"plugin:jsx-a11y/recommended"
],
"parser": "@typescript-eslint/parser",
"plugins": [
"jsx-a11y",
"import",
"sort-keys-fix",
"react-hooks",
"@typescript-eslint",
"prettier"
],
"env": {
"browser": true,
"node": true,
"es6": true,
"jest": true
},
"globals": {
"JSX": "readonly"
},
"settings": {
"react": {
"version": "detect"
},
"import/parsers": {
"@typescript-eslint/parser": [".ts", ".tsx"]
},
"import/resolver": {
"node": {
"extensions": [".js", ".jsx", ".ts", ".tsx"]
},
"typescript": {
"alwaysTryTypes": true,
// always try to resolve types under `<root>@types` directory even it doesn't contain any source code, like `@types/unist`
"project": ["tsconfig.json"]
}
}
},
"rules": {
"no-alert": "error",
"no-console": "error",
"react-hooks/rules-of-hooks": "error",
"prettier/prettier": [
"warn",
{},
{
"properties": {
"usePrettierrc": true
}
}
],
"import/order": [
"warn",
{
"groups": [
"builtin",
"external",
"internal",
"parent",
"sibling",
"index",
"object"
],
"newlines-between": "always",
"alphabetize": {
"order": "asc",
"caseInsensitive": true
}
}
],
"import/named": "error",
"import/default": "error",
"import/export": "error",
"import/no-named-as-default": "warn",
"import/no-duplicates": "error",
"sort-keys-fix/sort-keys-fix": "warn",
"@import/no-named-as-default-member": "off",
"@typescript-eslint/consistent-type-imports": "warn",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/ban-types": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-empty-function": "off"
},
"overrides": [
{
"files": ["*.js"],
"rules": {
"@typescript-eslint/explicit-module-boundary-types": ["off"],
"@typescript-eslint/no-var-requires": ["off"]
}
}
]
}

View File

@@ -1,3 +0,0 @@
.jest/* linguist-vendored
mocks/* linguist-vendored
mockServiceWorker.js linguist-vendored

View File

@@ -1,18 +0,0 @@
name: Build
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2-beta
with:
node-version: '18.1.0'
- run: yarn install
- run: yarn build

View File

@@ -1,18 +0,0 @@
name: Lint
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2-beta
with:
node-version: '18.1.0'
- run: yarn install
- run: yarn lint

View File

@@ -1,18 +0,0 @@
name: Test
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2-beta
with:
node-version: '18.1.0'
- run: yarn install
- run: yarn test

View File

@@ -1,18 +0,0 @@
name: Typecheck
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2-beta
with:
node-version: '18.1.0'
- run: yarn install
- run: yarn typecheck

View File

@@ -1,5 +0,0 @@
node_modules
.DS_Store
dist
dist-ssr
*.local

View File

@@ -1,5 +0,0 @@
.git
node_modules
.eslintignore
.gitignore
LICENSE

View File

@@ -1,4 +0,0 @@
{
"singleQuote": true,
"semi": false
}

View File

@@ -1,39 +0,0 @@
# Use an official Node.js runtime as a parent image
FROM node:18
# Set the working directory inside the container
WORKDIR /app
# Install xsel for clipboard access (useful for some applications)
RUN apt-get update && apt-get install -y xsel
# Copy only package.json and package-lock.json (or yarn.lock) initially
# This takes advantage of cached Docker layers
COPY package*.json ./
# Install dependencies
# npm ci is used instead of npm install when you want a clean, exact installation
#RUN npm ci --verbose
# Try to install dependencies with a retry mechanism
RUN for i in 1 2 3; do npm ci --verbose && break || sleep 15; done
# Copy the rest of your application code
COPY . .
# Set necessary environment variables (if they are not secrets)
ENV NODE_ENV=development
ENV VITE_API_URL_LOCAL=https://localhost:5001
ENV VITE_API_URL_SERVER=https://localhost
# Expose port 3000 for the application
EXPOSE 3000
# Install global dependencies if absolutely necessary (generally not recommended to do globally)
RUN npm install -g serve vite
# Build the application
RUN node --max-old-space-size=4096 node_modules/.bin/vite build
# Command to run the application
CMD ["npm", "run", "serve"]

View File

@@ -1,43 +0,0 @@
ARG NODE_VERSION=21.4.0
ARG ALPINE_VERSION=3.19.0
FROM node:${NODE_VERSION}-alpine AS node
FROM alpine:${ALPINE_VERSION} AS builder
COPY --from=node /usr/lib /usr/lib
COPY --from=node /usr/local/lib /usr/local/lib
COPY --from=node /usr/local/include /usr/local/include
COPY --from=node /usr/local/bin /usr/local/bin
RUN node -v
# Set the working directory in the container
WORKDIR /app
# Copy the package.json and package-lock.json first to leverage Docker's cache
COPY ./src/Managing.WebApp/package*.json ./
# Install dependencies
#RUN npm ci --production --loglevel=verbose
RUN npm i --omit=dev --loglevel=verbose
# Copy the application code
COPY . .
# Build the Vite application
RUN npm run build
# Stage 2: Create the runtime image
FROM nginx:alpine
# Copy the built Vite application from the builder stage
COPY --from=builder /app/dist /usr/share/nginx/html
# Copy a custom Nginx configuration file (if you need one)
# COPY nginx.conf /etc/nginx/nginx.conf
# Expose port 80
EXPOSE 80
# Start the Nginx server
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) Managing
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,94 +0,0 @@
# vite-react-ts-extended [![Typecheck](https://github.com/laststance/vite-react-ts-extended/actions/workflows/typecheck.yml/badge.svg)](https://github.com/laststance/vite-react-ts-extended/actions/workflows/typecheck.yml) [![Test](https://github.com/laststance/vite-react-ts-extended/actions/workflows/test.yml/badge.svg)](https://github.com/laststance/vite-react-ts-extended/actions/workflows/test.yml) [![Build](https://github.com/laststance/vite-react-ts-extended/actions/workflows/build.yml/badge.svg)](https://github.com/laststance/vite-react-ts-extended/actions/workflows/build.yml) [![Lint](https://github.com/laststance/vite-react-ts-extended/actions/workflows/lint.yml/badge.svg)](https://github.com/laststance/vite-react-ts-extended/actions/workflows/lint.yml) [![Depfu](https://badges.depfu.com/badges/6c7775918ccc8647160750e168617a65/overview.svg)](https://depfu.com/github/laststance/vite-react-ts-extended?project_id=32682)
> My CRA alternative.
> Create plain and lightweight React+TS programming environment with familiar pre-setup tooling
> eslint/prettier, jest/TS/react-testing-library/msw, tailwindcss, CI.
## [Trying this Online!](https://codesandbox.io/s/vite-react-ts-extended-cbgyfz?file=/src/App.tsx)
<img src="https://digital3.nyc3.cdn.digitaloceanspaces.com/ext.png" />
This is the official [Vite](https://vitejs.dev/) template(`npm init vite@latest myapp -- --template react-ts`) and some extended setup.
- [eslint-typescript](https://github.com/typescript-eslint/typescript-eslint) and [Prettier](https://prettier.io/) integration. Rules are 100% my personal setup 💅
- [jest](https://jestjs.io/), [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/), [react-hooks-testing-library](https://github.com/testing-library/react-hooks-testing-library), [MSW](https://mswjs.io/)
- [tailwindcss](https://tailwindcss.com/)
- [Github Actions](https://github.com/features/actions)
All npm package are keeping least release version powered by [Depfu](https://depfu.com/).
# Installation
```
npx degit laststance/vite-react-ts-extended myapp
```
### yarn
```sh
cd myapp
yarn install
yarn validate # The installation was successful if no error occurs after running 'validate'.
yarn dev
```
### npm
```sh
cd myapp
npm install
npm run validate # The installation was successful if no error occurs after running 'validate'.
npm run dev
```
### Commands
```sh
yarn dev # start development server
yarn validate # run test,lint,build,typecheck concurrently
yarn test # run jest
yarn lint # run eslint
yarn lint:fix # run eslint with --fix option
yarn typecheck # run TypeScript compiler check
yarn build # build production bundle to 'dist' directly
yarn prettier # run prettier for json|yml|css|md|mdx files
yarn clean # remove 'node_modules' 'yarn.lock' 'dist' completely
yarn serve # launch server for production bundle in local
```
# Background
The evolution of the React framework is accelerating more than ever before.
[Next.js](https://nextjs.org/), [Remix](https://remix.run/), [RedwoodJS](https://redwoodjs.com/), [Gatsby](https://www.gatsbyjs.com/), [Blitz](https://blitzjs.com/) etc...
Ahthough I still need plain React programming starter some reason. (.e.g Demo, Experiment like Deep Dive React Core.)
So far, [create-react-app](https://github.com/facebook/create-react-app) **was** it.
In short, [create-react-app](https://github.com/facebook/create-react-app) development couldn't say active. Please read the [Issue](https://github.com/facebook/create-react-app/issues/11180) in details.
So I created an alternative to [create-react-app](https://github.com/facebook/create-react-app) for myself, based on [Vite](https://github.com/facebook/create-react-app).
This project contains my very opinionted setup,
but I hope it will be a useful tool for people who have similar needs to mine! 😀
# License
MIT
## Contributors ✨
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tr>
<td align="center"><a href="http://ryota-murakami.github.io/"><img src="https://avatars1.githubusercontent.com/u/5501268?s=400&u=7bf6b1580b95930980af2588ef0057f3e9ec1ff8&v=4?s=100" width="100px;" alt=""/><br /><sub><b>ryota-murakami</b></sub></a><br /><a href="https://github.com/laststance/vite-react-ts-extended/laststance/vite-react-ts-extended/commits?author=ryota-murakami" title="Code">💻</a> <a href="https://github.com/laststance/vite-react-ts-extended/laststance/vite-react-ts-extended/commits?author=ryota-murakami" title="Documentation">📖</a> <a href="https://github.com/laststance/vite-react-ts-extended/laststance/vite-react-ts-extended/commits?author=ryota-murakami" title="Tests">⚠️</a></td>
</tr>
</table>
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!

View File

@@ -1,16 +0,0 @@
<!DOCTYPE html>
<html lang="en" data-theme="black">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Managing</title>
</head>
<body>
<div id="root" class="min-h-screen flex flex-col"></div>
<script>
global = globalThis
</script>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -1,32 +0,0 @@
#!/bin/bash
# Array of known potentially hanging packages
hanging_packages=("xmlhttprequest-ssl@latest" "engine.io-parser@latest")
# Timeout in seconds for each package installation attempt
install_with_timeout() {
package=$1
echo "Attempting to install $package with a timeout of $timeout_duration seconds."
# Start npm install in the background
npm install $package --verbose &> $package.log &
# Get PID of the npm process
pid=$!
# Wait for the npm process to finish or timeout
(sleep $timeout_duration && kill -0 $pid 2>/dev/null && kill -9 $pid && echo "Timeout reached for $package, process killed." && echo $package >> exclude.log) &
waiter_pid=$!
# Wait for the npm process to complete
wait $pid
# Kill the waiter process in case npm finished before the timeout
kill -0 $waiter_pid 2>/dev/null && kill -9 $waiter_pid
}
# Install potentially hanging packages first with a timeout
for package in "${hanging_packages[@]}"; do
install_with_timeout $package
done

View File

@@ -1,35 +0,0 @@
const config = {
collectCoverageFrom: ['<rootDir>/src/**/*.{js,jsx,ts,tsx}', '!src/**/*.d.ts'],
moduleDirectories: ['node_modules'],
moduleFileExtensions: ['js', 'mjs', 'jsx', 'ts', 'tsx', 'json'],
moduleNameMapper: {
'^.+\\.module\\.(css|sass|scss)$': 'identity-obj-proxy',
},
notify: true,
notifyMode: 'success-change',
resetMocks: true,
roots: ['<rootDir>'],
setupFilesAfterEnv: ['<rootDir>/jest/setupTests.ts'],
testEnvironment: 'jsdom',
testMatch: [
'<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}',
'<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}',
],
transform: {
'^(?!.*\\.(js|jsx|mjs|cjs|ts|tsx|css|json)$)':
'<rootDir>/jest/fileTransform.js',
'^.+\\.[jt]sx?$': 'esbuild-jest',
'^.+\\.css$': '<rootDir>/jest/cssTransform.js',
},
transformIgnorePatterns: [
'[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|cjs|ts|tsx)$',
'^.+\\.module\\.(css|sass|scss)$',
],
verbose: true,
watchPlugins: [
'jest-watch-typeahead/filename',
'jest-watch-typeahead/testname',
],
}
module.exports = config

View File

@@ -1,338 +0,0 @@
/* eslint-disable */
/* tslint:disable */
/**
* Mock Service Worker (0.36.5).
* @see https://github.com/mswjs/msw
* - Please do NOT modify this file.
* - Please do NOT serve this file on production.
*/
const INTEGRITY_CHECKSUM = '02f4ad4a2797f85668baf196e553d929'
const bypassHeaderName = 'x-msw-bypass'
const activeClientIds = new Set()
self.addEventListener('install', function () {
return self.skipWaiting()
})
self.addEventListener('activate', async function (event) {
return self.clients.claim()
})
self.addEventListener('message', async function (event) {
const clientId = event.source.id
if (!clientId || !self.clients) {
return
}
const client = await self.clients.get(clientId)
if (!client) {
return
}
const allClients = await self.clients.matchAll()
switch (event.data) {
case 'KEEPALIVE_REQUEST': {
sendToClient(client, {
type: 'KEEPALIVE_RESPONSE',
})
break
}
case 'INTEGRITY_CHECK_REQUEST': {
sendToClient(client, {
type: 'INTEGRITY_CHECK_RESPONSE',
payload: INTEGRITY_CHECKSUM,
})
break
}
case 'MOCK_ACTIVATE': {
activeClientIds.add(clientId)
sendToClient(client, {
type: 'MOCKING_ENABLED',
payload: true,
})
break
}
case 'MOCK_DEACTIVATE': {
activeClientIds.delete(clientId)
break
}
case 'CLIENT_CLOSED': {
activeClientIds.delete(clientId)
const remainingClients = allClients.filter((client) => {
return client.id !== clientId
})
// Unregister itself when there are no more clients
if (remainingClients.length === 0) {
self.registration.unregister()
}
break
}
}
})
// Resolve the "main" client for the given event.
// Client that issues a request doesn't necessarily equal the client
// that registered the worker. It's with the latter the worker should
// communicate with during the response resolving phase.
async function resolveMainClient(event) {
const client = await self.clients.get(event.clientId)
if (client.frameType === 'top-level') {
return client
}
const allClients = await self.clients.matchAll()
return allClients
.filter((client) => {
// Get only those clients that are currently visible.
return client.visibilityState === 'visible'
})
.find((client) => {
// Find the client ID that's recorded in the
// set of clients that have registered the worker.
return activeClientIds.has(client.id)
})
}
async function handleRequest(event, requestId) {
const client = await resolveMainClient(event)
const response = await getResponse(event, client, requestId)
// Send back the response clone for the "response:*" life-cycle events.
// Ensure MSW is active and ready to handle the message, otherwise
// this message will pend indefinitely.
if (client && activeClientIds.has(client.id)) {
;(async function () {
const clonedResponse = response.clone()
sendToClient(client, {
type: 'RESPONSE',
payload: {
requestId,
type: clonedResponse.type,
ok: clonedResponse.ok,
status: clonedResponse.status,
statusText: clonedResponse.statusText,
body:
clonedResponse.body === null ? null : await clonedResponse.text(),
headers: serializeHeaders(clonedResponse.headers),
redirected: clonedResponse.redirected,
},
})
})()
}
return response
}
async function getResponse(event, client, requestId) {
const { request } = event
const requestClone = request.clone()
const getOriginalResponse = () => fetch(requestClone)
// Bypass mocking when the request client is not active.
if (!client) {
return getOriginalResponse()
}
// Bypass initial page load requests (i.e. static assets).
// The absence of the immediate/parent client in the map of the active clients
// means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
// and is not ready to handle requests.
if (!activeClientIds.has(client.id)) {
return await getOriginalResponse()
}
// Bypass requests with the explicit bypass header
if (requestClone.headers.get(bypassHeaderName) === 'true') {
const cleanRequestHeaders = serializeHeaders(requestClone.headers)
// Remove the bypass header to comply with the CORS preflight check.
delete cleanRequestHeaders[bypassHeaderName]
const originalRequest = new Request(requestClone, {
headers: new Headers(cleanRequestHeaders),
})
return fetch(originalRequest)
}
// Send the request to the client-side MSW.
const reqHeaders = serializeHeaders(request.headers)
const body = await request.text()
const clientMessage = await sendToClient(client, {
type: 'REQUEST',
payload: {
id: requestId,
url: request.url,
method: request.method,
headers: reqHeaders,
cache: request.cache,
mode: request.mode,
credentials: request.credentials,
destination: request.destination,
integrity: request.integrity,
redirect: request.redirect,
referrer: request.referrer,
referrerPolicy: request.referrerPolicy,
body,
bodyUsed: request.bodyUsed,
keepalive: request.keepalive,
},
})
switch (clientMessage.type) {
case 'MOCK_SUCCESS': {
return delayPromise(
() => respondWithMock(clientMessage),
clientMessage.payload.delay,
)
}
case 'MOCK_NOT_FOUND': {
return getOriginalResponse()
}
case 'NETWORK_ERROR': {
const { name, message } = clientMessage.payload
const networkError = new Error(message)
networkError.name = name
// Rejecting a request Promise emulates a network error.
throw networkError
}
case 'INTERNAL_ERROR': {
const parsedBody = JSON.parse(clientMessage.payload.body)
console.error(
`\
[MSW] Uncaught exception in the request handler for "%s %s":
${parsedBody.location}
This exception has been gracefully handled as a 500 response, however, it's strongly recommended to resolve this error, as it indicates a mistake in your code. If you wish to mock an error response, please see this guide: https://mswjs.io/docs/recipes/mocking-error-responses\
`,
request.method,
request.url,
)
return respondWithMock(clientMessage)
}
}
return getOriginalResponse()
}
self.addEventListener('fetch', function (event) {
const { request } = event
const accept = request.headers.get('accept') || ''
// Bypass server-sent events.
if (accept.includes('text/event-stream')) {
return
}
// Bypass navigation requests.
if (request.mode === 'navigate') {
return
}
// Opening the DevTools triggers the "only-if-cached" request
// that cannot be handled by the worker. Bypass such requests.
if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
return
}
// Bypass all requests when there are no active clients.
// Prevents the self-unregistered worked from handling requests
// after it's been deleted (still remains active until the next reload).
if (activeClientIds.size === 0) {
return
}
const requestId = uuidv4()
return event.respondWith(
handleRequest(event, requestId).catch((error) => {
if (error.name === 'NetworkError') {
console.warn(
'[MSW] Successfully emulated a network error for the "%s %s" request.',
request.method,
request.url,
)
return
}
// At this point, any exception indicates an issue with the original request/response.
console.error(
`\
[MSW] Caught an exception from the "%s %s" request (%s). This is probably not a problem with Mock Service Worker. There is likely an additional logging output above.`,
request.method,
request.url,
`${error.name}: ${error.message}`,
)
}),
)
})
function serializeHeaders(headers) {
const reqHeaders = {}
headers.forEach((value, name) => {
reqHeaders[name] = reqHeaders[name]
? [].concat(reqHeaders[name]).concat(value)
: value
})
return reqHeaders
}
function sendToClient(client, message) {
return new Promise((resolve, reject) => {
const channel = new MessageChannel()
channel.port1.onmessage = (event) => {
if (event.data && event.data.error) {
return reject(event.data.error)
}
resolve(event.data)
}
client.postMessage(JSON.stringify(message), [channel.port2])
})
}
function delayPromise(cb, duration) {
return new Promise((resolve) => {
setTimeout(() => resolve(cb()), duration)
})
}
function respondWithMock(clientMessage) {
return new Response(clientMessage.payload.body, {
...clientMessage.payload,
headers: clientMessage.payload.headers,
})
}
function uuidv4() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
const r = (Math.random() * 16) | 0
const v = c == 'x' ? r : (r & 0x3) | 0x8
return v.toString(16)
})
}

View File

@@ -1,104 +0,0 @@
{
"name": "managing",
"version": "2.0.0",
"license": "MIT",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"serve": "serve -s dist -p 3000",
"test": "jest",
"lint": "eslint . --ext .ts,.tsx,.js,jsx",
"lint:fix": "eslint . --ext .ts,.tsx,.js,jsx --fix",
"typecheck": "tsc --noEmit",
"prettier": "prettier --write \"**/*.+(json|yml|css|md|mdx)\"",
"clean": "rimraf node_modules yarn.lock dist",
"validate": "./scripts/validate"
},
"dependencies": {
"@heroicons/react": "^1.0.6",
"@microsoft/signalr": "^6.0.5",
"@tanstack/react-query": "^5.0.0",
"@wagmi/chains": "^0.2.9",
"@wagmi/connectors": "^4.3.2",
"@wagmi/core": "^2.9.0",
"@walletconnect/universal-provider": "^2.8.6",
"axios": "^0.27.2",
"classnames": "^2.3.1",
"connectkit": "^1.7.3",
"date-fns": "^2.30.0",
"jotai": "^1.6.7",
"latest-version": "^9.0.0",
"lightweight-charts": "git+https://github.com/ntf/lightweight-charts.git",
"moment": "^2.29.3",
"plotly.js": "^2.18.1",
"react": "^18.1.0",
"react-cookie": "^4.1.1",
"react-dom": "^18.1.0",
"react-grid-layout": "^1.3.4",
"react-hook-form": "^7.31.2",
"react-hot-toast": "^2.2.0",
"react-icons": "^4.3.1",
"react-iframe": "^1.8.0",
"react-plotly.js": "^2.6.0",
"react-router-dom": "^6.3.0",
"react-slider": "^2.0.1",
"react-table": "^7.8.0",
"react-toastify": "^9.0.1",
"reactflow": "^11.8.3",
"viem": "^2.0.6",
"wagmi": "^2.2.1",
"web3": "^4.0.2",
"zustand": "^4.4.1"
},
"devDependencies": {
"@tailwindcss/aspect-ratio": "^0.4.0",
"@tailwindcss/forms": "^0.5.1",
"@tailwindcss/line-clamp": "^0.4.0",
"@tailwindcss/typography": "^0.5.2",
"@tanstack/eslint-plugin-query": "^4.34.1",
"@testing-library/dom": "^8.13.0",
"@testing-library/react": "^13.2.0",
"@types/jest": "^27.5.1",
"@types/react": "^18.0.9",
"@types/react-dom": "^18.0.4",
"@types/react-grid-layout": "^1.3.2",
"@types/react-plotly.js": "^2.6.0",
"@types/react-slider": "^1.3.1",
"@types/react-table": "^7.7.12",
"@types/signalr": "^2.2.37",
"@types/testing-library__jest-dom": "^5.14.3",
"@typescript-eslint/eslint-plugin": "^5.23.0",
"@typescript-eslint/parser": "^5.23.0",
"@vitejs/plugin-react": "^1.3.2",
"all-contributors-cli": "^6.20.0",
"autoprefixer": "^10.4.7",
"daisyui": "^3.5.1",
"esbuild-jest": "^0.4.0",
"eslint": "^8.15.0",
"eslint-config-prettier": "^8.5.0",
"eslint-config-typescript": "^3.0.0",
"eslint-import-resolver-typescript": "^2.7.1",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react-hooks": "^4.5.0",
"eslint-plugin-sort-keys-fix": "^1.1.2",
"identity-obj-proxy": "^3.0.0",
"jest": "^28.0.0",
"jest-watch-typeahead": "^1.1.0",
"node-notifier": "^10.0.1",
"postcss": "^8.4.13",
"prettier": "^2.6.1",
"prettier-plugin-tailwind-css": "^1.5.0",
"rimraf": "^3.0.2",
"serve": "^14.2.0",
"tailwindcss": "^3.0.23",
"typescript": "^5.0.4",
"vite": "^4.4.9",
"whatwg-fetch": "^3.6.2"
},
"msw": {
"workerDirectory": ""
}
}

View File

@@ -1,6 +0,0 @@
module.exports = {
plugins: {
autoprefixer: {},
tailwindcss: {},
},
}

View File

@@ -1,4 +0,0 @@
module.exports = {
plugins: [require('prettier-plugin-tailwindcss')],
tailwindConfig: './tailwind.config.js',
}

View File

@@ -1,11 +0,0 @@
#!/bin/sh
npx concurrently \
--kill-others-on-fail \
--prefix "[{name}]" \
--names "test,lint:fix,typecheck,build" \
--prefix-colors "bgRed.bold.white,bgGreen.bold.white,bgBlue.bold.white,bgMagenta.bold.white" \
"npm run test --silent -- --watch=false" \
"npm run lint:fix --silent" \
"npm run typecheck --silent" \
"npm run build --silent"

View File

@@ -1,13 +0,0 @@
import { Auth } from '../pages/authPage/auth'
import MyRoutes from './routes'
const App = () => {
return (
<Auth>
<MyRoutes />
</Auth>
)
}
export default App

View File

@@ -1,18 +0,0 @@
import type { HubConnection } from '@microsoft/signalr'
import { HubConnectionBuilder } from '@microsoft/signalr'
// https://www.abrahamberg.com/blog/aspnet-signalr-and-react/
export class Hub {
public hub: HubConnection
constructor(name: string, baseUrl: string) {
this.hub = new HubConnectionBuilder()
.withUrl(baseUrl + '/' + name)
.withAutomaticReconnect()
.build()
try {
this.hub.start()
} catch (err) {
// eslint-disable-next-line no-console
console.log(err)
}
}
}

View File

@@ -1,118 +0,0 @@
import { Suspense, lazy } from 'react'
import { Route, Routes } from 'react-router-dom'
import LayoutMain from '../../layouts'
import DeskWidget from '../../pages/desk/deskWidget'
import Scenario from '../../pages/scenarioPage/scenario'
import Tools from '../../pages/toolsPage/tools'
import Workflows from '../../pages/workflow/workflows'
const Backtest = lazy(() => import('../../pages/backtestPage/backtest'))
const Bots = lazy(() => import('../../pages/botsPage/bots'))
const Dashboard = lazy(() => import('../../pages/dashboardPage/dashboard'))
const Settings = lazy(() => import('../../pages/settingsPage/settings'))
const MyRoutes = () => {
return (
<Routes>
<Route path="/" element={<LayoutMain />}>
<Route
index
element={
<Suspense fallback={null}>
<Dashboard />
</Suspense>
}
/>
</Route>
<Route path="/bots" element={<LayoutMain />}>
<Route
index
element={
<Suspense fallback={null}>
<Bots />
</Suspense>
}
/>
</Route>
<Route path="/scenarios" element={<LayoutMain />}>
<Route
index
element={
<Suspense fallback={null}>
<Scenario />
</Suspense>
}
/>
</Route>
<Route path="/workflow" element={<LayoutMain />}>
<Route
index
element={
<Suspense fallback={null}>
<Workflows />
</Suspense>
}
/>
</Route>
<Route path="/settings" element={<LayoutMain />}>
<Route
index
element={
<Suspense fallback={null}>
<Settings />
</Suspense>
}
/>
</Route>
<Route path="/backtest" element={<LayoutMain />}>
<Route
index
element={
<Suspense fallback={null}>
<Backtest />
</Suspense>
}
/>
</Route>
<Route path="/tools" element={<LayoutMain />}>
<Route
index
element={
<Suspense fallback={null}>
<Tools />
</Suspense>
}
/>
</Route>
<Route path="/desk" element={<LayoutMain />}>
<Route
index
element={
<Suspense fallback={null}>
<DeskWidget />
</Suspense>
}
/>
</Route>
{/* <Route path="/web3" element={<LayoutMain />}>
<Route
index
element={
<Suspense fallback={null}>
<Web3 />
</Suspense>
}
/>
</Route> */}
</Routes>
)
}
export default MyRoutes

View File

@@ -1,22 +0,0 @@
import { create } from 'zustand'
import type { AccountStore } from '../../global/type'
export const useAuthStore = create<AccountStore>((set) => ({
accounts: [],
onInitialize: () => {
console.log('useFlowStore onInitialize')
// const accountClient = new AccountClient({}, apiUrl)
// const accounts = await accountClient.account_GetAccounts()
// if (accounts.length > 0) {
// get().setAccounts(accounts)
// }
},
setAccounts: (accounts) => {
set((state) => ({
...state,
accounts: accounts,
}))
},
}))

View File

@@ -1,28 +0,0 @@
import create from 'zustand'
type ApiStore = {
isProd: boolean
apiUrl: string
workerUrl: string
toggleApiUrl: () => void
}
const useApiUrlStore = create<ApiStore>((set) => ({
// Mettez la valeur initiale de isProd ici
apiUrl: import.meta.env.VITE_API_URL_SERVER,
isProd: true,
toggleApiUrl: () => {
set((state) => ({
apiUrl: state.isProd
? import.meta.env.VITE_API_URL_LOCAL
: import.meta.env.VITE_API_URL_SERVER,
isProd: !state.isProd,
workerUrl: state.isProd
? import.meta.env.VITE_WORKER_URL_LOCAL
: import.meta.env.VITE_WORKER_URL_SERVER,
}))
},
workerUrl: import.meta.env.VITE_WORKER_URL_SERVER,
}))
export default useApiUrlStore

View File

@@ -1,28 +0,0 @@
import { create } from 'zustand'
import type { IFlow } from '../../generated/ManagingApi'
import { WorkflowClient } from '../../generated/ManagingApi'
type FlowStore = {
setFlows: (flows: IFlow[]) => void
getFlows: (apiUrl: string) => void
flows: IFlow[]
}
export const useFlowStore = create<FlowStore>((set) => ({
flows: [] as IFlow[],
getFlows: async (apiUrl) => {
const client = new WorkflowClient({}, apiUrl)
await client.workflow_GetAvailableFlows().then((data) => {
set(() => ({
flows: data,
}))
})
},
setFlows: (flows) => {
set((state) => ({
...state,
flows: flows,
}))
},
}))

View File

@@ -1,13 +0,0 @@
import type { IWorkflowStore } from '../workflowStore'
export const WorkflowSelector = (state: IWorkflowStore) => ({
edges: state.edges,
initWorkFlow: state.initWorkFlow,
nodes: state.nodes,
onConnect: state.onConnect,
onEdgesChange: state.onEdgesChange,
onNodesChange: state.onNodesChange,
resetWorkflow: state.resetWorkflow,
setNodes: state.setNodes,
updateNodeData: state.updateNodeData,
})

View File

@@ -1,113 +0,0 @@
import type {
Connection,
Edge,
EdgeChange,
Node,
NodeChange,
OnNodesChange,
OnEdgesChange,
OnConnect,
} from 'reactflow'
import { addEdge, applyNodeChanges, applyEdgeChanges } from 'reactflow'
import { create } from 'zustand'
import type {
SyntheticFlowParameter,
FlowParameter,
IFlow,
} from '../../generated/ManagingApi'
export type IWorkflowStore = {
nodes: Node<IFlow>[]
initialNodes: Node<IFlow>[]
edges: Edge[]
initialEdges: Edge[]
onNodesChange: OnNodesChange
onEdgesChange: OnEdgesChange
onConnect: OnConnect
updateNodeData: (nodeId: string, parameterName: string, value: string) => void
initWorkFlow: (nodes: Node<IFlow>[], edges: Edge[]) => void
setNodes: (nodes: Node<IFlow>[]) => void
resetWorkflow: () => void
}
// this is our useStore hook that we can use in our components to get parts of the store and call actions
const useWorkflowStore = create<IWorkflowStore>((set, get) => ({
edges: [],
initWorkFlow: (nodes: Node<IFlow>[], edges: Edge[]) => {
set({
edges: edges,
initialEdges: edges,
initialNodes: nodes,
nodes: nodes,
})
},
initialEdges: [],
initialNodes: [],
nodes: [],
onConnect: (connection: Connection, callback: void) => {
set({
edges: addEdge(connection, get().edges),
})
},
onEdgesChange: (changes: EdgeChange[]) => {
set({
edges: applyEdgeChanges(changes, get().edges),
})
},
onNodesChange: (changes: NodeChange[]) => {
set({
nodes: applyNodeChanges(changes, get().nodes),
})
},
resetWorkflow: () => {
set({
edges: get().initialEdges,
initialEdges: get().initialEdges,
initialNodes: get().initialNodes,
nodes: get().initialNodes,
})
},
setNodes: (nodes: Node<IFlow>[]) => {
set({
nodes: nodes,
})
},
updateNodeData: (nodeId: string, parameterName: string, value: string) => {
set({
nodes: get().nodes.map((node) => {
if (node.id === nodeId) {
node.data.parameters = updateParameters(
node.data.parameters,
parameterName,
value
)
}
return node
}),
})
},
}))
const updateParameters = (
parameters: FlowParameter[],
name: string,
value: string
) => {
if (!parameters.find((parameter) => parameter.name === name)) {
parameters.push({
name: name,
value: value,
} as SyntheticFlowParameter)
} else {
parameters = parameters.map((parameter) => {
if (parameter.name === name) {
parameter.value = value
}
return parameter
})
}
return parameters
}
export default useWorkflowStore

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

View File

@@ -1,7 +0,0 @@
import type { IListProp } from '../../../global/type'
const List = (prop: IListProp): JSX.Element => (
<li {...prop}>{prop.children}</li>
)
export default List

View File

@@ -1,16 +0,0 @@
import type { ILoader } from '../../../global/type'
const defaultClasses = 'loading loading-ring '
const loaderSize = {
lg: defaultClasses + 'loading-lg',
md: defaultClasses + 'loading-md',
sm: defaultClasses + 'loading-sm',
xs: defaultClasses + 'loading-xs',
}
const Loader = ({ size = 'md' }: ILoader) => {
return <span className={loaderSize[size]}></span>
}
export default Loader

View File

@@ -1,42 +0,0 @@
import { Link, NavLink } from 'react-router-dom'
import type { IMyLinkProps } from '../../../global/type'
const MyLink = ({
children,
href,
as: asof = 'link',
...props
}: IMyLinkProps): JSX.Element => {
const navLink = asof === 'navlink'
const onlyLink = asof === 'link'
if (navLink) {
return (
<NavLink {...props} to={href}>
{children}
</NavLink>
)
}
if (onlyLink) {
return (
<Link className={props.className as string} to={href}>
{children}
</Link>
)
}
return (
<a
href={href}
className={props.className as string}
rel="noopener noreferrer"
target="_blank"
>
{children}
</a>
)
}
export default MyLink

View File

@@ -1,7 +0,0 @@
import type { IListProp } from '../../../global/type'
const Select = (prop: IListProp): JSX.Element => (
<option {...prop}>{prop.children}</option>
)
export default Select

View File

@@ -1,30 +0,0 @@
import type { FunctionComponent } from 'react'
import type { IPropsComponent } from '../../../global/type'
const Slider: FunctionComponent<IPropsComponent> = (props: IPropsComponent) => {
return (
<>
<div className="w-10/12 px-3">
<input
id={props.id}
value={props.value}
type="range"
className="range"
onChange={props.onChange}
min={props.min}
max={props.max}
step={props.step}
disabled={props.disabled}
/>
</div>
<div className="w-2/12">
{props.prefixValue}
{props.value}
{props.suffixValue}
</div>
</>
)
}
export default Slider

View File

@@ -1,5 +0,0 @@
export { default as List } from './List/List'
export { default as MyLink } from './MyLink/MyLink'
export { default as Select } from './Select/Select'
export { default as Slider } from './Slider/Slider'
export { default as Loader } from './Loader/Loader'

View File

@@ -1,45 +0,0 @@
import type { CardProps } from '../../../global/type'
const Card = ({ name, children, showCloseButton, info }: CardProps) => {
return (
<div
key={name}
className={`card bg-base-200 sm:hover:bg-base-300 transition border border-base-100 sm:hover:border-base-200`}
>
<div className="flex border-b-[1px] border-base-300">
<div className="card-title drag-handle justify-start w-full ml-3">
{name}
{info && (
<div className="tooltip" data-tip={info}>
<span className="badge badge-outline badge-accent ml-2">i</span>
</div>
)}
</div>
<div className="card-actions justify-self-end">
{showCloseButton && (
<button className="btn btn-square btn-sm">
<svg
xmlns="http://www.w3.org/2000/svg"
className="w-6 h-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
)}
</div>
</div>
<div className="card-body">{children}</div>
</div>
)
}
export default Card

View File

@@ -1,77 +0,0 @@
import ArrowDownIcon from '@heroicons/react/solid/ArrowDownIcon'
import ArrowUpIcon from '@heroicons/react/solid/ArrowUpIcon'
import { Position, TradeDirection } from '../../../generated/ManagingApi'
import type { ICardPosition, ICardSignal, ICardText } from '../../../global/type'
function getItemTextHeaderClass() {
return 'text-xs '
}
function getItemTextValueClass() {
return 'text-md '
}
export function CardText({ title, content }: ICardText) {
return (
<div>
<p className={getItemTextHeaderClass()}>{title}</p>
<p className={getItemTextValueClass()}>{content}</p>
</div>
)
}
export function CardPosition({ positions, positivePosition }: ICardPosition) {
return (
<>
<div>
<p className={getItemTextHeaderClass()}>
{positivePosition ? 'Winning position' : 'Losing position'}
</p>
<p className={getItemTextValueClass()}>
{
positions.filter((p: Position) => p.originDirection == TradeDirection.Long)
.length
}{' '}
<ArrowUpIcon
width="10"
className="text-primary inline"
display="initial"
></ArrowUpIcon>{' '}
{
positions.filter((p) => p.originDirection == TradeDirection.Short)
.length
}{' '}
<ArrowDownIcon
width="10"
className="text-accent inline"
display="initial"
></ArrowDownIcon>
</p>
</div>
</>
)
}
export function CardSignal({ signals }: ICardSignal) {
return (
<>
<div>
<p className={getItemTextHeaderClass()}>Signals</p>
<p className={getItemTextValueClass()}>
{signals.filter((p) => p.direction == TradeDirection.Long).length}{' '}
<ArrowUpIcon
width="10"
className="text-primary inline"
display="initial"
></ArrowUpIcon>{' '}
{signals.filter((p) => p.direction == TradeDirection.Short).length}{' '}
<ArrowDownIcon
width="10"
className="text-accent inline"
display="initial"
></ArrowDownIcon>
</p>
</div>
</>
)
}

View File

@@ -1,24 +0,0 @@
import React from 'react'
import type { IFormInput } from '../../../global/type'
const FormInput: React.FC<IFormInput> = ({
children,
label,
htmlFor,
inline = false,
}) => {
const groupStyle = inline ? 'flex-wrap' : ''
return (
<div className="form-control mb-2">
<div className={'input-group ' + groupStyle}>
<label htmlFor={htmlFor} className={'label h-auto w-full'}>
{label}
</label>
{children}
</div>
</div>
)
}
export default FormInput

View File

@@ -1,38 +0,0 @@
import type { IGridTile } from '../../../global/type'
const GridTile = ({ children, title }: IGridTile) => {
return (
<div
key={title}
className={`card bg-base-200 sm:hover:bg-base-300 transition border border-base-100 sm:hover:border-base-200`}
>
<div className="flex border-b-[1px] border-base-300">
<div className="card-title drag-handle justify-start w-full ml-3">
{title}
</div>
<div className="card-actions justify-self-end ">
<button className="btn btn-square btn-sm">
<svg
xmlns="http://www.w3.org/2000/svg"
className="w-6 h-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
</div>
<div className="card-body no-drag">{children}</div>
</div>
)
}
export default GridTile

View File

@@ -1,101 +0,0 @@
import { StatusOfflineIcon } from '@heroicons/react/solid'
import type { SubmitHandler } from 'react-hook-form'
import { useForm } from 'react-hook-form'
import { useAccount, useDisconnect, useSignMessage } from 'wagmi'
import useApiUrlStore from '../../../app/store/apiStore'
import { UserClient } from '../../../generated/ManagingApi'
import type { ILoginFormInput } from '../../../global/type'
import useCookie from '../../../hooks/useCookie'
import { SecondaryNavbar } from '../NavBar/NavBar'
const LogIn = () => {
const { apiUrl } = useApiUrlStore()
const { register, handleSubmit } = useForm<ILoginFormInput>()
const { disconnect } = useDisconnect()
const { address } = useAccount()
const { isLoading, signMessageAsync } = useSignMessage({})
const { setCookie } = useCookie()
const onSubmit: SubmitHandler<ILoginFormInput> = async (form) => {
const message = 'wagmi'
const signature = await signMessageAsync({ message })
if (signature && address) {
const userClient = new UserClient({}, apiUrl)
await userClient
.user_CreateToken({
address: address.toString(),
message: message,
name: form.name,
signature: signature,
})
.then((data) => {
setCookie('token', data, 1)
location.reload()
})
.catch((err) => {
// eslint-disable-next-line no-console
console.error(err)
})
} else {
}
}
return (
<>
<div style={{ display: 'flex', justifyContent: 'right' }}>
<SecondaryNavbar />
</div>
<section className="bg-gray-50 dark:bg-gray-900">
<div className="md:h-screen lg:py-0 flex flex-col items-center justify-center px-6 py-8 mx-auto">
<div className="dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700 w-full bg-white rounded-lg shadow">
<div className="md:space-y-6 sm:p-8 p-6 space-y-4">
<h1
className="md:text-2xl dark:text-white text-xl font-bold leading-tight tracking-tight text-gray-900"
style={{ textAlign: 'center' }}
>
Login
</h1>
<form
className="md:space-y-6 space-y-4"
action="#"
onSubmit={handleSubmit(onSubmit)}
>
<div>
<label
htmlFor="name"
className="dark:text-white block mb-2 text-sm font-medium text-gray-900"
>
Name
</label>
<input
className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
{...register('name')}
></input>
</div>
<button
type="submit"
disabled={isLoading}
className="btn bg-primary w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800"
>
Sign and login
</button>
<button
onClick={() => disconnect}
className="btn bg-primary w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800"
>
Disconnect wallet{' '}
<StatusOfflineIcon width={20}></StatusOfflineIcon>
</button>
</form>
</div>
</div>
</div>
</section>
</>
)
}
export default LogIn

View File

@@ -1,26 +0,0 @@
import { useConnect } from 'wagmi'
// export function Profile() {
// const { connect, connectors, error, isLoading, pendingConnector } =
// useConnect()
// return (
// <div>
// {connectors.map((connector) => (
// <button
// disabled={!connector.ready}
// key={connector.id}
// onClick={() => connect({ connector })}
// >
// {connector.name}
// {!connector.ready && ' (unsupported)'}
// {isLoading &&
// connector.id === pendingConnector?.id &&
// ' (connecting)'}
// </button>
// ))}
// {error && <div>{error.message}</div>}
// </div>
// )
// }

View File

@@ -1,34 +0,0 @@
import React from 'react'
import type { IModalProps } from '../../../global/type'
import ModalHeader from './ModalHeader'
const Modal: React.FC<IModalProps> = ({
showModal,
onSubmit,
onClose,
titleHeader,
children,
}) => {
return (
<div className="container mx-auto">
{showModal ? (
<form onSubmit={onSubmit}>
<div className="modal modal-bottom sm:modal-middle modal-open">
<div className="modal-box">
<ModalHeader
titleHeader={titleHeader}
onClose={onClose}
onSubmit={onSubmit}
/>
{children}
</div>
</div>
</form>
) : null}
</div>
)
}
export default Modal

View File

@@ -1,19 +0,0 @@
import React from 'react'
import type { IModalProps } from '../../../global/type'
const ModalHeader: React.FC<IModalProps> = ({ onClose, titleHeader }: any) => {
return (
<div style={{ alignItems: 'center', display: 'flex' }}>
<button
onClick={onClose}
className="btn btn-sm btn-circle right-2 top-2 absolute"
>
</button>
<div className="text-primary mb-3 text-lg">{titleHeader}</div>
</div>
)
}
export default ModalHeader

View File

@@ -1,130 +0,0 @@
import { useIsFetching } from '@tanstack/react-query'
import { ConnectKitButton } from 'connectkit'
import type { ReactNode } from 'react'
import { useState } from 'react'
import { Link } from 'react-router-dom'
import { NavItem } from '..'
import useApiUrlStore from '../../../app/store/apiStore'
import Logo from '../../../assets/img/logo.png'
import { Loader } from '../../atoms'
const navigation = [
{ href: '/desk', name: 'Desk' },
{ href: '/bots', name: 'Bots' },
{ href: '/workflow', name: 'Workflows' },
{ href: '/scenarios', name: 'Scenarios' },
{ href: '/backtest', name: 'Backtest' },
{ href: '/tools', name: 'Tools' },
{ href: '/settings', name: 'Settings' },
]
function navItems(isMobile = false) {
return navigation.map((item) => (
<NavItem key={item.name} href={item.href} isMobile={isMobile}>
{item.name}
</NavItem>
))
}
function PrimaryNavbar() {
return (
<div className="flex">
<Link className="btn btn-ghost text-xl normal-case" to={'/'}>
<img src={Logo} className="App-logo" alt="logo" />
</Link>
{/* <NavItem href="#" /> */}
<div className="md:flex items-center hidden space-x-1">{navItems()}</div>
</div>
)
}
const GlobalLoader = () => {
const isFetching = useIsFetching()
return isFetching ? <Loader size="xs"></Loader> : null
}
export function SecondaryNavbar() {
const { toggleApiUrl, isProd } = useApiUrlStore()
return (
<div className="md:flex items-center hidden space-x-3">
<GlobalLoader />
<div className="form-control">
<label className="label cursor-pointer">
<span className="label-text px-2">{isProd ? 'Server' : 'Debug'}</span>
<input
type="checkbox"
className="toggle"
checked={isProd}
onChange={toggleApiUrl}
/>
</label>
</div>
<ConnectKitButton />
</div>
)
}
type MobileMenuButtonProps = {
onClick: VoidFunction
}
function MobileMenuButton({ onClick }: MobileMenuButtonProps) {
return (
<div className="md:hidden flex items-center">
<button className="mobile-menu-button outline-none" onClick={onClick}>
<svg
className=" hover:text-primary text-accent w-6 h-6"
x-show="!showMenu"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
</div>
)
}
type MobileMenuProps = {
isOpen: boolean
}
function MobileMenu({ isOpen }: MobileMenuProps) {
return (
<div className={isOpen ? 'mobile-menu' : 'hidden mobile-menu'}>
<ul>{navItems(true)}</ul>
</div>
)
}
type NavContainerProps = {
children: ReactNode
isMenuOpen: boolean
}
function NavContainer({ children, isMenuOpen }: NavContainerProps) {
return (
<nav className="bg-base-300 shadow-lg">
<div className="max-w-6xl px-4 mx-auto">
<div className="flex justify-between">{children}</div>
</div>
<MobileMenu isOpen={isMenuOpen} />
</nav>
)
}
export default function NavBar() {
const [isMenuOpen, setIsMenuOpen] = useState<boolean>(false)
return (
<NavContainer isMenuOpen={isMenuOpen}>
<PrimaryNavbar />
<SecondaryNavbar />
<MobileMenuButton onClick={() => setIsMenuOpen(!isMenuOpen)} />
</NavContainer>
)
}

View File

@@ -1,33 +0,0 @@
import { NavLink } from 'react-router-dom'
import type { INavItemProps } from '../../../global/interface'
function navLinkClasses(isActive: boolean, isMobile: boolean) {
let commonClasses = 'block text-sm px-2 py-4'
if (isMobile) {
return `${commonClasses} ${
isActive
? 'text-base-content bg-primary font-semibold'
: 'hover:bg-primary transition duration-300'
}`
}
commonClasses =
'py-4 px-2 font-semibold hover:text-primary transition duration-300'
return `${commonClasses} ${isActive ? 'text-primary' : 'text-base-content'}`
}
export default function NavItem({
children,
href,
isMobile = false,
}: INavItemProps) {
const item = (
<NavLink
to={href}
className={({ isActive }) => navLinkClasses(isActive, isMobile)}
>
{children}
</NavLink>
)
return isMobile ? <li>{item}</li> : item
}

View File

@@ -1,46 +0,0 @@
import * as React from 'react'
import Plot from 'react-plotly.js'
type IPieChart = {
data: number[]
labels: string[]
colors: string[]
}
const PieChart: React.FC<IPieChart> = ({ data, labels, colors }) => {
return (
<>
<Plot
data={[
{
labels: labels,
marker: {
colors: colors,
},
type: 'pie',
values: data,
},
]}
layout={{
height: 150,
margin: {
b: 20,
l: 0,
pad: 0,
r: 0,
t: 0,
},
paper_bgcolor: 'rgba(0,0,0,0)',
plot_bgcolor: 'rgba(0,0,0,0)',
showlegend: false,
width: 150,
}}
config={{
displayModeBar: false,
}}
/>
</>
)
}
export default PieChart

View File

@@ -1,36 +0,0 @@
// This is a custom filter UI for selecting
import React from 'react'
// a unique option from a list
export default function SelectColumnFilter({
column: { filterValue, setFilter, preFilteredRows, id },
}: any) {
// Calculate the options for filtering
// using the preFilteredRows
const options = React.useMemo(() => {
const options = new Set()
preFilteredRows.forEach((row: any) => {
options.add(row.values[id])
})
return [...options.values()]
}, [id, preFilteredRows])
// Render a multi-select box
return (
<select
className="select bg-base-300 text-xs"
value={filterValue}
onChange={(e) => {
setFilter(e.target.value || undefined)
}}
>
<option value="">All</option>
{options.map((option: any, i) => (
<option key={i} value={option}>
{option}
</option>
))}
</select>
)
}

View File

@@ -1,231 +0,0 @@
import { ArrowDownIcon, ArrowUpIcon } from '@heroicons/react/solid'
import React from 'react'
import {
useTable,
usePagination,
useSortBy,
useFilters,
useExpanded,
} from 'react-table'
import type { TableInstanceWithHooks } from '../../../global/type'
// Define a default UI for filtering
function DefaultColumnFilter({
column: { filterValue, preFilteredRows, setFilter },
}: any) {
const count = preFilteredRows.length
return (
<input
value={filterValue || ''}
onChange={(e) => {
setFilter(e.target.value || undefined) // Set undefined to remove the filter entirely
}}
placeholder={`Search ${count} records...`}
/>
)
}
export default function Table({
columns,
data,
renderRowSubCompontent,
showPagination,
hiddenColumns,
showTotal = false,
}: any) {
const defaultColumn = React.useMemo<any>(
() => ({
// Let's set up our default Filter UI
Filter: DefaultColumnFilter,
}),
[]
)
// Use the state and functions returned from useTable to build your UI
const {
getTableProps,
getTableBodyProps,
headerGroups,
prepareRow,
visibleColumns,
page, // Instead of using 'rows', we'll use page,
// which has only the rows for the active page
// The rest of these things are super handy, too ;)
canPreviousPage,
canNextPage,
pageOptions,
pageCount,
gotoPage,
nextPage,
previousPage,
setPageSize,
state: { pageIndex, pageSize },
} = useTable(
{
columns,
data,
defaultColumn, // Be sure to pass the defaultColumn option
initialState: {
hiddenColumns: hiddenColumns ? hiddenColumns : [],
},
},
useFilters,
useSortBy,
useExpanded,
usePagination
) as TableInstanceWithHooks<any>
// Calculez le total des valeurs dans la colonne USD
const total = data
? data
.reduce((sum: number, row: any) => {
return sum + (row.value || 0) // Si la valeur est undefined = 0
}, 0)
.toFixed(2) + ' $'
: '0.00 $'
// Render the UI for your table
return (
<>
<div className="w-full mt-3 mb-3 overflow-x-auto">
<table {...getTableProps()} className="table-compact table">
<thead>
{headerGroups.map((headerGroup: any) => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map((column: any) => (
<th {...column.getHeaderProps(column.getSortByToggleProps())}>
<p className="mb-2 text-center">
{column.render('Header')}
</p>
<span className="relative">
{column.isSorted ? (
column.isSortedDesc ? (
<ArrowUpIcon className="text-primary absolute right-0 w-4" />
) : (
<ArrowDownIcon className="text-secondary absolute right-0 w-4" />
)
) : (
''
)}
</span>
<div>
{column.canFilter ? column.render('Filter') : null}
</div>
</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{page.map((row: any) => {
prepareRow(row)
return (
<>
<tr {...row.getRowProps()}>
{row.cells.map((cell: any) => {
return (
<td {...cell.getCellProps()}>{cell.render('Cell')}</td>
)
})}
</tr>
{row.isExpanded ? (
<tr>
<td colSpan={visibleColumns.length}>
{/*
Inside it, call our renderRowSubComponent function. In reality,
you could pass whatever you want as props to
a component like this, including the entire
table instance. But for this example, we'll just
pass the row
*/}
{renderRowSubCompontent({ row })}
</td>
</tr>
) : null}
</>
)
})}
</tbody>
{/* Afficher le total ici */}
{showTotal ? (
<tr>
<td colSpan={visibleColumns.length}>
<strong>Total: {total}</strong>
</td>
</tr>
) : null}
</table>
</div>
{/*
Pagination can be built however you'd like.
This is just a very basic UI implementation:
*/}
<br />
{showPagination ? (
<div className="pagination">
<button
className="btn"
onClick={() => gotoPage(0)}
disabled={!canPreviousPage}
>
{'<<'}
</button>{' '}
<button
className="btn"
onClick={() => previousPage()}
disabled={!canPreviousPage}
>
{'<'}
</button>{' '}
<button
className="btn"
onClick={() => nextPage()}
disabled={!canNextPage}
>
{'>'}
</button>{' '}
<button
className="btn"
onClick={() => gotoPage(pageCount - 1)}
disabled={!canNextPage}
>
{'>>'}
</button>{' '}
<span>
Page{' '}
<strong>
{pageIndex + 1} of {pageOptions.length}
</strong>{' '}
</span>
{/* <span>
| Go to page:{' '}
<input
type="number"
defaultValue={pageIndex + 1}
onChange={(e) => {
const page = e.target.value ? Number(e.target.value) - 1 : 0
gotoPage(page)
}}
style={{ width: '100px' }}
/>
</span>{' '} */}
<select
className="select select-bordered"
value={pageSize}
onChange={(e) => {
setPageSize(Number(e.target.value))
}}
>
{[10, 20, 30, 40, 50].map((pageSize) => (
<option key={pageSize} value={pageSize}>
Show {pageSize}
</option>
))}
</select>
</div>
) : null}
</>
)
}

View File

@@ -1,77 +0,0 @@
import type { FC } from 'react'
import type { ITabsProps } from '../../../global/type'
/**
* Avalible Props
* @param className string
* @param tab Array of object
* @param selectedTab number
* @param onClick Function to set the active tab
* @param orientation Tab orientation Vertical | Horizontal
*/
const Tabs: FC<ITabsProps> = ({
className = 'tabs-component',
tabs = [],
selectedTab = 0,
onClick,
orientation = 'horizontal',
addButton = false,
onAddButton,
}) => {
const Panel = tabs && tabs.find((tab) => tab.index === selectedTab)
return (
<div
className={
orientation === 'vertical' ? className + ' vertical' : className
}
>
<div className="tabs" role="tablist" aria-orientation={orientation}>
{tabs.map((tab) => (
<button
className={
'mb-5 tab tab-bordered ' +
(selectedTab === tab.index ? 'tab-active' : '')
}
onClick={() => onClick(tab.index)}
key={tab.index}
type="button"
role="tab"
aria-selected={selectedTab === tab.index}
aria-controls={`tabpanel-${tab.index}`}
tabIndex={selectedTab === tab.index ? 0 : -1}
id={`btn-${tab.index}`}
>
{tab.label}
</button>
))}
{addButton && (
<button
className="tab tab-bordered mb-5"
onClick={onAddButton}
key={'add'}
type="button"
role="tab"
aria-selected={false}
aria-controls={`tabpanel-${'add'}`}
tabIndex={-1}
id={`btn-${'add'}`}
>
+
</button>
)}
</div>
<div
role="tabpanel"
aria-labelledby={`btn-${selectedTab}`}
id={`tabpanel-${selectedTab}`}
>
{Panel && (
<Panel.Component index={selectedTab} data-props={Panel.props} />
)}
</div>
</div>
)
}
export default Tabs

View File

@@ -1,21 +0,0 @@
import useTheme from '../../../hooks/useTheme'
const themes = ['black', 'coffee', 'cyberpunk', 'lofi', 'retro']
const ThemeSelector = (): JSX.Element => {
const { setTheme } = useTheme()
return (
<select
className="select w-full max-w-xs"
onChange={(event) => setTheme(event.target.value)}
>
{themes.map((item) => (
<option key={item} value={item}>
{item}
</option>
))}
</select>
)
}
export default ThemeSelector

View File

@@ -1,31 +0,0 @@
import type { Id, TypeOptions, UpdateOptions } from 'react-toastify'
import { toast } from 'react-toastify'
const baseOptions: UpdateOptions = {
autoClose: 5000,
closeOnClick: true,
draggable: true,
hideProgressBar: false,
isLoading: false,
position: 'top-right',
progress: undefined,
theme: 'dark',
}
class Toast {
private id: Id
constructor(content: string) {
this.id = toast.loading(content)
}
update(type: TypeOptions, content: string, opts?: any) {
const options = { ...baseOptions, ...opts }
options.type = type
options.render = content
options.isLoading = false
toast.update(this.id, options)
}
}
export default Toast

View File

@@ -1,14 +0,0 @@
export { CardText, CardPosition, CardSignal } from './CardText/CardText'
export { default as NavItem } from './NavItem/NavItem'
export { default as Tabs } from './Tabs/Tabs'
export { default as Modal } from './Modal/Modal'
export { default as Toast } from './Toast/Toast'
export { default as ThemeSelector } from './ThemeSelector/ThemeSelector'
export { default as Table } from './Table/Table'
export { default as NavBar } from './NavBar/NavBar'
export { default as PieChart } from './PieChart/PieChart'
export { default as FormInput } from './FormInput/FormInput'
export { default as LogIn } from './LogIn/LogIn'
export { default as GridTile } from './GridTile/GridTile'
export { default as SelectColumnFilter } from './Table/SelectColumnFilter'
export { default as Card } from './Card/Card'

View File

@@ -1,13 +0,0 @@
import { useAccount, useEnsName } from 'wagmi'
export function Account() {
const { address } = useAccount()
const { data: ensName } = useEnsName({ address })
return (
<div>
{ensName ?? address?.slice(0, -35)}
{ensName ? ` (${address})` : null}
</div>
)
}

View File

@@ -1,211 +0,0 @@
import {
ArrowDownIcon,
ArrowUpIcon,
ChevronDownIcon,
ChevronRightIcon,
PlayIcon,
} from '@heroicons/react/solid'
import React, { useEffect, useState } from 'react'
import { Hub } from '../../../app/providers/Hubs'
import useApiUrlStore from '../../../app/store/apiStore'
import type { Account, TradingBot } from '../../../generated/ManagingApi'
import {
AccountClient,
BotClient,
TradeDirection,
TradeStatus,
} from '../../../generated/ManagingApi'
import { SelectColumnFilter, Table } from '../../mollecules'
import BacktestRowDetails from '../Backtest/backtestRowDetails'
import StatusBadge from '../StatusBadge/StatusBadge'
import Summary from '../Trading/Summary'
export default function ActiveBots() {
const [bots, setBots] = useState<TradingBot[]>([])
const [accounts, setAccounts] = useState<Account[]>([])
const { apiUrl } = useApiUrlStore()
const columns = React.useMemo(
() => [
{
Cell: ({ row }: any) => (
// Use Cell to render an expander for each row.
// We can use the getToggleRowExpandedProps prop-getter
// to build the expander.
<span {...row.getToggleRowExpandedProps()}>
{row.isExpanded ? (
<ChevronDownIcon></ChevronDownIcon>
) : (
<ChevronRightIcon></ChevronRightIcon>
)}
</span>
),
// Make sure it has an ID
Header: ({ getToggleAllRowsExpandedProps, isAllRowsExpanded }: any) => (
<span {...getToggleAllRowsExpandedProps()}>
{isAllRowsExpanded ? 'v' : '>'}
</span>
),
// Build our expander column
id: 'expander',
},
{
Cell: ({ cell }: any) => (
<>
<StatusBadge
status={cell.row.values.status}
isForWatchOnly={cell.row.values.isForWatchingOnly}
/>
</>
),
Header: 'Status',
accessor: 'status',
disableFilters: true,
sortType: 'basic',
},
{
accessor: 'isForWatchingOnly',
},
{
Filter: SelectColumnFilter,
Header: 'Ticker',
accessor: 'ticker',
disableSortBy: true,
},
{
Header: 'Account',
accessor: 'accountName',
},
{
Filter: SelectColumnFilter,
Header: 'Timeframe',
accessor: 'timeframe',
disableSortBy: true,
},
{
Filter: SelectColumnFilter,
Header: 'Scenario',
accessor: 'scenario',
disableSortBy: true,
},
{
Cell: ({ cell }: any) => (
<>
{
<>
{
cell.row.values.positions.filter(
(p: any) => p.originDirection == TradeDirection.Long
).length
}{' '}
<ArrowUpIcon
width="10"
className="text-primary inline"
display="initial"
></ArrowUpIcon>
{' | '}
{
cell.row.values.positions.filter(
(p: any) => p.originDirection == TradeDirection.Short
).length
}{' '}
<ArrowDownIcon
width="10"
className="text-accent inline"
display="initial"
></ArrowDownIcon>{' '}
{
cell.row.values.positions.filter(
(p: any) => p.status == TradeStatus.Filled
).length
}{' '}
<PlayIcon
width="10"
className="text-accent inline"
display="initial"
></PlayIcon>{' '}
</>
}
</>
),
Header: 'Positions',
accessor: 'positions',
disableFilters: true,
},
{
Cell: ({ cell }) => <>{cell.row.values.winRate} %</>,
Header: 'Winrate',
accessor: 'winRate',
disableFilters: true,
},
{
Cell: ({ cell }) => <>{cell.row.values.profitAndLoss} $</>,
Header: 'PNL',
accessor: 'profitAndLoss',
disableFilters: true,
},
],
[]
)
useEffect(() => {
setupHubConnection().then(() => {
if (bots.length == 0) {
const client = new BotClient({}, apiUrl)
client.bot_GetActiveBots().then((data) => {
setBots(data)
})
}
})
const client = new AccountClient({}, apiUrl)
client.account_GetAccounts().then((data) => {
setAccounts(data)
})
}, [])
const setupHubConnection = async () => {
const hub = new Hub('bothub', apiUrl).hub
hub.on('BotsSubscription', (data: TradingBot[]) => {
// eslint-disable-next-line no-console
console.log(
'bot List',
bots.map((bot) => {
return bot.name
})
)
setBots(data)
})
return hub
}
const renderRowSubComponent = React.useCallback(
({ row }: any) => (
<>
<BacktestRowDetails
candles={row.original.candles}
positions={row.original.positions}
></BacktestRowDetails>
</>
),
[]
)
return (
<>
<div className="flex flex-wrap">
<Summary bots={bots} accounts={accounts}></Summary>
</div>
<div className="flex flex-wrap">
<Table
columns={columns}
data={bots}
renderRowSubCompontent={renderRowSubComponent}
/>
</div>
</>
)
}

View File

@@ -1,309 +0,0 @@
import { DotsVerticalIcon, TrashIcon } from '@heroicons/react/solid'
import moment from 'moment'
import React from 'react'
import useApiUrlStore from '../../../app/store/apiStore'
import type {
Backtest,
MoneyManagement,
StartBotRequest,
Ticker,
} from '../../../generated/ManagingApi'
import {
BacktestClient,
BotClient,
BotType,
} from '../../../generated/ManagingApi'
import type { IBacktestCards } from '../../../global/type'
import MoneyManagementModal from '../../../pages/settingsPage/moneymanagement/moneyManagementModal'
import { CardPosition, CardText, Toast } from '../../mollecules'
import CardPositionItem from '../Trading/CardPositionItem'
import TradeChart from '../Trading/TradeChart/TradeChart'
function baseBadgeClass(isOutlined = false) {
let classes = 'text-xs badge '
if (isOutlined) {
classes += 'badge-outline '
}
return classes
}
function botStatusResult(
growthPercentage: number | undefined,
hodlPercentage: number | undefined
) {
if (growthPercentage != undefined && hodlPercentage != undefined) {
const isWinning = growthPercentage > hodlPercentage
const classes =
baseBadgeClass() + (isWinning ? 'badge-success' : 'badge-content')
return <div className={classes}>{isWinning ? 'Winning' : 'Losing'}</div>
}
}
// function that return the number of day between a date and today
function daysBetween(date: Date) {
const oneDay = 24 * 60 * 60 * 1000 // hours*minutes*seconds*milliseconds
const firstDate = new Date(date)
const secondDate = new Date()
const diffDays = Math.round(
Math.abs((firstDate.getTime() - secondDate.getTime()) / oneDay)
)
return diffDays
}
const BacktestCards: React.FC<IBacktestCards> = ({ list, setBacktests }) => {
const { apiUrl } = useApiUrlStore()
const [showMoneyManagementModal, setShowMoneyManagementModal] =
React.useState(false)
const [selectedMoneyManagement, setSelectedMoneyManagement] =
React.useState<MoneyManagement>()
async function runBot(backtest: Backtest, isForWatchOnly: boolean) {
const t = new Toast('Bot is starting')
const client = new BotClient({}, apiUrl)
const request: StartBotRequest = {
accountName: backtest.accountName,
botName: backtest.ticker + '-' + backtest.timeframe.toString(),
botType: BotType.ScalpingBot,
isForWatchOnly: isForWatchOnly,
moneyManagementName: backtest.moneyManagement?.name,
scenario: backtest.scenario,
ticker: backtest.ticker as Ticker,
timeframe: backtest.timeframe,
}
await client
.bot_Start(request)
.then((botStatus: string) => {
t.update('info', 'Bot status :' + botStatus)
})
.catch((err) => {
t.update('error', 'Error :' + err)
})
}
async function runOptimizedBacktest(backtest: Backtest) {
const t = new Toast('Optimized backtest is running')
const client = new BacktestClient({}, apiUrl)
await client
.backtest_Run(
backtest.accountName,
backtest.botType,
backtest.ticker as Ticker,
backtest.scenario,
backtest.timeframe,
false,
daysBetween(backtest.candles[0].date),
backtest.walletBalances[0].value,
'',
false,
backtest.optimizedMoneyManagement
)
.then((backtest: Backtest) => {
t.update('success', `${backtest.ticker} Backtest Succeeded`)
setBacktests((arr) => [...arr, backtest])
})
.catch((err) => {
t.update('error', 'Error :' + err)
})
}
function saveMoneyManagement(moneyManagement: MoneyManagement) {
setSelectedMoneyManagement(moneyManagement)
setShowMoneyManagementModal(true)
}
return (
<div className="flex flex-wrap m-4 -mx-4">
{list.map((backtest: Backtest, index) => (
<div
key={index.toString()}
className="sm:w-1/2 md:w-1/2 xl:w-1/2 w-full p-2"
>
<div className="indicator">
<div className="indicator-item indicator-top">
<button className="btn btn-primary h-5 min-h-0 px-2 mr-5 rounded-full">
<TrashIcon width={15}></TrashIcon>
</button>
</div>
<div className="card bg-base-300 shadow-xl">
<figure className="z-0">
{
<TradeChart
candles={backtest.candles}
positions={backtest.positions}
walletBalances={backtest.walletBalances}
signals={backtest.signals}
width={720}
height={512}
></TradeChart>
}
</figure>
<div className="card-body">
<h2 className="card-title text-sm">
<div className="dropdown">
<label
htmlFor={index.toString()}
tabIndex={index}
className=""
>
<DotsVerticalIcon className="text-primary w-5 h-5" />
</label>
<ul
id={index.toString()}
className="dropdown-content menu bg-base-100 rounded-box w-52 p-2 shadow"
>
<li>
<button
className="text-xs"
onClick={() => runBot(backtest, false)}
>
Run bot
</button>
</li>
<li>
<button
className="text-xs"
onClick={() => runBot(backtest, true)}
>
Run watcher
</button>
</li>
<li>
<button
className="text-xs"
onClick={() =>
saveMoneyManagement(backtest.moneyManagement)
}
>
Save money management
</button>
</li>
<li>
<button
className="text-xs"
onClick={() => runOptimizedBacktest(backtest)}
>
Run optimized money management
</button>
</li>
</ul>
</div>
{backtest.ticker}
{botStatusResult(
backtest.growthPercentage,
backtest.hodlPercentage
)}
</h2>
<div className="columns-4 mb-2">
<div>
<CardText
title="Ticker"
content={backtest.ticker}
></CardText>
<CardText
title="Account"
content={backtest.accountName}
></CardText>
<CardText
title="Scenario"
content={backtest.scenario}
></CardText>
<CardText
title="Timeframe"
content={backtest.timeframe}
></CardText>
</div>
</div>
<div className="columns-4 mb-2">
<CardText
title="Duration"
content={moment
.duration(
moment(
backtest.candles[backtest.candles.length - 1].date
).diff(backtest.candles[0].date)
)
.humanize()}
></CardText>
{/* <CardSignal signals={backtest.signals}></CardSignal> */}
<CardPosition
positivePosition={true}
positions={backtest.positions.filter((p) => {
const realized = p.profitAndLoss?.realized ?? 0
return realized > 0 ? p : null
})}
></CardPosition>
<CardPosition
positivePosition={false}
positions={backtest.positions.filter((p) => {
const realized = p.profitAndLoss?.realized ?? 0
return realized <= 0 ? p : null
})}
></CardPosition>
<CardPositionItem
positions={backtest.positions}
></CardPositionItem>
</div>
<div className="columns-4 mb-2">
<div>
<CardText
title="Max Drowdown"
content={
backtest.statistics.maxDrawdown?.toFixed(4).toString() +
'$'
}
></CardText>
<CardText
title="PNL"
content={backtest.finalPnl?.toFixed(4).toString() + '$'}
></CardText>
<CardText
title="Sharpe Ratio"
content={
(backtest.statistics.sharpeRatio
? backtest.statistics.sharpeRatio * 100
: 0
)
.toFixed(4)
.toString() + '%'
}
></CardText>
<CardText
title="%Hodl"
content={
backtest.hodlPercentage?.toFixed(2).toString() + '%'
}
></CardText>
</div>
</div>
<div className="card-actions justify-center pt-2 text-sm">
<div className={baseBadgeClass(true)}>
WR {backtest.winRate?.toFixed(2).toString()} %
</div>
<div className={baseBadgeClass(true)}>
PNL {backtest.growthPercentage?.toFixed(2).toString()} %
</div>
</div>
</div>
</div>
</div>
</div>
))}
<MoneyManagementModal
showModal={showMoneyManagementModal}
moneyManagement={selectedMoneyManagement}
onClose={() => setShowMoneyManagementModal(false)}
/>
</div>
)
}
export default BacktestCards

View File

@@ -1,361 +0,0 @@
import { useQuery } from '@tanstack/react-query'
import React, { useEffect, useState } from 'react'
import { useForm, type SubmitHandler } from 'react-hook-form'
import useApiUrlStore from '../../../app/store/apiStore'
import type {
Backtest,
MoneyManagement,
Ticker,
} from '../../../generated/ManagingApi'
import {
AccountClient,
BacktestClient,
BotType,
DataClient,
MoneyManagementClient,
ScenarioClient,
Timeframe,
} from '../../../generated/ManagingApi'
import type {
BacktestModalProps,
IBacktestsFormInput,
} from '../../../global/type'
import { Loader, Slider } from '../../atoms'
import { Modal, Toast } from '../../mollecules'
import FormInput from '../../mollecules/FormInput/FormInput'
import CustomMoneyManagement from '../CustomMoneyManagement/CustomMoneyManagement'
const BacktestModal: React.FC<BacktestModalProps> = ({
showModal,
closeModal,
setBacktests,
showLoopSlider = false,
}) => {
const [selectedAccount, setSelectedAccount] = React.useState<string>()
const [selectedTimeframe, setSelectedTimeframe] = React.useState<Timeframe>()
const [selectedLoopQuantity, setLoopQuantity] = React.useState<number>(
showLoopSlider ? 3 : 1
)
const [balance, setBalance] = React.useState<number>(10000)
const [days, setDays] = React.useState<number>(-10)
const [customMoneyManagement, setCustomMoneyManagement] =
React.useState<MoneyManagement>()
const [selectedMoneyManagement, setSelectedMoneyManagement] =
useState<string>()
const [showCustomMoneyManagement, setShowCustomMoneyManagement] =
useState(false)
const { apiUrl } = useApiUrlStore()
const scenarioClient = new ScenarioClient({}, apiUrl)
const accountClient = new AccountClient({}, apiUrl)
const dataClient = new DataClient({}, apiUrl)
const moneyManagementClient = new MoneyManagementClient({}, apiUrl)
const backtestClient = new BacktestClient({}, apiUrl)
const { register, handleSubmit } = useForm<IBacktestsFormInput>()
const onSubmit: SubmitHandler<IBacktestsFormInput> = async (form) => {
const { scenarioName, tickers } = form
closeModal()
for (let sIndex = 0; sIndex < scenarioName.length; sIndex++) {
for (let tIndex = 0; tIndex < tickers.length; tIndex++) {
await runBacktest(
form,
form.tickers[tIndex],
form.scenarioName[sIndex],
customMoneyManagement,
1
)
}
}
}
async function runBacktest(
form: IBacktestsFormInput,
ticker: string,
scenarioName: string,
moneyManagement: MoneyManagement | undefined,
loopCount: number
) {
const t = new Toast(ticker + ' is running')
await backtestClient
.backtest_Run(
form.accountName,
form.botType,
ticker as Ticker,
scenarioName,
form.timeframe,
false,
days,
balance,
selectedMoneyManagement,
form.save,
selectedMoneyManagement ? undefined : moneyManagement
)
.then((backtest: Backtest) => {
t.update('success', `${backtest.ticker} Backtest Succeeded`)
setBacktests((arr) => [...arr, backtest])
if (showLoopSlider && selectedLoopQuantity > loopCount) {
const nextCount = loopCount + 1
const mm: MoneyManagement = {
balanceAtRisk: backtest.optimizedMoneyManagement.balanceAtRisk,
leverage: backtest.optimizedMoneyManagement.leverage,
name: backtest.optimizedMoneyManagement.name + nextCount,
stopLoss: backtest.optimizedMoneyManagement.stopLoss,
takeProfit: backtest.optimizedMoneyManagement.takeProfit,
timeframe: backtest.optimizedMoneyManagement.timeframe,
}
runBacktest(form, ticker, scenarioName, mm, nextCount)
}
})
.catch((err) => {
t.update('error', 'Error :' + err)
})
}
function setSelectedAccountEvent(e: React.ChangeEvent<HTMLInputElement>) {
setSelectedAccount(e.target.value)
}
function setSelectedTimeframeEvent(e: any) {
setSelectedTimeframe(e.target.value)
}
function onMoneyManagementChange(e: any) {
if (e.target.value == 'Custom') {
setShowCustomMoneyManagement(true)
setSelectedMoneyManagement(e.target.value)
} else {
setShowCustomMoneyManagement(false)
setCustomMoneyManagement(undefined)
setSelectedMoneyManagement(undefined)
}
}
const { data: scenarios } = useQuery({
queryFn: () => scenarioClient.scenario_GetScenarios(),
queryKey: ['scenarios'],
})
const { data: accounts } = useQuery({
onSuccess: () => {
if (accounts) {
setSelectedAccount(accounts[0].name)
}
setSelectedTimeframe(Timeframe.FiveMinutes)
},
queryFn: () => accountClient.account_GetAccounts(),
queryKey: ['accounts'],
})
const { data: tickers, refetch: refetchTickers } = useQuery({
enabled: !!selectedAccount && !!selectedTimeframe,
queryFn: () => {
if (selectedAccount && selectedTimeframe) {
return dataClient.data_GetTickers(selectedAccount, selectedTimeframe)
}
},
queryKey: ['tickers', selectedAccount, selectedTimeframe],
})
const { data: moneyManagements } = useQuery({
enabled: !!selectedTimeframe,
onSuccess: (data) => {
if (data) {
setSelectedMoneyManagement(data[0].name)
}
},
queryFn: async () => {
if (selectedTimeframe) {
const mm =
await moneyManagementClient.moneyManagement_GetMoneyManagements()
mm.push({
balanceAtRisk: 1,
leverage: 1,
name: 'Custom',
stopLoss: 1,
takeProfit: 1,
timeframe: selectedTimeframe,
})
return mm
}
},
queryKey: ['moneyManagements', selectedTimeframe],
})
useEffect(() => {
if (selectedAccount && selectedTimeframe) {
refetchTickers()
}
}, [selectedAccount, selectedTimeframe])
if (!accounts || !scenarios || !moneyManagements) {
return <Loader></Loader>
}
return (
<Modal
titleHeader="Run backtest"
showModal={showModal}
onClose={closeModal}
onSubmit={handleSubmit(onSubmit)}
>
<FormInput label="Account" htmlFor="accountName">
<select
className="select select-bordered w-full h-auto max-w-xs"
{...register('accountName', {
onChange(e) {
setSelectedAccountEvent(e)
},
})}
>
{accounts.map((item) => (
<option key={item.name} value={item.name}>
{item.name}
</option>
))}
</select>
</FormInput>
<FormInput label="Money Management" htmlFor="moneyManagement">
<select
className="select w-full max-w-xs"
{...register('moneyManagement', {
onChange(event) {
onMoneyManagementChange(event)
},
})}
>
{moneyManagements.map((item) => (
<option key={item.name} value={item.name}>
{item.name}
</option>
))}
</select>
</FormInput>
<CustomMoneyManagement
onCreateMoneyManagement={setCustomMoneyManagement}
timeframe={selectedTimeframe || Timeframe.FiveMinutes}
showCustomMoneyManagement={showCustomMoneyManagement}
></CustomMoneyManagement>
<FormInput label="Type" htmlFor="botType">
<select className="select w-full max-w-xs" {...register('botType')}>
{Object.keys(BotType).map((item) => (
<option key={item} value={item}>
{item}
</option>
))}
</select>
</FormInput>
<FormInput label="Timeframe" htmlFor="timeframe">
<select
className="select w-full max-w-xs"
{...register('timeframe', {
onChange(event) {
setSelectedTimeframeEvent(event)
},
})}
>
{Object.keys(Timeframe).map((item) => (
<option key={item} value={item}>
{item}
</option>
))}
</select>
</FormInput>
<FormInput label="Days" htmlFor="days">
<Slider
id="days"
value={days}
onChange={(e: any) => setDays(e.target.value)}
step="1"
min="-360"
max="-1"
></Slider>
</FormInput>
<FormInput label="Balance" htmlFor="balance">
<Slider
id="balance"
value={balance}
onChange={(e: any) => setBalance(e.target.value)}
step="1000"
min="1000"
max="100000"
></Slider>
</FormInput>
<FormInput label="Scenario" htmlFor="scenarioName">
<select
multiple
className="select select-bordered w-full h-auto max-w-xs"
{...register('scenarioName')}
>
{scenarios.map((item) => (
<option key={item.name} value={item.name}>
{item.name}
</option>
))}
</select>
</FormInput>
<FormInput label="Tickers" htmlFor="tickers">
<select
multiple
className="select select-bordered w-full h-auto max-w-xs"
{...register('tickers')}
>
{tickers ? (
tickers.map((item) => (
<option key={item} value={item}>
{item}
</option>
))
) : (
<option key="NoTicker" value="No Ticker">
No ticker
</option>
)}
</select>
</FormInput>
{/* Loop Quantity */}
{showLoopSlider ? (
<FormInput label="Loop" htmlFor="loopQuantity">
<Slider
id="takeProfit"
value={selectedLoopQuantity}
onChange={(e: any) => setLoopQuantity(e.target.value)}
step="1"
min="1"
max="20"
></Slider>
</FormInput>
) : null}
<div className="form-control">
<label htmlFor="save" className="label w-full cursor-pointer">
<span className="label mr-6">Save backtest</span>
<input
type="checkbox"
className="checkbox checkbox-primary"
{...register('save')}
/>
</label>
</div>
<div className="modal-action">
<button type="submit" className="btn">
Run
</button>
</div>
</Modal>
)
}
export default BacktestModal

View File

@@ -1,47 +0,0 @@
import { TradeChart, CardPositionItem } from '..'
import type { IBotRowDetails } from '../../../global/interface'
import { CardPosition } from '../../mollecules'
const BacktestRowDetails: React.FC<IBotRowDetails> = ({
candles,
positions,
walletBalances,
}) => {
return (
<>
<div className="grid grid-flow-row">
<div className="grid grid-cols-4 p-5">
<CardPosition
positivePosition={true}
positions={positions.filter((p) => {
const realized = p.profitAndLoss?.realized ?? 0
return realized > 0 ? p : null
})}
></CardPosition>
<CardPosition
positivePosition={false}
positions={positions.filter((p) => {
const realized = p.profitAndLoss?.realized ?? 0
return realized <= 0 ? p : null
})}
></CardPosition>
<CardPositionItem positions={positions}></CardPositionItem>
</div>
<div>
<figure>
<TradeChart
width={1000}
height={500}
candles={candles}
positions={positions}
walletBalances={walletBalances}
signals={[]}
></TradeChart>
</figure>
</div>
</div>
</>
)
}
export default BacktestRowDetails

View File

@@ -1,275 +0,0 @@
import {
ChevronDownIcon,
ChevronRightIcon,
PlayIcon,
TrashIcon,
} from '@heroicons/react/solid'
import React, { useEffect, useState } from 'react'
import useApiUrlStore from '../../../app/store/apiStore'
import type {
Backtest,
StartBotRequest,
Ticker,
} from '../../../generated/ManagingApi'
import { BacktestClient, BotClient } from '../../../generated/ManagingApi'
import type { IBacktestCards } from '../../../global/type'
import { Toast, SelectColumnFilter, Table } from '../../mollecules'
import BacktestRowDetails from './backtestRowDetails'
const BacktestTable: React.FC<IBacktestCards> = ({ list, isFetching }) => {
const [rows, setRows] = useState<Backtest[]>([])
const { apiUrl } = useApiUrlStore()
async function runBot(backtest: Backtest, isForWatchOnly: boolean) {
const t = new Toast('Bot is starting')
const client = new BotClient({}, apiUrl)
const request: StartBotRequest = {
accountName: backtest.accountName,
botName: backtest.ticker + '-' + backtest.timeframe.toString(),
botType: backtest.botType,
isForWatchOnly: isForWatchOnly,
moneyManagementName: '',
scenario: backtest.scenario,
ticker: backtest.ticker as Ticker,
timeframe: backtest.timeframe,
}
await client
.bot_Start(request)
.then((botStatus: string) => {
t.update('info', 'Bot status :' + botStatus)
})
.catch((err) => {
t.update('error', 'Error :' + err)
})
}
async function deleteBacktest(id: string) {
const t = new Toast('Deleting backtest')
const client = new BacktestClient({}, apiUrl)
await client
.backtest_DeleteBacktest(id)
.then(() => {
t.update('success', 'Backtest deleted')
})
.catch((err) => {
t.update('error', err)
})
}
const columns = React.useMemo(
() => [
{
Header: 'Informations',
columns: [
{
Cell: ({ row }: any) => (
// Use Cell to render an expander for each row.
// We can use the getToggleRowExpandedProps prop-getter
// to build the expander.
<span {...row.getToggleRowExpandedProps()}>
{row.isExpanded ? (
<ChevronDownIcon></ChevronDownIcon>
) : (
<ChevronRightIcon></ChevronRightIcon>
)}
</span>
),
// Make sure it has an ID
Header: ({
getToggleAllRowsExpandedProps,
isAllRowsExpanded,
}: any) => (
<span {...getToggleAllRowsExpandedProps()}>
{isAllRowsExpanded ? 'v' : '>'}
</span>
),
// Build our expander column
id: 'expander',
},
{
Filter: SelectColumnFilter,
Header: 'Ticker',
accessor: 'ticker',
disableSortBy: true,
},
{
Filter: SelectColumnFilter,
Header: 'Timeframe',
accessor: 'timeframe',
disableSortBy: true,
},
{
Filter: SelectColumnFilter,
Header: 'Scenario',
accessor: 'scenario',
disableSortBy: true,
},
{
Filter: SelectColumnFilter,
Header: 'BotType',
accessor: 'botType',
disableSortBy: true,
},
{
Filter: SelectColumnFilter,
Header: 'Account',
accessor: 'accountName',
disableSortBy: true,
},
],
},
{
Header: 'Results',
columns: [
{
Cell: ({ cell }: any) => (
<>{cell.row.values.finalPnl.toFixed(2)} $</>
),
Header: 'Pnl $',
accessor: 'finalPnl',
disableFilters: true,
sortType: 'basic',
},
{
Cell: ({ cell }: any) => (
<>{cell.row.values.hodlPercentage.toFixed(2)} %</>
),
Header: 'Hodl %',
accessor: 'hodlPercentage',
disableFilters: true,
sortType: 'basic',
},
{
Cell: ({ cell }: any) => <>{cell.row.values.winRate} %</>,
Header: 'Winrate',
accessor: 'winRate',
disableFilters: true,
},
{
Cell: ({ cell }: any) => (
<>{cell.row.values.growthPercentage.toFixed(2)} %</>
),
Header: 'Pnl %',
accessor: 'growthPercentage',
disableFilters: true,
sortType: 'basic',
},
{
Cell: ({ cell }: any) => (
<>
{(
cell.row.values.growthPercentage -
cell.row.values.hodlPercentage
).toFixed(2)}
</>
),
Header: 'H/P',
accessor: 'diff',
disableFilters: true,
sortType: 'basic',
},
],
},
{
Header: 'Action',
columns: [
{
Cell: ({ cell }: any) => (
<>
<div className="tooltip" data-tip="Delete backtest">
<button
data-value={cell.row.values.name}
onClick={() => deleteBacktest(cell.row.values.id)}
>
<TrashIcon className="text-accent w-4"></TrashIcon>
</button>
</div>
</>
),
Header: '',
accessor: 'id',
disableFilters: true,
},
// {
// Cell: ({ cell }) => (
// <>
// <div className="tooltip" data-tip="Run watcher">
// <button
// data-value={cell.row.values.name}
// onClick={() => runBot(cell.row.values, true)}
// >
// <EyeIcon className="text-primary w-4"></EyeIcon>
// </button>
// </div>
// </>
// ),
// Header: '',
// accessor: 'watcher',
// disableFilters: true,
// },
{
Cell: ({ cell }: any) => (
<>
<div className="tooltip" data-tip="Run bot">
<button
data-value={cell.row.values.name}
onClick={() => runBot(cell.row.values, false)}
>
<PlayIcon className="text-primary-focus w-4"></PlayIcon>
</button>
</div>
</>
),
Header: '',
accessor: 'bot',
disableFilters: true,
},
],
},
],
[]
)
useEffect(() => {
setRows(list)
}, [list])
const renderRowSubComponent = React.useCallback(
({ row }: any) => (
<>
<BacktestRowDetails
candles={row.original.candles}
positions={row.original.positions}
walletBalances={row.original.walletBalances}
></BacktestRowDetails>
</>
),
[]
)
return (
<div
className="flex flex-wrap"
style={{ display: 'flex', justifyContent: 'center', width: '110%' }}
>
{isFetching ? (
<progress className="progress progress-primary w-56"></progress>
) : (
<Table
columns={columns}
data={rows}
renderRowSubCompontent={renderRowSubComponent}
showPagination={true}
/>
)}
</div>
)
}
export default BacktestTable

View File

@@ -1,104 +0,0 @@
import React, { useEffect, useState } from 'react'
import type { MoneyManagement, Timeframe } from '../../../generated/ManagingApi'
import { Slider } from '../../atoms'
import FormInput from '../../mollecules/FormInput/FormInput'
type ICustomMoneyManagement = {
onCreateMoneyManagement: (moneyManagement: MoneyManagement) => void
timeframe: Timeframe
showCustomMoneyManagement: boolean
}
const CustomMoneyManagement: React.FC<ICustomMoneyManagement> = ({
onCreateMoneyManagement,
timeframe,
showCustomMoneyManagement,
}) => {
const [balanceAtRisk, setBalanceAtRisk] = useState<number>(1)
const [leverage, setLeverage] = useState<number>(1)
const [takeProfit, setTakeProfit] = useState<number>(1)
const [stopLoss, setStopLoss] = useState<number>(1)
const handleCreateMoneyManagement = () => {
const moneyManagement: MoneyManagement = {
balanceAtRisk,
leverage,
name: 'custom',
stopLoss,
takeProfit,
timeframe,
}
onCreateMoneyManagement(moneyManagement)
}
useEffect(() => {
handleCreateMoneyManagement()
}, [balanceAtRisk, leverage, takeProfit, stopLoss])
return (
<>
{showCustomMoneyManagement ? (
<div className="collapse bg-base-200">
<input type="checkbox" />
<div className="collapse-title text-xs font-medium">
Custom MoneyManagement
</div>
<div className="collapse-content">
<FormInput
label="Balance at risk"
htmlFor="balanceAtRisk"
inline={true}
>
<Slider
id="balanceAtRisk"
value={balanceAtRisk}
onChange={(e) => setBalanceAtRisk(parseInt(e.target.value))}
min="1"
max="100"
step="1"
suffixValue=" %"
></Slider>
</FormInput>
<FormInput label="Leverage" htmlFor="leverage" inline={true}>
<Slider
id="leverage"
value={leverage}
max="50"
min="1"
step="1"
onChange={(e: any) => setLeverage(e.target.value)}
prefixValue="x "
></Slider>
</FormInput>
<FormInput label="TP %" htmlFor="takeProfit" inline={true}>
<Slider
id="takeProfit"
value={takeProfit}
onChange={(e: any) => setTakeProfit(e.target.value)}
step="0.01"
max="20"
suffixValue=" %"
></Slider>
</FormInput>
<FormInput label="SL %" htmlFor="stopLoss" inline={true}>
<Slider
id="stopLoss"
value={stopLoss}
onChange={(e: any) => setStopLoss(e.target.value)}
step="0.01"
max="20"
suffixValue=" %"
></Slider>
</FormInput>
</div>
</div>
) : null}
</>
)
}
export default CustomMoneyManagement

View File

@@ -1,128 +0,0 @@
import { StopIcon } from '@heroicons/react/solid'
import moment from 'moment'
import React from 'react'
import useApiUrlStore from '../../../app/store/apiStore'
import {
TradeDirection,
type Position,
Ticker,
PositionStatus,
TradingClient,
} from '../../../generated/ManagingApi'
import { Toast, Table } from '../../mollecules'
import PositionStatusBadge from './PositionStatusBadge'
type IPositionList = {
positions: Position[]
isFetching: boolean
}
const PositionsList: React.FC<IPositionList> = ({ positions, isFetching }) => {
const { apiUrl } = useApiUrlStore()
async function closePosition(identifier: string) {
const t = new Toast('Closing position')
const client = new TradingClient({}, apiUrl)
await client
.trading_ClosePosition(identifier)
.then(() => {
t.update('success', 'Position closed')
})
.catch((err) => {
t.update('error', 'Error :' + err)
})
}
const columns = React.useMemo(
() => [
{
Cell: ({ cell }: any) => (
<>
<div
className="tooltip z-20"
data-tip={Object.values(PositionStatus)[cell.value]}
>
<PositionStatusBadge
status={Object.values(PositionStatus)[cell.value]}
/>
</div>
</>
),
Header: 'Status',
accessor: 'status',
disableFilters: true,
sortType: 'basic',
},
{
Cell: ({ cell }: any) => <div>{Object.values(Ticker)[cell.value]}</div>,
Header: 'Ticker',
accessor: 'ticker',
disableFilters: true,
disableSortBy: true,
},
{
Cell: ({ cell }: any) => (
<div>{Object.values(TradeDirection)[cell.value]}</div>
),
Header: 'Direction',
accessor: 'originDirection',
disableFilters: true,
disableSortBy: true,
},
{
Header: 'Account',
accessor: 'accountName',
disableFilters: true,
disableSortBy: true,
},
{
Cell: ({ cell }: any) => moment(cell.value).fromNow(),
Header: 'Date',
accessor: 'date',
disableFilters: true,
},
{
Cell: ({ cell }: any) => (
<div>{(cell.value.realized as number).toFixed(4)} $</div>
),
Header: 'uPNL',
accessor: 'profitAndLoss',
disableFilters: true,
},
{
Cell: ({ cell }) => (
<>
<div className="tooltip" data-tip="Close position">
<button
data-value={cell.row.value}
onClick={() => closePosition(cell.value)}
>
<StopIcon className="text-error w-4"></StopIcon>
</button>
</div>
</>
),
Header: 'Actions',
accessor: 'identifier',
disableFilters: true,
},
],
[]
)
return (
<div className="flex flex-wrap">
{isFetching ? (
<div>
<progress className="progress progress-primary w-56"></progress>
</div>
) : (
<Table columns={columns} data={positions} />
)}
</div>
)
}
export default PositionsList

View File

@@ -1,30 +0,0 @@
import { PositionStatus } from '../../../generated/ManagingApi'
type IPositionStatusBadge = {
status: PositionStatus
}
function statusClasses(status: PositionStatus) {
let commonClasses = 'badge badge-xs '
switch (status) {
case PositionStatus.Canceled:
case PositionStatus.Rejected:
commonClasses += 'bg-red-100'
break
case PositionStatus.New:
commonClasses += 'bg-blue-100'
break
case PositionStatus.PartiallyFilled:
commonClasses += 'bg-orange-100'
break
case PositionStatus.Filled:
commonClasses += 'bg-green-100'
break
default:
break
}
return commonClasses
}
export default function PositionStatusBadge({ status }: IPositionStatusBadge) {
return <span className={statusClasses(status)}></span>
}

View File

@@ -1,31 +0,0 @@
import moment from 'moment'
import { TradeDirection } from '../../../generated/ManagingApi'
import type { ISpotlightBadge } from '../../../global/type'
function GetBadgeColor(direction: TradeDirection | undefined) {
switch (direction) {
case TradeDirection.Long:
return 'badge bg-success'
case TradeDirection.Short:
return 'badge bg-error'
case TradeDirection.None:
return 'badge bg-warning'
default:
return 'badge'
}
}
export default function SpotLightBadge({
direction,
date,
price,
}: ISpotlightBadge) {
const tooltipText =
date == undefined ? 'No signal' : moment(date).fromNow() + ' @ ' + price
return (
<div className="tooltip z-20" data-tip={tooltipText}>
<div className={GetBadgeColor(direction)}></div>
</div>
)
}

View File

@@ -1,14 +0,0 @@
import type { IStatusBadge } from '../../../global/type'
function statusClasses(status: string, isForWatchOnly: boolean) {
const commonClasses = 'badge badge-xs'
if (isForWatchOnly) {
return `${commonClasses} 'bg-blue-500'`
}
return `${commonClasses} ${status == 'Up' ? 'bg-green-500' : 'bg-red-500'}`
}
export default function StatusBadge({ status, isForWatchOnly }: IStatusBadge) {
return <span className={statusClasses(status, isForWatchOnly)}></span>
}

View File

@@ -1,34 +0,0 @@
import { PositionStatus } from '../../../generated/ManagingApi'
import type { ICardPositionFlipped } from '../../../global/type'
import { CardText } from '../../mollecules'
const CardPositionFlipped: React.FC<ICardPositionFlipped> = ({ positions }) => {
return (
<>
<CardText
title="Position Flipped"
content={
positions
.filter((p) => {
const realized = p.profitAndLoss?.realized ?? 0
return realized > 0 && p.status == PositionStatus.Flipped
? p
: null
})
.length.toString() +
' | ' +
positions
.filter((p) => {
const realized = p.profitAndLoss?.realized ?? 0
return realized <= 0 && p.status == PositionStatus.Flipped
? p
: null
})
.length.toString()
}
></CardText>
</>
)
}
export default CardPositionFlipped

View File

@@ -1,133 +0,0 @@
import { ArrowDownIcon, ArrowUpIcon } from '@heroicons/react/solid'
import React, { useEffect, useState } from 'react'
import type { TradingBot } from '../../../generated/ManagingApi'
import { PositionStatus, TradeDirection } from '../../../generated/ManagingApi'
import type { IAccountBalanceProps } from '../../../global/type'
function GetGlobalWinrate(bots: TradingBot[]) {
if (bots == null || bots == undefined || bots.length == 0) {
return 0
}
let totalPositions = 0
let winningPosition = 0
bots.forEach((bot) => {
totalPositions += bot.positions.filter(
(p) => p.status != PositionStatus.New
).length
winningPosition += bot.positions.filter((p) => {
const realized = p.profitAndLoss?.realized ?? 0
return realized > 0 &&
(p.status == PositionStatus.Finished ||
p.status == PositionStatus.Flipped)
? p
: null
}).length
})
if (totalPositions == 0) return 0
return (winningPosition * 100) / totalPositions
}
function GetPositionCount(
bots: TradingBot[],
direction: TradeDirection,
status: PositionStatus
) {
let totalPositions = 0
if (bots == null || bots == undefined) {
return 0
}
bots.forEach((bot) => {
totalPositions += bot.positions.filter(
(p) => p.status == status && p.originDirection == direction
).length
})
return totalPositions
}
const Summary: React.FC<IAccountBalanceProps> = ({ bots }) => {
const [globalPnl, setGlobalPnl] = useState<number>(0)
const [globalWinrate, setGlobalWinrate] = useState<number>(0)
const [openLong, setLong] = useState<number>(0)
const [openShort, setShort] = useState<number>(0)
const [closedLong, setClosedLong] = useState<number>(0)
const [closedShort, setClosedShort] = useState<number>(0)
useEffect(() => {
if (bots) {
const pnl = bots.reduce((acc, bot) => {
return acc + bot.profitAndLoss
}, 0)
setGlobalPnl(pnl)
setGlobalWinrate(GetGlobalWinrate(bots))
setLong(
GetPositionCount(bots, TradeDirection.Long, PositionStatus.Filled)
)
setShort(
GetPositionCount(bots, TradeDirection.Short, PositionStatus.Filled)
)
setClosedLong(
GetPositionCount(bots, TradeDirection.Long, PositionStatus.Finished) +
GetPositionCount(bots, TradeDirection.Long, PositionStatus.Flipped)
)
setClosedShort(
GetPositionCount(bots, TradeDirection.Short, PositionStatus.Finished) +
GetPositionCount(bots, TradeDirection.Short, PositionStatus.Flipped)
)
}
}, [bots])
return (
<div className="p-4">
<div className="stats bg-primary text-primary-content mb-4"></div>
<div className="stats bg-primary text-primary-content">
<div className="stat">
<div className="stat-title">Bots running</div>
<div className="stat-value">{bots.length}</div>
</div>
<div className="stat">
<div className="stat-title">Total Profit</div>
<div className="stat-value">{globalPnl.toFixed(4)} $</div>
</div>
<div className="stat">
<div className="stat-title">Global Winrate</div>
<div className="stat-value">
{globalWinrate ? globalWinrate.toFixed(2) : 0} %
</div>
</div>
<div className="stat">
<div className="stat-title">Positions Openend</div>
<div className="stat-value">
{openLong} <ArrowUpIcon width={20} className="inline"></ArrowUpIcon>{' '}
{openShort}{' '}
<ArrowDownIcon width={20} className="inline"></ArrowDownIcon>{' '}
</div>
</div>
<div className="stat">
<div className="stat-title">Positions Closed</div>
<div className="stat-value">
{closedLong}{' '}
<ArrowUpIcon width={20} className="inline"></ArrowUpIcon>{' '}
{closedShort}{' '}
<ArrowDownIcon width={20} className="inline"></ArrowDownIcon>{' '}
</div>
</div>
</div>
</div>
)
}
export default Summary

View File

@@ -1,302 +0,0 @@
import type {
CandlestickData,
IChartApi,
ISeriesApi,
PriceLineOptions,
SeriesMarker,
SeriesMarkerShape,
Time,
UTCTimestamp,
} from 'lightweight-charts'
import { LineStyle, createChart, CrosshairMode } from 'lightweight-charts'
import moment from 'moment'
import * as React from 'react'
import { useEffect, useRef, useState } from 'react'
import type {
Candle,
KeyValuePairOfDateTimeAndDecimal,
Position,
Signal,
} from '../../../../generated/ManagingApi'
import {
PositionStatus,
TradeDirection,
} from '../../../../generated/ManagingApi'
import useTheme from '../../../../hooks/useTheme'
type ITradeChartProps = {
candles: Candle[]
positions: Position[]
signals: Signal[]
walletBalances?: KeyValuePairOfDateTimeAndDecimal[] | null
stream?: Candle | null
width: number
height: number
}
const TradeChart = ({
candles,
positions,
signals,
walletBalances,
stream,
width,
height,
}: ITradeChartProps) => {
const chartRef = React.useRef<HTMLDivElement>(null)
const chart = useRef<IChartApi>()
const { themeProperty } = useTheme()
const theme = themeProperty()
const series1 = useRef<ISeriesApi<'Candlestick'>>()
const [timeDiff, setTimeDiff] = useState<number>(0)
const [candleCount, setCandleCount] = useState<number>(candles.length)
function buildLine(
color: string,
price: number,
title: string
): PriceLineOptions {
return {
axisLabelVisible: true,
color: color,
lineStyle: LineStyle.Dotted,
lineVisible: true,
lineWidth: 1,
price: price,
title: title,
}
}
function buildMarker(
shape: SeriesMarkerShape,
color: string,
direction: TradeDirection,
date: Date,
text?: string
): SeriesMarker<Time> {
return {
color: color,
position: direction == TradeDirection.Long ? 'belowBar' : 'aboveBar',
shape: shape,
size: 1,
text: text,
time: moment(date).unix() as UTCTimestamp,
}
}
function mapCandle(c: Candle): CandlestickData {
return {
close: c.close,
high: c.high,
low: c.low,
open: c.open,
time: moment(c.date).unix() as UTCTimestamp,
}
}
function mergeTickToBar(candle: CandlestickData) {
const previousCandle = series1.current?.dataByIndex(
candleCount - 1
) as CandlestickData
if ((candle.time as number) - (previousCandle?.time as number) > timeDiff) {
series1.current?.update(candle)
setCandleCount((prev) => prev + 1)
} else {
previousCandle.close = candle.close
previousCandle.high = Math.max(previousCandle.high, candle.high)
previousCandle.low = Math.min(previousCandle.low, candle.low)
series1.current?.update(previousCandle)
}
}
function getPositionColor(position: Position) {
let color = 'mintcream'
if (position == undefined) return color
const negativeColor = 'palevioletred'
const positiveColor = 'lightgreen'
const status = position.status
const realized = position.profitAndLoss?.realized ?? 0
if (status != undefined) {
if (
status == PositionStatus.Finished ||
status == PositionStatus.Flipped
) {
if (realized > 0) {
color = positiveColor
} else {
color = negativeColor
}
}
}
if (position.profitAndLoss?.realized == null) {
color = 'yellow'
}
return color
}
useEffect(() => {
if (chartRef.current) {
const lineColor = theme.secondary
chart.current = createChart(chartRef.current, {
crosshair: {
mode: CrosshairMode.Normal,
},
grid: {
horzLines: {
color: lineColor,
visible: false,
},
vertLines: {
color: lineColor,
visible: false,
},
},
height: height,
layout: {
background: { color: '#121212' },
textColor: theme.secondary,
},
localization: {
dateFormat: 'yyyy-MM-dd',
},
rightPriceScale: {
autoScale: true,
borderColor: lineColor,
borderVisible: true,
},
timeScale: {
borderColor: lineColor,
lockVisibleTimeRangeOnResize: true,
secondsVisible: true,
timeVisible: true,
},
width: width,
})
prepareChart()
}
}, [])
useEffect(() => {
if (series1.current === null) {
return
}
if (stream && candles.length > 1 && timeDiff) {
const c = mapCandle(stream)
mergeTickToBar(c)
}
}, [stream])
function prepareChart() {
if (!chart.current) return
series1.current = chart.current.addCandlestickSeries({
borderDownColor: theme.secondary,
borderUpColor: theme.primary,
downColor: theme.secondary,
upColor: theme.primary,
wickDownColor: theme.secondary,
wickUpColor: theme.primary,
})
const data: CandlestickData[] = candles.map((c) => mapCandle(c))
series1.current.setData(data)
series1.current.applyOptions({
priceFormat: {
minMove: 0.0001,
precision: 4,
type: 'price',
},
})
const diff =
(data[data.length - 2].time as number) -
(data[data.length - 3].time as number)
setTimeDiff(diff)
setCandleCount(data.length)
const markers: SeriesMarker<Time>[] = []
if (signals) {
const signalMarkers = signals.map((s) =>
buildMarker(
'circle',
s.direction == TradeDirection.Long ? theme.success : theme.error,
s.direction,
s.date,
undefined
)
)
markers.push(...signalMarkers)
}
if (positions) {
const positionMarkers = positions.map((p) =>
buildMarker(
p.originDirection == TradeDirection.Long ? 'arrowUp' : 'arrowDown',
getPositionColor(p),
p.originDirection,
p.date,
p.open.price.toString()
)
)
markers.push(...positionMarkers)
const lastPositionOpen = positions[positions.length - 1]
if (lastPositionOpen) {
series1.current.createPriceLine(
buildLine(theme.error, lastPositionOpen.stopLoss.price, 'SL')
)
series1.current.createPriceLine(
buildLine(theme.success, lastPositionOpen.takeProfit1.price, 'TP')
)
}
}
if (markers.length > 0) {
series1.current.setMarkers(markers)
}
if (walletBalances != null) {
const walletSeries = chart.current.addBaselineSeries({
baseValue: { price: walletBalances[0].value, type: 'price' },
bottomFillColor1: 'rgba( 239, 83, 80, 0.05)',
bottomFillColor2: 'rgba( 239, 83, 80, 0.28)',
bottomLineColor: 'rgba( 239, 83, 80, 1)',
pane: 1,
topFillColor1: 'rgba( 38, 166, 154, 0.28)',
topFillColor2: 'rgba( 38, 166, 154, 0.05)',
topLineColor: 'rgba( 38, 166, 154, 1)',
})
const walletData = walletBalances.map((w) => {
return {
time: moment(w.key).unix(),
value: w.value,
}
})
// @ts-ignore
walletSeries.setData(walletData)
walletSeries.applyOptions({
priceFormat: {
minMove: 0.0001,
precision: 4,
type: 'price',
},
})
}
}
return <div ref={chartRef} />
}
export default TradeChart

View File

@@ -1,22 +0,0 @@
import React from 'react'
import type { IFlow } from '../../../generated/ManagingApi'
type IFlowItem = {
onDragStart: (event: any, data: string) => void
flow: IFlow
}
const FlowItem: React.FC<IFlowItem> = ({ onDragStart, flow }) => {
return (
<div
className="btn btn-primary btn-xs w-full h-full my-2"
onDragStart={(event) => onDragStart(event, `${flow.type}`)}
draggable
>
{flow.name}
</div>
)
}
export default FlowItem

View File

@@ -1,40 +0,0 @@
import { Handle, Position } from 'reactflow'
import type { FlowOutput } from '../../../../generated/ManagingApi'
import type { IFlowProps } from '../../../../global/type'
import { Card } from '../../../mollecules'
const Flow = ({
name,
description,
children,
inputs,
outputs,
isConnectable,
}: IFlowProps) => {
return (
<Card name={name} info={description}>
{inputs?.map((input: FlowOutput) => {
return (
<Handle
type="target"
position={Position.Left}
isConnectable={isConnectable}
/>
)
})}
{children}
{outputs?.map((output: FlowOutput) => {
return (
<Handle
type="source"
position={Position.Right}
isConnectable={isConnectable}
/>
)
})}
</Card>
)
}
export default Flow

View File

@@ -1,62 +0,0 @@
import { useCallback } from 'react'
import type { NodeProps } from 'reactflow'
import { WorkflowSelector } from '../../../../../app/store/selectors/workflowSelector'
import useWorkflowStore from '../../../../../app/store/workflowStore'
import type { IFlow } from '../../../../../generated/ManagingApi'
import { Timeframe, Ticker } from '../../../../../generated/ManagingApi'
import { FormInput } from '../../../../mollecules'
import Flow from '../Flow'
const FeedTicker = ({ data, isConnectable, id }: NodeProps<IFlow>) => {
const { updateNodeData } = useWorkflowStore(WorkflowSelector)
const onTickerChange = useCallback((evt: any) => {
updateNodeData(id, 'Ticker', evt.target.value)
}, [])
const onTimeframeChange = useCallback((evt: any) => {
updateNodeData(id, 'Timeframe', evt.target.value)
}, [])
return (
<Flow
name={data.name}
description={data.description}
inputs={data.acceptedInputs}
outputs={data.outputTypes}
isConnectable={isConnectable}
>
<FormInput label="Timeframe" htmlFor="period">
<select
className="select no-drag w-full max-w-xs"
onChange={onTimeframeChange}
value={
data.parameters.find((p) => p.name === 'Timeframe')?.value || ''
}
>
{Object.keys(Timeframe).map((item) => (
<option key={item} value={item}>
{item}
</option>
))}
</select>
</FormInput>
<FormInput label="Ticker" htmlFor="ticker">
<select
className="select no-drag w-full max-w-xs"
onChange={onTickerChange}
value={data.parameters.find((p) => p.name === 'Ticker')?.value || ''}
>
{Object.keys(Ticker).map((item) => (
<option key={item} value={item}>
{item}
</option>
))}
</select>
</FormInput>
</Flow>
)
}
export default FeedTicker

View File

@@ -1,58 +0,0 @@
import { useCallback } from 'react'
import type { NodeProps } from 'reactflow'
import { WorkflowSelector } from '../../../../../app/store/selectors/workflowSelector'
import useWorkflowStore from '../../../../../app/store/workflowStore'
import type { IFlow } from '../../../../../generated/ManagingApi'
import { Timeframe } from '../../../../../generated/ManagingApi'
import { FormInput } from '../../../../mollecules'
import Flow from '../Flow'
const RsiDivergenceFlow = ({ data, isConnectable, id }: NodeProps<IFlow>) => {
const { updateNodeData } = useWorkflowStore(WorkflowSelector)
const onPeriodChange = useCallback((evt: any) => {
updateNodeData(id, 'Period', evt.target.value)
}, [])
const onTimeframeChange = useCallback((evt: any) => {
updateNodeData(id, 'Timeframe', evt.target.value)
}, [])
return (
<Flow
name={data.name || ''}
description={data.description}
inputs={data.acceptedInputs}
outputs={data.outputTypes}
isConnectable={isConnectable}
>
<FormInput label="Period" htmlFor="period">
<input
id="period"
name="text"
onChange={onPeriodChange}
className="input nodrag w-full max-w-xs"
value={data.parameters.find((p) => p.name === 'Period')?.value || ''}
/>
</FormInput>
<FormInput label="Timeframe" htmlFor="period">
<select
className="select no-drag w-full max-w-xs"
onChange={onTimeframeChange}
value={
data.parameters.find((p) => p.name === 'Timeframe')?.value || ''
}
>
{Object.keys(Timeframe).map((item) => (
<option key={item} value={item}>
{item}
</option>
))}
</select>
</FormInput>
</Flow>
)
}
export default RsiDivergenceFlow

View File

@@ -1,96 +0,0 @@
import { useQuery } from '@tanstack/react-query'
import React, { useCallback, useState } from 'react'
import type { NodeProps } from 'reactflow'
import useApiUrlStore from '../../../../../app/store/apiStore'
import { WorkflowSelector } from '../../../../../app/store/selectors/workflowSelector'
import useWorkflowStore from '../../../../../app/store/workflowStore'
import type { Account, IFlow } from '../../../../../generated/ManagingApi'
import { MoneyManagementClient } from '../../../../../generated/ManagingApi'
import useAccounts from '../../../../../hooks/useAccounts'
import { Loader } from '../../../../atoms'
import { FormInput } from '../../../../mollecules'
import Flow from '../Flow'
const OpenPositionFlow = ({ data, isConnectable, id }: NodeProps<IFlow>) => {
const { updateNodeData } = useWorkflowStore(WorkflowSelector)
const { apiUrl } = useApiUrlStore()
const [selectedAccount, setSelectedAccount] = React.useState<string>()
const [selectedMoneyManagement, setSelectedMoneyManagement] =
useState<string>()
const moneyManagementClient = new MoneyManagementClient({}, apiUrl)
const { data: accounts } = useAccounts({
callback: (data: Account[]) => {
setSelectedAccount(data[0].name)
},
})
const { data: moneyManagements } = useQuery({
onSuccess: (data) => {
if (data) {
setSelectedMoneyManagement(data[0].name)
}
},
queryFn: () => moneyManagementClient.moneyManagement_GetMoneyManagements(),
queryKey: ['moneyManagement'],
})
const onAccountChange = useCallback((evt: any) => {
updateNodeData(id, 'Account', evt.target.value)
}, [])
const onMoneyManagementChange = useCallback((evt: any) => {
updateNodeData(id, 'MoneyManagement', evt.target.value)
}, [])
if (!accounts || !moneyManagements) {
return <Loader />
}
return (
<Flow
name={data.name || ''}
description={data.description}
inputs={data.acceptedInputs}
outputs={data.outputTypes}
isConnectable={isConnectable}
>
<FormInput label="Account" htmlFor="accountName">
<select
className="select select-bordered w-full h-auto max-w-xs"
onChange={(evt) => onAccountChange(evt.target.value)}
value={
data.parameters.find((p) => p.name === 'Account')?.value ||
selectedAccount
}
>
{accounts.map((item) => (
<option key={item.name} value={item.name}>
{item.name}
</option>
))}
</select>
</FormInput>
<FormInput label="Money Management" htmlFor="moneyManagement">
<select
className="select w-full max-w-xs"
onChange={(evt) => onMoneyManagementChange(evt.target.value)}
value={
data.parameters.find((p) => p.name === 'MoneyManagement')?.value ||
selectedMoneyManagement
}
>
{moneyManagements.map((item) => (
<option key={item.name} value={item.name}>
{item.name}
</option>
))}
</select>
</FormInput>
</Flow>
)
}
export default OpenPositionFlow

View File

@@ -1,24 +0,0 @@
import { useQuery } from "@tanstack/react-query"
const fetchData = () => {
return {
fake: 'data',
}
}
const ParentComponent = () => {
const { data, isLoading } = useQuery({
queryFn: fetchData,
queryKey: ['data'],
})
if (isLoading) {
return <div>Loading...</div>
}
return (
<div>
<ChildComponent data={data} />
</div>
)
}

View File

@@ -1,255 +0,0 @@
import { useQuery } from '@tanstack/react-query'
import { useCallback, useMemo, useRef, useState } from 'react'
import type { NodeTypes, Node } from 'reactflow'
import ReactFlow, { Controls, Background, ReactFlowProvider } from 'reactflow'
import 'reactflow/dist/style.css'
import useApiUrlStore from '../../../app/store/apiStore'
import { WorkflowSelector } from '../../../app/store/selectors/workflowSelector'
import useWorkflowStore from '../../../app/store/workflowStore'
import type {
FlowType,
IFlow,
SyntheticFlow,
SyntheticFlowParameter,
SyntheticWorkflow,
} from '../../../generated/ManagingApi'
import { WorkflowUsage, WorkflowClient } from '../../../generated/ManagingApi'
import type { IWorkflow, IFlowItem } from '../../../global/type'
import { Toast } from '../../mollecules'
import FeedTicker from './flows/feed/feedTicker'
import RsiDivergenceFlow from './flows/strategies/RsiDivergenceFlow'
import OpenPositionFlow from './flows/trading/OpenPositionFlow'
import WorkflowForm from './workflowForm'
import WorkflowSidebar from './workflowSidebar'
let id = 0
const getId = () => `dndnode_${id++}`
const mapToFlowRequest = (
flow: IFlow,
id: string,
parentId: string
): SyntheticFlow => {
return {
id: id,
parameters: flow.parameters as SyntheticFlowParameter[],
parentId: parentId,
type: flow.type,
}
}
const WorkflowCanvas: React.FC = (props: any) => {
const tabs = props
const properties = tabs['data-props'] as IWorkflow
const reactFlowWrapper = useRef(null)
const [reactFlowInstance, setReactFlowInstance] = useState(null)
const [isUpdated, setIsUpdated] = useState(false)
const { apiUrl } = useApiUrlStore()
const [usage, setUsage] = useState<WorkflowUsage>(WorkflowUsage.Trading)
const [name, setName] = useState<string>(properties.name)
const client = new WorkflowClient({}, apiUrl)
const nodeTypes: NodeTypes = useMemo(
() => ({
FeedTicker: FeedTicker,
OpenPosition: OpenPositionFlow,
RsiDivergence: RsiDivergenceFlow,
}),
[]
)
const {
nodes,
edges,
onNodesChange,
onEdgesChange,
onConnect,
initWorkFlow,
setNodes,
resetWorkflow,
} = useWorkflowStore(WorkflowSelector)
const { data, isLoading } = useQuery({
onSuccess: (data) => {
initWorkFlow([], properties.edges)
if (data) {
setNodes(properties.nodes.map((n, i) => mapToNodeItem(n, data, i)))
}
},
queryFn: () => client.workflow_GetAvailableFlows(),
queryKey: ['availableFlows'],
})
const mapToNodeItem = (
flow: Node<IFlow>,
availableFlows: IFlow[],
index: number
): IFlowItem => {
const nodeData = availableFlows.find((f) => f.type === flow.type)
if (nodeData == null) {
return {} as IFlowItem
}
nodeData.parameters = flow.data.parameters
return {
data: nodeData,
id: flow.id,
isConnectable: nodeData.acceptedInputs?.length > 0,
position: { x: index * 400, y: index * 100 },
type: flow.type as FlowType,
}
}
const isValidConnection = (connection: any) => {
if (reactFlowInstance == null) {
return false
}
const sourceData: IFlow = reactFlowInstance.getNode(connection.source).data
const targetData: IFlow = reactFlowInstance.getNode(connection.target).data
return sourceData.outputTypes?.some(
(output) => targetData.acceptedInputs?.indexOf(output) >= 0
)
}
const handleOnConnect = useCallback((params: any) => {
setIsUpdated(true)
onConnect(params)
}, [])
const onDragOver = useCallback((event: any) => {
event.preventDefault()
event.dataTransfer.dropEffect = 'move'
}, [])
const onDrop = useCallback(
(event: any) => {
event.preventDefault()
if (reactFlowInstance == null || reactFlowWrapper.current == null) {
return
}
const reactFlowBounds = reactFlowWrapper.current.getBoundingClientRect()
const data = event.dataTransfer.getData('application/reactflow')
if (typeof data === 'undefined' || !data) {
return
}
const properties: string[] = data.split('-')
const position = reactFlowInstance.project({
x: event.clientX - reactFlowBounds.left,
y: event.clientY - reactFlowBounds.top,
})
const flow = flows.find((flow) => flow.type === properties[0])
const newNode: IFlowItem = {
data: flow || ({ parameters: [] as SyntheticFlowParameter[] } as IFlow),
id: getId(),
isConnectable:
(flow?.acceptedInputs && flow?.acceptedInputs?.length > 0) ?? false,
position,
type: properties[0] as FlowType,
}
newNode.data.parameters = []
setNodes(nodes.concat(newNode))
},
[reactFlowInstance, data, nodes]
)
const handleOnSave = () => {
const t = new Toast('Saving workflow')
const nodesRequest: SyntheticFlow[] = []
const client = new WorkflowClient({}, apiUrl)
if (edges.length <= 0) {
t.update('error', 'Workflow must have at least one edge')
return
}
for (const node of nodes) {
const data: IFlow = reactFlowInstance.getNode(node.id).data
const parentId = edges.find((edge) => edge.target === node.id)?.source
nodesRequest.push(mapToFlowRequest(data, node.id, parentId || ''))
}
const request: SyntheticWorkflow = {
description: 'Test',
flows: nodesRequest,
name: properties.name,
usage: usage,
}
client
.workflow_PostWorkflow(request)
.then((data) => {
t.update('success', 'Workflow saved')
})
.catch((err) => {
t.update('error', 'Error :' + err)
})
}
const handleOnReset = () => {
resetWorkflow()
}
const handleUsageChange = (event: any) => {
setUsage(event.target.value)
}
return (
<>
<WorkflowForm
name={name}
usage={usage}
handleUsageChange={handleUsageChange}
handleOnSave={handleOnSave}
handleOnReset={handleOnReset}
isUpdated={isUpdated}
setName={setName}
/>
<div className="grid grid-cols-10 gap-4">
<ReactFlowProvider>
<div>
<WorkflowSidebar flows={data} isLoading={isLoading} />
</div>
<div
className="reactflow-wrapper col-span-8"
ref={reactFlowWrapper}
style={{ height: 800 }}
>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={handleOnConnect}
onDrop={onDrop}
onInit={setReactFlowInstance}
onDragOver={onDragOver}
nodeTypes={nodeTypes}
isValidConnection={isValidConnection}
fitView
>
<Controls />
<Background />
</ReactFlow>
</div>
</ReactFlowProvider>
</div>
</>
)
}
export default WorkflowCanvas

View File

@@ -1,69 +0,0 @@
import { WorkflowUsage } from '../../../generated/ManagingApi'
import { FormInput } from '../../mollecules'
type IWorkflowForm = {
name: string
usage: WorkflowUsage
handleUsageChange: (evt: any) => void
handleOnSave: () => void
handleOnReset: () => void
isUpdated: boolean
setName: (name: string) => void
}
const WorkflowForm = ({
name,
handleUsageChange,
handleOnSave,
isUpdated,
handleOnReset,
setName,
}: IWorkflowForm) => {
return (
<>
<div className="flex">
<div className="flex w-full">
<FormInput label="Name" htmlFor="name" inline={true}>
<input
id="name"
name="text"
onChange={(evt) => setName(evt.target.value)}
className="nodrag input"
value={name}
/>
</FormInput>
<FormInput label="Usage" htmlFor="usage" inline={true}>
<select
className="select no-drag w-full max-w-xs"
onChange={handleUsageChange}
>
{Object.keys(WorkflowUsage).map((item) => (
<option key={item} value={item}>
{item}
</option>
))}
</select>
</FormInput>
</div>
<div className="flex justify-end w-full">
<button
className="btn btn-primary"
disabled={!isUpdated}
onClick={handleOnSave}
>
Save
</button>
<button
className="btn btn-primary"
disabled={!isUpdated}
onClick={handleOnReset}
>
Reset
</button>
</div>
</div>
</>
)
}
export default WorkflowForm

View File

@@ -1,35 +0,0 @@
import type { IFlow } from '../../../generated/ManagingApi'
import { Loader } from '../../atoms'
import FlowItem from './flowItem'
type IWorkflowSidebar = {
flows?: IFlow[]
isLoading: boolean
}
const WorkflowSidebar = ({ flows, isLoading = true }: IWorkflowSidebar) => {
const onDragStart = (event: any, data: string) => {
event.dataTransfer.setData('application/reactflow', data)
event.dataTransfer.effectAllowed = 'move'
}
if (isLoading) {
return <Loader />
}
return (
<aside>
<div className="bg-base-200 p-4">
<div className="mb-2 text-lg">Flows</div>
{flows
? flows.map((flow) => (
<FlowItem flow={flow} onDragStart={onDragStart} />
))
: null}
</div>
</aside>
)
}
export default WorkflowSidebar

View File

@@ -1,10 +0,0 @@
export { default as TradeChart } from './Trading/TradeChart/TradeChart'
export { default as CardPositionItem } from './Trading/CardPositionItem'
export { default as ActiveBots } from './ActiveBots/ActiveBots'
export { default as BacktestCards } from './Backtest/backtestCards'
export { default as BacktestModal } from './Backtest/backtestModal'
export { default as BacktestTable } from './Backtest/backtestTable'
export { default as SpotLightBadge } from './SpotLightBadge/SpotLightBadge'
export { default as StatusBadge } from './StatusBadge/StatusBadge'
export { default as PositionsList } from './Positions/PositionList'
export { default as WorkflowCanvas } from './Workflow/workflowCanvas'

View File

@@ -1,15 +0,0 @@
<svg width="410" height="404" viewBox="0 0 410 404" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M399.641 59.5246L215.643 388.545C211.844 395.338 202.084 395.378 198.228 388.618L10.5817 59.5563C6.38087 52.1896 12.6802 43.2665 21.0281 44.7586L205.223 77.6824C206.398 77.8924 207.601 77.8904 208.776 77.6763L389.119 44.8058C397.439 43.2894 403.768 52.1434 399.641 59.5246Z" fill="url(#paint0_linear)"/>
<path d="M292.965 1.5744L156.801 28.2552C154.563 28.6937 152.906 30.5903 152.771 32.8664L144.395 174.33C144.198 177.662 147.258 180.248 150.51 179.498L188.42 170.749C191.967 169.931 195.172 173.055 194.443 176.622L183.18 231.775C182.422 235.487 185.907 238.661 189.532 237.56L212.947 230.446C216.577 229.344 220.065 232.527 219.297 236.242L201.398 322.875C200.278 328.294 207.486 331.249 210.492 326.603L212.5 323.5L323.454 102.072C325.312 98.3645 322.108 94.137 318.036 94.9228L279.014 102.454C275.347 103.161 272.227 99.746 273.262 96.1583L298.731 7.86689C299.767 4.27314 296.636 0.855181 292.965 1.5744Z" fill="url(#paint1_linear)"/>
<defs>
<linearGradient id="paint0_linear" x1="6.00017" y1="32.9999" x2="235" y2="344" gradientUnits="userSpaceOnUse">
<stop stop-color="#41D1FF"/>
<stop offset="1" stop-color="#BD34FE"/>
</linearGradient>
<linearGradient id="paint1_linear" x1="194.651" y1="8.81818" x2="236.076" y2="292.989" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFEA83"/>
<stop offset="0.0833333" stop-color="#FFDD35"/>
<stop offset="1" stop-color="#FFA800"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,30 +0,0 @@
/**
* Configuration class needed in base class.
* The config is provided to the API client at initialization time.
* API clients inherit from #AuthorizedApiBase and provide the config.
*/
import { Cookies } from 'react-cookie'
import type IConfig from './IConfig'
export default class AuthorizedApiBase {
private readonly config: IConfig
constructor(config: IConfig) {
this.config = config
}
transformOptions = (options: any): Promise<RequestInit> => {
const cookies = new Cookies()
// Retrieve the bearer token from the cookies
const bearerToken = cookies.get('token')
// eslint-disable-next-line no-console
console.log(bearerToken)
if (bearerToken) {
options.headers.Authorization = `Bearer ${bearerToken}`
}
return Promise.resolve(options)
}
}

View File

@@ -1,7 +0,0 @@
export default interface IConfig {
/**
* Returns a valid value for the Authorization header.
* Used to dynamically inject the current auth header.
*/
getAuthorization?: string
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,158 +0,0 @@
//----------------------
// <auto-generated>
// Generated using the NSwag toolchain v13.16.0.0 (NJsonSchema v10.7.1.0 (Newtonsoft.Json v13.0.0.0)) (http://NSwag.org)
// </auto-generated>
//----------------------
import AuthorizedApiBase from './AuthorizedApiBase'
import type IConfig from './IConfig'
/* tslint:disable */
/* eslint-disable */
// ReSharper disable InconsistentNaming
export class WorkerClient extends AuthorizedApiBase {
private http: { fetch(url: RequestInfo, init?: RequestInit): Promise<Response> };
private baseUrl: string;
protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined;
constructor(configuration: IConfig, baseUrl?: string, http?: { fetch(url: RequestInfo, init?: RequestInit): Promise<Response> }) {
super(configuration);
this.http = http ? http : window as any;
this.baseUrl = baseUrl !== undefined && baseUrl !== null ? baseUrl : "https://localhost:5002";
}
worker_GetWorkers(): Promise<Worker[]> {
let url_ = this.baseUrl + "/Worker";
url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = {
method: "GET",
headers: {
"Accept": "application/json"
}
};
return this.transformOptions(options_).then(transformedOptions_ => {
return this.http.fetch(url_, transformedOptions_);
}).then((_response: Response) => {
return this.processWorker_GetWorkers(_response);
});
}
protected processWorker_GetWorkers(response: Response): Promise<Worker[]> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200) {
return response.text().then((_responseText) => {
let result200: any = null;
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as Worker[];
return result200;
});
} else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<Worker[]>(null as any);
}
worker_ToggleWorker(workerType: WorkerType | undefined): Promise<FileResponse> {
let url_ = this.baseUrl + "/Worker?";
if (workerType === null)
throw new Error("The parameter 'workerType' cannot be null.");
else if (workerType !== undefined)
url_ += "workerType=" + encodeURIComponent("" + workerType) + "&";
url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = {
method: "PATCH",
headers: {
"Accept": "application/octet-stream"
}
};
return this.transformOptions(options_).then(transformedOptions_ => {
return this.http.fetch(url_, transformedOptions_);
}).then((_response: Response) => {
return this.processWorker_ToggleWorker(_response);
});
}
protected processWorker_ToggleWorker(response: Response): Promise<FileResponse> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200 || status === 206) {
const contentDisposition = response.headers ? response.headers.get("content-disposition") : undefined;
const fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined;
const fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined;
return response.blob().then(blob => { return { fileName: fileName, data: blob, status: status, headers: _headers }; });
} else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<FileResponse>(null as any);
}
}
export interface Worker {
workerType?: WorkerType;
startTime?: Date;
lastRunTime?: Date | undefined;
executionCount?: number;
delay?: string;
isActive?: boolean;
}
export enum WorkerType {
PriceOneMinute = "PriceOneMinute",
PriceFiveMinutes = "PriceFiveMinutes",
PriceFifteenMinutes = "PriceFifteenMinutes",
PriceThirtyMinutes = "PriceThirtyMinutes",
PriceOneHour = "PriceOneHour",
PriceFourHour = "PriceFourHour",
PriceOneDay = "PriceOneDay",
TopVolumeTicker = "TopVolumeTicker",
PositionManager = "PositionManager",
Spotlight = "Spotlight",
Fee = "Fee",
}
export interface FileResponse {
data: Blob;
status: number;
fileName?: string;
headers?: { [name: string]: any };
}
export class ApiException extends Error {
override message: string;
status: number;
response: string;
headers: { [key: string]: any; };
result: any;
constructor(message: string, status: number, response: string, headers: { [key: string]: any; }, result: any) {
super();
this.message = message;
this.status = status;
this.response = response;
this.headers = headers;
this.result = result;
}
protected isApiException = true;
static isApiException(obj: any): obj is ApiException {
return obj.isApiException === true;
}
}
function throwException(message: string, status: number, response: string, headers: { [key: string]: any; }, result?: any): any {
if (result !== null && result !== undefined)
throw result;
else
throw new ApiException(message, status, response, headers, null);
}

View File

@@ -1,9 +0,0 @@
export function jsonToBase64(object: any) {
const json = JSON.stringify(object)
return Buffer.from(json).toString('base64')
}
export function base64ToJson<T>(base64String: string): T {
const json = Buffer.from(base64String, 'base64').toString()
return JSON.parse(json) as T
}

View File

@@ -1,289 +0,0 @@
import type {
TableInstance,
UsePaginationInstanceProps,
UsePaginationState,
UseSortByInstanceProps,
} from 'react-table'
import type { Edge, Node, Position } from 'reactflow'
import type { Account, AccountType, Backtest, Balance, BotType, Candle, FlowOutput, FlowType, IFlow, KeyValuePairOfDateTimeAndDecimal, MoneyManagement, RiskLevel, Scenario, Signal, Ticker, Timeframe, TradeDirection, TradingBot, TradingExchanges } from '../generated/ManagingApi'
import { ReactNode, FC } from 'react'
export type TabsType = {
label: string
index: number
Component: React.FC<{}>
}[]
export type TableInstanceWithHooks<T extends object> = TableInstance<T> &
UsePaginationInstanceProps<T> &
UseSortByInstanceProps<T> & {
state: UsePaginationState<T>
}
export type IWidgetProperties = {
id: string
title: string
layout: {
h: number
i: string
w: number
x: number
y: number
minW: number
minH: number
}
}
export type IFlowData = {
name: string
type: string
}
export type IFlowItem = {
data: IFlow
isConnectable: boolean
id: string
position: any
type: FlowType
}
export type IFlowProps = {
name: string
description?: string
children: React.ReactNode
inputs: FlowOutput[]
outputs: FlowOutput[]
isConnectable: boolean
}
export type IWorkflow = {
name: string
nodes: Node<IFlow>[]
edges: Edge[]
}
export type ILoginFormInput = {
name: string
}
export type BacktestModalProps = {
showModal: boolean
closeModal: () => void
setBacktests: React.Dispatch<React.SetStateAction<Backtest[]>>
showLoopSlider?: boolean
}
export type ISpotlightBadge = {
direction: TradeDirection | undefined
date?: Date | undefined
price?: number | undefined
}
export type IBacktestsFormInput = {
accountName: string
tickers: string[]
botType: BotType
timeframe: Timeframe
scenarioName: string
days: number
save: boolean
balance: number
moneyManagement: MoneyManagement
loop: number
}
export type IBacktestCards = {
list: Backtest[] | undefined
isFetching?: boolean
}
export type IFormInput = {
children: React.ReactNode
htmlFor: string
label: string
inline?: boolean
}
export type IModalProps = {
showModal?: boolean | undefined
onSubmit?: React.FormEventHandler
onClose?: React.FormEventHandler
titleHeader?: string
children?: React.ReactNode
toggleModal?: React.MouseEventHandler<HTMLButtonElement>
moneyManagement?: MoneyManagement
}
export type IMoneyManagementModalProps = {
showModal?: boolean | undefined
onSubmit?: React.FormEventHandler
onClose: () => void
toggleModal?: React.MouseEventHandler<HTMLButtonElement>
moneyManagement?: MoneyManagement
disableInputs?: boolean
}
export type IBotRowDetails = {
candles: Candle[]
positions: Position[]
walletBalances?: KeyValuePairOfDateTimeAndDecimal[] | null
}
export type IBacktestFormInput = {
accountName: string
botType: BotType
ticker: Ticker
timeframe: Timeframe
days: number
save: boolean
scenarioName: string
balance: number
moneyManagement: MoneyManagement
}
export type IOpenPositionFormInput = {
accountName: string
direction: TradeDirection
ticker: Ticker
riskLevel: RiskLevel
stopLoss?: number
takeProfit?: number
}
export type IBotList = {
list: TradingBot[]
}
export type IScenarioFormInput = {
name: string
strategies: string[]
}
export type IScenarioList = {
list: Scenario[]
}
export type IMoneyManagementList = {
list: MoneyManagement[]
}
export type IMoneyManagementFormInput = {
timeframe: Timeframe
riskLevel: RiskLevel
balanceAtRisk: number
stopLoss: number
takeProfit: number
leverage: number
name: string
}
export type IAccountFormInput = {
name: string
key: string
secret: string
exchange: TradingExchanges
type: AccountType
}
export type IAccountBalanceProps = {
bots: TradingBot[]
accounts: Account[]
}
export type IPropsComponent = {
disabled?: boolean | undefined
children?: React.ReactNode
value?: string | number
href?: string
as?: 'navlink' | 'link' | 'newtab'
className?: string | ((prop: = { isActive: boolean }) => string)
onClick?: () => void
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void
id: string
min?: string
max?: string
step?: string
prefixValue?: string
suffixValue?: string
}
export type IListProp = {
children: React.ReactNode
className?: string
}
export type IMyLinkProps = {
href: string
children: React.ReactNode
as?: 'navlink' | 'link' | 'newtab'
className?: string | ((prop: = { isActive: boolean }) => string)
onClick?: () => void
}
export type ICardText = {
title: string
content: string
}
export type ICardPosition = {
positions: Position[]
positivePosition: boolean
}
export type ICardSignal = {
signals: Signal[]
}
export type INavItemProps = {
children: ReactNode
href: string
isMobile?: boolean
}
export type IStatusBadge = {
status: string
isForWatchOnly: boolean
}
export type ITabsProps = {
tabs: ITabsType | undefined
selectedTab: number
onClick: (index: number) => void
orientation?: 'horizontal' | 'vertical'
className?: string
addButton?: boolean
onAddButton?: () => void
}
export type ITabsType = {
label: string
index: number
Component: FC<{ index: number }>
props?: any
}[]
export type ICardPositionFlipped = {
positions: Position[]
}
export type IAccountRowDetail = {
balances: Balance[]
showTotal?: boolean
}
export type IGridTile = {
children: any
title: string
}
export type CardProps = {
name: string
children: React.ReactNode
info?: string
showCloseButton?: boolean
}
export type AccountStore = {
setAccounts: (accounts: Account[]) => void
accounts: Account[]
}
export type ILoader = {
size?: 'xs' | 'sm' | 'md' | 'lg'
}

View File

@@ -1,27 +0,0 @@
import { useQuery } from '@tanstack/react-query'
import useApiUrlStore from '../app/store/apiStore'
import { AccountClient, type Account } from '../generated/ManagingApi'
type UseAccountsProps = {
callback?: (data: Account[]) => void | undefined
}
const useAcconuts = ({ callback }: UseAccountsProps) => {
const { apiUrl } = useApiUrlStore()
const accountClient = new AccountClient({}, apiUrl)
const query = useQuery({
onSuccess: (data) => {
if (data && callback) {
callback(data)
}
},
queryFn: () => accountClient.account_GetAccounts(),
queryKey: ['accounts'],
})
return query
}
export default useAcconuts

View File

@@ -1,40 +0,0 @@
interface UseCookieReturn {
getCookie: (name: string) => string
setCookie: (name: string, value: string, daysToExpire: number) => void
deleteCookie: (name: string) => void
}
interface SeparateCookies {
[key: string]: string | undefined
}
const useCookie = (): UseCookieReturn => {
const getCookie = (name: string) => {
const separateCookies: SeparateCookies = document.cookie.split(';').reduce(
(cookieAccumulator, cookie) =>
Object.assign(cookieAccumulator, {
[cookie.split('=')[0].trim()]: cookie.split('=')[1],
}),
{}
)
return separateCookies[name]
}
const setCookie = (name: string, value: string, daysToExpire?: number) => {
const expirationDate = `expires=${new Date(
new Date().getTime() +
(daysToExpire ? daysToExpire : 30) * 24 * 60 * 60 * 1000
)}`
document.cookie = `${name}=${value}; ${expirationDate}; path=/`
}
const deleteCookie = (name: string) => {
document.cookie = `${name}=''; expires=Thu, 01 Jan 2000 00:00:00 GMT; path=''`
}
return { deleteCookie, getCookie, setCookie }
}
export default useCookie

View File

@@ -1,6 +0,0 @@
import { useEffect, useLayoutEffect } from 'react'
const useCustomEffect =
typeof window !== 'undefined' ? useLayoutEffect : useEffect
export default useCustomEffect

View File

@@ -1,21 +0,0 @@
import { useRef, useEffect } from 'react'
/**
* Simulate componentDidUpdate() method of Class Component
* https://reactjs.org/docs/react-component.html#componentdidupdate
*/
const useDidUpdateEffect = (
effect: AnyFunction,
deps: any[] | undefined = undefined
): void => {
const mounted = useRef<boolean>()
useEffect(() => {
if (!mounted.current) {
// fire componentDidMount
mounted.current = true
} else {
effect()
}
}, deps)
}
export default useDidUpdateEffect

View File

@@ -1,39 +0,0 @@
import { useState } from 'react'
// Hook
export function useLocalStorage<T>(key: string, initialValue: T) {
// State to store our value
// Pass initial state function to useState so logic is only executed once
const [storedValue, setStoredValue] = useState<T>(() => {
if (typeof window === 'undefined') {
return initialValue
}
try {
// Get from local storage by key
const item = window.localStorage.getItem(key)
// Parse stored json or if none return initialValue
return item ? JSON.parse(item) : initialValue
} catch (error) {
// If error also return initialValue
return initialValue
}
})
// Return a wrapped version of useState's setter function that ...
// ... persists the new value to localStorage.
const setValue = (value: T | ((val: T) => T)) => {
try {
// Allow value to be a function so we have same API as useState
const valueToStore =
value instanceof Function ? value(storedValue) : value
// Save state
setStoredValue(valueToStore)
// Save to local storage
if (typeof window !== 'undefined') {
window.localStorage.setItem(key, JSON.stringify(valueToStore))
}
} catch (error) {
// A more advanced implementation would handle the error case
}
}
return [storedValue, setValue] as const
}

View File

@@ -1,11 +0,0 @@
import { useState } from 'react'
const useModal = () => {
const [isShowing, setIsShowing] = useState(false)
function toggle() {
setIsShowing(!isShowing)
}
return { isShowing, toggle }
}
export default useModal

View File

@@ -1,38 +0,0 @@
import { useEffect } from 'react'
const MOUSEDOWN = 'mousedown'
const TOUCHSTART = 'touchstart'
type HandledEvents = [typeof MOUSEDOWN, typeof TOUCHSTART]
type HandledEventsType = HandledEvents[number]
type PossibleEvent = {
[Type in HandledEventsType]: HTMLElementEventMap[Type]
}[HandledEventsType]
type Handler = (event: PossibleEvent) => void
export function useOnClickOutside(ref: any, handler: Handler) {
useEffect(
() => {
const listener = (event: any) => {
// Do nothing if clicking ref's element or descendent elements
if (!ref.current || ref.current.contains(event.target)) {
return
}
handler(event)
}
document.addEventListener('mousedown', listener)
document.addEventListener('touchstart', listener)
return () => {
document.removeEventListener('mousedown', listener)
document.removeEventListener('touchstart', listener)
}
},
// Add ref and handler to effect dependencies
// It's worth noting that because passed in handler is a new ...
// ... function on every render that will cause this effect ...
// ... callback/cleanup to run every render. It's not a big deal ...
// ... but to optimize you can wrap handler in useCallback before ...
// ... passing it into this hook.
[ref, handler]
)
}

View File

@@ -1,47 +0,0 @@
import type { HubConnection } from '@microsoft/signalr'
import { HubConnectionBuilder } from '@microsoft/signalr'
import { useState, useEffect } from 'react'
import useApiUrlStore from '../app/store/apiStore'
export const useHub = () => {
const { workerUrl } = useApiUrlStore()
const [connectionRef, setConnection] = useState<HubConnection>()
function createHubConnection() {
const con = new HubConnectionBuilder()
.withUrl(`${workerUrl}/positionhub`)
.withAutomaticReconnect()
.build()
setConnection(con)
}
useEffect(() => {
createHubConnection()
}, [])
useEffect(() => {
if (connectionRef) {
try {
connectionRef.on('PositionsSubscription', (positions) => {
console.log(positions)
})
connectionRef
.start()
.then((result) => {
console.log(result)
})
.catch((err) => {
console.error(`Error: ${err}`)
})
} catch (error) {
console.error(error as Error)
}
}
return () => {
connectionRef
}
}, [connectionRef])
}

View File

@@ -1,149 +0,0 @@
import { useAtom } from 'jotai'
import { useEffect } from 'react'
import * as atoms from '../stores/store'
/**
* a custom hook to switch between theme
* @default "dark"
* @returns
*/
interface ThemeColor {
[key: string]: string
accent: string
error: string
info: string
neutral: string
primary: string
secondary: string
success: string
warning: string
}
interface ThemesInterface {
[selector: string]: ThemeColor
}
const themes: ThemesInterface = {
'[data-theme=black]': {
'--animation-btn': '0',
'--animation-input': '0',
'--btn-focus-scale': '1',
'--btn-text-case': 'lowercase',
'--rounded-badge': '0',
'--rounded-box': '0',
'--rounded-btn': '0',
'--tab-radius': '0',
accent: '#343232',
'base-100': '#000000',
'base-200': '#0D0D0D',
'base-300': '#1A1919',
error: '#ff0000',
info: '#0000ff',
neutral: '#272626',
'neutral-focus': '#343232',
primary: '#343232',
secondary: '#343232',
success: '#008000',
warning: '#ffff00',
},
'[data-theme=coffee]': {
accent: '#10576D',
'base-100': '#20161F',
'base-content': '#756E63',
error: '#FC9581',
info: '#8DCAC1',
neutral: '#120C12',
primary: '#DB924B',
secondary: '#263E3F',
success: '#9DB787',
warning: '#FFD25F',
},
'[data-theme=cyberpunk]': {
'--rounded-badge': '0',
'--rounded-box': '0',
'--rounded-btn': '0',
'--tab-radius': '0',
accent: '#c07eec',
'base-100': '#ffee00',
neutral: '#423f00',
'neutral-content': '#ffee00',
primary: '#ff7598',
secondary: '#75d1f0',
},
'[data-theme=lofi]': {
'--animation-btn': '0',
'--animation-input': '0',
'--btn-focus-scale': '1',
'--rounded-badge': '0.125rem',
'--rounded-box': '0.25rem',
'--rounded-btn': '0.125rem',
'--tab-radius': '0',
accent: '#262626',
'accent-content': '#ffffff',
'base-100': '#ffffff',
'base-200': '#F2F2F2',
'base-300': '#E6E5E5',
'base-content': '#000000',
error: '#DE1C8D',
'error-content': '#ffffff',
info: '#0070F3',
'info-content': '#ffffff',
neutral: '#000000',
'neutral-content': '#ffffff',
primary: '#0D0D0D',
'primary-content': '#ffffff',
secondary: '#1A1919',
'secondary-content': '#ffffff',
success: '#21CC51',
'success-content': '#ffffff',
warning: '#FF6154',
'warning-content': '#ffffff',
},
'[data-theme=retro]': {
'--rounded-badge': '0.4rem',
'--rounded-box': '0.4rem',
'--rounded-btn': '0.4rem',
accent: '#ebdc99',
'accent-content': '#282425',
'base-100': '#e4d8b4',
'base-200': '#d2c59d',
'base-300': '#c6b386',
'base-content': '#282425',
error: '#dc2626',
info: '#2563eb',
neutral: '#7d7259',
'neutral-content': '#e4d8b4',
primary: '#ef9995',
'primary-content': '#282425',
secondary: '#a4cbb4',
'secondary-content': '#282425',
success: '#16a34a',
warning: '#d97706',
},
}
const useTheme = () => {
const [themeName, setTheme] = useAtom(atoms.themeAtom)
useEffect(() => {
if (typeof window !== 'undefined') {
document.documentElement.setAttribute('data-theme', themeName)
}
}, [themeName])
function themeProperty() {
const key = '[data-theme=' + themeName + ']'
return themes[key]
}
return {
setTheme,
themeName,
themeProperty,
}
}
export default useTheme

View File

@@ -1,9 +0,0 @@
import { useCallback, useState } from 'react'
const useToggle = (initialState = false) => {
const [state, setState] = useState<boolean>(initialState)
const toggle = useCallback(() => setState((state) => !state), [])
return [state, toggle]
}
export default useToggle

View File

@@ -1,31 +0,0 @@
import { Outlet } from 'react-router-dom'
import { ToastContainer } from 'react-toastify'
import '../styles/app.css'
import { NavBar } from '../components/mollecules'
const LayoutMain = (): JSX.Element => {
return (
<>
<NavBar></NavBar>
<main className="layout mt-6">
<div className="container mx-auto">
<Outlet />
</div>
</main>
<ToastContainer
position="top-right"
autoClose={10000}
hideProgressBar={false}
newestOnTop={false}
closeOnClick
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover
/>
</>
)
}
export default LayoutMain

View File

@@ -1,7 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
<g fill="#61DAFB">
<path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
<circle cx="420.9" cy="296.5" r="45.7"/>
<path d="M520.5 78.1z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

Some files were not shown because too many files have changed in this diff Show More