update
This commit is contained in:
6
src/Managing.WebApp/.env
Normal file
6
src/Managing.WebApp/.env
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
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
|
||||||
2
src/Managing.WebApp/.eslintignore
Normal file
2
src/Managing.WebApp/.eslintignore
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
98
src/Managing.WebApp/.eslintrc
Normal file
98
src/Managing.WebApp/.eslintrc
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
{
|
||||||
|
"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"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
3
src/Managing.WebApp/.gitattributes
vendored
Normal file
3
src/Managing.WebApp/.gitattributes
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.jest/* linguist-vendored
|
||||||
|
mocks/* linguist-vendored
|
||||||
|
mockServiceWorker.js linguist-vendored
|
||||||
18
src/Managing.WebApp/.github/workflows/build.yml
vendored
Normal file
18
src/Managing.WebApp/.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
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
|
||||||
18
src/Managing.WebApp/.github/workflows/lint.yml
vendored
Normal file
18
src/Managing.WebApp/.github/workflows/lint.yml
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
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
|
||||||
18
src/Managing.WebApp/.github/workflows/test.yml
vendored
Normal file
18
src/Managing.WebApp/.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
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
|
||||||
18
src/Managing.WebApp/.github/workflows/typecheck.yml
vendored
Normal file
18
src/Managing.WebApp/.github/workflows/typecheck.yml
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
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
|
||||||
5
src/Managing.WebApp/.gitignore
vendored
Normal file
5
src/Managing.WebApp/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
5
src/Managing.WebApp/.prettierignore
Normal file
5
src/Managing.WebApp/.prettierignore
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
.git
|
||||||
|
node_modules
|
||||||
|
.eslintignore
|
||||||
|
.gitignore
|
||||||
|
LICENSE
|
||||||
4
src/Managing.WebApp/.prettierrc
Normal file
4
src/Managing.WebApp/.prettierrc
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"semi": false
|
||||||
|
}
|
||||||
39
src/Managing.WebApp/Dockerfile
Normal file
39
src/Managing.WebApp/Dockerfile
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# 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"]
|
||||||
43
src/Managing.WebApp/Dockerfile-web-ui-dev
Normal file
43
src/Managing.WebApp/Dockerfile-web-ui-dev
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
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;"]
|
||||||
21
src/Managing.WebApp/LICENSE
Normal file
21
src/Managing.WebApp/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
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.
|
||||||
94
src/Managing.WebApp/README.md
Normal file
94
src/Managing.WebApp/README.md
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
# vite-react-ts-extended [](https://github.com/laststance/vite-react-ts-extended/actions/workflows/typecheck.yml) [](https://github.com/laststance/vite-react-ts-extended/actions/workflows/test.yml) [](https://github.com/laststance/vite-react-ts-extended/actions/workflows/build.yml) [](https://github.com/laststance/vite-react-ts-extended/actions/workflows/lint.yml) [](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!
|
||||||
16
src/Managing.WebApp/index.html
Normal file
16
src/Managing.WebApp/index.html
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<!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>
|
||||||
32
src/Managing.WebApp/install_problematic_packages.sh
Normal file
32
src/Managing.WebApp/install_problematic_packages.sh
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
#!/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
|
||||||
35
src/Managing.WebApp/jest.config.js
Normal file
35
src/Managing.WebApp/jest.config.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
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
|
||||||
338
src/Managing.WebApp/mockServiceWorker.js
vendored
Normal file
338
src/Managing.WebApp/mockServiceWorker.js
vendored
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
/* 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
104
src/Managing.WebApp/package.json
Normal file
104
src/Managing.WebApp/package.json
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
{
|
||||||
|
"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": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
6
src/Managing.WebApp/postcss.config.js
Normal file
6
src/Managing.WebApp/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
autoprefixer: {},
|
||||||
|
tailwindcss: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
4
src/Managing.WebApp/prettier.config.js
Normal file
4
src/Managing.WebApp/prettier.config.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: [require('prettier-plugin-tailwindcss')],
|
||||||
|
tailwindConfig: './tailwind.config.js',
|
||||||
|
}
|
||||||
11
src/Managing.WebApp/scripts/validate
Normal file
11
src/Managing.WebApp/scripts/validate
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
#!/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"
|
||||||
13
src/Managing.WebApp/src/app/index.tsx
Normal file
13
src/Managing.WebApp/src/app/index.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { Auth } from '../pages/authPage/auth'
|
||||||
|
|
||||||
|
import MyRoutes from './routes'
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
|
return (
|
||||||
|
<Auth>
|
||||||
|
<MyRoutes />
|
||||||
|
</Auth>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
||||||
18
src/Managing.WebApp/src/app/providers/Hubs.tsx
Normal file
18
src/Managing.WebApp/src/app/providers/Hubs.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
118
src/Managing.WebApp/src/app/routes/index.tsx
Normal file
118
src/Managing.WebApp/src/app/routes/index.tsx
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
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
|
||||||
22
src/Managing.WebApp/src/app/store/accountStore.tsx
Normal file
22
src/Managing.WebApp/src/app/store/accountStore.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
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,
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
}))
|
||||||
28
src/Managing.WebApp/src/app/store/apiStore.tsx
Normal file
28
src/Managing.WebApp/src/app/store/apiStore.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
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
|
||||||
28
src/Managing.WebApp/src/app/store/flowStore.tsx
Normal file
28
src/Managing.WebApp/src/app/store/flowStore.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
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,
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
}))
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
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,
|
||||||
|
})
|
||||||
113
src/Managing.WebApp/src/app/store/workflowStore.tsx
Normal file
113
src/Managing.WebApp/src/app/store/workflowStore.tsx
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
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
|
||||||
BIN
src/Managing.WebApp/src/assets/img/logo.png
Normal file
BIN
src/Managing.WebApp/src/assets/img/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
BIN
src/Managing.WebApp/src/assets/img/tradingview.png
Normal file
BIN
src/Managing.WebApp/src/assets/img/tradingview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 93 KiB |
7
src/Managing.WebApp/src/components/atoms/List/List.tsx
Normal file
7
src/Managing.WebApp/src/components/atoms/List/List.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import type { IListProp } from '../../../global/type'
|
||||||
|
|
||||||
|
const List = (prop: IListProp): JSX.Element => (
|
||||||
|
<li {...prop}>{prop.children}</li>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default List
|
||||||
16
src/Managing.WebApp/src/components/atoms/Loader/Loader.tsx
Normal file
16
src/Managing.WebApp/src/components/atoms/Loader/Loader.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
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
|
||||||
42
src/Managing.WebApp/src/components/atoms/MyLink/MyLink.tsx
Normal file
42
src/Managing.WebApp/src/components/atoms/MyLink/MyLink.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import type { IListProp } from '../../../global/type'
|
||||||
|
|
||||||
|
const Select = (prop: IListProp): JSX.Element => (
|
||||||
|
<option {...prop}>{prop.children}</option>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default Select
|
||||||
30
src/Managing.WebApp/src/components/atoms/Slider/Slider.tsx
Normal file
30
src/Managing.WebApp/src/components/atoms/Slider/Slider.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
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
|
||||||
5
src/Managing.WebApp/src/components/atoms/index.tsx
Normal file
5
src/Managing.WebApp/src/components/atoms/index.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
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'
|
||||||
45
src/Managing.WebApp/src/components/mollecules/Card/Card.tsx
Normal file
45
src/Managing.WebApp/src/components/mollecules/Card/Card.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
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>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
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
|
||||||
101
src/Managing.WebApp/src/components/mollecules/LogIn/LogIn.tsx
Normal file
101
src/Managing.WebApp/src/components/mollecules/LogIn/LogIn.tsx
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
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>
|
||||||
|
// )
|
||||||
|
// }
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
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
|
||||||
130
src/Managing.WebApp/src/components/mollecules/NavBar/NavBar.tsx
Normal file
130
src/Managing.WebApp/src/components/mollecules/NavBar/NavBar.tsx
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
231
src/Managing.WebApp/src/components/mollecules/Table/Table.tsx
Normal file
231
src/Managing.WebApp/src/components/mollecules/Table/Table.tsx
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
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}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
77
src/Managing.WebApp/src/components/mollecules/Tabs/Tabs.tsx
Normal file
77
src/Managing.WebApp/src/components/mollecules/Tabs/Tabs.tsx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
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
|
||||||
14
src/Managing.WebApp/src/components/mollecules/index.tsx
Normal file
14
src/Managing.WebApp/src/components/mollecules/index.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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'
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
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>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,309 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,361 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,275 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
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>
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
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>
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
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
|
||||||
133
src/Managing.WebApp/src/components/organism/Trading/Summary.tsx
Normal file
133
src/Managing.WebApp/src/components/organism/Trading/Summary.tsx
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,302 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
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
|
||||||
10
src/Managing.WebApp/src/components/organism/index.tsx
Normal file
10
src/Managing.WebApp/src/components/organism/index.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
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'
|
||||||
15
src/Managing.WebApp/src/favicon.svg
Normal file
15
src/Managing.WebApp/src/favicon.svg
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
30
src/Managing.WebApp/src/generated/AuthorizedApiBase.ts
Normal file
30
src/Managing.WebApp/src/generated/AuthorizedApiBase.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/Managing.WebApp/src/generated/IConfig.ts
Normal file
7
src/Managing.WebApp/src/generated/IConfig.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export default interface IConfig {
|
||||||
|
/**
|
||||||
|
* Returns a valid value for the Authorization header.
|
||||||
|
* Used to dynamically inject the current auth header.
|
||||||
|
*/
|
||||||
|
getAuthorization?: string
|
||||||
|
}
|
||||||
2310
src/Managing.WebApp/src/generated/ManagingApi.ts
Normal file
2310
src/Managing.WebApp/src/generated/ManagingApi.ts
Normal file
File diff suppressed because it is too large
Load Diff
158
src/Managing.WebApp/src/generated/ManagingWorkerApi.tsx
Normal file
158
src/Managing.WebApp/src/generated/ManagingWorkerApi.tsx
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
//----------------------
|
||||||
|
// <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);
|
||||||
|
}
|
||||||
0
src/Managing.WebApp/src/global/enum.tsx
Normal file
0
src/Managing.WebApp/src/global/enum.tsx
Normal file
9
src/Managing.WebApp/src/global/helpers.tsx
Normal file
9
src/Managing.WebApp/src/global/helpers.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
289
src/Managing.WebApp/src/global/type.tsx
Normal file
289
src/Managing.WebApp/src/global/type.tsx
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
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'
|
||||||
|
}
|
||||||
27
src/Managing.WebApp/src/hooks/useAccounts.tsx
Normal file
27
src/Managing.WebApp/src/hooks/useAccounts.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
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
|
||||||
40
src/Managing.WebApp/src/hooks/useCookie.ts
Normal file
40
src/Managing.WebApp/src/hooks/useCookie.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
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
|
||||||
6
src/Managing.WebApp/src/hooks/useCustomEffect.tsx
Normal file
6
src/Managing.WebApp/src/hooks/useCustomEffect.tsx
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { useEffect, useLayoutEffect } from 'react'
|
||||||
|
|
||||||
|
const useCustomEffect =
|
||||||
|
typeof window !== 'undefined' ? useLayoutEffect : useEffect
|
||||||
|
|
||||||
|
export default useCustomEffect
|
||||||
21
src/Managing.WebApp/src/hooks/useDidUpdateEffect.ts
Normal file
21
src/Managing.WebApp/src/hooks/useDidUpdateEffect.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
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
|
||||||
39
src/Managing.WebApp/src/hooks/useLocalStorage.tsx
Normal file
39
src/Managing.WebApp/src/hooks/useLocalStorage.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
11
src/Managing.WebApp/src/hooks/useModal.tsx
Normal file
11
src/Managing.WebApp/src/hooks/useModal.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
const useModal = () => {
|
||||||
|
const [isShowing, setIsShowing] = useState(false)
|
||||||
|
function toggle() {
|
||||||
|
setIsShowing(!isShowing)
|
||||||
|
}
|
||||||
|
return { isShowing, toggle }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useModal
|
||||||
38
src/Managing.WebApp/src/hooks/useOutsideClick.tsx
Normal file
38
src/Managing.WebApp/src/hooks/useOutsideClick.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
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]
|
||||||
|
)
|
||||||
|
}
|
||||||
47
src/Managing.WebApp/src/hooks/usePositionHub.tsx
Normal file
47
src/Managing.WebApp/src/hooks/usePositionHub.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
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])
|
||||||
|
}
|
||||||
149
src/Managing.WebApp/src/hooks/useTheme.tsx
Normal file
149
src/Managing.WebApp/src/hooks/useTheme.tsx
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
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
|
||||||
9
src/Managing.WebApp/src/hooks/useToggle.tsx
Normal file
9
src/Managing.WebApp/src/hooks/useToggle.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
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
|
||||||
31
src/Managing.WebApp/src/layouts/index.tsx
Normal file
31
src/Managing.WebApp/src/layouts/index.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
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
|
||||||
7
src/Managing.WebApp/src/logo.svg
Normal file
7
src/Managing.WebApp/src/logo.svg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 2.6 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user