初始化仓库,b87213827e08344908cdd718039f3120d047b44e
7
.dockerignore
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
/contracts
|
||||||
|
.env.*
|
||||||
|
/linode
|
||||||
|
/frontend
|
||||||
|
/desktop
|
||||||
|
/data
|
||||||
|
/output
|
13
.env.local.example
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
SLACK_TOKEN=YOUR_TOKEN
|
||||||
|
SLACK_CHANNEL=CHANNEL_NAME
|
||||||
|
|
||||||
|
# DB_DRIVER="sqlite3"
|
||||||
|
# DB_DSN="bbgo.sqlite3"
|
||||||
|
DB_DRIVER=mysql
|
||||||
|
DB_DSN=root@tcp(127.0.0.1:3306)/bbgo
|
||||||
|
|
||||||
|
MAX_API_KEY=YOUR_API_KEY
|
||||||
|
MAX_API_SECRET=YOUR_API_SECRET
|
||||||
|
|
||||||
|
BINANCE_API_KEY=YOUR_API_KEY
|
||||||
|
BINANCE_API_SECRET=YOUR_API_SECRET
|
3
.evans.toml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[default]
|
||||||
|
protoFile = ["pkg/pb/bbgo.proto"]
|
||||||
|
package = "bbgo"
|
0
.gitattributes
vendored
Normal file
68
.gitignore
vendored
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
# Created by .ignore support plugin (hsz.mobi)
|
||||||
|
### Go template
|
||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
*.out
|
||||||
|
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Dependency directories (remove the comment below to include it)
|
||||||
|
# vendor/
|
||||||
|
/.mod
|
||||||
|
/_mod
|
||||||
|
|
||||||
|
/.env.local
|
||||||
|
/.env.*.local
|
||||||
|
/.env.production
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
/build
|
||||||
|
/dist
|
||||||
|
/bbgow*
|
||||||
|
|
||||||
|
/config/bbgo.yaml
|
||||||
|
|
||||||
|
/localconfig
|
||||||
|
|
||||||
|
/pkg/server/assets.go
|
||||||
|
|
||||||
|
*.sqlite3
|
||||||
|
node_modules
|
||||||
|
output
|
||||||
|
|
||||||
|
otp*png
|
||||||
|
|
||||||
|
/.deploy
|
||||||
|
|
||||||
|
testoutput
|
||||||
|
|
||||||
|
*.swp
|
||||||
|
/pkg/backtest/assets.go
|
||||||
|
|
||||||
|
coverage.txt
|
||||||
|
coverage_dum.txt
|
||||||
|
|
||||||
|
*.cpuprofile
|
||||||
|
|
||||||
|
.systemd.*
|
||||||
|
|
||||||
|
/coverage.txt
|
||||||
|
|
||||||
|
/otp.png
|
||||||
|
|
||||||
|
/profile*.png
|
||||||
|
|
||||||
|
*_local.yaml
|
||||||
|
/.chglog/
|
||||||
|
|
||||||
|
/.credentials
|
9
.golangci.yml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
run:
|
||||||
|
issues-exit-code: 0
|
||||||
|
tests: true
|
||||||
|
timeout: 5m
|
||||||
|
linters:
|
||||||
|
disable-all: true
|
||||||
|
enable:
|
||||||
|
- gofmt
|
||||||
|
- gosimple
|
5
.markdownlint.yaml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
default: true
|
||||||
|
extends: null
|
||||||
|
MD033: false
|
||||||
|
MD010: false
|
||||||
|
MD013: false
|
14
.pre-commit-config.yaml
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
repos:
|
||||||
|
# Secret Detection
|
||||||
|
- repo: https://github.com/Yelp/detect-secrets
|
||||||
|
rev: v1.2.0
|
||||||
|
hooks:
|
||||||
|
- id: detect-secrets
|
||||||
|
args: ['--exclude-secrets', '3899a918953e01bfe218116cdfeccbed579e26275c4a89abcbc70d2cb9e9bbb8']
|
||||||
|
exclude: pacakge.lock.json
|
||||||
|
# Markdown
|
||||||
|
- repo: https://github.com/igorshubovych/markdownlint-cli
|
||||||
|
rev: v0.31.1
|
||||||
|
hooks:
|
||||||
|
- id: markdownlint
|
29
CLA.md
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
# BBGO Individual Contributor License Agreement
|
||||||
|
|
||||||
|
### *Adapted from http://www.apache.org/licenses/ © Apache Software Foundation.*
|
||||||
|
|
||||||
|
In order to clarify the intellectual property license granted with Contributions from any person or entity, BBGO must have a Contributor License Agreement ("CLA") on file that has been signed by each Contributor, indicating agreement to the license terms below. This CLA is for your protection as a Contributor as well as the protection of BBGO and its users; it does not change your rights to use your own Contributions for any other purpose.
|
||||||
|
|
||||||
|
Please read this document carefully before signing and keep a copy for your records.
|
||||||
|
|
||||||
|
You accept and agree to the following terms and conditions for your present and future Contributions Submitted to BBGO. In return, BBGO shall not use your Contributions in a way that is contrary to the public benefit or inconsistent with its nonprofit status and bylaws in effect at the time of the Contribution. Except for the license granted herein to BBGO and recipients of software distributed by BBGO, You reserve all right, title, and interest in and to your Contributions.
|
||||||
|
|
||||||
|
1. Definitions. "You" (or "Contributor") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this CLA with BBGO. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor.
|
||||||
|
|
||||||
|
For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"Contribution" shall mean any original work of authorship, including any modifications or additions to an existing work, that is intentionally Submitted by You to BBGO for inclusion in, or documentation of, any of the products owned or managed by BBGO (the "Work"). For the purposes of this definition, "Submitted" means any form of electronic, verbal, or written communication sent to BBGO or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, BBGO for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution."
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of this CLA, You hereby grant to BBGO and to recipients of software distributed by BBGO a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute your Contributions and such derivative works.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of this CLA, You hereby grant to BBGO and to recipients of software distributed by BBGO a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by your Contribution(s) alone or by combination of your Contribution(s) with the Work to which such Contribution(s) was Submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this CLA for that Contribution or Work shall terminate as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. You represent that you are legally entitled to grant the above license. If your employer(s) has rights to intellectual property that you create that includes your Contributions, you represent that you have received permission to make Contributions on behalf of that employer, that your employer has waived such rights for your Contributions to BBGO, or that your employer has executed a separate Corporate CLA with BBGO.
|
||||||
|
|
||||||
|
5. You represent that each of your Contributions is your original creation (see section 7 for submissions on behalf of others). You represent that your Contribution submissions include complete details of any third-party license or other restriction (including, but not limited to, related patents and trademarks) of which you are personally aware and which are associated with any part of your Contributions.
|
||||||
|
|
||||||
|
6. You are not expected to provide support for your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
|
||||||
|
|
||||||
|
7. You commit not to copy code from another project which license does not allow the duplication / reuse / modification of their source code and / or license is not compatible with the project you are contributing to. As a reminder, a project without an explicit license must be considered as a project with a copyrighted license.
|
||||||
|
|
||||||
|
8. You agree to notify BBGO of any facts or circumstances of which you become aware that would make these representations inaccurate in any respect.
|
2
CODEOWNERS
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
pkg/strategy/grid2 @kbearXD @gx578007
|
||||||
|
python @narumiruna
|
127
CODE_OF_CONDUCT.md
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We as members, contributors, and leaders pledge to participate in our
|
||||||
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
|
nationality, personal appearance, race, religion, or sexual identity
|
||||||
|
and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to a positive environment for our
|
||||||
|
community includes:
|
||||||
|
|
||||||
|
* Demonstrating empathy and kindness toward other people
|
||||||
|
* Being respectful of differing opinions, viewpoints, and experiences
|
||||||
|
* Giving and gracefully accepting constructive feedback
|
||||||
|
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
|
and learning from the experience
|
||||||
|
* Focusing on what is best not just for us as individuals, but for the
|
||||||
|
overall community
|
||||||
|
|
||||||
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery, and sexual attention or
|
||||||
|
advances of any kind
|
||||||
|
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or email
|
||||||
|
address, without their explicit permission
|
||||||
|
* Other conduct that could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
|
Community leaders are responsible for clarifying and enforcing our standards of
|
||||||
|
acceptable behavior and will take appropriate and fair corrective action in
|
||||||
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
|
or harmful.
|
||||||
|
|
||||||
|
Community leaders have the right and responsibility to remove, edit, or reject
|
||||||
|
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||||
|
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||||
|
decisions when appropriate.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all community spaces and also applies when
|
||||||
|
an individual is officially representing the community in public spaces.
|
||||||
|
Examples of representing our community include using an official e-mail address,
|
||||||
|
posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported to the community leaders responsible for enforcement at
|
||||||
|
yoanlin93@gmail.com.
|
||||||
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
|
reporter of any incident.
|
||||||
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Community leaders will follow these Community Impact Guidelines in determining
|
||||||
|
the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||||
|
unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from community leaders, providing
|
||||||
|
clarity around the nature of the violation and an explanation of why the
|
||||||
|
behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: This violation occurs through a single incident or a series of actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued behavior. No
|
||||||
|
interaction with the people involved, including unsolicited interaction with
|
||||||
|
those enforcing the Code of Conduct, for a specified period. This
|
||||||
|
includes avoiding interactions in community spaces as well as external channels
|
||||||
|
like social media. Violating these terms may lead to a temporary or
|
||||||
|
permanent ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards, including
|
||||||
|
sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or public
|
||||||
|
communication with the community for a specified period. No public or
|
||||||
|
private interaction with the people involved, including unsolicited interaction
|
||||||
|
with those enforcing the Code of Conduct, is allowed during this period.
|
||||||
|
Violating these terms may lead to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction within
|
||||||
|
the community.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
|
version 2.0, available at
|
||||||
|
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||||
|
|
||||||
|
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||||
|
enforcement ladder](https://github.com/mozilla/diversity).
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
|
https://www.contributor-covenant.org/faq. Translations are available at
|
||||||
|
https://www.contributor-covenant.org/translations.
|
80
CONTRIBUTING.md
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
# Contributing
|
||||||
|
|
||||||
|
Thank you for investing your time in contributing to our project! :sparkles:.
|
||||||
|
|
||||||
|
Read our [Code of Conduct](./CODE_OF_CONDUCT.md) to keep our community approachable and respectable.
|
||||||
|
|
||||||
|
In this guide you will get an overview of the contribution workflow from opening an issue, creating a PR, reviewing, and merging the PR.
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
### Issues
|
||||||
|
|
||||||
|
#### Create a new issue
|
||||||
|
|
||||||
|
If you spot a problem, search if an issue already exists. If a related issue doesn't exist, you can open a new issue using a relevant issue form.
|
||||||
|
|
||||||
|
#### Solve an issue
|
||||||
|
|
||||||
|
Scan through our [existing issues](https://github.com/c9s/bbgo/issues) to find one that interests you.
|
||||||
|
You can narrow down the search using `labels` as filters. As a general rule, we don’t assign issues to anyone.
|
||||||
|
If you find an issue to work on, you are welcome to open a PR with a fix.
|
||||||
|
|
||||||
|
### Making Changes
|
||||||
|
|
||||||
|
Install pre-commit to check your changes before you commit:
|
||||||
|
|
||||||
|
pip install pre-commit
|
||||||
|
pre-commit install
|
||||||
|
pre-commit run markdownlint --files=README.md --verbose
|
||||||
|
pre-commit run detect-secrets --all-files --verbose
|
||||||
|
|
||||||
|
See <https://pre-commit.com/> for more details.
|
||||||
|
|
||||||
|
For new large features, such as integrating Binance futures contracts, please propose a discussion first before you start working on it.
|
||||||
|
|
||||||
|
For new small features, you could open a pull request directly.
|
||||||
|
|
||||||
|
For each contributor, you have chance to receive the BBG token through the polygon network.
|
||||||
|
|
||||||
|
Each issue has its BBG label, by completing the issue with a pull request, you can get corresponding amount of BBG.
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
### By contributing pull requests
|
||||||
|
|
||||||
|
Any pull request is welcome, documentation, format fixing, testing, and features.
|
||||||
|
|
||||||
|
### By registering account with referral ID
|
||||||
|
|
||||||
|
You may register your exchange account with my referral ID to support this project.
|
||||||
|
|
||||||
|
- For MAX Exchange: <https://max.maicoin.com/signup?r=c7982718> (default commission rate to your account)
|
||||||
|
- For Binance Exchange: <https://www.binancezh.com/en/register?ref=VGDGLT80> (5% commission back to your account)
|
||||||
|
|
||||||
|
### By small amount of cryptos
|
||||||
|
|
||||||
|
- BTC address `3J6XQJNWT56amqz9Hz2BEVQ7W4aNmb5kiU`
|
||||||
|
- USDT ERC20 address `0xeBcf7887A5b767DEb2e0C77E46A22c6Adc64E427`
|
||||||
|
- USDT POLYGON address `0xeBcf7887A5b767DEb2e0C77E46A22c6Adc64E427`
|
||||||
|
|
||||||
|
### Buying BBG token
|
||||||
|
|
||||||
|
BBGO issued a token BBG for the ecosystem (contract
|
||||||
|
address: <https://etherscan.io/address/0x3afe98235d680e8d7a52e1458a59d60f45f935c0> on ethereum).
|
||||||
|
|
||||||
|
If you have feature request, you can offer your BBG for contributors.
|
||||||
|
|
||||||
|
BBG/ETH liquidity pool on Uniswap: <https://app.uniswap.org/#/pool/28377>
|
||||||
|
|
||||||
|
BBG/MATIC pool on quickswap: https://quickswap.exchange/#/swap?outputCurrency=0x3Afe98235d680e8d7A52e1458a59D60f45F935C0
|
||||||
|
|
||||||
|
For further request, please contact us: <https://t.me/c123456789s>
|
||||||
|
|
||||||
|
## Community
|
||||||
|
|
||||||
|
You can join our telegram channels:
|
||||||
|
|
||||||
|
- BBGO International <https://t.me/bbgo_intl>
|
||||||
|
- BBGO Taiwan <https://t.me/bbgocrypto>
|
||||||
|
|
33
Dockerfile
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
# First stage container
|
||||||
|
FROM golang:1.21-alpine3.18 AS builder
|
||||||
|
RUN apk add --no-cache git ca-certificates gcc musl-dev libc-dev pkgconfig
|
||||||
|
# gcc is for github.com/mattn/go-sqlite3
|
||||||
|
# ADD . $GOPATH/src/github.com/c9s/bbgo
|
||||||
|
|
||||||
|
|
||||||
|
WORKDIR $GOPATH/src/github.com/c9s/bbgo
|
||||||
|
ARG GO_MOD_CACHE
|
||||||
|
ENV WORKDIR=$GOPATH/src/github.com/c9s/bbgo
|
||||||
|
ENV GOPATH_ORIG=$GOPATH
|
||||||
|
ENV GOPATH=${GO_MOD_CACHE:+$WORKDIR/$GO_MOD_CACHE}
|
||||||
|
ENV GOPATH=${GOPATH:-$GOPATH_ORIG}
|
||||||
|
ENV CGO_ENABLED=1
|
||||||
|
RUN cd $WORKDIR
|
||||||
|
ADD . .
|
||||||
|
RUN go get github.com/mattn/go-sqlite3
|
||||||
|
RUN go build -o $GOPATH_ORIG/bin/bbgo ./cmd/bbgo
|
||||||
|
|
||||||
|
# Second stage container
|
||||||
|
FROM alpine:3.18
|
||||||
|
|
||||||
|
# Create the default user 'bbgo' and assign to env 'USER'
|
||||||
|
ENV USER=bbgo
|
||||||
|
RUN adduser -D -G wheel "$USER"
|
||||||
|
USER ${USER}
|
||||||
|
|
||||||
|
COPY --from=builder /go/bin/bbgo /usr/local/bin
|
||||||
|
|
||||||
|
WORKDIR /home/${USER}
|
||||||
|
ENTRYPOINT ["/usr/local/bin/bbgo"]
|
||||||
|
CMD ["run", "--config", "/config/bbgo.yaml", "--no-compile"]
|
||||||
|
# vim:filetype=dockerfile:
|
661
LICENSE
Normal file
|
@ -0,0 +1,661 @@
|
||||||
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU Affero General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works, specifically designed to ensure
|
||||||
|
cooperation with the community in the case of network server software.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
our General Public Licenses are intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains free
|
||||||
|
software for all its users.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
Developers that use our General Public Licenses protect your rights
|
||||||
|
with two steps: (1) assert copyright on the software, and (2) offer
|
||||||
|
you this License which gives you legal permission to copy, distribute
|
||||||
|
and/or modify the software.
|
||||||
|
|
||||||
|
A secondary benefit of defending all users' freedom is that
|
||||||
|
improvements made in alternate versions of the program, if they
|
||||||
|
receive widespread use, become available for other developers to
|
||||||
|
incorporate. Many developers of free software are heartened and
|
||||||
|
encouraged by the resulting cooperation. However, in the case of
|
||||||
|
software used on network servers, this result may fail to come about.
|
||||||
|
The GNU General Public License permits making a modified version and
|
||||||
|
letting the public access it on a server without ever releasing its
|
||||||
|
source code to the public.
|
||||||
|
|
||||||
|
The GNU Affero General Public License is designed specifically to
|
||||||
|
ensure that, in such cases, the modified source code becomes available
|
||||||
|
to the community. It requires the operator of a network server to
|
||||||
|
provide the source code of the modified version running there to the
|
||||||
|
users of that server. Therefore, public use of a modified version, on
|
||||||
|
a publicly accessible server, gives the public access to the source
|
||||||
|
code of the modified version.
|
||||||
|
|
||||||
|
An older license, called the Affero General Public License and
|
||||||
|
published by Affero, was designed to accomplish similar goals. This is
|
||||||
|
a different license, not a version of the Affero GPL, but Affero has
|
||||||
|
released a new version of the Affero GPL which permits relicensing under
|
||||||
|
this license.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, if you modify the
|
||||||
|
Program, your modified version must prominently offer all users
|
||||||
|
interacting with it remotely through a computer network (if your version
|
||||||
|
supports such interaction) an opportunity to receive the Corresponding
|
||||||
|
Source of your version by providing access to the Corresponding Source
|
||||||
|
from a network server at no charge, through some standard or customary
|
||||||
|
means of facilitating copying of software. This Corresponding Source
|
||||||
|
shall include the Corresponding Source for any work covered by version 3
|
||||||
|
of the GNU General Public License that is incorporated pursuant to the
|
||||||
|
following paragraph.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the work with which it is combined will remain governed by version
|
||||||
|
3 of the GNU General Public License.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU Affero General Public License from time to time. Such new versions
|
||||||
|
will be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Program specifies that a certain numbered version of the GNU Affero General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU Affero General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU Affero General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If your software can interact with users remotely through a computer
|
||||||
|
network, you should also make sure that it provides a way for users to
|
||||||
|
get its source. For example, if your program is a web application, its
|
||||||
|
interface could display a "Source" link that leads users to an archive
|
||||||
|
of the code. There are many ways you could offer source, and different
|
||||||
|
solutions will be better for different programs; see section 13 for the
|
||||||
|
specific requirements.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||||
|
<https://www.gnu.org/licenses/>.
|
277
Makefile
Normal file
|
@ -0,0 +1,277 @@
|
||||||
|
TARGET_ARCH ?= amd64
|
||||||
|
BUILD_DIR ?= build
|
||||||
|
BIN_DIR := $(BUILD_DIR)/bbgo
|
||||||
|
DIST_DIR ?= dist
|
||||||
|
GIT_DESC := $(shell git describe --tags)
|
||||||
|
|
||||||
|
VERSION ?= $(shell git describe --tags)
|
||||||
|
|
||||||
|
OSX_APP_NAME = BBGO.app
|
||||||
|
OSX_APP_DIR = build/$(OSX_APP_NAME)
|
||||||
|
OSX_APP_CONTENTS_DIR = $(OSX_APP_DIR)/Contents
|
||||||
|
OSX_APP_RESOURCES_DIR = $(OSX_APP_CONTENTS_DIR)/Resources
|
||||||
|
OSX_APP_CODESIGN_IDENTITY ?=
|
||||||
|
|
||||||
|
# OSX_APP_GUI ?= lorca
|
||||||
|
OSX_APP_GUI ?= webview
|
||||||
|
|
||||||
|
FRONTEND_EXPORT_DIR = apps/frontend/out
|
||||||
|
|
||||||
|
BACKTEST_REPORT_APP_DIR = apps/backtest-report
|
||||||
|
BACKTEST_REPORT_EXPORT_DIR = apps/backtest-report/out
|
||||||
|
|
||||||
|
all: bbgo-linux bbgo-darwin
|
||||||
|
|
||||||
|
$(BIN_DIR):
|
||||||
|
mkdir -p $@
|
||||||
|
|
||||||
|
|
||||||
|
# build native bbgo
|
||||||
|
bbgo: static
|
||||||
|
go build -tags web,release -o $(BIN_DIR)/bbgo ./cmd/bbgo
|
||||||
|
|
||||||
|
# build native bbgo (slim version)
|
||||||
|
bbgo-slim:
|
||||||
|
go build -tags release -o $(BIN_DIR)/$@ ./cmd/bbgo
|
||||||
|
|
||||||
|
# build cross-compile linux bbgo
|
||||||
|
bbgo-linux: bbgo-linux-amd64 bbgo-linux-arm64
|
||||||
|
|
||||||
|
bbgo-linux-amd64: $(BIN_DIR) pkg/server/assets.go
|
||||||
|
GOOS=linux GOARCH=amd64 go build -tags web,release -o $(BIN_DIR)/$@ ./cmd/bbgo
|
||||||
|
|
||||||
|
bbgo-linux-arm64: $(BIN_DIR) pkg/server/assets.go
|
||||||
|
GOOS=linux GOARCH=arm64 go build -tags web,release -o $(BIN_DIR)/$@ ./cmd/bbgo
|
||||||
|
|
||||||
|
# build cross-compile linux bbgo (slim version)
|
||||||
|
bbgo-slim-linux: bbgo-slim-linux-amd64 bbgo-slim-linux-arm64
|
||||||
|
|
||||||
|
bbgo-slim-linux-amd64: $(BIN_DIR)
|
||||||
|
GOOS=linux GOARCH=amd64 go build -tags release -o $(BIN_DIR)/$@ ./cmd/bbgo
|
||||||
|
|
||||||
|
bbgo-slim-linux-arm64: $(BIN_DIR)
|
||||||
|
GOOS=linux GOARCH=arm64 go build -tags release -o $(BIN_DIR)/$@ ./cmd/bbgo
|
||||||
|
|
||||||
|
bbgo-darwin: bbgo-darwin-arm64 bbgo-darwin-amd64
|
||||||
|
|
||||||
|
bbgo-darwin-arm64: $(BIN_DIR) pkg/server/assets.go
|
||||||
|
GOOS=darwin GOARCH=arm64 go build -tags web,release -o $(BIN_DIR)/$@ ./cmd/bbgo
|
||||||
|
|
||||||
|
bbgo-darwin-amd64: $(BIN_DIR) pkg/server/assets.go
|
||||||
|
GOOS=darwin GOARCH=amd64 go build -tags web,release -o $(BIN_DIR)/$@ ./cmd/bbgo
|
||||||
|
|
||||||
|
bbgo-slim-darwin-arm64: $(BIN_DIR)
|
||||||
|
GOOS=darwin GOARCH=arm64 go build -tags release -o $(BIN_DIR)/$@ ./cmd/bbgo
|
||||||
|
|
||||||
|
bbgo-slim-darwin-amd64: $(BIN_DIR)
|
||||||
|
GOOS=darwin GOARCH=amd64 go build -tags release -o $(BIN_DIR)/$@ ./cmd/bbgo
|
||||||
|
|
||||||
|
bbgo-slim-darwin: bbgo-slim-darwin-amd64 bbgo-slim-darwin-arm64
|
||||||
|
|
||||||
|
# build native bbgo
|
||||||
|
bbgo-dnum: static
|
||||||
|
go build -tags web,release,dnum -o $(BIN_DIR)/bbgo ./cmd/bbgo
|
||||||
|
|
||||||
|
# build native bbgo (slim version)
|
||||||
|
bbgo-slim-dnum:
|
||||||
|
go build -tags release,dnum -o $(BIN_DIR)/$@ ./cmd/bbgo
|
||||||
|
|
||||||
|
# build cross-compile linux bbgo
|
||||||
|
bbgo-dnum-linux: bbgo-dnum-linux-amd64 bbgo-dnum-linux-arm64
|
||||||
|
|
||||||
|
bbgo-dnum-linux-amd64: $(BIN_DIR) pkg/server/assets.go
|
||||||
|
GOOS=linux GOARCH=amd64 go build -tags web,release,dnum -o $(BIN_DIR)/$@ ./cmd/bbgo
|
||||||
|
|
||||||
|
bbgo-dnum-linux-arm64: $(BIN_DIR) pkg/server/assets.go
|
||||||
|
GOOS=linux GOARCH=arm64 go build -tags web,release,dnum -o $(BIN_DIR)/$@ ./cmd/bbgo
|
||||||
|
|
||||||
|
# build cross-compile linux bbgo (slim version)
|
||||||
|
bbgo-slim-dnum-linux: bbgo-slim-dnum-linux-amd64 bbgo-slim-dnum-linux-arm64
|
||||||
|
|
||||||
|
bbgo-slim-dnum-linux-amd64: $(BIN_DIR)
|
||||||
|
GOOS=linux GOARCH=amd64 go build -tags release,dnum -o $(BIN_DIR)/$@ ./cmd/bbgo
|
||||||
|
|
||||||
|
bbgo-slim-dnum-linux-arm64: $(BIN_DIR)
|
||||||
|
GOOS=linux GOARCH=arm64 go build -tags release,dnum -o $(BIN_DIR)/$@ ./cmd/bbgo
|
||||||
|
|
||||||
|
bbgo-dnum-darwin: bbgo-dnum-darwin-arm64 bbgo-dnum-darwin-amd64
|
||||||
|
|
||||||
|
bbgo-dnum-darwin-arm64: $(BIN_DIR) pkg/server/assets.go
|
||||||
|
GOOS=darwin GOARCH=arm64 go build -tags web,release,dnum -o $(BIN_DIR)/$@ ./cmd/bbgo
|
||||||
|
|
||||||
|
bbgo-dnum-darwin-amd64: $(BIN_DIR) pkg/server/assets.go
|
||||||
|
GOOS=darwin GOARCH=amd64 go build -tags web,release,dnum -o $(BIN_DIR)/$@ ./cmd/bbgo
|
||||||
|
|
||||||
|
bbgo-slim-dnum-darwin-arm64: $(BIN_DIR)
|
||||||
|
GOOS=darwin GOARCH=arm64 go build -tags release,dnum -o $(BIN_DIR)/$@ ./cmd/bbgo
|
||||||
|
|
||||||
|
bbgo-slim-dnum-darwin-amd64: $(BIN_DIR)
|
||||||
|
GOOS=darwin GOARCH=amd64 go build -tags release,dnum -o $(BIN_DIR)/$@ ./cmd/bbgo
|
||||||
|
|
||||||
|
bbgo-slim-dnum-darwin: bbgo-slim-dnum-darwin-amd64 bbgo-slim-dnum-darwin-arm64
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
$(OSX_APP_CONTENTS_DIR):
|
||||||
|
mkdir -p $@
|
||||||
|
|
||||||
|
$(OSX_APP_CONTENTS_DIR)/MacOS: $(OSX_APP_CONTENTS_DIR)
|
||||||
|
mkdir -p $@
|
||||||
|
|
||||||
|
$(OSX_APP_RESOURCES_DIR): $(OSX_APP_CONTENTS_DIR)
|
||||||
|
mkdir -p $@
|
||||||
|
|
||||||
|
$(OSX_APP_RESOURCES_DIR)/icon.icns: $(OSX_APP_RESOURCES_DIR)
|
||||||
|
cp -v desktop/icons/icon.icns $@
|
||||||
|
|
||||||
|
$(OSX_APP_CONTENTS_DIR)/Info.plist: $(OSX_APP_CONTENTS_DIR)
|
||||||
|
bash desktop/build-osx-info-plist.sh > $@
|
||||||
|
|
||||||
|
$(OSX_APP_CONTENTS_DIR)/MacOS/bbgo-desktop: $(OSX_APP_CONTENTS_DIR)/MacOS .FORCE
|
||||||
|
go build -tags web -o $@ ./cmd/bbgo-$(OSX_APP_GUI)
|
||||||
|
|
||||||
|
desktop-osx: $(OSX_APP_CONTENTS_DIR)/MacOS/bbgo-desktop $(OSX_APP_CONTENTS_DIR)/Info.plist $(OSX_APP_RESOURCES_DIR)/icon.icns
|
||||||
|
if [[ -n "$(OSX_APP_CODESIGN_IDENTITY)" ]] ; then codesign --deep --force --verbose --sign "$(OSX_APP_CODESIGN_IDENTITY)" $(OSX_APP_DIR) \
|
||||||
|
&& codesign --verify -vvvv $(OSX_APP_DIR) ; fi
|
||||||
|
|
||||||
|
desktop: desktop-osx
|
||||||
|
|
||||||
|
$(DIST_DIR)/$(VERSION):
|
||||||
|
mkdir -p $(DIST_DIR)/$(VERSION)
|
||||||
|
|
||||||
|
$(DIST_DIR)/$(VERSION)/bbgo-slim-$(VERSION)-%.tar.gz: bbgo-slim-% $(DIST_DIR)/$(VERSION)
|
||||||
|
tar -C $(BIN_DIR) -cvzf $@ $<
|
||||||
|
ifeq ($(SIGN),1)
|
||||||
|
gpg --yes --detach-sign --armor $@
|
||||||
|
endif
|
||||||
|
|
||||||
|
$(DIST_DIR)/$(VERSION)/bbgo-$(VERSION)-%.tar.gz: bbgo-% $(DIST_DIR)/$(VERSION)
|
||||||
|
tar -C $(BIN_DIR) -cvzf $@ $<
|
||||||
|
ifeq ($(SIGN),1)
|
||||||
|
gpg --yes --detach-sign --armor $@
|
||||||
|
endif
|
||||||
|
|
||||||
|
$(DIST_DIR)/$(VERSION)/bbgo-slim-dnum-$(VERSION)-%.tar.gz: bbgo-slim-dnum-% $(DIST_DIR)/$(VERSION)
|
||||||
|
tar -C $(BIN_DIR) -cvzf $@ $<
|
||||||
|
ifeq ($(SIGN),1)
|
||||||
|
gpg --yes --detach-sign --armor $@
|
||||||
|
endif
|
||||||
|
|
||||||
|
$(DIST_DIR)/$(VERSION)/bbgo-dnum-$(VERSION)-%.tar.gz: bbgo-dnum-% $(DIST_DIR)/$(VERSION)
|
||||||
|
tar -C $(BIN_DIR) -cvzf $@ $<
|
||||||
|
ifeq ($(SIGN),1)
|
||||||
|
gpg --yes --detach-sign --armor $@
|
||||||
|
endif
|
||||||
|
|
||||||
|
dist-bbgo-linux: \
|
||||||
|
$(DIST_DIR)/$(VERSION)/bbgo-$(VERSION)-linux-arm64.tar.gz \
|
||||||
|
$(DIST_DIR)/$(VERSION)/bbgo-$(VERSION)-linux-amd64.tar.gz \
|
||||||
|
$(DIST_DIR)/$(VERSION)/bbgo-slim-$(VERSION)-linux-arm64.tar.gz \
|
||||||
|
$(DIST_DIR)/$(VERSION)/bbgo-slim-$(VERSION)-linux-amd64.tar.gz \
|
||||||
|
$(DIST_DIR)/$(VERSION)/bbgo-dnum-$(VERSION)-linux-arm64.tar.gz \
|
||||||
|
$(DIST_DIR)/$(VERSION)/bbgo-dnum-$(VERSION)-linux-amd64.tar.gz \
|
||||||
|
$(DIST_DIR)/$(VERSION)/bbgo-slim-dnum-$(VERSION)-linux-arm64.tar.gz \
|
||||||
|
$(DIST_DIR)/$(VERSION)/bbgo-slim-dnum-$(VERSION)-linux-amd64.tar.gz
|
||||||
|
|
||||||
|
dist-bbgo-darwin: \
|
||||||
|
$(DIST_DIR)/$(VERSION)/bbgo-$(VERSION)-darwin-arm64.tar.gz \
|
||||||
|
$(DIST_DIR)/$(VERSION)/bbgo-$(VERSION)-darwin-amd64.tar.gz \
|
||||||
|
$(DIST_DIR)/$(VERSION)/bbgo-slim-$(VERSION)-darwin-arm64.tar.gz \
|
||||||
|
$(DIST_DIR)/$(VERSION)/bbgo-slim-$(VERSION)-darwin-amd64.tar.gz \
|
||||||
|
$(DIST_DIR)/$(VERSION)/bbgo-dnum-$(VERSION)-darwin-arm64.tar.gz \
|
||||||
|
$(DIST_DIR)/$(VERSION)/bbgo-dnum-$(VERSION)-darwin-amd64.tar.gz \
|
||||||
|
$(DIST_DIR)/$(VERSION)/bbgo-slim-dnum-$(VERSION)-darwin-arm64.tar.gz \
|
||||||
|
$(DIST_DIR)/$(VERSION)/bbgo-slim-dnum-$(VERSION)-darwin-amd64.tar.gz
|
||||||
|
|
||||||
|
dist: static dist-bbgo-linux dist-bbgo-darwin desktop
|
||||||
|
|
||||||
|
pkg/version/version.go: .FORCE
|
||||||
|
BUILD_FLAGS="release" bash utils/generate-version-file.sh > $@
|
||||||
|
|
||||||
|
pkg/version/dev.go: .FORCE
|
||||||
|
BUILD_FLAGS="!release" VERSION_SUFFIX="-dev" bash utils/generate-version-file.sh > $@
|
||||||
|
gofmt -s -w $@
|
||||||
|
|
||||||
|
dev-version: pkg/version/dev.go
|
||||||
|
git add $<
|
||||||
|
git commit $< -m "update dev build version"
|
||||||
|
|
||||||
|
cmd-doc: .FORCE
|
||||||
|
go run ./cmd/update-doc
|
||||||
|
git add -v doc/commands
|
||||||
|
git commit -m "update command doc files" doc/commands || true
|
||||||
|
|
||||||
|
version: pkg/version/version.go pkg/version/dev.go migrations cmd-doc
|
||||||
|
git add $< $(word 2,$^)
|
||||||
|
git commit $< $(word 2,$^) -m "bump version to $(VERSION)" || true
|
||||||
|
[[ -e doc/release/$(VERSION).md ]] || (echo "file doc/release/$(VERSION).md does not exist" ; exit 1)
|
||||||
|
git add -v doc/release/$(VERSION).md && git commit doc/release/$(VERSION).md -m "add $(VERSION) release note" || true
|
||||||
|
git tag -f $(VERSION)
|
||||||
|
git push origin HEAD
|
||||||
|
git push -f origin $(VERSION)
|
||||||
|
|
||||||
|
migrations:
|
||||||
|
rockhopper compile --config rockhopper_mysql.yaml --output pkg/migrations/mysql
|
||||||
|
rockhopper compile --config rockhopper_sqlite.yaml --output pkg/migrations/sqlite3
|
||||||
|
git add -v pkg/migrations && git commit -m "compile and update migration package" pkg/migrations || true
|
||||||
|
|
||||||
|
docker:
|
||||||
|
GOPATH=$(PWD)/_mod go mod download
|
||||||
|
docker build --build-arg GO_MOD_CACHE=_mod --tag yoanlin/bbgo .
|
||||||
|
bash -c "[[ -n $(DOCKER_TAG) ]] && docker tag yoanlin/bbgo yoanlin/bbgo:$(DOCKER_TAG)"
|
||||||
|
|
||||||
|
docker-push:
|
||||||
|
docker push yoanlin/bbgo
|
||||||
|
bash -c "[[ -n $(DOCKER_TAG) ]] && docker push yoanlin/bbgo:$(DOCKER_TAG)"
|
||||||
|
|
||||||
|
apps/frontend/node_modules:
|
||||||
|
cd apps/frontend && yarn install
|
||||||
|
|
||||||
|
apps/frontend/out/index.html: apps/frontend/node_modules
|
||||||
|
cd apps/frontend && yarn export
|
||||||
|
|
||||||
|
pkg/server/assets.go: apps/frontend/out/index.html
|
||||||
|
go run ./utils/embed -package server -tag web -output $@ $(FRONTEND_EXPORT_DIR)
|
||||||
|
|
||||||
|
$(BACKTEST_REPORT_APP_DIR)/node_modules:
|
||||||
|
cd $(BACKTEST_REPORT_APP_DIR) && yarn install
|
||||||
|
|
||||||
|
$(BACKTEST_REPORT_APP_DIR)/out/index.html: .FORCE $(BACKTEST_REPORT_APP_DIR)/node_modules
|
||||||
|
cd $(BACKTEST_REPORT_APP_DIR) && yarn build && yarn export
|
||||||
|
|
||||||
|
pkg/backtest/assets.go: $(BACKTEST_REPORT_APP_DIR)/out/index.html
|
||||||
|
go run ./utils/embed -package backtest -tag web -output $@ $(BACKTEST_REPORT_EXPORT_DIR)
|
||||||
|
|
||||||
|
embed: pkg/server/assets.go pkg/backtest/assets.go
|
||||||
|
|
||||||
|
static: apps/frontend/out/index.html pkg/server/assets.go pkg/backtest/assets.go
|
||||||
|
|
||||||
|
PROTOS := \
|
||||||
|
$(wildcard pkg/pb/*.proto)
|
||||||
|
|
||||||
|
GRPC_GO_DEPS := $(subst .proto,.pb.go,$(PROTOS))
|
||||||
|
|
||||||
|
%.pb.go: %.proto .FORCE
|
||||||
|
protoc --go-grpc_out=. --go-grpc_opt=paths=source_relative --go_out=paths=source_relative:. --proto_path=. $<
|
||||||
|
|
||||||
|
grpc-go: $(GRPC_GO_DEPS)
|
||||||
|
|
||||||
|
grpc: grpc-go grpc-py
|
||||||
|
|
||||||
|
install-grpc-tools:
|
||||||
|
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.26
|
||||||
|
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.1
|
||||||
|
pip install grpcio-tools
|
||||||
|
|
||||||
|
# https://github.com/protocolbuffers/protobuf/issues/1491#issuecomment-261914766
|
||||||
|
# replace `import bbgo_pb2` by `from . import bbgo_pb2` to use relative import
|
||||||
|
grpc-py:
|
||||||
|
python -m grpc_tools.protoc -I$(PWD)/pkg/pb \
|
||||||
|
--python_out=$(PWD)/python \
|
||||||
|
--grpc_python_out=$(PWD)/python \
|
||||||
|
$(PWD)/pkg/pb/bbgo.proto
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf $(BUILD_DIR) $(DIST_DIR) $(FRONTEND_EXPORT_DIR) $(GRPC_GO_DEPS) pkg/pb/*.pb.go coverage.txt
|
||||||
|
|
||||||
|
.PHONY: bbgo bbgo-slim-darwin bbgo-slim-darwin-amd64 bbgo-slim-darwin-arm64 bbgo-darwin version dist pack migrations static embed desktop grpc grpc-go grpc-py .FORCE
|
667
README.md
Normal file
|
@ -0,0 +1,667 @@
|
||||||
|
* [English👈](./README.md)
|
||||||
|
* [中文](./README.zh_TW.md)
|
||||||
|
|
||||||
|
# BBGO
|
||||||
|
|
||||||
|
A modern crypto trading bot framework written in Go.
|
||||||
|
|
||||||
|
## Current Status
|
||||||
|
|
||||||
|
[![Go](https://github.com/c9s/bbgo/actions/workflows/go.yml/badge.svg?branch=main)](https://github.com/c9s/bbgo/actions/workflows/go.yml)
|
||||||
|
[![GoDoc](https://godoc.org/github.com/c9s/bbgo?status.svg)](https://pkg.go.dev/github.com/c9s/bbgo)
|
||||||
|
[![Go Report Card](https://goreportcard.com/badge/github.com/c9s/bbgo)](https://goreportcard.com/report/github.com/c9s/bbgo)
|
||||||
|
[![DockerHub](https://img.shields.io/docker/pulls/yoanlin/bbgo.svg)](https://hub.docker.com/r/yoanlin/bbgo)
|
||||||
|
[![Coverage Status](http://codecov.io/github/c9s/bbgo/coverage.svg?branch=main)](http://codecov.io/github/c9s/bbgo?branch=main)
|
||||||
|
<img alt="open collective badge" src="https://opencollective.com/bbgo/tiers/badge.svg">
|
||||||
|
<img alt="open collective badge" src="https://opencollective.com/bbgo/tiers/backer/badge.svg?label=backer&color=brightgreen" />
|
||||||
|
|
||||||
|
## Community
|
||||||
|
|
||||||
|
[![Telegram Global](https://img.shields.io/badge/telegram-global-blue.svg)](https://t.me/bbgo_intl)
|
||||||
|
[![Telegram Taiwan](https://img.shields.io/badge/telegram-tw-blue.svg)](https://t.me/bbgocrypto)
|
||||||
|
[![Twitter](https://img.shields.io/twitter/follow/bbgotrading?label=Follow&style=social)](https://twitter.com/bbgotrading)
|
||||||
|
|
||||||
|
## What You Can Do With BBGO
|
||||||
|
|
||||||
|
### Trading Bot Users 💁♀️ 💁♂️
|
||||||
|
|
||||||
|
You can use BBGO to run the built-in strategies.
|
||||||
|
|
||||||
|
### Strategy Developers 🥷
|
||||||
|
|
||||||
|
You can use BBGO's trading unit and back-test unit to implement your own strategies.
|
||||||
|
|
||||||
|
### Trading Unit Developers 🧑💻
|
||||||
|
|
||||||
|
You can use BBGO's underlying common exchange API; currently, it supports 4+ major exchanges, so you don't have to repeat
|
||||||
|
the implementation.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Exchange abstraction interface.
|
||||||
|
- Stream integration (user data web socket, market data web socket).
|
||||||
|
- Real-time orderBook integration through a web socket.
|
||||||
|
- TWAP order execution support. See [TWAP Order Execution](./doc/topics/twap.md)
|
||||||
|
- PnL calculation.
|
||||||
|
- Slack/Telegram notification.
|
||||||
|
- Back-testing: KLine-based back-testing engine. See [Back-testing](./doc/topics/back-testing.md)
|
||||||
|
- Built-in parameter optimization tool.
|
||||||
|
- Built-in Grid strategy and many other built-in strategies.
|
||||||
|
- Multi-exchange session support: you can connect to more than 2 exchanges with different accounts or subaccounts.
|
||||||
|
- Indicators with interface similar
|
||||||
|
to `pandas.Series`([series](https://github.com/c9s/bbgo/blob/main/doc/development/series.md))([usage](https://github.com/c9s/bbgo/blob/main/doc/development/indicator.md)):
|
||||||
|
- [Accumulation/Distribution Indicator](./pkg/indicator/ad.go)
|
||||||
|
- [Arnaud Legoux Moving Average](./pkg/indicator/alma.go)
|
||||||
|
- [Average True Range](./pkg/indicator/atr.go)
|
||||||
|
- [Bollinger Bands](./pkg/indicator/boll.go)
|
||||||
|
- [Commodity Channel Index](./pkg/indicator/cci.go)
|
||||||
|
- [Cumulative Moving Average](./pkg/indicator/cma.go)
|
||||||
|
- [Double Exponential Moving Average](./pkg/indicator/dema.go)
|
||||||
|
- [Directional Movement Index](./pkg/indicator/dmi.go)
|
||||||
|
- [Brownian Motion's Drift Factor](./pkg/indicator/drift.go)
|
||||||
|
- [Ease of Movement](./pkg/indicator/emv.go)
|
||||||
|
- [Exponentially Weighted Moving Average](./pkg/indicator/ewma.go)
|
||||||
|
- [Hull Moving Average](./pkg/indicator/hull.go)
|
||||||
|
- [Trend Line (Tool)](./pkg/indicator/line.go)
|
||||||
|
- [Moving Average Convergence Divergence Indicator](./pkg/indicator/macd.go)
|
||||||
|
- [On-Balance Volume](./pkg/indicator/obv.go)
|
||||||
|
- [Pivot](./pkg/indicator/pivot.go)
|
||||||
|
- [Running Moving Average](./pkg/indicator/rma.go)
|
||||||
|
- [Relative Strength Index](./pkg/indicator/rsi.go)
|
||||||
|
- [Simple Moving Average](./pkg/indicator/sma.go)
|
||||||
|
- [Ehler's Super Smoother Filter](./pkg/indicator/ssf.go)
|
||||||
|
- [Stochastic Oscillator](./pkg/indicator/stoch.go)
|
||||||
|
- [SuperTrend](./pkg/indicator/supertrend.go)
|
||||||
|
- [Triple Exponential Moving Average](./pkg/indicator/tema.go)
|
||||||
|
- [Tillson T3 Moving Average](./pkg/indicator/till.go)
|
||||||
|
- [Triangular Moving Average](./pkg/indicator/tma.go)
|
||||||
|
- [Variable Index Dynamic Average](./pkg/indicator/vidya.go)
|
||||||
|
- [Volatility Indicator](./pkg/indicator/volatility.go)
|
||||||
|
- [Volume Weighted Average Price](./pkg/indicator/vwap.go)
|
||||||
|
- [Zero Lag Exponential Moving Average](./pkg/indicator/zlema.go)
|
||||||
|
- And more...
|
||||||
|
- HeikinAshi OHLC / Normal OHLC (check [this config](https://github.com/c9s/bbgo/blob/main/config/skeleton.yaml#L5))
|
||||||
|
- React-powered Web Dashboard.
|
||||||
|
- Docker image ready.
|
||||||
|
- Kubernetes support.
|
||||||
|
- Helm chart ready.
|
||||||
|
- High precision float point (up to 16 digits, run with `-tags dnum`).
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
|
![bbgo dashboard](assets/screenshots/dashboard.jpeg)
|
||||||
|
|
||||||
|
![bbgo backtest report](assets/screenshots/backtest-report.jpg)
|
||||||
|
|
||||||
|
## Built-in Strategies
|
||||||
|
|
||||||
|
| strategy | description | type | backtest support |
|
||||||
|
|-------------|-----------------------------------------------------------------------------------------------------------------------------------------|------------|------------------|
|
||||||
|
| grid | the first generation grid strategy, it provides more flexibility, but you need to prepare inventories | maker | |
|
||||||
|
| grid2 | the second generation grid strategy, it can convert your quote asset into a grid, supports base+quote mode | maker | |
|
||||||
|
| bollgrid | strategy implements a basic grid strategy with the built-in bollinger indicator | maker | |
|
||||||
|
| xmaker | cross exchange market making strategy, it hedges your inventory risk on the other side | maker | no |
|
||||||
|
| xnav | this strategy helps you record the current net asset value | tool | no |
|
||||||
|
| xalign | this strategy aligns your balance position automatically | tool | no |
|
||||||
|
| xfunding | a funding rate fee strategy | funding | no |
|
||||||
|
| autoborrow | this strategy uses margin to borrow assets, to help you keep a minimal balance | tool | no |
|
||||||
|
| pivotshort | this strategy finds the pivot low and enters the trade when the price breaks the previous low | long/short | |
|
||||||
|
| schedule | this strategy buy/sell with a fixed quantity periodically, you can use this as a single DCA, or to refill the fee asset like BNB. | tool |
|
||||||
|
| irr | this strategy opens the position based on the predicated return rate | long/short | |
|
||||||
|
| bollmaker | this strategy holds a long-term long/short position, places maker orders on both sides, and uses a bollinger band to control the position size | maker | |
|
||||||
|
| wall | this strategy creates a wall (large amount of order) on the order book | maker | no |
|
||||||
|
| scmaker | this market making strategy is designed for stable coin markets, like USDC/USDT | maker | |
|
||||||
|
| drift | | long/short | |
|
||||||
|
| rsicross | this strategy opens a long position when the fast rsi crosses over the slow rsi, this is a demo strategy for using the v2 indicator | long/short | |
|
||||||
|
| marketcap | this strategy implements a strategy that rebalances the portfolio based on the market capitalization | rebalance | no |
|
||||||
|
| supertrend | this strategy uses DEMA and Supertrend indicator to open the long/short position | long/short | |
|
||||||
|
| trendtrader | this strategy opens a long/short position based on the trendline breakout | long/short | |
|
||||||
|
| elliottwave | | long/short | |
|
||||||
|
| ewoDgtrd | | long/short | |
|
||||||
|
| fixedmaker | | maker | |
|
||||||
|
| factoryzoo | | long/short | |
|
||||||
|
| fmaker | | maker | |
|
||||||
|
| linregmaker | a linear regression based market maker | maker | |
|
||||||
|
| convert | convert strategy is a tool that helps you convert a specific asset to a target asset | tool | no |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Supported Exchanges
|
||||||
|
|
||||||
|
- Binance Spot Exchange (and binance.us)
|
||||||
|
- OKEx Spot Exchange
|
||||||
|
- Kucoin Spot Exchange
|
||||||
|
- MAX Spot Exchange (located in Taiwan)
|
||||||
|
- Bitget Exchange
|
||||||
|
- Bybit Exchange
|
||||||
|
|
||||||
|
## Documentation and General Topics
|
||||||
|
|
||||||
|
- Check the [documentation index](doc/README.md)
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
* Go SDK 1.20
|
||||||
|
|
||||||
|
* Linux / MacOS / Windows (WSL)
|
||||||
|
|
||||||
|
* Get your exchange API key and secret after you register the accounts (you can choose one or more exchanges):
|
||||||
|
|
||||||
|
- MAX: <https://max.maicoin.com/signup?r=c7982718>
|
||||||
|
- Binance: <https://accounts.binance.com/en/register?ref=38192708>
|
||||||
|
- OKEx: <https://www.okex.com/join/2412712?src=from:ios-share>
|
||||||
|
- Kucoin: <https://www.kucoin.com/ucenter/signup?rcode=r3KX2D4>
|
||||||
|
|
||||||
|
This project is maintained and supported by a small group of people. If you would like to support this project, please
|
||||||
|
Register on the exchanges using the provided links with the referral codes above.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Install from binary
|
||||||
|
|
||||||
|
The following script will help you set up a config file and a dotenv file:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# grid trading strategy for binance exchange
|
||||||
|
bash <(curl -s https://raw.githubusercontent.com/c9s/bbgo/main/scripts/setup-grid.sh) binance
|
||||||
|
|
||||||
|
# grid trading strategy for max exchange
|
||||||
|
bash <(curl -s https://raw.githubusercontent.com/c9s/bbgo/main/scripts/setup-grid.sh) max
|
||||||
|
|
||||||
|
# bollinger grid trading strategy for binance exchange
|
||||||
|
bash <(curl -s https://raw.githubusercontent.com/c9s/bbgo/main/scripts/setup-bollgrid.sh) binance
|
||||||
|
|
||||||
|
# bollinger grid trading strategy for max exchange
|
||||||
|
bash <(curl -s https://raw.githubusercontent.com/c9s/bbgo/main/scripts/setup-bollgrid.sh) max
|
||||||
|
```
|
||||||
|
|
||||||
|
If you already have configuration somewhere, a download-only script might be suitable for you:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bash <(curl -s https://raw.githubusercontent.com/c9s/bbgo/main/scripts/download.sh)
|
||||||
|
```
|
||||||
|
|
||||||
|
Or refer to the [Release Page](https://github.com/c9s/bbgo/releases) and download manually.
|
||||||
|
|
||||||
|
Since v2, we've added a new float point implementation from dnum to support decimals with higher precision. To download &
|
||||||
|
setup, please refer to [Dnum Installation](doc/topics/dnum-binary.md)
|
||||||
|
|
||||||
|
### One-click Linode StackScript
|
||||||
|
|
||||||
|
StackScript allows you to one-click deploy a lightweight instance with bbgo.
|
||||||
|
|
||||||
|
- BBGO grid on Binance <https://cloud.linode.com/stackscripts/950715>
|
||||||
|
- BBGO grid USDT/TWD on MAX <https://cloud.linode.com/stackscripts/793380>
|
||||||
|
- BBGO grid USDC/TWD on MAX <https://cloud.linode.com/stackscripts/797776>
|
||||||
|
- BBGO grid LINK/TWD on MAX <https://cloud.linode.com/stackscripts/797774>
|
||||||
|
- BBGO grid USDC/USDT on MAX <https://cloud.linode.com/stackscripts/797777>
|
||||||
|
- BBGO grid on MAX <https://cloud.linode.com/stackscripts/795788>
|
||||||
|
- BBGO bollmaker on Binance <https://cloud.linode.com/stackscripts/1002384>
|
||||||
|
|
||||||
|
### Build from source
|
||||||
|
|
||||||
|
See [Build from source](./doc/build-from-source.md)
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Add your dotenv file:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# for Binance Exchange, if you have one
|
||||||
|
BINANCE_API_KEY=
|
||||||
|
BINANCE_API_SECRET=
|
||||||
|
|
||||||
|
# if you want to use binance.us, change this to 1
|
||||||
|
BINANCE_US=0
|
||||||
|
|
||||||
|
# for MAX exchange, if you have one
|
||||||
|
MAX_API_KEY=
|
||||||
|
MAX_API_SECRET=
|
||||||
|
|
||||||
|
# for OKEx exchange, if you have one
|
||||||
|
OKEX_API_KEY=
|
||||||
|
OKEX_API_SECRET=
|
||||||
|
OKEX_API_PASSPHRASE
|
||||||
|
|
||||||
|
# for kucoin exchange, if you have one
|
||||||
|
KUCOIN_API_KEY=
|
||||||
|
KUCOIN_API_SECRET=
|
||||||
|
KUCOIN_API_PASSPHRASE=
|
||||||
|
KUCOIN_API_KEY_VERSION=2
|
||||||
|
|
||||||
|
# for Bybit exchange, if you have one
|
||||||
|
BYBIT_API_KEY=
|
||||||
|
BYBIT_API_SECRET=
|
||||||
|
```
|
||||||
|
|
||||||
|
Prepare your dotenv file `.env.local` and BBGO yaml config file `bbgo.yaml`.
|
||||||
|
|
||||||
|
To check the available environment variables, please see [Environment Variables](./doc/configuration/envvars.md)
|
||||||
|
|
||||||
|
The minimal bbgo.yaml could be generated by:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -o bbgo.yaml https://raw.githubusercontent.com/c9s/bbgo/main/config/minimal.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
To run strategy:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bbgo run
|
||||||
|
```
|
||||||
|
|
||||||
|
To start bbgo with the frontend dashboard:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bbgo run --enable-webserver
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to switch to another dotenv file, you can add an `--dotenv` option or `--config`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bbgo sync --dotenv .env.dev --config config/grid.yaml --session binance
|
||||||
|
```
|
||||||
|
|
||||||
|
To query transfer history:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bbgo transfer-history --session max --asset USDT --since "2019-01-01"
|
||||||
|
```
|
||||||
|
|
||||||
|
<!--
|
||||||
|
To calculate pnl:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bbgo pnl --exchange binance --asset BTC --since "2019-01-01"
|
||||||
|
```
|
||||||
|
--->
|
||||||
|
|
||||||
|
## Advanced Configuration
|
||||||
|
|
||||||
|
### Synchronize System Time With Binance
|
||||||
|
|
||||||
|
BBGO provides the script for UNIX systems/subsystems to synchronize date with Binance. `jq` and `bc` are required to be
|
||||||
|
installed in previous.
|
||||||
|
To install the dependencies in Ubuntu, try the following commands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt install -y bc jq
|
||||||
|
```
|
||||||
|
|
||||||
|
And to synchronize the date, try:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo ./scripts/sync_time.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
You could also add the script to crontab so that the system time could get synchronized with Binance regularly.
|
||||||
|
|
||||||
|
### Testnet (Paper Trading)
|
||||||
|
|
||||||
|
Currently only supports Binance testnet. To run bbgo in testnet, apply new API keys
|
||||||
|
from [Binance Test Network](https://testnet.binance.vision), and set the following env before you start bbgo:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export PAPER_TRADE=1
|
||||||
|
export DISABLE_MARKET_CACHE=1 # the symbols supported in testnet is far less than the mainnet
|
||||||
|
```
|
||||||
|
|
||||||
|
### Notification
|
||||||
|
|
||||||
|
- [Setting up Telegram notification](./doc/configuration/telegram.md)
|
||||||
|
- [Setting up Slack notification](./doc/configuration/slack.md)
|
||||||
|
|
||||||
|
### Synchronizing Trading Data
|
||||||
|
|
||||||
|
By default, BBGO does not sync your trading data from the exchange sessions, so it's hard to calculate your profit and
|
||||||
|
loss correctly.
|
||||||
|
|
||||||
|
By synchronizing trades and orders to the local database, you can earn some benefits like PnL calculations, backtesting
|
||||||
|
and asset calculation.
|
||||||
|
|
||||||
|
You can only use one database driver MySQL or SQLite to store your trading data.
|
||||||
|
|
||||||
|
**Notice**: SQLite is not fully supported, we recommend you use MySQL instead of SQLite.
|
||||||
|
|
||||||
|
#### Configure MySQL Database
|
||||||
|
|
||||||
|
To use MySQL database for data syncing, first, you need to install your MySQL server:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# For Ubuntu Linux
|
||||||
|
sudo apt-get install -y mysql-server
|
||||||
|
|
||||||
|
# For newer Ubuntu Linux
|
||||||
|
sudo apt install -y mysql-server
|
||||||
|
```
|
||||||
|
|
||||||
|
Or [run it in docker](https://hub.docker.com/_/mysql)
|
||||||
|
|
||||||
|
Create your mysql database:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mysql -uroot -e "CREATE DATABASE bbgo CHARSET utf8"
|
||||||
|
```
|
||||||
|
|
||||||
|
Then put these environment variables in your `.env.local` file:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
DB_DRIVER=mysql
|
||||||
|
DB_DSN="user:password@tcp(127.0.0.1:3306)/bbgo"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Configure Sqlite3 Database
|
||||||
|
|
||||||
|
To use SQLite3 instead of MySQL, simply put these environment variables in your `.env.local` file:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
DB_DRIVER=sqlite3
|
||||||
|
DB_DSN=bbgo.sqlite3
|
||||||
|
```
|
||||||
|
|
||||||
|
## Synchronizing your own trading data
|
||||||
|
|
||||||
|
Once you have your database configured, you can sync your own trading data from the exchange.
|
||||||
|
|
||||||
|
See [Configure Sync For Private Trading Data](./doc/configuration/sync.md)
|
||||||
|
|
||||||
|
## Using Redis to keep persistence between BBGO sessions
|
||||||
|
|
||||||
|
To use Redis, first you need to install your Redis server:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# For Ubuntu/Debian Linux
|
||||||
|
sudo apt-get install -y redis
|
||||||
|
|
||||||
|
# For newer Ubuntu/Debian Linux
|
||||||
|
sudo apt install -y redis
|
||||||
|
```
|
||||||
|
|
||||||
|
Set the following environment variables in your `bbgo.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
persistence:
|
||||||
|
redis:
|
||||||
|
host: 127.0.0.1 # The IP address or the hostname to your Redis server, 127.0.0.1 if same as BBGO
|
||||||
|
port: 6379 # Port to Redis server, default 6379
|
||||||
|
db: 0 # DB number to use. You can set to another DB to avoid conflict if other applications are using Redis too.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Built-in Strategies
|
||||||
|
|
||||||
|
Check out the strategy directory [strategy](pkg/strategy) for all built-in strategies:
|
||||||
|
|
||||||
|
- `pricealert` strategy demonstrates how to use the notification system [pricealert](pkg/strategy/pricealert). See
|
||||||
|
[document](./doc/strategy/pricealert.md).
|
||||||
|
- `buyandhold` strategy demonstrates how to subscribe kline events and submit market
|
||||||
|
order [buyandhold](pkg/strategy/pricedrop)
|
||||||
|
- `bollgrid` strategy implements a basic grid strategy with the built-in bollinger
|
||||||
|
indicator [bollgrid](pkg/strategy/bollgrid)
|
||||||
|
- `grid` strategy implements the fixed price band grid strategy [grid](pkg/strategy/grid). See
|
||||||
|
[document](./doc/strategy/grid.md).
|
||||||
|
- `supertrend` strategy uses Supertrend indicator as trend, and DEMA indicator as noise
|
||||||
|
filter [supertrend](pkg/strategy/supertrend). See
|
||||||
|
[document](./doc/strategy/supertrend.md).
|
||||||
|
- `support` strategy uses K-lines with high volume as support [support](pkg/strategy/support). See
|
||||||
|
[document](./doc/strategy/support.md).
|
||||||
|
- `flashcrash` strategy implements a strategy that catches the flashcrash [flashcrash](pkg/strategy/flashcrash)
|
||||||
|
- `marketcap` strategy implements a strategy that rebalances the portfolio based on the market
|
||||||
|
capitalization [marketcap](pkg/strategy/marketcap). See [document](./doc/strategy/marketcap.md).
|
||||||
|
- `pivotshort` - shorting focused strategy.
|
||||||
|
- `irr` - return rate strategy.
|
||||||
|
- `drift` - drift strategy.
|
||||||
|
- `grid2` - the second-generation grid strategy.
|
||||||
|
- `rebalance` - rebalances your portfolio based on target weights. [rebalance](pkg/strategy/rebalance). See [document](./doc/strategy/rebalance.md).
|
||||||
|
|
||||||
|
To run these built-in strategies, just modify the config file to make the configuration suitable for you, for example, if
|
||||||
|
you want to run
|
||||||
|
`buyandhold` strategy:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
vim config/buyandhold.yaml
|
||||||
|
|
||||||
|
# run bbgo with the config
|
||||||
|
bbgo run --config config/buyandhold.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Back-testing
|
||||||
|
|
||||||
|
See [Back-testing](./doc/topics/back-testing.md)
|
||||||
|
|
||||||
|
## Adding Strategy
|
||||||
|
|
||||||
|
See [Developing Strategy](./doc/topics/developing-strategy.md)
|
||||||
|
|
||||||
|
## Write your own private strategy
|
||||||
|
|
||||||
|
Create your go package, initialize the repository with `go mod`, and add bbgo as a dependency:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go mod init
|
||||||
|
go get github.com/c9s/bbgo@main
|
||||||
|
```
|
||||||
|
|
||||||
|
Write your own strategy in the strategy file:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
vim strategy.go
|
||||||
|
```
|
||||||
|
|
||||||
|
You can grab the skeleton strategy from <https://github.com/c9s/bbgo/blob/main/pkg/strategy/skeleton/strategy.go>
|
||||||
|
|
||||||
|
Now add your config:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mkdir config
|
||||||
|
(cd config && curl -o bbgo.yaml https://raw.githubusercontent.com/c9s/bbgo/main/config/minimal.yaml)
|
||||||
|
```
|
||||||
|
|
||||||
|
Add your strategy package path to the config file `config/bbgo.yaml`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
build:
|
||||||
|
dir: build
|
||||||
|
imports:
|
||||||
|
- github.com/your_id/your_swing
|
||||||
|
targets:
|
||||||
|
- name: swing-amd64-linux
|
||||||
|
os: linux
|
||||||
|
arch: amd64
|
||||||
|
- name: swing-amd64-darwin
|
||||||
|
os: darwin
|
||||||
|
arch: amd64
|
||||||
|
```
|
||||||
|
|
||||||
|
Run `bbgo run` command, bbgo will compile a wrapper binary that imports your strategy:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
dotenv -f .env.local -- bbgo run --config config/bbgo.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Or you can build your own wrapper binary via:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
bbgo build --config config/bbgo.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
See also:
|
||||||
|
|
||||||
|
- <https://github.com/narumiruna/bbgo-template>
|
||||||
|
- <https://github.com/narumiruna/bbgo-marketcap>
|
||||||
|
- <https://github.com/austin362667/shadow>
|
||||||
|
- <https://github.com/jnlin/bbgo-strategy-infinite-grid>
|
||||||
|
- <https://github.com/yubing744/trading-gpt>
|
||||||
|
|
||||||
|
## Command Usages
|
||||||
|
|
||||||
|
### Submitting Orders to a specific exchange session
|
||||||
|
|
||||||
|
```shell
|
||||||
|
bbgo submit-order --session=okex --symbol=OKBUSDT --side=buy --price=10.0 --quantity=1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Listing Open Orders of a specific exchange session
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bbgo list-orders open --session=okex --symbol=OKBUSDT
|
||||||
|
bbgo list-orders open --session=max --symbol=MAXUSDT
|
||||||
|
bbgo list-orders open --session=binance --symbol=BNBUSDT
|
||||||
|
```
|
||||||
|
|
||||||
|
### Canceling an open order
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# both order id and symbol is required for okex
|
||||||
|
bbgo cancel-order --session=okex --order-id=318223238325248000 --symbol=OKBUSDT
|
||||||
|
|
||||||
|
# for max, you can just give your order id
|
||||||
|
bbgo cancel-order --session=max --order-id=1234566
|
||||||
|
```
|
||||||
|
|
||||||
|
### Debugging user data stream
|
||||||
|
|
||||||
|
```shell
|
||||||
|
bbgo userdatastream --session okex
|
||||||
|
bbgo userdatastream --session max
|
||||||
|
bbgo userdatastream --session binance
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dynamic Injection
|
||||||
|
|
||||||
|
In order to minimize the strategy code, bbgo supports dynamic dependency injection.
|
||||||
|
|
||||||
|
Before executing your strategy, bbgo injects the components into your strategy object if it finds the embedded field
|
||||||
|
that is using bbgo component. for example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Strategy struct {
|
||||||
|
Symbol string `json:"symbol"
|
||||||
|
Market types.Market
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Supported components (single exchange strategy only for now):
|
||||||
|
|
||||||
|
- `*bbgo.ExchangeSession`
|
||||||
|
- `bbgo.OrderExecutor`
|
||||||
|
|
||||||
|
If you have `Symbol string` field in your strategy, your strategy will be detected as a symbol-based strategy, then the
|
||||||
|
following types could be injected automatically:
|
||||||
|
|
||||||
|
- `types.Market`
|
||||||
|
|
||||||
|
## Strategy Execution Phases
|
||||||
|
|
||||||
|
1. Load config from the config file.
|
||||||
|
2. Allocate and initialize exchange sessions.
|
||||||
|
3. Add exchange sessions to the environment (the data layer).
|
||||||
|
4. Use the given environment to initialize the trader object (the logic layer).
|
||||||
|
5. The trader initializes the environment and starts the exchange connections.
|
||||||
|
6. Call strategy.Run() method sequentially.
|
||||||
|
|
||||||
|
## Exchange API Examples
|
||||||
|
|
||||||
|
Please check out the example directory: [examples](examples)
|
||||||
|
|
||||||
|
Initialize MAX API:
|
||||||
|
|
||||||
|
```go
|
||||||
|
key := os.Getenv("MAX_API_KEY")
|
||||||
|
secret := os.Getenv("MAX_API_SECRET")
|
||||||
|
|
||||||
|
maxRest := maxapi.NewRestClient(maxapi.ProductionAPIURL)
|
||||||
|
maxRest.Auth(key, secret)
|
||||||
|
```
|
||||||
|
|
||||||
|
Creating user data stream to get the order book (depth):
|
||||||
|
|
||||||
|
```go
|
||||||
|
stream := max.NewStream(key, secret)
|
||||||
|
stream.Subscribe(types.BookChannel, symbol, types.SubscribeOptions{})
|
||||||
|
|
||||||
|
streambook := types.NewStreamBook(symbol)
|
||||||
|
streambook.BindStream(stream)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
- [Helm Chart](./doc/deployment/helm-chart.md)
|
||||||
|
- Baremetal machine or a VPS
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
- [Adding New Exchange](./doc/development/adding-new-exchange.md)
|
||||||
|
- [Migration](./doc/development/migration.md)
|
||||||
|
|
||||||
|
### Setting up your local repository
|
||||||
|
|
||||||
|
1. Click the "Fork" button from the GitHub repository.
|
||||||
|
2. Clone your forked repository into `$GOPATH/github.com/c9s/bbgo`.
|
||||||
|
3. Change the directory to `$GOPATH/github.com/c9s/bbgo`.
|
||||||
|
4. Create a branch and start your development.
|
||||||
|
5. Test your changes.
|
||||||
|
6. Push your changes to your fork.
|
||||||
|
7. Send a pull request.
|
||||||
|
|
||||||
|
### Testing Desktop App
|
||||||
|
|
||||||
|
for webview
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make embed && go run -tags web ./cmd/bbgo-webview
|
||||||
|
```
|
||||||
|
|
||||||
|
for lorca
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make embed && go run -tags web ./cmd/bbgo-lorca
|
||||||
|
```
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
### What's Position?
|
||||||
|
|
||||||
|
- Base Currency & Quote Currency <https://www.ig.com/au/glossary-trading-terms/base-currency-definition>
|
||||||
|
- How to calculate the average cost? <https://www.janushenderson.com/en-us/investor/planning/calculate-average-cost/>
|
||||||
|
|
||||||
|
### Looking For A New Strategy?
|
||||||
|
|
||||||
|
You can write an article about BBGO on any topic, in 750-1500 words for exchange, and I can implement the strategy for
|
||||||
|
you (depending on the complexity and effort). If you're interested in, DM me in telegram <https://t.me/c123456789s> or
|
||||||
|
twitter <https://twitter.com/c9s>, and we can discuss.
|
||||||
|
|
||||||
|
### Adding New Crypto Exchange support?
|
||||||
|
|
||||||
|
If you want BBGO to support a new crypto exchange that is not included in the current BBGO, we can implement it for you.
|
||||||
|
The cost is 10 ETH. If you're interested in it, DM me in telegram <https://t.me/c123456789s>.
|
||||||
|
|
||||||
|
## Community
|
||||||
|
|
||||||
|
- Telegram Group <https://t.me/bbgo_intl>
|
||||||
|
- Telegram Group (Taiwan) <https://t.me/bbgocrypto>
|
||||||
|
- Twitter <https://twitter.com/bbgotrading>
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
See [Contributing](./CONTRIBUTING.md)
|
||||||
|
|
||||||
|
### Financial Contributors
|
||||||
|
|
||||||
|
[[Become a backer](https://opencollective.com/bbgo#backer)]
|
||||||
|
|
||||||
|
<a href="https://opencollective.com/bbgo#backers" target="_blank"><img src="https://opencollective.com/bbgo/tiers/backer.svg?width=890"></a>
|
||||||
|
|
||||||
|
## BBGO Tokenomics
|
||||||
|
|
||||||
|
To support the development of BBGO, we have created a bounty pool to support contributors by giving away $BBG tokens.
|
||||||
|
Check the details in [$BBG Contract Page](contracts/README.md) and our [official website](https://bbgo.finance)
|
||||||
|
|
||||||
|
## Supporter
|
||||||
|
|
||||||
|
- GitBook
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
AGPL License
|
624
README.zh_TW.md
Normal file
|
@ -0,0 +1,624 @@
|
||||||
|
* [English](./README.md)
|
||||||
|
* [中文👈](./README.zh_TW.md)
|
||||||
|
|
||||||
|
# BBGO
|
||||||
|
|
||||||
|
一個用Go編寫的現代加密貨幣交易機器人框架。
|
||||||
|
A modern crypto trading bot framework written in Go.
|
||||||
|
|
||||||
|
|
||||||
|
## 目前狀態
|
||||||
|
|
||||||
|
[![Go](https://github.com/c9s/bbgo/actions/workflows/go.yml/badge.svg?branch=main)](https://github.com/c9s/bbgo/actions/workflows/go.yml)
|
||||||
|
[![GoDoc](https://godoc.org/github.com/c9s/bbgo?status.svg)](https://pkg.go.dev/github.com/c9s/bbgo)
|
||||||
|
[![Go Report Card](https://goreportcard.com/badge/github.com/c9s/bbgo)](https://goreportcard.com/report/github.com/c9s/bbgo)
|
||||||
|
[![DockerHub](https://img.shields.io/docker/pulls/yoanlin/bbgo.svg)](https://hub.docker.com/r/yoanlin/bbgo)
|
||||||
|
[![Coverage Status](http://codecov.io/github/c9s/bbgo/coverage.svg?branch=main)](http://codecov.io/github/c9s/bbgo?branch=main)
|
||||||
|
<img alt="open collective badge" src="https://opencollective.com/bbgo/tiers/badge.svg">
|
||||||
|
<img alt="open collective badge" src="https://opencollective.com/bbgo/tiers/backer/badge.svg?label=backer&color=brightgreen" />
|
||||||
|
|
||||||
|
## 社群
|
||||||
|
|
||||||
|
[![Telegram Global](https://img.shields.io/badge/telegram-global-blue.svg)](https://t.me/bbgo_intl)
|
||||||
|
[![Telegram Taiwan](https://img.shields.io/badge/telegram-tw-blue.svg)](https://t.me/bbgocrypto)
|
||||||
|
[![Twitter](https://img.shields.io/twitter/follow/bbgotrading?label=Follow&style=social)](https://twitter.com/bbgotrading)
|
||||||
|
|
||||||
|
## 你可以用 BBGO 做什麼
|
||||||
|
### 交易機器人用戶 💁♀️ 💁♂️
|
||||||
|
您可以使用 BBGO 運行內置策略。
|
||||||
|
|
||||||
|
### 策略開發者 🥷
|
||||||
|
您可以使用 BBGO 的交易單元和回測單元來實現您自己的策略。
|
||||||
|
|
||||||
|
### 交易單元開發者 🧑💻
|
||||||
|
您可以使用 BBGO 的底層共用交易所 API;目前它支持 4+ 個主要交易所,因此您不必重複實現。
|
||||||
|
|
||||||
|
## 特色
|
||||||
|
* 交易所抽象介面。
|
||||||
|
* 整合串流(用戶資料 websocket,市場資料 websocket)。
|
||||||
|
* 通過 websocket 實時訂單簿整合。
|
||||||
|
* TWAP 訂單執行支持。參見 [TWAP 訂單執行](./doc/topics/twap.md)
|
||||||
|
* 盈虧計算。
|
||||||
|
* Slack / Telegram 通知。
|
||||||
|
* 回測:基於K線的回測引擎。參見[回測](./doc/topics/back-testing.md)
|
||||||
|
* 內置參數優化工具。
|
||||||
|
* 內置網格策略和許多其他內置策略。
|
||||||
|
* 多交易所 session 支持:您可以連接到2個以上不同帳戶或子帳戶的交易所。
|
||||||
|
* 類似於 `pandas.Series` 的指標介面 ([series](https://github.com/c9s/bbgo/blob/main/doc/development/series.md))([usage](https://github.com/c9s/bbgo/blob/main/doc/development/indicator.md))
|
||||||
|
- [Accumulation/Distribution Indicator](./pkg/indicator/ad.go)
|
||||||
|
- [Arnaud Legoux Moving Average](./pkg/indicator/alma.go)
|
||||||
|
- [Average True Range](./pkg/indicator/atr.go)
|
||||||
|
- [Bollinger Bands](./pkg/indicator/boll.go)
|
||||||
|
- [Commodity Channel Index](./pkg/indicator/cci.go)
|
||||||
|
- [Cumulative Moving Average](./pkg/indicator/cma.go)
|
||||||
|
- [Double Exponential Moving Average](./pkg/indicator/dema.go)
|
||||||
|
- [Directional Movement Index](./pkg/indicator/dmi.go)
|
||||||
|
- [Brownian Motion's Drift Factor](./pkg/indicator/drift.go)
|
||||||
|
- [Ease of Movement](./pkg/indicator/emv.go)
|
||||||
|
- [Exponentially Weighted Moving Average](./pkg/indicator/ewma.go)
|
||||||
|
- [Hull Moving Average](./pkg/indicator/hull.go)
|
||||||
|
- [Trend Line (Tool)](./pkg/indicator/line.go)
|
||||||
|
- [Moving Average Convergence Divergence Indicator](./pkg/indicator/macd.go)
|
||||||
|
- [On-Balance Volume](./pkg/indicator/obv.go)
|
||||||
|
- [Pivot](./pkg/indicator/pivot.go)
|
||||||
|
- [Running Moving Average](./pkg/indicator/rma.go)
|
||||||
|
- [Relative Strength Index](./pkg/indicator/rsi.go)
|
||||||
|
- [Simple Moving Average](./pkg/indicator/sma.go)
|
||||||
|
- [Ehler's Super Smoother Filter](./pkg/indicator/ssf.go)
|
||||||
|
- [Stochastic Oscillator](./pkg/indicator/stoch.go)
|
||||||
|
- [SuperTrend](./pkg/indicator/supertrend.go)
|
||||||
|
- [Triple Exponential Moving Average](./pkg/indicator/tema.go)
|
||||||
|
- [Tillson T3 Moving Average](./pkg/indicator/till.go)
|
||||||
|
- [Triangular Moving Average](./pkg/indicator/tma.go)
|
||||||
|
- [Variable Index Dynamic Average](./pkg/indicator/vidya.go)
|
||||||
|
- [Volatility Indicator](./pkg/indicator/volatility.go)
|
||||||
|
- [Volume Weighted Average Price](./pkg/indicator/vwap.go)
|
||||||
|
- [Zero Lag Exponential Moving Average](./pkg/indicator/zlema.go)
|
||||||
|
- 更多...
|
||||||
|
|
||||||
|
## 截圖
|
||||||
|
|
||||||
|
![BBGO 儀表板](assets/screenshots/dashboard.jpeg)
|
||||||
|
|
||||||
|
![BBGO 回測報告](assets/screenshots/backtest-report.jpg)
|
||||||
|
|
||||||
|
## 內建策略
|
||||||
|
|
||||||
|
| 策略 | 描述 | 交易類型 | 是否支援回測 |
|
||||||
|
|-------------|-----------------------------------------------------------------------------------------------------------------------------------------|------------|------------------|
|
||||||
|
| grid | 第一代網格策略,提供更多的靈活性,但您需要準備庫存。 | maker | |
|
||||||
|
| grid2 | 第二代網格策略,可以將您的報價資產轉換成網格,支持基礎+報價模式。 | maker | |
|
||||||
|
| bollgrid | 實現了一個基本的網格策略,內置布林通道 (bollinger band)。 | maker | |
|
||||||
|
| xmaker | 跨交易所市場製造策略,它在另一邊對您的庫存風險進行對沖。 | maker | 不 |
|
||||||
|
| xnav | 這個策略幫助您記錄當前的淨資產價值。 | tool | 不 |
|
||||||
|
| xalign | 這個策略自動對齊您的餘額位置。 | tool | 不 |
|
||||||
|
| xfunding | 一種資金費率策略。 | funding | 不 |
|
||||||
|
| autoborrow | 這個策略使用保證金借入資產,幫助您保持最小餘額。 | tool | 不 |
|
||||||
|
| pivotshort | 這個策略找到支點低點並在價格突破前一低點時進行交易。 | long/short | |
|
||||||
|
| schedule | 這個策略定期以固定數量買賣,您可以將其用作單一的DCA,或補充像BNB這樣的費用資產。 | tool |
|
||||||
|
| irr | 這個策略基於預測的回報率開倉。 | long/short | |
|
||||||
|
| bollmaker | 這個策略持有長期多空倉位,在兩邊下單,並使用布林通道 (bollinger band) 控制倉位大小。| maker | |
|
||||||
|
| wall | 這個策略在訂單簿上創建一堵牆(大量訂單)。 | maker | 不 |
|
||||||
|
| scmaker | 這個市場製造策略是為穩定幣市場設計的,如USDC/USDT。 | maker | |
|
||||||
|
| drift | | long/short | |
|
||||||
|
| rsicross | 這個策略在快速 RSI 越過慢速 RSI 時開啟多倉,這是使用 v2 指標的演示策略。 | long/short | |
|
||||||
|
| marketcap | 這個策略實現了一個基於市值資本化重新平衡投資組合的策略。 | rebalance | 不 |
|
||||||
|
| supertrend | 這個策略使用 DEMA 和超級趨勢指標開啟多空倉位。 | long/short | |
|
||||||
|
| trendtrader | 這個策略基於趨勢線突破開啟多空倉位。 | long/short | |
|
||||||
|
| elliottwave | | long/short | |
|
||||||
|
| ewoDgtrd | | long/short | |
|
||||||
|
| fixedmaker | | maker | |
|
||||||
|
| factoryzoo | | long/short | |
|
||||||
|
| fmaker | | maker | |
|
||||||
|
| linregmaker | 一個基於線性回歸的市場製造商。 | maker | |
|
||||||
|
| convert | 轉換策略是一個幫助您將特定資產轉換為目標資產的工具。 | tool | 不 |
|
||||||
|
|
||||||
|
## 已支援交易所
|
||||||
|
|
||||||
|
- Binance Spot Exchange (以及 binance.us)
|
||||||
|
- OKEx Spot Exchange
|
||||||
|
- Kucoin Spot Exchange
|
||||||
|
- MAX Spot Exchange (台灣交易所)
|
||||||
|
- Bitget Exchange
|
||||||
|
- Bybit Exchange
|
||||||
|
|
||||||
|
## 文件
|
||||||
|
|
||||||
|
- [參考文件](doc/README.md)
|
||||||
|
|
||||||
|
## 要求
|
||||||
|
|
||||||
|
* Go SDK 1.20
|
||||||
|
* Linux / MacOS / Windows (WSL)
|
||||||
|
* 在您註冊賬戶後獲取您的交易所 API 密鑰和密碼(您可以選擇一個或多個交易所):
|
||||||
|
- MAX: https://max.maicoin.com/signup?r=c7982718
|
||||||
|
- Binance: https://accounts.binance.com/en/register?ref=38192708
|
||||||
|
- OKEx: https://www.okex.com/join/2412712?src=from:ios-share
|
||||||
|
- Kucoin: https://www.kucoin.com/ucenter/signup?rcode=r3KX2D4
|
||||||
|
|
||||||
|
這個項目由一小群人維護和支持。如果您想支持這個項目,請使用上面提供的鏈接和推薦碼在交易所註冊。
|
||||||
|
|
||||||
|
## 安裝
|
||||||
|
|
||||||
|
### 從 binary 安裝
|
||||||
|
|
||||||
|
以下 script 將幫助你設置文件和 dotenv 文件:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# 針對 Binance 交易所的網格交易策略
|
||||||
|
bash <(curl -s https://raw.githubusercontent.com/c9s/bbgo/main/scripts/setup-grid.sh) binance
|
||||||
|
|
||||||
|
# 針對 MAX 交易所的網格交易策略
|
||||||
|
bash <(curl -s https://raw.githubusercontent.com/c9s/bbgo/main/scripts/setup-grid.sh) max
|
||||||
|
|
||||||
|
# 針對 Binance 交易所的布林格網格交易策略
|
||||||
|
bash <(curl -s https://raw.githubusercontent.com/c9s/bbgo/main/scripts/setup-bollgrid.sh) binance
|
||||||
|
|
||||||
|
# 針對 MAX 交易所的布林格網格交易策略
|
||||||
|
bash <(curl -s https://raw.githubusercontent.com/c9s/bbgo/main/scripts/setup-bollgrid.sh) max
|
||||||
|
```
|
||||||
|
|
||||||
|
如果您已經在某處有配置,則可能適合您的是僅下載腳本:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bash <(curl -s https://raw.githubusercontent.com/c9s/bbgo/main/scripts/download.sh)
|
||||||
|
```
|
||||||
|
|
||||||
|
或者參考[發布頁面](https://github.com/c9s/bbgo/releases)並手動下載。
|
||||||
|
|
||||||
|
自 v2 起,我們添加了一個新的浮點實現 dnum,以支持更高精度的小數。要下載和設置,請參考[Dnum安裝](doc/topics/dnum-binary.md)
|
||||||
|
|
||||||
|
### 一鍵Linode StackScript
|
||||||
|
|
||||||
|
StackScript 允許您一鍵部署一個輕量級實體與 bbgo。
|
||||||
|
|
||||||
|
- BBGO grid on Binance <https://cloud.linode.com/stackscripts/950715>
|
||||||
|
- BBGO grid USDT/TWD on MAX <https://cloud.linode.com/stackscripts/793380>
|
||||||
|
- BBGO grid USDC/TWD on MAX <https://cloud.linode.com/stackscripts/797776>
|
||||||
|
- BBGO grid LINK/TWD on MAX <https://cloud.linode.com/stackscripts/797774>
|
||||||
|
- BBGO grid USDC/USDT on MAX <https://cloud.linode.com/stackscripts/797777>
|
||||||
|
- BBGO grid on MAX <https://cloud.linode.com/stackscripts/795788>
|
||||||
|
- BBGO bollmaker on Binance <https://cloud.linode.com/stackscripts/1002384>
|
||||||
|
|
||||||
|
### 從程式碼構建
|
||||||
|
參見[從程式碼構建](./doc/build-from-source.md)
|
||||||
|
|
||||||
|
## 配置
|
||||||
|
|
||||||
|
添加您的 dotenv 文件:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# 針對 Binance 交易所
|
||||||
|
BINANCE_API_KEY=
|
||||||
|
BINANCE_API_SECRET=
|
||||||
|
|
||||||
|
# 如果您想使用 binance.us,將此更改為1
|
||||||
|
BINANCE_US=0
|
||||||
|
|
||||||
|
# 針對 MAX 交易所
|
||||||
|
MAX_API_KEY=
|
||||||
|
MAX_API_SECRET=
|
||||||
|
|
||||||
|
# 針對 OKEx 交易所
|
||||||
|
OKEX_API_KEY=
|
||||||
|
OKEX_API_SECRET=
|
||||||
|
OKEX_API_PASSPHRASE
|
||||||
|
|
||||||
|
# 針對 Kucoin 交易所
|
||||||
|
KUCOIN_API_KEY=
|
||||||
|
KUCOIN_API_SECRET=
|
||||||
|
KUCOIN_API_PASSPHRASE=
|
||||||
|
KUCOIN_API_KEY_VERSION=2
|
||||||
|
|
||||||
|
# 針對 Bybit 交易所
|
||||||
|
BYBIT_API_KEY=
|
||||||
|
BYBIT_API_SECRET=
|
||||||
|
```
|
||||||
|
|
||||||
|
準備您的dotenv文件 `.env.local` 和 BBGO yaml 配置文件 `bbgo.yaml`。
|
||||||
|
|
||||||
|
要檢查可用的環境變量,請參見[環境變量](./doc/configuration/envvars.md)
|
||||||
|
|
||||||
|
最小的 bbgo.yaml 可以通過以下方式生成:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -o bbgo.yaml https://raw.githubusercontent.com/c9s/bbgo/main/config/minimal.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
要運行策略
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bbgo run
|
||||||
|
```
|
||||||
|
|
||||||
|
要啟動帶有前端儀表板的 bbgo
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bbgo run --enable-webserver
|
||||||
|
```
|
||||||
|
|
||||||
|
如果您想切換到另一個 dotenv 文件,您可以添加 `--dotenv` 選項或 `--config` :
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bbgo sync --dotenv .env.dev --config config/grid.yaml --session binance
|
||||||
|
```
|
||||||
|
|
||||||
|
要查詢轉賬歷史
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bbgo transfer-history --session max --asset USDT --since "2019-01-01"
|
||||||
|
```
|
||||||
|
|
||||||
|
<!--
|
||||||
|
計算盈虧:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bbgo pnl --exchange binance --asset BTC --since "2019-01-01"
|
||||||
|
```
|
||||||
|
--->
|
||||||
|
|
||||||
|
## 進階配置
|
||||||
|
|
||||||
|
### 與 Binance 同步系統時間
|
||||||
|
|
||||||
|
BBGO 提供了用於 UNIX 系統 / 子系統的腳本,以便與 Binance 同步日期。需要事先安裝 jq 和 bc。在 Ubuntu 中安裝相依套件,嘗試以下命令:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt install -y bc jq
|
||||||
|
```
|
||||||
|
|
||||||
|
要同步日期,嘗試
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo ./scripts/sync_time.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
您還可以將腳本添加到 crontab 中,這樣系統時間就可以定期與 Binance 同步
|
||||||
|
|
||||||
|
### Testnet (Paper Trading)
|
||||||
|
|
||||||
|
目前僅支持 [Binance Test Network](https://testnet.binance.vision)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export PAPER_TRADE=1
|
||||||
|
export DISABLE_MARKET_CACHE=1 # 測試網路支援的市場遠少於主網路
|
||||||
|
```
|
||||||
|
|
||||||
|
### 通知
|
||||||
|
|
||||||
|
- [設定 Telegram 通知](./doc/configuration/telegram.md)
|
||||||
|
- [設定 Slack 通知](./doc/configuration/slack.md)
|
||||||
|
|
||||||
|
### 同步交易資料
|
||||||
|
|
||||||
|
預設情況下, BBGO 不會從交易所同步您的交易資料,因此很難正確計算您的盈虧。
|
||||||
|
|
||||||
|
通過將交易和訂單同步到本地資料庫,您可以獲得一些好處,如盈虧計算、回測和資產計算。
|
||||||
|
|
||||||
|
您只能使用一個資料庫驅動程序 MySQL 或 SQLite 來存儲您的交易資料。
|
||||||
|
|
||||||
|
**注意**:SQLite 不完全支援,我們建議您使用 MySQL 而不是 SQLite。
|
||||||
|
|
||||||
|
配置 MySQL 資料庫
|
||||||
|
要使用 MySQL 資料庫進行資料同步,首先您需要安裝 MySQL 服務器:
|
||||||
|
#### Configure MySQL Database
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Ubuntu Linux
|
||||||
|
sudo apt-get install -y mysql-server
|
||||||
|
|
||||||
|
# 對於更新的 Ubuntu Linux
|
||||||
|
sudo apt install -y mysql-server
|
||||||
|
```
|
||||||
|
|
||||||
|
或者[在 docker 中執行它](https://hub.docker.com/_/mysql)
|
||||||
|
|
||||||
|
創建您的 mysql 資料庫:
|
||||||
|
|
||||||
|
Create your mysql database:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mysql -uroot -e "CREATE DATABASE bbgo CHARSET utf8"
|
||||||
|
```
|
||||||
|
|
||||||
|
然後將這些環境變數放入您的 `.env.local` 文件中:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
DB_DRIVER=mysql
|
||||||
|
DB_DSN="user:password@tcp(127.0.0.1:3306)/bbgo"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Configure Sqlite3 Database
|
||||||
|
|
||||||
|
配置 Sqlite3 資料庫
|
||||||
|
要使用 SQLite3 而不是 MySQL,只需將這些環境變數放入您的 `.env.local` 文件中:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
DB_DRIVER=sqlite3
|
||||||
|
DB_DSN=bbgo.sqlite3
|
||||||
|
```
|
||||||
|
|
||||||
|
## 同步您自己的交易資料
|
||||||
|
|
||||||
|
一旦您配置了資料庫,您就可以從交易所同步您自己的交易資料。
|
||||||
|
|
||||||
|
參見[配置私人交易資料同步](./doc/configuration/sync.md)
|
||||||
|
|
||||||
|
## 使用 Redis 在 BBGO session 之間保持持久性
|
||||||
|
|
||||||
|
要使用 Redis,首先您需要安裝您的 Redis 服務器
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# 對於 Ubuntu/Debian Linux
|
||||||
|
sudo apt-get install -y redis
|
||||||
|
|
||||||
|
# 對於更新的 Ubuntu/Debian Linux
|
||||||
|
sudo apt install -y redis
|
||||||
|
```
|
||||||
|
|
||||||
|
在您的 `bbgo.yaml` 中設定以下環境變數:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
persistence:
|
||||||
|
redis:
|
||||||
|
host: 127.0.0.1 # 指向您的 Redis 服務器的 IP 地址或主機名,如果與 BBGO 相同則為 127.0.0.1
|
||||||
|
port: 6379 # Redis 服務器的端口,預設為 6379
|
||||||
|
db: 0 # 使用的 DB 號碼。如果其他應用程序也在使用 Redis,您可以設置為另一個 DB 以避免衝突
|
||||||
|
```
|
||||||
|
|
||||||
|
## 內建策略
|
||||||
|
|
||||||
|
查看策略目錄 [strategy](pkg/strategy) 以獲得所有內置策略:
|
||||||
|
|
||||||
|
- `pricealert` 策略演示如何使用通知系統 [pricealert](pkg/strategy/pricealert)。參見[文件](./doc/strategy/pricealert.md).
|
||||||
|
- `buyandhold` 策略演示如何訂閱 kline 事件並提交市場訂單 [buyandhold](pkg/strategy/pricedrop)
|
||||||
|
- `bollgrid` 策略實現了一個基本的網格策略,使用內置的布林通道指標 [bollgrid](pkg/strategy/bollgrid)
|
||||||
|
- `grid` 策略實現了固定價格帶網格策略 [grid](pkg/strategy/grid)。參見[文件](./doc/strategy/grid.md).
|
||||||
|
- `supertrend` 策略使用 Supertrend 指標作為趨勢,並使用 DEMA 指標作為噪聲
|
||||||
|
過濾器 [supertrend](pkg/strategy/supertrend)。參見[文件](./doc/strategy/supertrend.md).
|
||||||
|
- `support` 策略使用具有高交易量的 K 線作為支撐 [support](pkg/strategy/support). 參見[文件](./doc/strategy/support.md).
|
||||||
|
- `flashcrash` 策略實現了一個捕捉閃崩的策略 [flashcrash](pkg/strategy/flashcrash)
|
||||||
|
- `marketcap`策略實現了一個基於市場資本化重新平衡投資組合的策略 [marketcap](pkg/strategy/marketcap). 參見[文件](./doc/strategy/marketcap.md).
|
||||||
|
- `pivotshort` - 以做空為重點的策略。
|
||||||
|
- `irr` - 回報率策略。
|
||||||
|
- `drift` - 漂移策略。
|
||||||
|
- `grid2` - 第二代網格策略。
|
||||||
|
|
||||||
|
要運行這些內置策略,只需修改配置文件以使配置適合您,例如,如果您想運行 `buyandhold` 策略
|
||||||
|
|
||||||
|
```sh
|
||||||
|
vim config/buyandhold.yaml
|
||||||
|
|
||||||
|
# 使用配置運行 bbgo
|
||||||
|
bbgo run --config config/buyandhold.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## 回測
|
||||||
|
|
||||||
|
參考[回測](./doc/topics/back-testing.md)
|
||||||
|
|
||||||
|
## 添加策略
|
||||||
|
|
||||||
|
參見[開發策略](./doc/topics/developing-strategy.md)
|
||||||
|
|
||||||
|
## 開發您自己的私人策略
|
||||||
|
|
||||||
|
創建您的 go 包,使用 `go mod`` 初始化存儲庫,並添加 bbgo 作為依賴:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go mod init
|
||||||
|
go get github.com/c9s/bbgo@main
|
||||||
|
```
|
||||||
|
|
||||||
|
建立您的 go 套件,使用 go mod 初始化存儲庫,並添加 bbgo 作為依賴:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
vim strategy.go
|
||||||
|
```
|
||||||
|
|
||||||
|
您可以從 <https://github.com/c9s/bbgo/blob/main/pkg/strategy/skeleton/strategy.go> 獲取策略骨架。 現在添加您的配置
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mkdir config
|
||||||
|
(cd config && curl -o bbgo.yaml https://raw.githubusercontent.com/c9s/bbgo/main/config/minimal.yaml)
|
||||||
|
```
|
||||||
|
|
||||||
|
將您的策略包路徑添加到配置文件 `config/bbgo.yaml`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
build:
|
||||||
|
dir: build
|
||||||
|
imports:
|
||||||
|
- github.com/your_id/your_swing
|
||||||
|
targets:
|
||||||
|
- name: swing-amd64-linux
|
||||||
|
os: linux
|
||||||
|
arch: amd64
|
||||||
|
- name: swing-amd64-darwin
|
||||||
|
os: darwin
|
||||||
|
arch: amd64
|
||||||
|
```
|
||||||
|
|
||||||
|
運行 `bbgo run` 命令,bbgo 將編譯一個導入您策略的包裝 binary 文件:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
dotenv -f .env.local -- bbgo run --config config/bbgo.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
或者您可以通過以下方式構建您自己的包裝 binary 文件
|
||||||
|
|
||||||
|
```shell
|
||||||
|
bbgo build --config config/bbgo.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
參考
|
||||||
|
- <https://github.com/narumiruna/bbgo-template>
|
||||||
|
- <https://github.com/narumiruna/bbgo-marketcap>
|
||||||
|
- <https://github.com/austin362667/shadow>
|
||||||
|
- <https://github.com/jnlin/bbgo-strategy-infinite-grid>
|
||||||
|
- <https://github.com/yubing744/trading-gpt>
|
||||||
|
|
||||||
|
## 命令用法
|
||||||
|
|
||||||
|
### 向特定交易所 session 提交訂單
|
||||||
|
|
||||||
|
```shell
|
||||||
|
bbgo submit-order --session=okex --symbol=OKBUSDT --side=buy --price=10.0 --quantity=1
|
||||||
|
```
|
||||||
|
|
||||||
|
### 列出特定交易所 session 的未平倉訂單
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bbgo list-orders open --session=okex --symbol=OKBUSDT
|
||||||
|
bbgo list-orders open --session=max --symbol=MAXUSDT
|
||||||
|
bbgo list-orders open --session=binance --symbol=BNBUSDT
|
||||||
|
```
|
||||||
|
|
||||||
|
### 取消一個未平倉訂單
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# 對於 okex,order id 和 symbol 都是必需的
|
||||||
|
bbgo cancel-order --session=okex --order-id=318223238325248000 --symbol=OKBUSDT
|
||||||
|
|
||||||
|
# 對於 max,您只需要提供您的 order id
|
||||||
|
bbgo cancel-order --session=max --order-id=1234566
|
||||||
|
```
|
||||||
|
|
||||||
|
### 除錯用戶資料流
|
||||||
|
|
||||||
|
```shell
|
||||||
|
bbgo userdatastream --session okex
|
||||||
|
bbgo userdatastream --session max
|
||||||
|
bbgo userdatastream --session binance
|
||||||
|
```
|
||||||
|
|
||||||
|
## 動態注入
|
||||||
|
|
||||||
|
為了最小化策略代碼,bbgo 支持動態依賴注入。
|
||||||
|
|
||||||
|
在執行您的策略之前,如果 bbgo 發現使用 bbgo 組件的嵌入字段,則會將組件注入到您的策略對象中。例如:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Strategy struct {
|
||||||
|
Symbol string `json:"symbol"
|
||||||
|
Market types.Market
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
支援的組件(目前僅限單一交易所策略)
|
||||||
|
|
||||||
|
- `*bbgo.ExchangeSession`
|
||||||
|
- `bbgo.OrderExecutor`
|
||||||
|
|
||||||
|
如果您的策略中有 `Symbol string` 字段,您的策略將被檢測為基於符號的策略,然後以下類型可以自動注入:
|
||||||
|
|
||||||
|
- `types.Market`
|
||||||
|
|
||||||
|
## 策略執行階段
|
||||||
|
|
||||||
|
1. 從配置文件加載配置。
|
||||||
|
1. 分配並初始化交易所 session 。
|
||||||
|
1. 將交易所 session 添加到環境(資料層)。
|
||||||
|
1. 使用給定的環境初始化交易者對象(邏輯層)。
|
||||||
|
1. 交易者初始化環境並啟動交易所連接。
|
||||||
|
1. 依次調用 strategy.Run() 方法。
|
||||||
|
|
||||||
|
## 交易所 API 範例
|
||||||
|
|
||||||
|
請查看範例 [examples](examples)
|
||||||
|
|
||||||
|
初始化 MAX API:
|
||||||
|
|
||||||
|
```go
|
||||||
|
key := os.Getenv("MAX_API_KEY")
|
||||||
|
secret := os.Getenv("MAX_API_SECRET")
|
||||||
|
|
||||||
|
maxRest := maxapi.NewRestClient(maxapi.ProductionAPIURL)
|
||||||
|
maxRest.Auth(key, secret)
|
||||||
|
```
|
||||||
|
|
||||||
|
創建用戶資料流以獲取訂單簿(深度)
|
||||||
|
|
||||||
|
```go
|
||||||
|
stream := max.NewStream(key, secret)
|
||||||
|
stream.Subscribe(types.BookChannel, symbol, types.SubscribeOptions{})
|
||||||
|
|
||||||
|
streambook := types.NewStreamBook(symbol)
|
||||||
|
streambook.BindStream(stream)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 部署
|
||||||
|
|
||||||
|
- [Helm Chart](./doc/deployment/helm-chart.md)
|
||||||
|
- 裸機或 VPS
|
||||||
|
|
||||||
|
## 開發
|
||||||
|
|
||||||
|
- [添加新交易所](./doc/development/adding-new-exchange.md)
|
||||||
|
- [遷移](./doc/development/migration.md)
|
||||||
|
|
||||||
|
### 設置您的本地存儲庫
|
||||||
|
|
||||||
|
1. 點擊 GitHub 儲存庫的 "Fork" 按鈕。
|
||||||
|
1. 將你分叉的儲存庫複製到 `$GOPATH/github.com/c9s/bbgo`。
|
||||||
|
1. 更改目錄到 `$GOPATH/github.com/c9s/bbgo`。
|
||||||
|
1. 創建一個分支並開始你的開發。
|
||||||
|
1. 測試你的更改。
|
||||||
|
1. 將你的更改推送到你的分叉。
|
||||||
|
1. 發送一個拉取請求。
|
||||||
|
|
||||||
|
### 測試桌面應用
|
||||||
|
|
||||||
|
對於 webview
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make embed && go run -tags web ./cmd/bbgo-webview
|
||||||
|
```
|
||||||
|
|
||||||
|
對於 lorca
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make embed && go run -tags web ./cmd/bbgo-lorca
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常見問題
|
||||||
|
|
||||||
|
### 什麼是倉位 ?
|
||||||
|
|
||||||
|
- 基礎貨幣 & 報價貨幣 <https://www.ig.com/au/glossary-trading-terms/base-currency-definition>
|
||||||
|
- 如何計算平均成本? <https://www.janushenderson.com/en-us/investor/planning/calculate-average-cost/>
|
||||||
|
|
||||||
|
### 尋找新策略?
|
||||||
|
|
||||||
|
你可以寫一篇關於 BBGO 的文章,主題不限,750-1500 字以換取,我可以為你實現策略(取決於複雜性和努力程度)。如果你有興趣,可以在 telegram <https://t.me/c123456789s> 或 twitter <https://twitter.com/c9s> 私訊我,我們可以討論。
|
||||||
|
|
||||||
|
### 添加新的加密貨幣交易所支持?
|
||||||
|
|
||||||
|
如果你希望 BBGO 支持一個目前 BBGO 未包含的新加密貨幣交易所,我們可以為你實現。成本是 10 ETH。如果你對此感興趣,請在 telegram <https://t.me/c123456789s> 私訊我。
|
||||||
|
|
||||||
|
## 社群
|
||||||
|
|
||||||
|
- Telegram <https://t.me/bbgo_intl>
|
||||||
|
- Telegram (台灣社群) <https://t.me/bbgocrypto>
|
||||||
|
- Twitter <https://twitter.com/bbgotrading>
|
||||||
|
|
||||||
|
## 貢獻
|
||||||
|
|
||||||
|
參見[貢獻](./CONTRIBUTING.md)
|
||||||
|
|
||||||
|
### 歡迎[抖內](https://opencollective.com/bbgo#backer)
|
||||||
|
|
||||||
|
<a href="https://opencollective.com/bbgo#backers" target="_blank"><img src="https://opencollective.com/bbgo/tiers/backer.svg?width=890"></a>
|
||||||
|
|
||||||
|
## BBGO 代幣經濟
|
||||||
|
|
||||||
|
為了支持 BBGO 的開發,我們創建了一個獎勵池來支持貢獻者,通過贈送 $BBG 代幣。查看詳情在 [$BBG 合約頁面](contracts/README.md) 和我們的[官方網站](https://bbgo.finance)
|
||||||
|
|
||||||
|
## 支持者
|
||||||
|
|
||||||
|
- GitBook
|
||||||
|
|
||||||
|
## 授權
|
||||||
|
|
||||||
|
AGPL 授權
|
26
app.json
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"name": "bbgo",
|
||||||
|
"description": "a modern cryptocurrency trading bot",
|
||||||
|
"repository": "https://github.com/c9s/bbgo",
|
||||||
|
"keywords": [
|
||||||
|
"trading",
|
||||||
|
"cryptocurrency",
|
||||||
|
"crypto"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"MAX_API_KEY": {
|
||||||
|
"description": "The API key of your MAX Exchange account"
|
||||||
|
},
|
||||||
|
"MAX_API_SECRET": {
|
||||||
|
"description": "The API secret of your MAX Exchange account"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"buildpacks": [
|
||||||
|
{
|
||||||
|
"url": "heroku/go"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"addons": [
|
||||||
|
"jawsdb:kitefin"
|
||||||
|
]
|
||||||
|
}
|
3
apps/backtest-report/.eslintrc.json
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"extends": "next/core-web-vitals"
|
||||||
|
}
|
35
apps/backtest-report/.gitignore
vendored
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
54
apps/backtest-report/README.md
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
Install the dependencies:
|
||||||
|
|
||||||
|
```
|
||||||
|
yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Create a symlink to your back-test report output directory:
|
||||||
|
|
||||||
|
```
|
||||||
|
(cd public && ln -s ../../../output output)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Generate some back-test reports:
|
||||||
|
|
||||||
|
```
|
||||||
|
(cd ../.. && go run ./cmd/bbgo backtest --config bollmaker_ethusdt.yaml --debug --output output --subdir)
|
||||||
|
```
|
||||||
|
|
||||||
|
Start the development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
# or
|
||||||
|
yarn dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||||
|
|
||||||
|
You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
|
||||||
|
|
||||||
|
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
|
||||||
|
|
||||||
|
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
To learn more about Next.js, take a look at the following resources:
|
||||||
|
|
||||||
|
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||||
|
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||||
|
|
||||||
|
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||||
|
|
||||||
|
## Deploy on Vercel
|
||||||
|
|
||||||
|
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||||
|
|
||||||
|
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
81
apps/backtest-report/components/OrderListTable.tsx
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
import {Button, Checkbox, Group, Table} from "@mantine/core";
|
||||||
|
import React, {useState} from "react";
|
||||||
|
import {Order} from "../types";
|
||||||
|
import moment from "moment";
|
||||||
|
|
||||||
|
interface OrderListTableProps {
|
||||||
|
orders: Order[];
|
||||||
|
onClick?: (order: Order) => void;
|
||||||
|
limit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const OrderListTable = (props: OrderListTableProps) => {
|
||||||
|
let orders = props.orders;
|
||||||
|
|
||||||
|
const [showCanceledOrders, setShowCanceledOrders] = useState(false);
|
||||||
|
const [limit, setLimit] = useState(props.limit || 100);
|
||||||
|
|
||||||
|
if (!showCanceledOrders) {
|
||||||
|
orders = orders.filter((order: Order) => {
|
||||||
|
return order.status != "CANCELED"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orders.length > limit) {
|
||||||
|
orders = orders.slice(0, limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = orders.map((order: Order) => (
|
||||||
|
<tr key={order.order_id} onClick={(e) => {
|
||||||
|
props.onClick ? props.onClick(order) : null;
|
||||||
|
const nodes = e.currentTarget?.parentNode?.querySelectorAll(".selected")
|
||||||
|
nodes?.forEach((node, i) => {
|
||||||
|
node.classList.remove("selected")
|
||||||
|
})
|
||||||
|
e.currentTarget.classList.add("selected")
|
||||||
|
}}>
|
||||||
|
<td>{order.order_id}</td>
|
||||||
|
<td>{order.symbol}</td>
|
||||||
|
<td>{order.side}</td>
|
||||||
|
<td>{order.order_type}</td>
|
||||||
|
<td>{order.price}</td>
|
||||||
|
<td>{order.quantity}</td>
|
||||||
|
<td>{order.status}</td>
|
||||||
|
<td>{formatDate(order.creation_time)}</td>
|
||||||
|
<td>{order.tag}</td>
|
||||||
|
</tr>
|
||||||
|
));
|
||||||
|
|
||||||
|
return <div>
|
||||||
|
<Group>
|
||||||
|
<Checkbox label="Show Canceled" checked={showCanceledOrders}
|
||||||
|
onChange={(event) => setShowCanceledOrders(event.currentTarget.checked)}/>
|
||||||
|
<Button onClick={() => {
|
||||||
|
setLimit(limit + 500)
|
||||||
|
}}>Load More</Button>
|
||||||
|
</Group>
|
||||||
|
<Table highlightOnHover striped>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Order ID</th>
|
||||||
|
<th>Symbol</th>
|
||||||
|
<th>Side</th>
|
||||||
|
<th>Order Type</th>
|
||||||
|
<th>Price</th>
|
||||||
|
<th>Quantity</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Creation Time</th>
|
||||||
|
<th>Tag</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>{rows}</tbody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDate = (d : Date) : string => {
|
||||||
|
return moment(d).format("MMM Do YY hh:mm:ss A Z");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default OrderListTable;
|
248
apps/backtest-report/components/ReportDetails.tsx
Normal file
|
@ -0,0 +1,248 @@
|
||||||
|
import React, {useEffect, useState} from 'react';
|
||||||
|
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
import TradingViewChart from './TradingViewChart';
|
||||||
|
|
||||||
|
import {BalanceMap, ReportSummary} from "../types";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Badge,
|
||||||
|
Container,
|
||||||
|
createStyles,
|
||||||
|
Grid,
|
||||||
|
Group,
|
||||||
|
Paper,
|
||||||
|
SimpleGrid,
|
||||||
|
Skeleton,
|
||||||
|
Table,
|
||||||
|
Text,
|
||||||
|
ThemeIcon,
|
||||||
|
Title
|
||||||
|
} from '@mantine/core';
|
||||||
|
|
||||||
|
import {ArrowDownRight, ArrowUpRight,} from 'tabler-icons-react';
|
||||||
|
|
||||||
|
const useStyles = createStyles((theme) => ({
|
||||||
|
root: {
|
||||||
|
paddingTop: theme.spacing.xl * 1.5,
|
||||||
|
paddingBottom: theme.spacing.xl * 1.5,
|
||||||
|
},
|
||||||
|
|
||||||
|
label: {
|
||||||
|
fontFamily: `Greycliff CF, ${theme.fontFamily}`,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface StatsGridIconsProps {
|
||||||
|
data: {
|
||||||
|
title: string;
|
||||||
|
value: string;
|
||||||
|
diff?: number
|
||||||
|
dir?: string;
|
||||||
|
desc?: string;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatsGridIcons({data}: StatsGridIconsProps) {
|
||||||
|
const {classes} = useStyles();
|
||||||
|
const stats = data.map((stat) => {
|
||||||
|
const DiffIcon = stat.diff && stat.diff > 0 ? ArrowUpRight : ArrowDownRight;
|
||||||
|
const DirIcon = stat.dir && stat.dir == "up" ? ArrowUpRight : ArrowDownRight;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper withBorder p="xs" radius="md" key={stat.title}>
|
||||||
|
<Group position="left">
|
||||||
|
<div>
|
||||||
|
<Text
|
||||||
|
color="dimmed"
|
||||||
|
weight={700}
|
||||||
|
size="xs"
|
||||||
|
className={classes.label}
|
||||||
|
>
|
||||||
|
{stat.title}
|
||||||
|
{stat.dir ?
|
||||||
|
<ThemeIcon
|
||||||
|
color="gray"
|
||||||
|
variant="light"
|
||||||
|
sx={(theme) => ({color: stat.dir == "up" ? theme.colors.teal[6] : theme.colors.red[6]})}
|
||||||
|
size={16}
|
||||||
|
radius="xs"
|
||||||
|
>
|
||||||
|
<DirIcon size={16}/>
|
||||||
|
</ThemeIcon>
|
||||||
|
: null}
|
||||||
|
</Text>
|
||||||
|
<Text weight={700} size="xs">
|
||||||
|
{stat.value}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{stat.diff ?
|
||||||
|
<ThemeIcon
|
||||||
|
color="gray"
|
||||||
|
variant="light"
|
||||||
|
sx={(theme) => ({color: stat.diff && stat.diff > 0 ? theme.colors.teal[6] : theme.colors.red[6]})}
|
||||||
|
size={38}
|
||||||
|
radius="md"
|
||||||
|
>
|
||||||
|
<DiffIcon size={28}/>
|
||||||
|
</ThemeIcon>
|
||||||
|
: null}
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{stat.diff ?
|
||||||
|
<Text color="dimmed" size="sm" mt="md">
|
||||||
|
<Text component="span" color={stat.diff && stat.diff > 0 ? 'teal' : 'red'} weight={700}>
|
||||||
|
{stat.diff}%
|
||||||
|
</Text>{' '}
|
||||||
|
{stat.diff && stat.diff > 0 ? 'increase' : 'decrease'} compared to last month
|
||||||
|
</Text> : null}
|
||||||
|
|
||||||
|
{stat.desc ? (
|
||||||
|
<Text color="dimmed" size="sm" mt="md">
|
||||||
|
{stat.desc}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SimpleGrid cols={5} breakpoints={[{maxWidth: 'sm', cols: 1, spacing: 'xl'}]} py="xl">
|
||||||
|
{stats}
|
||||||
|
</SimpleGrid>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
interface ReportDetailsProps {
|
||||||
|
basePath: string;
|
||||||
|
runID: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchReportSummary = (basePath: string, runID: string) => {
|
||||||
|
return fetch(
|
||||||
|
`${basePath}/${runID}/summary.json`,
|
||||||
|
)
|
||||||
|
.then((res) => res.json())
|
||||||
|
.catch((e) => {
|
||||||
|
console.error("failed to fetch index", e)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const skeleton = <Skeleton height={140} radius="md" animate={false}/>;
|
||||||
|
|
||||||
|
|
||||||
|
interface BalanceDetailsProps {
|
||||||
|
balances: BalanceMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BalanceDetails = (props: BalanceDetailsProps) => {
|
||||||
|
const rows = Object.entries(props.balances).map(([k, v]) => {
|
||||||
|
return <tr key={k}>
|
||||||
|
<td>{k}</td>
|
||||||
|
<td>{v.available}</td>
|
||||||
|
</tr>;
|
||||||
|
});
|
||||||
|
|
||||||
|
return <Table verticalSpacing="xs" fontSize="xs">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Currency</th>
|
||||||
|
<th>Balance</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>{rows}</tbody>
|
||||||
|
</Table>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ReportDetails = (props: ReportDetailsProps) => {
|
||||||
|
const [reportSummary, setReportSummary] = useState<ReportSummary>()
|
||||||
|
useEffect(() => {
|
||||||
|
fetchReportSummary(props.basePath, props.runID).then((summary: ReportSummary) => {
|
||||||
|
console.log("summary", props.runID, summary);
|
||||||
|
setReportSummary(summary)
|
||||||
|
})
|
||||||
|
}, [props.runID])
|
||||||
|
|
||||||
|
if (!reportSummary) {
|
||||||
|
return <div>
|
||||||
|
<h2>Loading {props.runID}</h2>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const strategyName = props.runID.split("_")[1]
|
||||||
|
const runID = props.runID.split("_").pop()
|
||||||
|
const totalProfit = Math.round(reportSummary.symbolReports.map((report) => report.pnl.profit).reduce((prev, cur) => prev + cur) * 100) / 100
|
||||||
|
const totalUnrealizedProfit = Math.round(reportSummary.symbolReports.map((report) => report.pnl.unrealizedProfit).reduce((prev, cur) => prev + cur) * 100) / 100
|
||||||
|
const totalTrades = reportSummary.symbolReports.map((report) => report.pnl.numTrades).reduce((prev, cur) => prev + cur) || 0
|
||||||
|
|
||||||
|
const totalBuyVolume = reportSummary.symbolReports.map((report) => report.pnl.buyVolume).reduce((prev, cur) => prev + cur) || 0
|
||||||
|
const totalSellVolume = reportSummary.symbolReports.map((report) => report.pnl.sellVolume).reduce((prev, cur) => prev + cur) || 0
|
||||||
|
|
||||||
|
const volumeUnit = reportSummary.symbolReports.length == 1 ? reportSummary.symbolReports[0].market.baseCurrency : '';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// size xl and padding xs
|
||||||
|
return <Container size={"xl"} px="xs">
|
||||||
|
<div>
|
||||||
|
<Badge key={strategyName} color="teal">Strategy: {strategyName}</Badge>
|
||||||
|
{reportSummary.sessions.map((session) => <Badge key={session} color="teal">Exchange: {session}</Badge>)}
|
||||||
|
{reportSummary.symbols.map((symbol) => <Badge key={symbol} color="teal">Symbol: {symbol}</Badge>)}
|
||||||
|
|
||||||
|
<Badge color="teal">{reportSummary.startTime.toString()} — {reportSummary.endTime.toString()} ~ {
|
||||||
|
moment.duration((new Date(reportSummary.endTime)).getTime() - (new Date(reportSummary.startTime)).getTime()).humanize()
|
||||||
|
}</Badge>
|
||||||
|
<Badge key={runID} color="gray" size="xs">Run ID: {runID}</Badge>
|
||||||
|
</div>
|
||||||
|
<StatsGridIcons data={[
|
||||||
|
{title: "Profit", value: "$" + totalProfit.toString(), dir: totalProfit >= 0 ? "up" : "down"},
|
||||||
|
{
|
||||||
|
title: "Unr. Profit",
|
||||||
|
value: totalUnrealizedProfit.toString() + "$",
|
||||||
|
dir: totalUnrealizedProfit > 0 ? "up" : "down"
|
||||||
|
},
|
||||||
|
{title: "Trades", value: totalTrades.toString()},
|
||||||
|
{title: "Buy Vol", value: totalBuyVolume.toString() + ` ${volumeUnit}`},
|
||||||
|
{title: "Sell Vol", value: totalSellVolume.toString() + ` ${volumeUnit}`},
|
||||||
|
]}/>
|
||||||
|
|
||||||
|
<Grid py="xl">
|
||||||
|
<Grid.Col xs={6}>
|
||||||
|
<Title order={6}>Initial Total Balances</Title>
|
||||||
|
<BalanceDetails balances={reportSummary.initialTotalBalances}/>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col xs={6}>
|
||||||
|
<Title order={6}>Final Total Balances</Title>
|
||||||
|
<BalanceDetails balances={reportSummary.finalTotalBalances}/>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<Skeleton height={300} radius="md" animate={false}/>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col xs={4}>{skeleton}</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
reportSummary.symbols.map((symbol: string, i: number) => {
|
||||||
|
return <TradingViewChart key={i} basePath={props.basePath} runID={props.runID} reportSummary={reportSummary}
|
||||||
|
symbol={symbol}/>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</Container>;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default ReportDetails;
|
79
apps/backtest-report/components/ReportNavigator.tsx
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
import React, {useEffect, useState} from 'react';
|
||||||
|
import {List, ThemeIcon} from '@mantine/core';
|
||||||
|
import {CircleCheck} from 'tabler-icons-react';
|
||||||
|
|
||||||
|
import {ReportEntry, ReportIndex} from '../types';
|
||||||
|
|
||||||
|
function fetchIndex(basePath: string, setter: (data: any) => void) {
|
||||||
|
return fetch(
|
||||||
|
`${basePath}/index.json`,
|
||||||
|
)
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => {
|
||||||
|
console.log("reportIndex", data);
|
||||||
|
data.runs.reverse() // last reports render first
|
||||||
|
setter(data);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error("failed to fetch index", e)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ReportNavigatorProps {
|
||||||
|
onSelect: (reportEntry: ReportEntry) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ReportNavigator = (props: ReportNavigatorProps) => {
|
||||||
|
const [isLoading, setLoading] = useState(false)
|
||||||
|
const [reportIndex, setReportIndex] = useState<ReportIndex>({runs: []});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading(true)
|
||||||
|
fetchIndex('/output', setReportIndex).then(() => {
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <div>Loading...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reportIndex.runs.length == 0) {
|
||||||
|
return <div>No back-test report data</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className={"report-navigator"}>
|
||||||
|
<List
|
||||||
|
spacing="xs"
|
||||||
|
size="xs"
|
||||||
|
center
|
||||||
|
icon={
|
||||||
|
<ThemeIcon color="teal" size={16} radius="xl">
|
||||||
|
<CircleCheck size={16}/>
|
||||||
|
</ThemeIcon>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
reportIndex.runs.map((entry) => {
|
||||||
|
return <List.Item key={entry.id} onClick={() => {
|
||||||
|
if (props.onSelect) {
|
||||||
|
props.onSelect(entry);
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
"textOverflow": "ellipsis",
|
||||||
|
"overflow": "hidden",
|
||||||
|
"inlineSize": "190px",
|
||||||
|
}}>
|
||||||
|
{entry.id}
|
||||||
|
</div>
|
||||||
|
</List.Item>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</List>
|
||||||
|
</div>;
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ReportNavigator;
|
|
@ -0,0 +1,44 @@
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const Handle = ({
|
||||||
|
error,
|
||||||
|
domain: [min, max],
|
||||||
|
handle: { id, value, percent = 0 },
|
||||||
|
disabled,
|
||||||
|
getHandleProps,
|
||||||
|
}) => {
|
||||||
|
const leftPosition = `${percent}%`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className='react_time_range__handle_wrapper' style={{ left: leftPosition }} {...getHandleProps(id)} />
|
||||||
|
<div
|
||||||
|
role='slider'
|
||||||
|
aria-valuemin={min}
|
||||||
|
aria-valuemax={max}
|
||||||
|
aria-valuenow={value}
|
||||||
|
className={`react_time_range__handle_container${disabled ? '__disabled' : ''}`}
|
||||||
|
style={{ left: leftPosition }}
|
||||||
|
>
|
||||||
|
<div className={`react_time_range__handle_marker${error ? '__error' : ''}`} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Handle.propTypes = {
|
||||||
|
domain: PropTypes.array.isRequired,
|
||||||
|
handle: PropTypes.shape({
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
value: PropTypes.number.isRequired,
|
||||||
|
percent: PropTypes.number.isRequired
|
||||||
|
}).isRequired,
|
||||||
|
getHandleProps: PropTypes.func.isRequired,
|
||||||
|
disabled: PropTypes.bool,
|
||||||
|
style: PropTypes.object,
|
||||||
|
}
|
||||||
|
|
||||||
|
Handle.defaultProps = { disabled: false }
|
||||||
|
|
||||||
|
export default Handle
|
|
@ -0,0 +1,32 @@
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const KeyboardHandle = ({ domain: [min, max], handle: { id, value, percent = 0 }, disabled, getHandleProps }) => (
|
||||||
|
<button
|
||||||
|
role='slider'
|
||||||
|
aria-valuemin={min}
|
||||||
|
aria-valuemax={max}
|
||||||
|
aria-valuenow={value}
|
||||||
|
className='react_time_range__keyboard_handle'
|
||||||
|
style={{
|
||||||
|
left: `${percent}%`,
|
||||||
|
backgroundColor: disabled ? '#666' : '#ffc400'
|
||||||
|
}}
|
||||||
|
{...getHandleProps(id)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
KeyboardHandle.propTypes = {
|
||||||
|
domain: PropTypes.array.isRequired,
|
||||||
|
handle: PropTypes.shape({
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
value: PropTypes.number.isRequired,
|
||||||
|
percent: PropTypes.number.isRequired
|
||||||
|
}).isRequired,
|
||||||
|
getHandleProps: PropTypes.func.isRequired,
|
||||||
|
disabled: PropTypes.bool
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyboardHandle.defaultProps = { disabled: false }
|
||||||
|
|
||||||
|
export default KeyboardHandle
|
|
@ -0,0 +1,13 @@
|
||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
export const SliderRail = ({ getRailProps }) => (
|
||||||
|
<>
|
||||||
|
<div className='react_time_range__rail__outer' {...getRailProps()} />
|
||||||
|
<div className='react_time_range__rail__inner' />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
|
SliderRail.propTypes = { getRailProps: PropTypes.func.isRequired }
|
||||||
|
|
||||||
|
export default SliderRail
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { getMinutes } from 'date-fns'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const Tick = ({ tick, count, format }) => {
|
||||||
|
const isFullHour = !getMinutes(tick.value)
|
||||||
|
|
||||||
|
const tickLabelStyle = {
|
||||||
|
marginLeft: `${-(100 / count) / 2}%`,
|
||||||
|
width: `${100 / count}%`,
|
||||||
|
left: `${tick.percent}%`,
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={`react_time_range__tick_marker${isFullHour ? '__large' : ''}`}
|
||||||
|
style={{ left: `${tick.percent}%` }}
|
||||||
|
/>
|
||||||
|
{isFullHour && (
|
||||||
|
<div className='react_time_range__tick_label' style={tickLabelStyle}>
|
||||||
|
{format(tick.value)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Tick.propTypes = {
|
||||||
|
tick: PropTypes.shape({
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
value: PropTypes.number.isRequired,
|
||||||
|
percent: PropTypes.number.isRequired
|
||||||
|
}).isRequired,
|
||||||
|
count: PropTypes.number.isRequired,
|
||||||
|
format: PropTypes.func.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
Tick.defaultProps = { format: d => d }
|
||||||
|
|
||||||
|
export default Tick
|
|
@ -0,0 +1,52 @@
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const getTrackConfig = ({ error, source, target, disabled }) => {
|
||||||
|
const basicStyle = {
|
||||||
|
left: `${source.percent}%`,
|
||||||
|
width: `calc(${target.percent - source.percent}% - 1px)`,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (disabled) return basicStyle
|
||||||
|
|
||||||
|
const coloredTrackStyle = error
|
||||||
|
? {
|
||||||
|
backgroundColor: 'rgba(214,0,11,0.5)',
|
||||||
|
borderLeft: '1px solid rgba(214,0,11,0.5)',
|
||||||
|
borderRight: '1px solid rgba(214,0,11,0.5)',
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
backgroundColor: 'rgba(98, 203, 102, 0.5)',
|
||||||
|
borderLeft: '1px solid #62CB66',
|
||||||
|
borderRight: '1px solid #62CB66',
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...basicStyle, ...coloredTrackStyle }
|
||||||
|
}
|
||||||
|
|
||||||
|
const Track = ({ error, source, target, getTrackProps, disabled }) => (
|
||||||
|
<div
|
||||||
|
className={`react_time_range__track${disabled ? '__disabled' : ''}`}
|
||||||
|
style={getTrackConfig({ error, source, target, disabled })}
|
||||||
|
{...getTrackProps()}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
Track.propTypes = {
|
||||||
|
source: PropTypes.shape({
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
value: PropTypes.number.isRequired,
|
||||||
|
percent: PropTypes.number.isRequired
|
||||||
|
}).isRequired,
|
||||||
|
target: PropTypes.shape({
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
value: PropTypes.number.isRequired,
|
||||||
|
percent: PropTypes.number.isRequired
|
||||||
|
}).isRequired,
|
||||||
|
getTrackProps: PropTypes.func.isRequired,
|
||||||
|
disabled: PropTypes.bool
|
||||||
|
}
|
||||||
|
|
||||||
|
Track.defaultProps = { disabled: false }
|
||||||
|
|
||||||
|
export default Track
|
256
apps/backtest-report/components/TimeRangeSlider/index.jsx
Normal file
|
@ -0,0 +1,256 @@
|
||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import {scaleTime} from 'd3-scale'
|
||||||
|
import {Handles, Rail, Slider, Ticks, Tracks} from 'react-compound-slider'
|
||||||
|
import {
|
||||||
|
addHours,
|
||||||
|
addMinutes,
|
||||||
|
differenceInMilliseconds,
|
||||||
|
endOfToday,
|
||||||
|
format,
|
||||||
|
isAfter,
|
||||||
|
isBefore,
|
||||||
|
set,
|
||||||
|
startOfToday,
|
||||||
|
} from 'date-fns'
|
||||||
|
|
||||||
|
import SliderRail from './components/SliderRail'
|
||||||
|
import Track from './components/Track'
|
||||||
|
import Tick from './components/Tick'
|
||||||
|
import Handle from './components/Handle'
|
||||||
|
|
||||||
|
|
||||||
|
const getTimelineConfig = (timelineStart, timelineLength) => (date) => {
|
||||||
|
const percent = differenceInMilliseconds(date, timelineStart) / timelineLength * 100
|
||||||
|
const value = Number(format(date, 'T'))
|
||||||
|
return {percent, value}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getFormattedBlockedIntervals = (blockedDates = [], [startTime, endTime]) => {
|
||||||
|
if (!blockedDates.length) return null
|
||||||
|
|
||||||
|
const timelineLength = differenceInMilliseconds(endTime, startTime)
|
||||||
|
const getConfig = getTimelineConfig(startTime, timelineLength)
|
||||||
|
|
||||||
|
const formattedBlockedDates = blockedDates.map((interval, index) => {
|
||||||
|
let {start, end} = interval
|
||||||
|
|
||||||
|
if (isBefore(start, startTime)) start = startTime
|
||||||
|
if (isAfter(end, endTime)) end = endTime
|
||||||
|
|
||||||
|
const source = getConfig(start)
|
||||||
|
const target = getConfig(end)
|
||||||
|
|
||||||
|
return {id: `blocked-track-${index}`, source, target}
|
||||||
|
})
|
||||||
|
|
||||||
|
return formattedBlockedDates
|
||||||
|
}
|
||||||
|
|
||||||
|
const getNowConfig = ([startTime, endTime]) => {
|
||||||
|
const timelineLength = differenceInMilliseconds(endTime, startTime)
|
||||||
|
const getConfig = getTimelineConfig(startTime, timelineLength)
|
||||||
|
|
||||||
|
const source = getConfig(new Date())
|
||||||
|
const target = getConfig(addMinutes(new Date(), 1))
|
||||||
|
|
||||||
|
return {id: 'now-track', source, target}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TimeRange extends React.Component {
|
||||||
|
get disabledIntervals() {
|
||||||
|
return getFormattedBlockedIntervals(this.props.disabledIntervals, this.props.timelineInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
get now() {
|
||||||
|
return getNowConfig(this.props.timelineInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange = newTime => {
|
||||||
|
const formattedNewTime = newTime.map(t => new Date(t))
|
||||||
|
if (this.props.onChange) {
|
||||||
|
this.props.onChange(formattedNewTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkIsSelectedIntervalNotValid = ([start, end], source, target) => {
|
||||||
|
const {value: startInterval} = source
|
||||||
|
const {value: endInterval} = target
|
||||||
|
|
||||||
|
if (startInterval > start && endInterval <= end || startInterval >= start && endInterval < end)
|
||||||
|
return true
|
||||||
|
if (start >= startInterval && end <= endInterval) return true
|
||||||
|
|
||||||
|
const isStartInBlockedInterval = start > startInterval && start < endInterval && end >= endInterval
|
||||||
|
const isEndInBlockedInterval = end < endInterval && end > startInterval && start <= startInterval
|
||||||
|
|
||||||
|
return isStartInBlockedInterval || isEndInBlockedInterval
|
||||||
|
}
|
||||||
|
|
||||||
|
onUpdate = newTime => {
|
||||||
|
const {onUpdate} = this.props
|
||||||
|
if (!onUpdate) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const disabledIntervals = this.disabledIntervals
|
||||||
|
|
||||||
|
if (disabledIntervals?.length) {
|
||||||
|
const isValuesNotValid = disabledIntervals.some(({source, target}) =>
|
||||||
|
this.checkIsSelectedIntervalNotValid(newTime, source, target))
|
||||||
|
const formattedNewTime = newTime.map(t => new Date(t))
|
||||||
|
onUpdate({error: isValuesNotValid, time: formattedNewTime})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedNewTime = newTime.map(t => new Date(t))
|
||||||
|
onUpdate({error: false, time: formattedNewTime})
|
||||||
|
}
|
||||||
|
|
||||||
|
getDateTicks = () => {
|
||||||
|
const {timelineInterval, ticksNumber} = this.props
|
||||||
|
return scaleTime().domain(timelineInterval).ticks(ticksNumber).map(t => +t)
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
sliderRailClassName,
|
||||||
|
timelineInterval,
|
||||||
|
selectedInterval,
|
||||||
|
containerClassName,
|
||||||
|
error,
|
||||||
|
step,
|
||||||
|
showNow,
|
||||||
|
formatTick,
|
||||||
|
mode,
|
||||||
|
} = this.props
|
||||||
|
|
||||||
|
const domain = timelineInterval.map(t => Number(t))
|
||||||
|
|
||||||
|
const disabledIntervals = this.disabledIntervals
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={containerClassName || 'react_time_range__time_range_container'}>
|
||||||
|
<Slider
|
||||||
|
mode={mode}
|
||||||
|
step={step}
|
||||||
|
domain={domain}
|
||||||
|
onUpdate={this.onUpdate}
|
||||||
|
onChange={this.onChange}
|
||||||
|
values={selectedInterval?.map(t => +t)}
|
||||||
|
rootStyle={{position: 'relative', width: '100%'}}
|
||||||
|
>
|
||||||
|
<Rail>
|
||||||
|
{({getRailProps}) =>
|
||||||
|
<SliderRail className={sliderRailClassName} getRailProps={getRailProps}/>}
|
||||||
|
</Rail>
|
||||||
|
|
||||||
|
<Handles>
|
||||||
|
{({handles, getHandleProps}) => (
|
||||||
|
<>
|
||||||
|
{handles.map(handle => (
|
||||||
|
<Handle
|
||||||
|
error={error}
|
||||||
|
key={handle.id}
|
||||||
|
handle={handle}
|
||||||
|
domain={domain}
|
||||||
|
getHandleProps={getHandleProps}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Handles>
|
||||||
|
|
||||||
|
<Tracks left={false} right={false}>
|
||||||
|
{({tracks, getTrackProps}) => (
|
||||||
|
<>
|
||||||
|
{tracks?.map(({id, source, target}) =>
|
||||||
|
<Track
|
||||||
|
error={error}
|
||||||
|
key={id}
|
||||||
|
source={source}
|
||||||
|
target={target}
|
||||||
|
getTrackProps={getTrackProps}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Tracks>
|
||||||
|
|
||||||
|
{disabledIntervals?.length && (
|
||||||
|
<Tracks left={false} right={false}>
|
||||||
|
{({getTrackProps}) => (
|
||||||
|
<>
|
||||||
|
{disabledIntervals.map(({id, source, target}) => (
|
||||||
|
<Track
|
||||||
|
key={id}
|
||||||
|
source={source}
|
||||||
|
target={target}
|
||||||
|
getTrackProps={getTrackProps}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Tracks>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showNow && (
|
||||||
|
<Tracks left={false} right={false}>
|
||||||
|
{({getTrackProps}) => (
|
||||||
|
<Track
|
||||||
|
key={this.now?.id}
|
||||||
|
source={this.now?.source}
|
||||||
|
target={this.now?.target}
|
||||||
|
getTrackProps={getTrackProps}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Tracks>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Ticks values={this.getDateTicks()}>
|
||||||
|
{({ticks}) => (
|
||||||
|
<>
|
||||||
|
{ticks.map(tick => (
|
||||||
|
<Tick
|
||||||
|
key={tick.id}
|
||||||
|
tick={tick}
|
||||||
|
count={ticks.length}
|
||||||
|
format={formatTick}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Ticks>
|
||||||
|
</Slider>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TimeRange.propTypes = {
|
||||||
|
ticksNumber: PropTypes.number.isRequired,
|
||||||
|
selectedInterval: PropTypes.arrayOf(PropTypes.object),
|
||||||
|
timelineInterval: PropTypes.arrayOf(PropTypes.object),
|
||||||
|
disabledIntervals: PropTypes.arrayOf(PropTypes.object),
|
||||||
|
containerClassName: PropTypes.string,
|
||||||
|
sliderRailClassName: PropTypes.string,
|
||||||
|
step: PropTypes.number,
|
||||||
|
formatTick: PropTypes.func,
|
||||||
|
}
|
||||||
|
|
||||||
|
TimeRange.defaultProps = {
|
||||||
|
selectedInterval: [
|
||||||
|
set(new Date(), {minutes: 0, seconds: 0, milliseconds: 0}),
|
||||||
|
set(addHours(new Date(), 1), {minutes: 0, seconds: 0, milliseconds: 0})
|
||||||
|
],
|
||||||
|
timelineInterval: [startOfToday(), endOfToday()],
|
||||||
|
formatTick: ms => format(new Date(ms), 'HH:mm'),
|
||||||
|
disabledIntervals: [],
|
||||||
|
step: 1000 * 60 * 30,
|
||||||
|
ticksNumber: 48,
|
||||||
|
error: false,
|
||||||
|
mode: 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TimeRange
|
126
apps/backtest-report/components/TimeRangeSlider/index.scss
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
$react-time-range--gray: #C8CACC;
|
||||||
|
$react-time-range--highlight-tap: #000000;
|
||||||
|
$react-time-range--rail-bg: #F5F7FA;
|
||||||
|
$react-time-range--handle-bg: #FFFFFF;
|
||||||
|
$react-time-range--handle-bg--disabled: #666;
|
||||||
|
$react-time-range--track--valid: rgb(98, 203, 102);
|
||||||
|
$react-time-range--track--not-valid: rgb(214, 0, 11);
|
||||||
|
$react-time-range--tick-label: #77828C;
|
||||||
|
$react-time-range--track--disabled: repeating-linear-gradient( -45deg, transparent, transparent 3px, #D0D3D7 4px, #D0D3D7 2px);
|
||||||
|
|
||||||
|
.react_time_range__time_range_container {
|
||||||
|
padding: 30px 52px 0 52px;
|
||||||
|
height: 90px;
|
||||||
|
// width: 90%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react_time_range__keyboard_handle {
|
||||||
|
position: absolute;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
z-index: 3;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: 1px 1px 1px 1px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.react_time_range__track {
|
||||||
|
position: absolute;
|
||||||
|
transform: translate(0%, -50%);
|
||||||
|
height: 50px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color .15s ease-in-out, border-color .15s ease;
|
||||||
|
z-index: 3;
|
||||||
|
&__disabled {
|
||||||
|
@extend .react_time_range__track;
|
||||||
|
z-index: 1;
|
||||||
|
border-left: 1px solid $react-time-range--gray;
|
||||||
|
border-right: 1px solid $react-time-range--gray;
|
||||||
|
background: $react-time-range--track--disabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.react_time_range__rail {
|
||||||
|
&__outer {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 50px;
|
||||||
|
transform: translate(0%, -50%);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
&__inner {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 50px;
|
||||||
|
transform: translate(0%, -50%);
|
||||||
|
pointer-events: none;
|
||||||
|
background-color: $react-time-range--rail-bg;
|
||||||
|
border-bottom: 1px solid $react-time-range--gray;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.react_time_range__handle {
|
||||||
|
&_wrapper {
|
||||||
|
position: absolute;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
-webkit-tap-highlight-color: $react-time-range--highlight-tap;
|
||||||
|
z-index: 6;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
&_container {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
z-index: 4;
|
||||||
|
width: 10px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 0 3px rgba(0,0,0, 0.4);
|
||||||
|
background-color: $react-time-range--handle-bg;
|
||||||
|
&__disabled {
|
||||||
|
@extend .react_time_range__handle_container;
|
||||||
|
background-color: $react-time-range--handle-bg--disabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&_marker {
|
||||||
|
width: 2px;
|
||||||
|
height: 12px;
|
||||||
|
margin: auto;
|
||||||
|
border-radius: 2px;
|
||||||
|
background-color: $react-time-range--track--valid;
|
||||||
|
transition: background-color .2s ease;
|
||||||
|
&__error {
|
||||||
|
@extend .react_time_range__handle_marker;
|
||||||
|
background-color: $react-time-range--track--not-valid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.react_time_range__tick {
|
||||||
|
&_marker {
|
||||||
|
position: absolute;
|
||||||
|
margin-top: 20px;
|
||||||
|
width: 1px;
|
||||||
|
height: 5px;
|
||||||
|
background-color: $react-time-range--gray;
|
||||||
|
z-index: 2;
|
||||||
|
&__large {
|
||||||
|
@extend .react_time_range__tick_marker;
|
||||||
|
margin-top: 15px;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&_label {
|
||||||
|
position: absolute;
|
||||||
|
margin-top: 28px;
|
||||||
|
font-size: 10px;
|
||||||
|
text-align: center;
|
||||||
|
z-index: 2;
|
||||||
|
color: $react-time-range--tick-label;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
}
|
709
apps/backtest-report/components/TradingViewChart.tsx
Normal file
|
@ -0,0 +1,709 @@
|
||||||
|
import React, {useEffect, useRef, useState} from 'react';
|
||||||
|
import {tsvParse} from "d3-dsv";
|
||||||
|
import {Checkbox, Group, SegmentedControl} from '@mantine/core';
|
||||||
|
|
||||||
|
// https://github.com/tradingview/lightweight-charts/issues/543
|
||||||
|
// const createChart = dynamic(() => import('lightweight-charts'));
|
||||||
|
import {createChart, CrosshairMode, MouseEventParams, TimeRange} from 'lightweight-charts';
|
||||||
|
import {Order, ReportSummary} from "../types";
|
||||||
|
import moment from "moment";
|
||||||
|
import {format} from 'date-fns';
|
||||||
|
|
||||||
|
import OrderListTable from './OrderListTable';
|
||||||
|
|
||||||
|
// See https://codesandbox.io/s/ve7w2?file=/src/App.js
|
||||||
|
import TimeRangeSlider from './TimeRangeSlider';
|
||||||
|
|
||||||
|
const parseKline = () => {
|
||||||
|
return (d: any) => {
|
||||||
|
d.startTime = new Date(Number(d.startTime) * 1000);
|
||||||
|
d.endTime = new Date(Number(d.endTime) * 1000);
|
||||||
|
d.time = d.startTime.getTime() / 1000;
|
||||||
|
|
||||||
|
for (const key in d) {
|
||||||
|
// convert number fields
|
||||||
|
if (Object.prototype.hasOwnProperty.call(d, key)) {
|
||||||
|
switch (key) {
|
||||||
|
case "open":
|
||||||
|
case "high":
|
||||||
|
case "low":
|
||||||
|
case "close":
|
||||||
|
case "volume":
|
||||||
|
d[key] = +d[key];
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return d;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectKLines = (klines: KLine[], startTime: Date, endTime: Date): KLine[] => {
|
||||||
|
const selected = [];
|
||||||
|
for (let i = 0; i < klines.length; i++) {
|
||||||
|
const k = klines[i]
|
||||||
|
if (k.startTime < startTime) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (k.startTime > endTime) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
selected.push(k)
|
||||||
|
}
|
||||||
|
|
||||||
|
return selected
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const parseOrder = () => {
|
||||||
|
return (d: any) => {
|
||||||
|
for (const key in d) {
|
||||||
|
// convert number fields
|
||||||
|
if (Object.prototype.hasOwnProperty.call(d, key)) {
|
||||||
|
switch (key) {
|
||||||
|
case "order_id":
|
||||||
|
case "price":
|
||||||
|
case "quantity":
|
||||||
|
d[key] = +d[key];
|
||||||
|
break;
|
||||||
|
case "update_time":
|
||||||
|
case "creation_time":
|
||||||
|
case "time":
|
||||||
|
d[key] = moment(d[key], 'dddd, DD MMM YYYY h:mm:ss').toDate();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return d;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsePosition = () => {
|
||||||
|
return (d: any) => {
|
||||||
|
for (const key in d) {
|
||||||
|
// convert number fields
|
||||||
|
if (Object.prototype.hasOwnProperty.call(d, key)) {
|
||||||
|
switch (key) {
|
||||||
|
case "accumulated_profit":
|
||||||
|
case "average_cost":
|
||||||
|
case "quote":
|
||||||
|
case "base":
|
||||||
|
d[key] = +d[key];
|
||||||
|
break
|
||||||
|
case "time":
|
||||||
|
d[key] = new Date(d[key]);
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return d;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchPositionHistory = (basePath: string, runID: string, filename: string) => {
|
||||||
|
return fetch(
|
||||||
|
`${basePath}/${runID}/${filename}`,
|
||||||
|
)
|
||||||
|
.then((response) => response.text())
|
||||||
|
.then((data) => tsvParse(data, parsePosition()) as Array<PositionHistoryEntry>)
|
||||||
|
.catch((e) => {
|
||||||
|
console.error("failed to fetch orders", e)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectPositionHistory = (data: PositionHistoryEntry[], since: Date, until: Date): PositionHistoryEntry[] => {
|
||||||
|
const entries: PositionHistoryEntry[] = [];
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
const d = data[i];
|
||||||
|
if (d.time < since || d.time > until) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.push(d)
|
||||||
|
}
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchOrders = (basePath: string, runID: string) => {
|
||||||
|
return fetch(
|
||||||
|
`${basePath}/${runID}/orders.tsv`,
|
||||||
|
)
|
||||||
|
.then((response) => response.text())
|
||||||
|
.then((data: string) => tsvParse(data, parseOrder()) as Array<Order>)
|
||||||
|
.catch((e) => {
|
||||||
|
console.error("failed to fetch orders", e)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectOrders = (data: Order[], since: Date, until: Date): Order[] => {
|
||||||
|
const entries: Order[] = [];
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
const d = data[i];
|
||||||
|
if (d.time && (d.time < since || d.time > until)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.push(d);
|
||||||
|
}
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseInterval = (s: string) => {
|
||||||
|
switch (s) {
|
||||||
|
case "1m":
|
||||||
|
return 60;
|
||||||
|
case "5m":
|
||||||
|
return 60 * 5;
|
||||||
|
case "15m":
|
||||||
|
return 60 * 15;
|
||||||
|
case "30m":
|
||||||
|
return 60 * 30;
|
||||||
|
case "1h":
|
||||||
|
return 60 * 60;
|
||||||
|
case "4h":
|
||||||
|
return 60 * 60 * 4;
|
||||||
|
case "6h":
|
||||||
|
return 60 * 60 * 6;
|
||||||
|
case "12h":
|
||||||
|
return 60 * 60 * 12;
|
||||||
|
case "1d":
|
||||||
|
return 60 * 60 * 24;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 60;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
interface Marker {
|
||||||
|
time: number;
|
||||||
|
position: string;
|
||||||
|
color: string;
|
||||||
|
shape: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ordersToMarkers = (interval: string, orders: Array<Order> | void): Array<Marker> => {
|
||||||
|
const markers: Array<Marker> = [];
|
||||||
|
const intervalSecs = parseInterval(interval);
|
||||||
|
|
||||||
|
if (!orders) {
|
||||||
|
return markers;
|
||||||
|
}
|
||||||
|
|
||||||
|
// var markers = [{ time: data[data.length - 48].time, position: 'aboveBar', color: '#f68410', shape: 'circle', text: 'D' }];
|
||||||
|
for (let i = 0; i < orders.length; i++) {
|
||||||
|
let order = orders[i];
|
||||||
|
let t = (order.update_time || order.time).getTime() / 1000.0;
|
||||||
|
let lastMarker = markers.length > 0 ? markers[markers.length - 1] : null;
|
||||||
|
if (lastMarker) {
|
||||||
|
let remainder = lastMarker.time % intervalSecs;
|
||||||
|
let startTime = lastMarker.time - remainder;
|
||||||
|
let endTime = (startTime + intervalSecs);
|
||||||
|
// skip the marker in the same interval of the last marker
|
||||||
|
if (t < endTime) {
|
||||||
|
// continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let text = '' + order.price
|
||||||
|
if (order.tag) {
|
||||||
|
text += " #" + order.tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (order.side) {
|
||||||
|
case "BUY":
|
||||||
|
markers.push({
|
||||||
|
time: t,
|
||||||
|
position: 'belowBar',
|
||||||
|
color: '#239D10',
|
||||||
|
shape: 'arrowUp',
|
||||||
|
text: text,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "SELL":
|
||||||
|
markers.push({
|
||||||
|
time: t,
|
||||||
|
position: 'aboveBar',
|
||||||
|
color: '#e91e63',
|
||||||
|
shape: 'arrowDown',
|
||||||
|
text: text,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return markers;
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeDuplicatedKLines = (klines: Array<KLine>): Array<KLine> => {
|
||||||
|
const newK = [];
|
||||||
|
for (let i = 0; i < klines.length; i++) {
|
||||||
|
const k = klines[i];
|
||||||
|
|
||||||
|
if (i > 0 && k.time === klines[i - 1].time) {
|
||||||
|
console.warn(`duplicated kline at index ${i}`, k)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
newK.push(k);
|
||||||
|
}
|
||||||
|
return newK;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchKLines(basePath: string, runID: string, symbol: string, interval: string, startTime: Date, endTime: Date) {
|
||||||
|
var duration = [moment(startTime).format('YYYYMMDD'), moment(endTime).format('YYYYMMDD')];
|
||||||
|
return fetch(
|
||||||
|
`${basePath}/shared/klines_${duration.join('-')}/${symbol}-${interval}.tsv`,
|
||||||
|
)
|
||||||
|
.then((response) => response.text())
|
||||||
|
.then((data) => tsvParse(data, parseKline()))
|
||||||
|
.catch((e) => {
|
||||||
|
console.error("failed to fetch klines", e)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KLine {
|
||||||
|
time: Date;
|
||||||
|
startTime: Date;
|
||||||
|
endTime: Date;
|
||||||
|
interval: string;
|
||||||
|
open: number;
|
||||||
|
high: number;
|
||||||
|
low: number;
|
||||||
|
close: number;
|
||||||
|
volume: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const klinesToVolumeData = (klines: Array<KLine>) => {
|
||||||
|
const volumes = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < klines.length; i++) {
|
||||||
|
const kline = klines[i];
|
||||||
|
volumes.push({
|
||||||
|
time: (kline.startTime.getTime() / 1000),
|
||||||
|
value: kline.volume,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return volumes;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
interface PositionHistoryEntry {
|
||||||
|
time: Date;
|
||||||
|
base: number;
|
||||||
|
quote: number;
|
||||||
|
average_cost: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const positionBaseHistoryToLineData = (interval: string, hs: Array<PositionHistoryEntry>) => {
|
||||||
|
const bases = [];
|
||||||
|
const intervalSeconds = parseInterval(interval);
|
||||||
|
for (let i = 0; i < hs.length; i++) {
|
||||||
|
const pos = hs[i];
|
||||||
|
if (!pos.time) {
|
||||||
|
console.warn('position history record missing time field', pos)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore duplicated entry
|
||||||
|
if (i > 0 && hs[i].time.getTime() === hs[i - 1].time.getTime()) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let t = pos.time.getTime() / 1000;
|
||||||
|
t = (t - t % intervalSeconds)
|
||||||
|
|
||||||
|
if (i > 0 && (pos.base === hs[i - 1].base)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bases.push({
|
||||||
|
time: t,
|
||||||
|
value: pos.base,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return bases;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const positionAverageCostHistoryToLineData = (interval: string, hs: Array<PositionHistoryEntry>) => {
|
||||||
|
const avgCosts = [];
|
||||||
|
const intervalSeconds = parseInterval(interval);
|
||||||
|
for (let i = 0; i < hs.length; i++) {
|
||||||
|
const pos = hs[i];
|
||||||
|
|
||||||
|
if (!pos.time) {
|
||||||
|
console.warn('position history record missing time field', pos)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore duplicated entry
|
||||||
|
if (i > 0 && hs[i].time.getTime() === hs[i - 1].time.getTime()) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let t = pos.time.getTime() / 1000;
|
||||||
|
t = (t - t % intervalSeconds)
|
||||||
|
|
||||||
|
if (i > 0 && (pos.average_cost === hs[i - 1].average_cost)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pos.base === 0) {
|
||||||
|
avgCosts.push({
|
||||||
|
time: t,
|
||||||
|
value: 0,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
avgCosts.push({
|
||||||
|
time: t,
|
||||||
|
value: pos.average_cost,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
return avgCosts;
|
||||||
|
}
|
||||||
|
|
||||||
|
const createBaseChart = (chartContainerRef: React.RefObject<any>) => {
|
||||||
|
return createChart(chartContainerRef.current, {
|
||||||
|
width: chartContainerRef.current.clientWidth,
|
||||||
|
height: chartContainerRef.current.clientHeight,
|
||||||
|
timeScale: {
|
||||||
|
timeVisible: true,
|
||||||
|
borderColor: '#D1D4DC',
|
||||||
|
},
|
||||||
|
rightPriceScale: {
|
||||||
|
borderColor: '#D1D4DC',
|
||||||
|
},
|
||||||
|
leftPriceScale: {
|
||||||
|
visible: true,
|
||||||
|
borderColor: 'rgba(197, 203, 206, 1)',
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
textColor: '#000',
|
||||||
|
},
|
||||||
|
crosshair: {
|
||||||
|
mode: CrosshairMode.Normal,
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
horzLines: {
|
||||||
|
color: '#F0F3FA',
|
||||||
|
},
|
||||||
|
vertLines: {
|
||||||
|
color: '#F0F3FA',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
interface TradingViewChartProps {
|
||||||
|
basePath: string;
|
||||||
|
runID: string;
|
||||||
|
reportSummary: ReportSummary;
|
||||||
|
symbol: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TradingViewChart = (props: TradingViewChartProps) => {
|
||||||
|
const chartContainerRef = useRef<any>();
|
||||||
|
const chart = useRef<any>();
|
||||||
|
const resizeObserver = useRef<any>();
|
||||||
|
const intervals = props.reportSummary.intervals || [];
|
||||||
|
|
||||||
|
intervals.sort((a, b) => {
|
||||||
|
const as = parseInterval(a)
|
||||||
|
const bs = parseInterval(b)
|
||||||
|
if (as < bs) {
|
||||||
|
return -1;
|
||||||
|
} else if (as > bs) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
})
|
||||||
|
|
||||||
|
const [currentInterval, setCurrentInterval] = useState(intervals.length > 0 ? intervals[intervals.length - 1] : '1m');
|
||||||
|
const [showPositionBase, setShowPositionBase] = useState(false);
|
||||||
|
const [showPositionAverageCost, setShowPositionAverageCost] = useState(false);
|
||||||
|
const [orders, setOrders] = useState<Order[]>([]);
|
||||||
|
|
||||||
|
const reportTimeRange = [
|
||||||
|
new Date(props.reportSummary.startTime),
|
||||||
|
new Date(props.reportSummary.endTime),
|
||||||
|
]
|
||||||
|
const [selectedTimeRange, setSelectedTimeRange] = useState(reportTimeRange)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!chartContainerRef.current || chartContainerRef.current.children.length > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chartData: any = {};
|
||||||
|
const fetchers = [];
|
||||||
|
const ordersFetcher = fetchOrders(props.basePath, props.runID).then((orders: Order[] | void) => {
|
||||||
|
if (orders) {
|
||||||
|
const markers = ordersToMarkers(currentInterval, selectOrders(orders, selectedTimeRange[0], selectedTimeRange[1]));
|
||||||
|
chartData.orders = orders;
|
||||||
|
chartData.markers = markers;
|
||||||
|
setOrders(orders);
|
||||||
|
}
|
||||||
|
return orders;
|
||||||
|
});
|
||||||
|
fetchers.push(ordersFetcher);
|
||||||
|
|
||||||
|
if (props.reportSummary && props.reportSummary.manifests && props.reportSummary.manifests.length === 1) {
|
||||||
|
const manifest = props.reportSummary?.manifests[0];
|
||||||
|
if (manifest && manifest.type === "strategyProperty" && manifest.strategyProperty === "position") {
|
||||||
|
const positionHistoryFetcher = fetchPositionHistory(props.basePath, props.runID, manifest.filename).then((data) => {
|
||||||
|
chartData.positionHistory = selectPositionHistory(data as PositionHistoryEntry[], selectedTimeRange[0], selectedTimeRange[1]);
|
||||||
|
// chartData.positionHistory = data;
|
||||||
|
});
|
||||||
|
fetchers.push(positionHistoryFetcher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const kLinesFetcher = fetchKLines(props.basePath, props.runID, props.symbol, currentInterval, new Date(props.reportSummary.startTime), new Date(props.reportSummary.endTime)).then((klines) => {
|
||||||
|
if (klines) {
|
||||||
|
chartData.allKLines = removeDuplicatedKLines(klines)
|
||||||
|
chartData.klines = selectKLines(chartData.allKLines, selectedTimeRange[0], selectedTimeRange[1])
|
||||||
|
}
|
||||||
|
});
|
||||||
|
fetchers.push(kLinesFetcher);
|
||||||
|
|
||||||
|
Promise.all(fetchers).then(() => {
|
||||||
|
console.log("createChart")
|
||||||
|
chart.current = createBaseChart(chartContainerRef);
|
||||||
|
|
||||||
|
const series = chart.current.addCandlestickSeries({
|
||||||
|
upColor: 'rgb(38,166,154)',
|
||||||
|
downColor: 'rgb(255,82,82)',
|
||||||
|
wickUpColor: 'rgb(38,166,154)',
|
||||||
|
wickDownColor: 'rgb(255,82,82)',
|
||||||
|
borderVisible: false,
|
||||||
|
});
|
||||||
|
series.setData(chartData.klines);
|
||||||
|
series.setMarkers(chartData.markers);
|
||||||
|
|
||||||
|
const ohlcLegend = createLegend(0, 'rgba(0, 0, 0, 1)');
|
||||||
|
chartContainerRef.current.appendChild(ohlcLegend);
|
||||||
|
|
||||||
|
const updateOHLCLegend = createOHLCLegendUpdater(ohlcLegend, "")
|
||||||
|
chart.current.subscribeCrosshairMove((param: MouseEventParams) => {
|
||||||
|
updateOHLCLegend(param.seriesPrices.get(series), param.time);
|
||||||
|
});
|
||||||
|
|
||||||
|
[9, 27, 99].forEach((w, i) => {
|
||||||
|
const emaValues = calculateEMA(chartData.klines, w)
|
||||||
|
const emaColor = 'rgba(' + w + ', ' + (111 - w) + ', 232, 0.9)'
|
||||||
|
const emaLine = chart.current.addLineSeries({
|
||||||
|
color: emaColor,
|
||||||
|
lineWidth: 1,
|
||||||
|
lastValueVisible: false,
|
||||||
|
});
|
||||||
|
emaLine.setData(emaValues);
|
||||||
|
|
||||||
|
const legend = createLegend(i + 1, emaColor)
|
||||||
|
chartContainerRef.current.appendChild(legend);
|
||||||
|
|
||||||
|
const updateLegendText = createLegendUpdater(legend, 'EMA ' + w)
|
||||||
|
|
||||||
|
updateLegendText(emaValues[emaValues.length - 1].value);
|
||||||
|
chart.current.subscribeCrosshairMove((param: MouseEventParams) => {
|
||||||
|
updateLegendText(param.seriesPrices.get(emaLine));
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
const volumeData = klinesToVolumeData(chartData.klines);
|
||||||
|
const volumeSeries = chart.current.addHistogramSeries({
|
||||||
|
color: '#182233',
|
||||||
|
lineWidth: 2,
|
||||||
|
priceFormat: {
|
||||||
|
type: 'volume',
|
||||||
|
},
|
||||||
|
overlay: true,
|
||||||
|
scaleMargins: {
|
||||||
|
top: 0.8,
|
||||||
|
bottom: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
volumeSeries.setData(volumeData);
|
||||||
|
|
||||||
|
if (chartData.positionHistory) {
|
||||||
|
if (showPositionAverageCost) {
|
||||||
|
const costLineSeries = chart.current.addLineSeries();
|
||||||
|
const costLine = positionAverageCostHistoryToLineData(currentInterval, chartData.positionHistory);
|
||||||
|
costLineSeries.setData(costLine);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showPositionBase) {
|
||||||
|
const baseLineSeries = chart.current.addLineSeries({
|
||||||
|
priceScaleId: 'left',
|
||||||
|
color: '#98338C',
|
||||||
|
});
|
||||||
|
const baseLine = positionBaseHistoryToLineData(currentInterval, chartData.positionHistory)
|
||||||
|
baseLineSeries.setData(baseLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chart.current.timeScale().fitContent();
|
||||||
|
|
||||||
|
/*
|
||||||
|
chart.current.timeScale().setVisibleRange({
|
||||||
|
from: (new Date(Date.UTC(2018, 0, 1, 0, 0, 0, 0))).getTime() / 1000,
|
||||||
|
to: (new Date(Date.UTC(2018, 1, 1, 0, 0, 0, 0))).getTime() / 1000,
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
|
// see:
|
||||||
|
// https://codesandbox.io/s/9inkb?file=/src/styles.css
|
||||||
|
resizeObserver.current = new ResizeObserver(entries => {
|
||||||
|
if (!chart.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {width, height} = entries[0].contentRect;
|
||||||
|
chart.current.applyOptions({width, height});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (chart.current) {
|
||||||
|
chart.current.timeScale().fitContent();
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
resizeObserver.current.observe(chartContainerRef.current);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
console.log("removeChart")
|
||||||
|
|
||||||
|
if (resizeObserver.current) {
|
||||||
|
resizeObserver.current.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chart.current) {
|
||||||
|
chart.current.remove();
|
||||||
|
}
|
||||||
|
if (chartContainerRef.current) {
|
||||||
|
// remove all the children because we created the legend elements
|
||||||
|
chartContainerRef.current.replaceChildren();
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
}, [props.runID, props.reportSummary, currentInterval, showPositionBase, showPositionAverageCost, selectedTimeRange])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Group>
|
||||||
|
<SegmentedControl
|
||||||
|
value={currentInterval}
|
||||||
|
data={intervals.map((interval) => {
|
||||||
|
return {label: interval, value: interval}
|
||||||
|
})}
|
||||||
|
onChange={setCurrentInterval}
|
||||||
|
/>
|
||||||
|
<Checkbox label="Position Base" checked={showPositionBase}
|
||||||
|
onChange={(event) => setShowPositionBase(event.currentTarget.checked)}/>
|
||||||
|
<Checkbox label="Position Average Cost" checked={showPositionAverageCost}
|
||||||
|
onChange={(event) => setShowPositionAverageCost(event.currentTarget.checked)}/>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<div ref={chartContainerRef} style={{'flex': 1, 'minHeight': 500, position: 'relative'}}>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TimeRangeSlider
|
||||||
|
selectedInterval={selectedTimeRange}
|
||||||
|
timelineInterval={reportTimeRange}
|
||||||
|
formatTick={(ms: Date) => format(new Date(ms), 'M d HH')}
|
||||||
|
step={1000 * parseInterval(currentInterval)}
|
||||||
|
onChange={(tr: any) => {
|
||||||
|
console.log("selectedTimeRange", tr)
|
||||||
|
setSelectedTimeRange(tr)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<OrderListTable orders={orders} onClick={(order) => {
|
||||||
|
console.log("selected order", order);
|
||||||
|
const visibleRange = chart.current.timeScale().getVisibleRange()
|
||||||
|
const seconds = parseInterval(currentInterval)
|
||||||
|
const bars = 12
|
||||||
|
const orderTime = order.creation_time.getTime() / 1000
|
||||||
|
const from = orderTime - bars * seconds
|
||||||
|
const to = orderTime + bars * seconds
|
||||||
|
|
||||||
|
console.log("orderTime", orderTime)
|
||||||
|
console.log("visibleRange", visibleRange)
|
||||||
|
console.log("setVisibleRange", from, to, to - from)
|
||||||
|
chart.current.timeScale().setVisibleRange({from, to} as TimeRange);
|
||||||
|
// chart.current.timeScale().scrollToPosition(20, true);
|
||||||
|
}}/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateEMA = (a: KLine[], r: number) => {
|
||||||
|
return a.map((k) => {
|
||||||
|
return {time: k.time, value: k.close}
|
||||||
|
}).reduce((p: any[], n: any, i: number) => {
|
||||||
|
if (i) {
|
||||||
|
const last = p[p.length - 1]
|
||||||
|
const v = 2 * n.value / (r + 1) + last.value * (r - 1) / (r + 1)
|
||||||
|
return p.concat({value: v, time: n.time})
|
||||||
|
}
|
||||||
|
|
||||||
|
return p
|
||||||
|
}, [{
|
||||||
|
value: a[0].close,
|
||||||
|
time: a[0].time
|
||||||
|
}])
|
||||||
|
}
|
||||||
|
|
||||||
|
const createLegend = (i: number, color: string) => {
|
||||||
|
const legend = document.createElement('div');
|
||||||
|
legend.className = 'ema-legend';
|
||||||
|
legend.style.display = 'block';
|
||||||
|
legend.style.position = 'absolute';
|
||||||
|
legend.style.left = 3 + 'px';
|
||||||
|
legend.style.color = color;
|
||||||
|
legend.style.zIndex = '99';
|
||||||
|
legend.style.top = 3 + (i * 22) + 'px';
|
||||||
|
return legend;
|
||||||
|
}
|
||||||
|
|
||||||
|
const createLegendUpdater = (legend: HTMLDivElement, prefix: string) => {
|
||||||
|
return (priceValue: any) => {
|
||||||
|
let val = '-';
|
||||||
|
if (priceValue !== undefined) {
|
||||||
|
val = (Math.round(priceValue * 100) / 100).toFixed(2);
|
||||||
|
}
|
||||||
|
legend.innerHTML = prefix + ' <span>' + val + '</span>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDate = (d: Date): string => {
|
||||||
|
return moment(d).format("MMM Do YY hh:mm:ss A Z");
|
||||||
|
}
|
||||||
|
|
||||||
|
const createOHLCLegendUpdater = (legend: HTMLDivElement, prefix: string) => {
|
||||||
|
return (param: any, time: any) => {
|
||||||
|
if (param) {
|
||||||
|
const change = Math.round((param.close - param.open) * 100.0) / 100.0
|
||||||
|
const changePercentage = Math.round((param.close - param.open) / param.close * 10000.0) / 100.0;
|
||||||
|
const ampl = Math.round((param.high - param.low) / param.low * 10000.0) / 100.0;
|
||||||
|
const t = new Date(time * 1000);
|
||||||
|
const dateStr = formatDate(t);
|
||||||
|
legend.innerHTML = prefix + ` O: ${param.open} H: ${param.high} L: ${param.low} C: ${param.close} CHG: ${change} (${changePercentage}%) AMP: ${ampl}% T: ${dateStr}`;
|
||||||
|
} else {
|
||||||
|
legend.innerHTML = prefix + ' O: - H: - L: - C: - T: -';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default TradingViewChart;
|
5
apps/backtest-report/next-env.d.ts
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
/// <reference types="next" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited
|
||||||
|
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
33
apps/backtest-report/next.config.js
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
|
||||||
|
// workaround for react financial charts
|
||||||
|
// https://github.com/react-financial/react-financial-charts/issues/606
|
||||||
|
|
||||||
|
// workaround for lightweight chart
|
||||||
|
// https://stackoverflow.com/questions/65936222/next-js-syntaxerror-unexpected-token-export
|
||||||
|
// https://stackoverflow.com/questions/66244968/cannot-use-import-statement-outside-a-module-error-when-importing-react-hook-m
|
||||||
|
const withTM = require('next-transpile-modules')([
|
||||||
|
'lightweight-charts',
|
||||||
|
'fancy-canvas',
|
||||||
|
// 'd3-array',
|
||||||
|
// 'd3-format',
|
||||||
|
// 'd3-time',
|
||||||
|
// 'd3-time-format',
|
||||||
|
// 'react-financial-charts',
|
||||||
|
// '@react-financial-charts/annotations',
|
||||||
|
// '@react-financial-charts/axes',
|
||||||
|
// '@react-financial-charts/coordinates',
|
||||||
|
// '@react-financial-charts/core',
|
||||||
|
// '@react-financial-charts/indicators',
|
||||||
|
// '@react-financial-charts/interactive',
|
||||||
|
// '@react-financial-charts/scales',
|
||||||
|
// '@react-financial-charts/series',
|
||||||
|
// '@react-financial-charts/tooltip',
|
||||||
|
// '@react-financial-charts/utils',
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
reactStrictMode: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = withTM(nextConfig);
|
42
apps/backtest-report/package.json
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
{
|
||||||
|
"name": "bbgo-backtest-report",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "yarn run next dev",
|
||||||
|
"build": "yarn run next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint",
|
||||||
|
"export": "yarn run next build && yarn run next export"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@mantine/core": "^4.2.5",
|
||||||
|
"@mantine/hooks": "^4.2.5",
|
||||||
|
"@mantine/next": "^4.2.5",
|
||||||
|
"d3-dsv": "^3.0.1",
|
||||||
|
"d3-format": "^3.1.0",
|
||||||
|
"d3-scale": "^4.0.2",
|
||||||
|
"d3-time-format": "^4.1.0",
|
||||||
|
"date-fns": "^2.28.0",
|
||||||
|
"lightweight-charts": "^3.8.0",
|
||||||
|
"moment": "^2.29.4",
|
||||||
|
"next": "12.1.6",
|
||||||
|
"react": "18.1.0",
|
||||||
|
"react-compound-slider": "^3.4.0",
|
||||||
|
"react-dom": "18.1.0",
|
||||||
|
"tabler-icons-react": "^1.48.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/d3-dsv": "^3.0.0",
|
||||||
|
"@types/d3-format": "^3.0.1",
|
||||||
|
"@types/d3-time-format": "^4.0.0",
|
||||||
|
"@types/node": "17.0.31",
|
||||||
|
"@types/react": "18.0.8",
|
||||||
|
"@types/react-dom": "18.0.3",
|
||||||
|
"eslint": "8.14.0",
|
||||||
|
"eslint-config-next": "12.1.6",
|
||||||
|
"next-transpile-modules": "^9.0.0",
|
||||||
|
"sass": "^1.53.0",
|
||||||
|
"typescript": "4.6.4"
|
||||||
|
}
|
||||||
|
}
|
28
apps/backtest-report/pages/_app.tsx
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import '../styles/globals.css'
|
||||||
|
import '../components/TimeRangeSlider/index.scss'
|
||||||
|
|
||||||
|
import type {AppProps} from 'next/app'
|
||||||
|
import Head from 'next/head';
|
||||||
|
|
||||||
|
import { MantineProvider } from '@mantine/core';
|
||||||
|
|
||||||
|
function MyApp({Component, pageProps}: AppProps) {
|
||||||
|
return <>
|
||||||
|
<Head>
|
||||||
|
<title>BBGO Back-test Report</title>
|
||||||
|
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width" />
|
||||||
|
</Head>
|
||||||
|
<MantineProvider
|
||||||
|
withGlobalStyles
|
||||||
|
withNormalizeCSS
|
||||||
|
theme={{
|
||||||
|
/** Put your mantine theme override here */
|
||||||
|
colorScheme: 'light',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Component {...pageProps} />
|
||||||
|
</MantineProvider>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MyApp
|
44
apps/backtest-report/pages/_document.tsx
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import Document, {DocumentContext, Head, Html, Main, NextScript} from 'next/document';
|
||||||
|
|
||||||
|
// ----- mantine setup
|
||||||
|
import {createStylesServer, ServerStyles} from '@mantine/next';
|
||||||
|
import {DocumentInitialProps} from "next/dist/shared/lib/utils";
|
||||||
|
|
||||||
|
// const getInitialProps = createGetInitialProps();
|
||||||
|
const stylesServer = createStylesServer();
|
||||||
|
// -----
|
||||||
|
|
||||||
|
class MyDocument extends Document {
|
||||||
|
// this is for mantine
|
||||||
|
// static getInitialProps = getInitialProps;
|
||||||
|
|
||||||
|
static async getInitialProps(ctx: DocumentContext): Promise<DocumentInitialProps> {
|
||||||
|
const initialProps = await Document.getInitialProps(ctx);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...initialProps,
|
||||||
|
|
||||||
|
// use bracket [] instead of () to fix the type error
|
||||||
|
styles: [
|
||||||
|
<>
|
||||||
|
{initialProps.styles}
|
||||||
|
<ServerStyles key="server-styles" html={initialProps.html} server={stylesServer}/>
|
||||||
|
</>
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Html lang="en">
|
||||||
|
<Head> </Head>
|
||||||
|
<body>
|
||||||
|
<Main/>
|
||||||
|
<NextScript/>
|
||||||
|
</body>
|
||||||
|
</Html>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MyDocument;
|
13
apps/backtest-report/pages/api/hello.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||||
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
|
|
||||||
|
type Data = {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function handler(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse<Data>
|
||||||
|
) {
|
||||||
|
res.status(200).json({ name: 'John Doe' })
|
||||||
|
}
|
51
apps/backtest-report/pages/index.tsx
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import type {NextPage} from 'next'
|
||||||
|
import Head from 'next/head'
|
||||||
|
import styles from '../styles/Home.module.css'
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import {AppShell, Header, Navbar, Text} from '@mantine/core';
|
||||||
|
|
||||||
|
import ReportDetails from '../components/ReportDetails';
|
||||||
|
import ReportNavigator from '../components/ReportNavigator';
|
||||||
|
import {useEffect, useState} from "react";
|
||||||
|
|
||||||
|
const Home: NextPage = () => {
|
||||||
|
const [currentReport, setCurrentReport] = useState<any>();
|
||||||
|
const { query } = useRouter();
|
||||||
|
const basePath = query.basePath ? query.basePath as string : '/output';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Head>
|
||||||
|
<title>BBGO Back-Test Report</title>
|
||||||
|
<meta name="description" content="Generated by create next app"/>
|
||||||
|
<link rel="icon" href="/favicon.ico"/>
|
||||||
|
</Head>
|
||||||
|
<main className={styles.main}>
|
||||||
|
<AppShell
|
||||||
|
padding="sm"
|
||||||
|
navbar={<Navbar width={{base: 250}} height={500} p="xs">
|
||||||
|
|
||||||
|
<ReportNavigator onSelect={(reportEntry) => {
|
||||||
|
setCurrentReport(reportEntry)
|
||||||
|
}}/>
|
||||||
|
|
||||||
|
</Navbar>}
|
||||||
|
header={
|
||||||
|
<Header height={50} p="sm">
|
||||||
|
<Text>BBGO Back-Test Report</Text>
|
||||||
|
</Header>
|
||||||
|
}
|
||||||
|
styles={(theme) => ({
|
||||||
|
main: {backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[8] : theme.colors.gray[0]},
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
currentReport ? <ReportDetails basePath={basePath} runID={currentReport.id}/> : null
|
||||||
|
}
|
||||||
|
</AppShell>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Home
|
BIN
apps/backtest-report/public/favicon.ico
Normal file
After Width: | Height: | Size: 25 KiB |
4
apps/backtest-report/public/vercel.svg
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<svg width="283" height="64" viewBox="0 0 283 64" fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
131
apps/backtest-report/src/Chart.js
vendored
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
|
||||||
|
import { format } from "d3-format";
|
||||||
|
import { timeFormat } from "d3-time-format";
|
||||||
|
|
||||||
|
import { ChartCanvas, Chart } from "react-stockcharts";
|
||||||
|
import {
|
||||||
|
CandlestickSeries,
|
||||||
|
LineSeries,
|
||||||
|
} from "react-stockcharts/lib/series";
|
||||||
|
import { XAxis, YAxis } from "react-stockcharts/lib/axes";
|
||||||
|
import {
|
||||||
|
CrossHairCursor,
|
||||||
|
EdgeIndicator,
|
||||||
|
CurrentCoordinate,
|
||||||
|
MouseCoordinateX,
|
||||||
|
MouseCoordinateY,
|
||||||
|
} from "react-stockcharts/lib/coordinates";
|
||||||
|
|
||||||
|
import { LabelAnnotation, Label, Annotate } from "react-stockcharts/lib/annotation";
|
||||||
|
import { discontinuousTimeScaleProvider } from "react-stockcharts/lib/scale";
|
||||||
|
import { OHLCTooltip, MovingAverageTooltip } from "react-stockcharts/lib/tooltip";
|
||||||
|
import { ema } from "react-stockcharts/lib/indicator";
|
||||||
|
// import { fitWidth } from "react-stockcharts/lib/helper";
|
||||||
|
import { last } from "react-stockcharts/lib/utils";
|
||||||
|
|
||||||
|
let CandleStickChartWithAnnotation = function(props) {
|
||||||
|
const annotationProps = {
|
||||||
|
fontFamily: "Glyphicons Halflings",
|
||||||
|
fontSize: 20,
|
||||||
|
fill: "#060F8F",
|
||||||
|
opacity: 0.8,
|
||||||
|
text: "\ue182",
|
||||||
|
y: ({ yScale }) => yScale.range()[0],
|
||||||
|
onClick: console.log.bind(console),
|
||||||
|
tooltip: d => timeFormat("%B")(d.date),
|
||||||
|
// onMouseOver: console.log.bind(console),
|
||||||
|
};
|
||||||
|
|
||||||
|
const margin = { left: 80, right: 80, top: 30, bottom: 50 };
|
||||||
|
const height = 400;
|
||||||
|
const { type, data: initialData, width, ratio } = props;
|
||||||
|
|
||||||
|
const [yAxisLabelX, yAxisLabelY] = [
|
||||||
|
width - margin.left - 40,
|
||||||
|
(height - margin.top - margin.bottom) / 2
|
||||||
|
];
|
||||||
|
|
||||||
|
const xScaleProvider = discontinuousTimeScaleProvider
|
||||||
|
.inputDateAccessor(d => d.date);
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
xScale,
|
||||||
|
xAccessor,
|
||||||
|
displayXAccessor,
|
||||||
|
} = xScaleProvider(initialData);
|
||||||
|
|
||||||
|
const start = xAccessor(last(data));
|
||||||
|
const end = xAccessor(data[Math.max(0, data.length - 150)]);
|
||||||
|
const xExtents = [start, end];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChartCanvas height={height}
|
||||||
|
ratio={ratio}
|
||||||
|
width={width}
|
||||||
|
margin={margin}
|
||||||
|
type={type}
|
||||||
|
seriesName="MSFT"
|
||||||
|
data={data}
|
||||||
|
xScale={xScale}
|
||||||
|
xAccessor={xAccessor}
|
||||||
|
displayXAccessor={displayXAccessor}
|
||||||
|
xExtents={xExtents}>
|
||||||
|
|
||||||
|
<Label x={(width - margin.left - margin.right) / 2} y={30}
|
||||||
|
fontSize="30" text="Chart title here" />
|
||||||
|
|
||||||
|
<Chart id={1}
|
||||||
|
yExtents={[d => [d.high, d.low]]}
|
||||||
|
padding={{ top: 10, bottom: 20 }}>
|
||||||
|
<XAxis axisAt="bottom" orient="bottom"/>
|
||||||
|
<MouseCoordinateX
|
||||||
|
at="bottom"
|
||||||
|
orient="bottom"
|
||||||
|
displayFormat={timeFormat("%Y-%m-%d")} />
|
||||||
|
<MouseCoordinateY
|
||||||
|
at="right"
|
||||||
|
orient="right"
|
||||||
|
displayFormat={format(".2f")} />
|
||||||
|
|
||||||
|
<Label x={(width - margin.left - margin.right) / 2} y={height - 45}
|
||||||
|
fontSize="12" text="XAxis Label here" />
|
||||||
|
|
||||||
|
<YAxis axisAt="right" orient="right" ticks={5} />
|
||||||
|
|
||||||
|
<Label x={yAxisLabelX} y={yAxisLabelY}
|
||||||
|
rotate={-90}
|
||||||
|
fontSize="12" text="YAxis Label here" />
|
||||||
|
|
||||||
|
<CandlestickSeries />
|
||||||
|
<EdgeIndicator itemType="last" orient="right" edgeAt="right"
|
||||||
|
yAccessor={d => d.close} fill={d => d.close > d.open ? "#6BA583" : "#FF0000"}/>
|
||||||
|
|
||||||
|
<OHLCTooltip origin={[-40, 0]}/>
|
||||||
|
|
||||||
|
<Annotate with={LabelAnnotation}
|
||||||
|
when={d => d.date.getDate() === 1 /* some condition */}
|
||||||
|
usingProps={annotationProps} />
|
||||||
|
|
||||||
|
</Chart>
|
||||||
|
<CrossHairCursor strokeDasharray="LongDashDot" />
|
||||||
|
|
||||||
|
</ChartCanvas>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
CandleStickChartWithAnnotation.propTypes = {
|
||||||
|
data: PropTypes.array.isRequired,
|
||||||
|
width: PropTypes.number.isRequired,
|
||||||
|
ratio: PropTypes.number.isRequired,
|
||||||
|
type: PropTypes.oneOf(["svg", "hybrid"]).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
CandleStickChartWithAnnotation.defaultProps = {
|
||||||
|
type: "svg",
|
||||||
|
};
|
||||||
|
*/
|
||||||
|
|
||||||
|
// CandleStickChartWithAnnotation = fitWidth(CandleStickChartWithAnnotation);
|
||||||
|
|
||||||
|
export default CandleStickChartWithAnnotation;
|
3
apps/backtest-report/src/utils.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import { tsvParse, csvParse } from "d3-dsv";
|
||||||
|
import { timeParse } from "d3-time-format";
|
||||||
|
|
78
apps/backtest-report/styles/Home.module.css
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
.main {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
padding: 2rem 0;
|
||||||
|
border-top: 1px solid #eaeaea;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer a {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code {
|
||||||
|
background: #fafafa;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 0.75rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
|
||||||
|
Bitstream Vera Sans Mono, Courier New, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
max-width: 800px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
margin: 1rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
text-align: left;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
border: 1px solid #eaeaea;
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: color 0.15s ease, border-color 0.15s ease;
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover,
|
||||||
|
.card:focus,
|
||||||
|
.card:active {
|
||||||
|
color: #0070f3;
|
||||||
|
border-color: #0070f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card h2 {
|
||||||
|
margin: 0 0 1rem 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
height: 1em;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.grid {
|
||||||
|
width: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
16
apps/backtest-report/styles/globals.css
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
|
||||||
|
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
20
apps/backtest-report/tsconfig.json
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
3
apps/backtest-report/types/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export * from './report';
|
||||||
|
export * from './order';
|
||||||
|
export * from './market';
|
16
apps/backtest-report/types/market.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
export interface Market {
|
||||||
|
symbol: string;
|
||||||
|
localSymbol: string;
|
||||||
|
pricePrecision: number;
|
||||||
|
volumePrecision: number;
|
||||||
|
quoteCurrency: string;
|
||||||
|
baseCurrency: string;
|
||||||
|
minNotional: number;
|
||||||
|
minAmount: number;
|
||||||
|
minQuantity: number;
|
||||||
|
maxQuantity: number;
|
||||||
|
stepSize: number;
|
||||||
|
minPrice: number;
|
||||||
|
maxPrice: number;
|
||||||
|
tickSize: number;
|
||||||
|
}
|
15
apps/backtest-report/types/order.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
|
||||||
|
export interface Order {
|
||||||
|
order_id: number;
|
||||||
|
order_type: string;
|
||||||
|
side: string;
|
||||||
|
symbol: string;
|
||||||
|
price: number;
|
||||||
|
quantity: number;
|
||||||
|
executed_quantity: number;
|
||||||
|
status: string;
|
||||||
|
update_time: Date;
|
||||||
|
creation_time: Date;
|
||||||
|
time?: Date;
|
||||||
|
tag?: string;
|
||||||
|
}
|
76
apps/backtest-report/types/report.ts
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
import {Market} from './market';
|
||||||
|
|
||||||
|
export interface ReportEntry {
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
config: object;
|
||||||
|
|
||||||
|
time: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReportIndex {
|
||||||
|
runs: Array<ReportEntry>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Balance {
|
||||||
|
currency: string;
|
||||||
|
available: number;
|
||||||
|
locked: number;
|
||||||
|
borrowed: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BalanceMap {
|
||||||
|
[currency: string]: Balance;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReportSummary {
|
||||||
|
startTime: Date;
|
||||||
|
endTime: Date;
|
||||||
|
sessions: string[];
|
||||||
|
symbols: string[];
|
||||||
|
intervals: string[];
|
||||||
|
initialTotalBalances: BalanceMap;
|
||||||
|
finalTotalBalances: BalanceMap;
|
||||||
|
symbolReports: SymbolReport[];
|
||||||
|
manifests: Manifest[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SymbolReport {
|
||||||
|
exchange: string;
|
||||||
|
symbol: string;
|
||||||
|
market: Market;
|
||||||
|
lastPrice: number;
|
||||||
|
startPrice: number;
|
||||||
|
pnl: PnL;
|
||||||
|
initialBalances: BalanceMap;
|
||||||
|
finalBalances: BalanceMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface Manifest {
|
||||||
|
type: string;
|
||||||
|
filename: string;
|
||||||
|
strategyID: string;
|
||||||
|
strategyInstance: string;
|
||||||
|
strategyProperty: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CurrencyFeeMap {
|
||||||
|
[currency: string]: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PnL {
|
||||||
|
lastPrice: number;
|
||||||
|
startTime: Date;
|
||||||
|
symbol: string;
|
||||||
|
market: Market;
|
||||||
|
numTrades: number;
|
||||||
|
profit: number;
|
||||||
|
netProfit: number;
|
||||||
|
unrealizedProfit: number;
|
||||||
|
averageCost: number;
|
||||||
|
buyVolume: number;
|
||||||
|
sellVolume: number;
|
||||||
|
feeInUSD: number;
|
||||||
|
currencyFees: CurrencyFeeMap;
|
||||||
|
}
|
2483
apps/backtest-report/yarn.lock
Normal file
34
apps/frontend/.gitignore
vendored
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
3
apps/frontend/.prettierignore
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
.next/
|
||||||
|
out/
|
||||||
|
node_modules/
|
3
apps/frontend/.prettierrc.json
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"singleQuote": true
|
||||||
|
}
|
34
apps/frontend/README.md
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
First, run the development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
# or
|
||||||
|
yarn dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||||
|
|
||||||
|
You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.
|
||||||
|
|
||||||
|
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`.
|
||||||
|
|
||||||
|
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
To learn more about Next.js, take a look at the following resources:
|
||||||
|
|
||||||
|
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||||
|
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||||
|
|
||||||
|
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||||
|
|
||||||
|
## Deploy on Vercel
|
||||||
|
|
||||||
|
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/import?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||||
|
|
||||||
|
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
190
apps/frontend/api/bbgo.ts
Normal file
|
@ -0,0 +1,190 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const baseURL =
|
||||||
|
process.env.NODE_ENV === 'development' ? 'http://localhost:8080' : '';
|
||||||
|
|
||||||
|
export function ping(cb) {
|
||||||
|
return axios.get(baseURL + '/api/ping').then((response) => {
|
||||||
|
cb(response.data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function queryOutboundIP(cb) {
|
||||||
|
return axios.get<any>(baseURL + '/api/outbound-ip').then((response) => {
|
||||||
|
cb(response.data.outboundIP);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function triggerSync() {
|
||||||
|
return axios.post<any>(baseURL + '/api/environment/sync');
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SyncStatus {
|
||||||
|
SyncNotStarted = 0,
|
||||||
|
Syncing = 1,
|
||||||
|
SyncDone = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function querySyncStatus(): Promise<SyncStatus> {
|
||||||
|
const resp = await axios.get<any>(baseURL + '/api/environment/syncing');
|
||||||
|
return resp.data.syncing;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function testDatabaseConnection(params, cb) {
|
||||||
|
return axios.post(baseURL + '/api/setup/test-db', params).then((response) => {
|
||||||
|
cb(response.data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function configureDatabase(params, cb) {
|
||||||
|
return axios
|
||||||
|
.post(baseURL + '/api/setup/configure-db', params)
|
||||||
|
.then((response) => {
|
||||||
|
cb(response.data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveConfig(cb) {
|
||||||
|
return axios.post(baseURL + '/api/setup/save').then((response) => {
|
||||||
|
cb(response.data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setupRestart(cb) {
|
||||||
|
return axios.post(baseURL + '/api/setup/restart').then((response) => {
|
||||||
|
cb(response.data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addSession(session, cb) {
|
||||||
|
return axios.post(baseURL + '/api/sessions', session).then((response) => {
|
||||||
|
cb(response.data || []);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function attachStrategyOn(session, strategyID, strategy, cb) {
|
||||||
|
return axios
|
||||||
|
.post(
|
||||||
|
baseURL + `/api/setup/strategy/single/${strategyID}/session/${session}`,
|
||||||
|
strategy
|
||||||
|
)
|
||||||
|
.then((response) => {
|
||||||
|
cb(response.data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function testSessionConnection(session, cb) {
|
||||||
|
return axios
|
||||||
|
.post(baseURL + '/api/sessions/test', session)
|
||||||
|
.then((response) => {
|
||||||
|
cb(response.data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function queryStrategies(cb) {
|
||||||
|
return axios.get<any>(baseURL + '/api/strategies/single').then((response) => {
|
||||||
|
cb(response.data.strategies || []);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function querySessions(cb) {
|
||||||
|
return axios.get<any>(baseURL + '/api/sessions', {}).then((response) => {
|
||||||
|
cb(response.data.sessions || []);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function querySessionSymbols(sessionName, cb) {
|
||||||
|
return axios
|
||||||
|
.get<any>(baseURL + `/api/sessions/${sessionName}/symbols`, {})
|
||||||
|
.then((response) => {
|
||||||
|
cb(response.data?.symbols || []);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function queryTrades(params, cb) {
|
||||||
|
axios
|
||||||
|
.get<any>(baseURL + '/api/trades', { params: params })
|
||||||
|
.then((response) => {
|
||||||
|
cb(response.data.trades || []);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function queryClosedOrders(params, cb) {
|
||||||
|
axios
|
||||||
|
.get<any>(baseURL + '/api/orders/closed', { params: params })
|
||||||
|
.then((response) => {
|
||||||
|
cb(response.data.orders || []);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function queryAssets(cb) {
|
||||||
|
axios.get<any>(baseURL + '/api/assets', {}).then((response) => {
|
||||||
|
cb(response.data.assets || []);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function queryTradingVolume(params, cb) {
|
||||||
|
axios
|
||||||
|
.get<any>(baseURL + '/api/trading-volume', { params: params })
|
||||||
|
.then((response) => {
|
||||||
|
cb(response.data.tradingVolumes || []);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GridStrategy {
|
||||||
|
id: string;
|
||||||
|
instanceID: string;
|
||||||
|
strategy: string;
|
||||||
|
grid: {
|
||||||
|
symbol: string;
|
||||||
|
};
|
||||||
|
stats: GridStats;
|
||||||
|
status: string;
|
||||||
|
startTime: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GridStats {
|
||||||
|
oneDayArbs: number;
|
||||||
|
totalArbs: number;
|
||||||
|
investment: number;
|
||||||
|
totalProfits: number;
|
||||||
|
gridProfits: number;
|
||||||
|
floatingPNL: number;
|
||||||
|
currentPrice: number;
|
||||||
|
lowestPrice: number;
|
||||||
|
highestPrice: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function queryStrategiesMetrics(): Promise<GridStrategy[]> {
|
||||||
|
const temp = {
|
||||||
|
id: 'uuid',
|
||||||
|
instanceID: 'testInstanceID',
|
||||||
|
strategy: 'grid',
|
||||||
|
grid: {
|
||||||
|
symbol: 'BTCUSDT',
|
||||||
|
},
|
||||||
|
stats: {
|
||||||
|
oneDayArbs: 0,
|
||||||
|
totalArbs: 3,
|
||||||
|
investment: 100,
|
||||||
|
totalProfits: 5.6,
|
||||||
|
gridProfits: 2.5,
|
||||||
|
floatingPNL: 3.1,
|
||||||
|
currentPrice: 29000,
|
||||||
|
lowestPrice: 25000,
|
||||||
|
highestPrice: 35000,
|
||||||
|
},
|
||||||
|
status: 'RUNNING',
|
||||||
|
startTime: 1654938187102,
|
||||||
|
};
|
||||||
|
|
||||||
|
const testArr = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < 11; i++) {
|
||||||
|
const cloned = { ...temp };
|
||||||
|
cloned.id = 'uuid' + i;
|
||||||
|
testArr.push(cloned);
|
||||||
|
}
|
||||||
|
|
||||||
|
return testArr;
|
||||||
|
}
|
342
apps/frontend/components/AddExchangeSessionForm.js
Normal file
|
@ -0,0 +1,342 @@
|
||||||
|
import React from 'react';
|
||||||
|
import Grid from '@mui/material/Grid';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import TextField from '@mui/material/TextField';
|
||||||
|
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||||
|
import FormHelperText from '@mui/material/FormHelperText';
|
||||||
|
import InputLabel from '@mui/material/InputLabel';
|
||||||
|
import FormControl from '@mui/material/FormControl';
|
||||||
|
import InputAdornment from '@mui/material/InputAdornment';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
|
||||||
|
import Checkbox from '@mui/material/Checkbox';
|
||||||
|
import Select from '@mui/material/Select';
|
||||||
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
|
import FilledInput from '@mui/material/FilledInput';
|
||||||
|
|
||||||
|
import Alert from '@mui/lab/Alert';
|
||||||
|
import VisibilityOff from '@mui/icons-material/VisibilityOff';
|
||||||
|
import Visibility from '@mui/icons-material/Visibility';
|
||||||
|
|
||||||
|
import { addSession, testSessionConnection } from '../api/bbgo';
|
||||||
|
|
||||||
|
import { makeStyles } from '@mui/styles';
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
formControl: {
|
||||||
|
marginTop: theme.spacing(1),
|
||||||
|
marginBottom: theme.spacing(1),
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
buttons: {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
marginTop: theme.spacing(2),
|
||||||
|
paddingTop: theme.spacing(2),
|
||||||
|
paddingBottom: theme.spacing(2),
|
||||||
|
'& > *': {
|
||||||
|
marginLeft: theme.spacing(1),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default function AddExchangeSessionForm({ onBack, onAdded }) {
|
||||||
|
const classes = useStyles();
|
||||||
|
const [exchangeType, setExchangeType] = React.useState('max');
|
||||||
|
const [customSessionName, setCustomSessionName] = React.useState(false);
|
||||||
|
const [sessionName, setSessionName] = React.useState(exchangeType);
|
||||||
|
|
||||||
|
const [testing, setTesting] = React.useState(false);
|
||||||
|
const [testResponse, setTestResponse] = React.useState(null);
|
||||||
|
const [response, setResponse] = React.useState(null);
|
||||||
|
|
||||||
|
const [apiKey, setApiKey] = React.useState('');
|
||||||
|
const [apiSecret, setApiSecret] = React.useState('');
|
||||||
|
|
||||||
|
const [showApiKey, setShowApiKey] = React.useState(false);
|
||||||
|
const [showApiSecret, setShowApiSecret] = React.useState(false);
|
||||||
|
|
||||||
|
const [isMargin, setIsMargin] = React.useState(false);
|
||||||
|
const [isIsolatedMargin, setIsIsolatedMargin] = React.useState(false);
|
||||||
|
const [isolatedMarginSymbol, setIsolatedMarginSymbol] = React.useState('');
|
||||||
|
|
||||||
|
const resetTestResponse = () => {
|
||||||
|
setTestResponse(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExchangeTypeChange = (event) => {
|
||||||
|
setExchangeType(event.target.value);
|
||||||
|
setSessionName(event.target.value);
|
||||||
|
resetTestResponse();
|
||||||
|
};
|
||||||
|
|
||||||
|
const createSessionConfig = () => {
|
||||||
|
return {
|
||||||
|
name: sessionName,
|
||||||
|
exchange: exchangeType,
|
||||||
|
key: apiKey,
|
||||||
|
secret: apiSecret,
|
||||||
|
margin: isMargin,
|
||||||
|
envVarPrefix: exchangeType.toUpperCase(),
|
||||||
|
isolatedMargin: isIsolatedMargin,
|
||||||
|
isolatedMarginSymbol: isolatedMarginSymbol,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAdd = (event) => {
|
||||||
|
const payload = createSessionConfig();
|
||||||
|
addSession(payload, (response) => {
|
||||||
|
setResponse(response);
|
||||||
|
if (onAdded) {
|
||||||
|
setTimeout(onAdded, 3000);
|
||||||
|
}
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
setResponse(error.response);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTestConnection = (event) => {
|
||||||
|
const payload = createSessionConfig();
|
||||||
|
setTesting(true);
|
||||||
|
testSessionConnection(payload, (response) => {
|
||||||
|
console.log(response);
|
||||||
|
setTesting(false);
|
||||||
|
setTestResponse(response);
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
setTesting(false);
|
||||||
|
setTestResponse(error.response);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
Add Exchange Session
|
||||||
|
</Typography>
|
||||||
|
<Grid container spacing={3}>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<FormControl className={classes.formControl}>
|
||||||
|
<InputLabel id="exchange-type-select-label">Exchange</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId="exchange-type-select-label"
|
||||||
|
id="exchange-type-select"
|
||||||
|
value={exchangeType}
|
||||||
|
onChange={handleExchangeTypeChange}
|
||||||
|
>
|
||||||
|
<MenuItem value={'binance'}>Binance</MenuItem>
|
||||||
|
<MenuItem value={'max'}>Max</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<TextField
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
label="Session Name"
|
||||||
|
fullWidth
|
||||||
|
required
|
||||||
|
disabled={!customSessionName}
|
||||||
|
onChange={(event) => {
|
||||||
|
setSessionName(event.target.value);
|
||||||
|
}}
|
||||||
|
value={sessionName}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
color="secondary"
|
||||||
|
name="custom_session_name"
|
||||||
|
onChange={(event) => {
|
||||||
|
setCustomSessionName(event.target.checked);
|
||||||
|
}}
|
||||||
|
value="1"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Custom exchange session name"
|
||||||
|
/>
|
||||||
|
<FormHelperText id="session-name-helper-text">
|
||||||
|
By default, the session name will be the exchange type name, e.g.{' '}
|
||||||
|
<code>binance</code> or <code>max</code>.<br />
|
||||||
|
If you're using multiple exchange sessions, you might need to custom
|
||||||
|
the session name. <br />
|
||||||
|
This is for advanced users.
|
||||||
|
</FormHelperText>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<FormControl fullWidth variant="filled">
|
||||||
|
<InputLabel htmlFor="apiKey">API Key</InputLabel>
|
||||||
|
<FilledInput
|
||||||
|
id="apiKey"
|
||||||
|
type={showApiKey ? 'text' : 'password'}
|
||||||
|
value={apiKey}
|
||||||
|
endAdornment={
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<IconButton
|
||||||
|
aria-label="toggle key visibility"
|
||||||
|
onClick={() => {
|
||||||
|
setShowApiKey(!showApiKey);
|
||||||
|
}}
|
||||||
|
onMouseDown={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
}}
|
||||||
|
edge="end"
|
||||||
|
>
|
||||||
|
{showApiKey ? <Visibility /> : <VisibilityOff />}
|
||||||
|
</IconButton>
|
||||||
|
</InputAdornment>
|
||||||
|
}
|
||||||
|
onChange={(event) => {
|
||||||
|
setApiKey(event.target.value);
|
||||||
|
resetTestResponse();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<FormControl fullWidth variant="filled">
|
||||||
|
<InputLabel htmlFor="apiSecret">API Secret</InputLabel>
|
||||||
|
<FilledInput
|
||||||
|
id="apiSecret"
|
||||||
|
type={showApiSecret ? 'text' : 'password'}
|
||||||
|
value={apiSecret}
|
||||||
|
endAdornment={
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<IconButton
|
||||||
|
aria-label="toggle key visibility"
|
||||||
|
onClick={() => {
|
||||||
|
setShowApiSecret(!showApiSecret);
|
||||||
|
}}
|
||||||
|
onMouseDown={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
}}
|
||||||
|
edge="end"
|
||||||
|
>
|
||||||
|
{showApiSecret ? <Visibility /> : <VisibilityOff />}
|
||||||
|
</IconButton>
|
||||||
|
</InputAdornment>
|
||||||
|
}
|
||||||
|
onChange={(event) => {
|
||||||
|
setApiSecret(event.target.value);
|
||||||
|
resetTestResponse();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{exchangeType === 'binance' ? (
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
color="secondary"
|
||||||
|
name="isMargin"
|
||||||
|
onChange={(event) => {
|
||||||
|
setIsMargin(event.target.checked);
|
||||||
|
resetTestResponse();
|
||||||
|
}}
|
||||||
|
value="1"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Use margin trading."
|
||||||
|
/>
|
||||||
|
<FormHelperText id="isMargin-helper-text">
|
||||||
|
This is only available for Binance. Please use the leverage at
|
||||||
|
your own risk.
|
||||||
|
</FormHelperText>
|
||||||
|
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
color="secondary"
|
||||||
|
name="isIsolatedMargin"
|
||||||
|
onChange={(event) => {
|
||||||
|
setIsIsolatedMargin(event.target.checked);
|
||||||
|
resetTestResponse();
|
||||||
|
}}
|
||||||
|
value="1"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Use isolated margin trading."
|
||||||
|
/>
|
||||||
|
<FormHelperText id="isIsolatedMargin-helper-text">
|
||||||
|
This is only available for Binance. If this is set, you can only
|
||||||
|
trade one symbol with one session.
|
||||||
|
</FormHelperText>
|
||||||
|
|
||||||
|
{isIsolatedMargin ? (
|
||||||
|
<TextField
|
||||||
|
id="isolatedMarginSymbol"
|
||||||
|
name="isolatedMarginSymbol"
|
||||||
|
label="Isolated Margin Symbol"
|
||||||
|
onChange={(event) => {
|
||||||
|
setIsolatedMarginSymbol(event.target.value);
|
||||||
|
resetTestResponse();
|
||||||
|
}}
|
||||||
|
fullWidth
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</Grid>
|
||||||
|
) : null}
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<div className={classes.buttons}>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (onBack) {
|
||||||
|
onBack();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
onClick={handleTestConnection}
|
||||||
|
disabled={testing}
|
||||||
|
>
|
||||||
|
{testing ? 'Testing' : 'Test Connection'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button variant="contained" color="primary" onClick={handleAdd}>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{testResponse ? (
|
||||||
|
testResponse.error ? (
|
||||||
|
<Box m={2}>
|
||||||
|
<Alert severity="error">{testResponse.error}</Alert>
|
||||||
|
</Box>
|
||||||
|
) : testResponse.success ? (
|
||||||
|
<Box m={2}>
|
||||||
|
<Alert severity="success">Connection Test Succeeded</Alert>
|
||||||
|
</Box>
|
||||||
|
) : null
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{response ? (
|
||||||
|
response.error ? (
|
||||||
|
<Box m={2}>
|
||||||
|
<Alert severity="error">{response.error}</Alert>
|
||||||
|
</Box>
|
||||||
|
) : response.success ? (
|
||||||
|
<Box m={2}>
|
||||||
|
<Alert severity="success">Exchange Session Added</Alert>
|
||||||
|
</Box>
|
||||||
|
) : null
|
||||||
|
) : null}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
209
apps/frontend/components/ConfigureDatabaseForm.js
Normal file
|
@ -0,0 +1,209 @@
|
||||||
|
import React from 'react';
|
||||||
|
import Grid from '@mui/material/Grid';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import TextField from '@mui/material/TextField';
|
||||||
|
import FormHelperText from '@mui/material/FormHelperText';
|
||||||
|
import Radio from '@mui/material/Radio';
|
||||||
|
import RadioGroup from '@mui/material/RadioGroup';
|
||||||
|
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||||
|
import FormControl from '@mui/material/FormControl';
|
||||||
|
import FormLabel from '@mui/material/FormLabel';
|
||||||
|
|
||||||
|
import Alert from '@mui/lab/Alert';
|
||||||
|
|
||||||
|
import { configureDatabase, testDatabaseConnection } from '../api/bbgo';
|
||||||
|
|
||||||
|
import { makeStyles } from '@mui/styles';
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
formControl: {
|
||||||
|
marginTop: theme.spacing(1),
|
||||||
|
marginBottom: theme.spacing(1),
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
buttons: {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
marginTop: theme.spacing(2),
|
||||||
|
paddingTop: theme.spacing(2),
|
||||||
|
paddingBottom: theme.spacing(2),
|
||||||
|
'& > *': {
|
||||||
|
marginLeft: theme.spacing(1),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default function ConfigureDatabaseForm({ onConfigured }) {
|
||||||
|
const classes = useStyles();
|
||||||
|
|
||||||
|
const [mysqlURL, setMysqlURL] = React.useState(
|
||||||
|
'root@tcp(127.0.0.1:3306)/bbgo'
|
||||||
|
);
|
||||||
|
|
||||||
|
const [driver, setDriver] = React.useState('sqlite3');
|
||||||
|
const [testing, setTesting] = React.useState(false);
|
||||||
|
const [testResponse, setTestResponse] = React.useState(null);
|
||||||
|
const [configured, setConfigured] = React.useState(false);
|
||||||
|
|
||||||
|
const getDSN = () => (driver === 'sqlite3' ? 'file:bbgo.sqlite3' : mysqlURL);
|
||||||
|
|
||||||
|
const resetTestResponse = () => {
|
||||||
|
setTestResponse(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfigureDatabase = (event) => {
|
||||||
|
const dsn = getDSN();
|
||||||
|
|
||||||
|
configureDatabase({ driver, dsn }, (response) => {
|
||||||
|
console.log(response);
|
||||||
|
setTesting(false);
|
||||||
|
setTestResponse(response);
|
||||||
|
if (onConfigured) {
|
||||||
|
setConfigured(true);
|
||||||
|
setTimeout(onConfigured, 3000);
|
||||||
|
}
|
||||||
|
}).catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
setTesting(false);
|
||||||
|
setTestResponse(err.response.data);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTestConnection = (event) => {
|
||||||
|
const dsn = getDSN();
|
||||||
|
|
||||||
|
setTesting(true);
|
||||||
|
testDatabaseConnection({ driver, dsn }, (response) => {
|
||||||
|
console.log(response);
|
||||||
|
setTesting(false);
|
||||||
|
setTestResponse(response);
|
||||||
|
}).catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
setTesting(false);
|
||||||
|
setTestResponse(err.response.data);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
Configure Database
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography variant="body1" gutterBottom>
|
||||||
|
If you have database installed on your machine, you can enter the DSN
|
||||||
|
string in the following field. Please note this is optional, you CAN
|
||||||
|
SKIP this step.
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Grid container spacing={3}>
|
||||||
|
<Grid item xs={12} sm={4}>
|
||||||
|
<Box m={6}>
|
||||||
|
<FormControl component="fieldset" required={true}>
|
||||||
|
<FormLabel component="legend">Database Driver</FormLabel>
|
||||||
|
<RadioGroup
|
||||||
|
aria-label="driver"
|
||||||
|
name="driver"
|
||||||
|
value={driver}
|
||||||
|
onChange={(event) => {
|
||||||
|
setDriver(event.target.value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormControlLabel
|
||||||
|
value="sqlite3"
|
||||||
|
control={<Radio />}
|
||||||
|
label="Standard (Default)"
|
||||||
|
/>
|
||||||
|
<FormControlLabel
|
||||||
|
value="mysql"
|
||||||
|
control={<Radio />}
|
||||||
|
label="MySQL"
|
||||||
|
/>
|
||||||
|
</RadioGroup>
|
||||||
|
</FormControl>
|
||||||
|
<FormHelperText></FormHelperText>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{driver === 'mysql' ? (
|
||||||
|
<Grid item xs={12} sm={8}>
|
||||||
|
<TextField
|
||||||
|
id="mysql_url"
|
||||||
|
name="mysql_url"
|
||||||
|
label="MySQL Data Source Name"
|
||||||
|
fullWidth
|
||||||
|
required
|
||||||
|
defaultValue={mysqlURL}
|
||||||
|
onChange={(event) => {
|
||||||
|
setMysqlURL(event.target.value);
|
||||||
|
resetTestResponse();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FormHelperText>MySQL DSN</FormHelperText>
|
||||||
|
|
||||||
|
<Typography variant="body1" gutterBottom>
|
||||||
|
If you have database installed on your machine, you can enter the
|
||||||
|
DSN string like the following format:
|
||||||
|
<br />
|
||||||
|
<pre>
|
||||||
|
<code>root:password@tcp(127.0.0.1:3306)/bbgo</code>
|
||||||
|
</pre>
|
||||||
|
<br />
|
||||||
|
Be sure to create your database before using it. You need to
|
||||||
|
execute the following statement to create a database:
|
||||||
|
<br />
|
||||||
|
<pre>
|
||||||
|
<code>CREATE DATABASE bbgo CHARSET utf8;</code>
|
||||||
|
</pre>
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
) : (
|
||||||
|
<Grid item xs={12} sm={8}>
|
||||||
|
<Box m={6}>
|
||||||
|
<Typography variant="body1" gutterBottom>
|
||||||
|
If you don't know what to choose, just pick the standard driver
|
||||||
|
(sqlite3).
|
||||||
|
<br />
|
||||||
|
For professionals, you can pick MySQL driver, BBGO works best
|
||||||
|
with MySQL, especially for larger data scale.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<div className={classes.buttons}>
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
onClick={handleTestConnection}
|
||||||
|
disabled={testing || configured}
|
||||||
|
>
|
||||||
|
{testing ? 'Testing' : 'Test Connection'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
disabled={testing || configured}
|
||||||
|
onClick={handleConfigureDatabase}
|
||||||
|
>
|
||||||
|
Configure
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{testResponse ? (
|
||||||
|
testResponse.error ? (
|
||||||
|
<Box m={2}>
|
||||||
|
<Alert severity="error">{testResponse.error}</Alert>
|
||||||
|
</Box>
|
||||||
|
) : testResponse.success ? (
|
||||||
|
<Box m={2}>
|
||||||
|
<Alert severity="success">Connection Test Succeeded</Alert>
|
||||||
|
</Box>
|
||||||
|
) : null
|
||||||
|
) : null}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
446
apps/frontend/components/ConfigureGridStrategyForm.js
Normal file
|
@ -0,0 +1,446 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import Grid from '@mui/material/Grid';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
|
||||||
|
import { makeStyles } from '@mui/styles';
|
||||||
|
import {
|
||||||
|
attachStrategyOn,
|
||||||
|
querySessions,
|
||||||
|
querySessionSymbols,
|
||||||
|
} from '../api/bbgo';
|
||||||
|
|
||||||
|
import TextField from '@mui/material/TextField';
|
||||||
|
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||||
|
import FormHelperText from '@mui/material/FormHelperText';
|
||||||
|
import InputLabel from '@mui/material/InputLabel';
|
||||||
|
import FormControl from '@mui/material/FormControl';
|
||||||
|
import Radio from '@mui/material/Radio';
|
||||||
|
import RadioGroup from '@mui/material/RadioGroup';
|
||||||
|
import FormLabel from '@mui/material/FormLabel';
|
||||||
|
import Select from '@mui/material/Select';
|
||||||
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
|
|
||||||
|
import Alert from '@mui/lab/Alert';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
|
||||||
|
import NumberFormat from 'react-number-format';
|
||||||
|
|
||||||
|
function parseFloatValid(s) {
|
||||||
|
if (s) {
|
||||||
|
const f = parseFloat(s);
|
||||||
|
if (!isNaN(f)) {
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseFloatCall(s, cb) {
|
||||||
|
if (s) {
|
||||||
|
const f = parseFloat(s);
|
||||||
|
if (!isNaN(f)) {
|
||||||
|
cb(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function StandardNumberFormat(props) {
|
||||||
|
const { inputRef, onChange, ...other } = props;
|
||||||
|
return (
|
||||||
|
<NumberFormat
|
||||||
|
{...other}
|
||||||
|
getInputRef={inputRef}
|
||||||
|
onValueChange={(values) => {
|
||||||
|
onChange({
|
||||||
|
target: {
|
||||||
|
name: props.name,
|
||||||
|
value: values.value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
thousandSeparator
|
||||||
|
isNumericString
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
StandardNumberFormat.propTypes = {
|
||||||
|
inputRef: PropTypes.func.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
function PriceNumberFormat(props) {
|
||||||
|
const { inputRef, onChange, ...other } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NumberFormat
|
||||||
|
{...other}
|
||||||
|
getInputRef={inputRef}
|
||||||
|
onValueChange={(values) => {
|
||||||
|
onChange({
|
||||||
|
target: {
|
||||||
|
name: props.name,
|
||||||
|
value: values.value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
thousandSeparator
|
||||||
|
isNumericString
|
||||||
|
prefix="$"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
PriceNumberFormat.propTypes = {
|
||||||
|
inputRef: PropTypes.func.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
formControl: {
|
||||||
|
marginTop: theme.spacing(1),
|
||||||
|
marginBottom: theme.spacing(1),
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
buttons: {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
marginTop: theme.spacing(2),
|
||||||
|
paddingTop: theme.spacing(2),
|
||||||
|
paddingBottom: theme.spacing(2),
|
||||||
|
'& > *': {
|
||||||
|
marginLeft: theme.spacing(1),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default function ConfigureGridStrategyForm({ onBack, onAdded }) {
|
||||||
|
const classes = useStyles();
|
||||||
|
|
||||||
|
const [errors, setErrors] = React.useState({});
|
||||||
|
|
||||||
|
const [sessions, setSessions] = React.useState([]);
|
||||||
|
|
||||||
|
const [activeSessionSymbols, setActiveSessionSymbols] = React.useState([]);
|
||||||
|
|
||||||
|
const [selectedSessionName, setSelectedSessionName] = React.useState(null);
|
||||||
|
|
||||||
|
const [selectedSymbol, setSelectedSymbol] = React.useState('');
|
||||||
|
|
||||||
|
const [quantityBy, setQuantityBy] = React.useState('fixedAmount');
|
||||||
|
|
||||||
|
const [upperPrice, setUpperPrice] = React.useState(30000.0);
|
||||||
|
const [lowerPrice, setLowerPrice] = React.useState(10000.0);
|
||||||
|
|
||||||
|
const [fixedAmount, setFixedAmount] = React.useState(100.0);
|
||||||
|
const [fixedQuantity, setFixedQuantity] = React.useState(1.234);
|
||||||
|
const [gridNumber, setGridNumber] = React.useState(20);
|
||||||
|
const [profitSpread, setProfitSpread] = React.useState(100.0);
|
||||||
|
|
||||||
|
const [response, setResponse] = React.useState({});
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
querySessions((sessions) => {
|
||||||
|
setSessions(sessions);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleAdd = (event) => {
|
||||||
|
const payload = {
|
||||||
|
symbol: selectedSymbol,
|
||||||
|
gridNumber: parseFloatValid(gridNumber),
|
||||||
|
profitSpread: parseFloatValid(profitSpread),
|
||||||
|
upperPrice: parseFloatValid(upperPrice),
|
||||||
|
lowerPrice: parseFloatValid(lowerPrice),
|
||||||
|
};
|
||||||
|
switch (quantityBy) {
|
||||||
|
case 'fixedQuantity':
|
||||||
|
payload.quantity = parseFloatValid(fixedQuantity);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'fixedAmount':
|
||||||
|
payload.amount = parseFloatValid(fixedAmount);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedSessionName) {
|
||||||
|
setErrors({ session: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedSymbol) {
|
||||||
|
setErrors({ symbol: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(payload);
|
||||||
|
attachStrategyOn(selectedSessionName, 'grid', payload, (response) => {
|
||||||
|
console.log(response);
|
||||||
|
setResponse(response);
|
||||||
|
if (onAdded) {
|
||||||
|
setTimeout(onAdded, 3000);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
setResponse(err.response.data);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setErrors({});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleQuantityBy = (event) => {
|
||||||
|
setQuantityBy(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSessionChange = (event) => {
|
||||||
|
const sessionName = event.target.value;
|
||||||
|
setSelectedSessionName(sessionName);
|
||||||
|
|
||||||
|
querySessionSymbols(sessionName, (symbols) => {
|
||||||
|
setActiveSessionSymbols(symbols);
|
||||||
|
}).catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
setResponse(err.response.data);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const sessionMenuItems = sessions.map((session, index) => {
|
||||||
|
return (
|
||||||
|
<MenuItem key={session.name} value={session.name}>
|
||||||
|
{session.name}
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const symbolMenuItems = activeSessionSymbols.map((symbol, index) => {
|
||||||
|
return (
|
||||||
|
<MenuItem key={symbol} value={symbol}>
|
||||||
|
{symbol}
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
Add Grid Strategy
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography variant="body1" gutterBottom>
|
||||||
|
Fixed price band grid strategy uses the fixed price band to place
|
||||||
|
buy/sell orders. This strategy places sell orders above the current
|
||||||
|
price, places buy orders below the current price. If any of the order is
|
||||||
|
executed, then it will automatically place a new profit order on the
|
||||||
|
reverse side.
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Grid container spacing={3}>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<FormControl
|
||||||
|
required
|
||||||
|
className={classes.formControl}
|
||||||
|
error={errors.session}
|
||||||
|
>
|
||||||
|
<InputLabel id="session-select-label">Session</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId="session-select-label"
|
||||||
|
id="session-select"
|
||||||
|
value={selectedSessionName ? selectedSessionName : ''}
|
||||||
|
onChange={handleSessionChange}
|
||||||
|
>
|
||||||
|
{sessionMenuItems}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormHelperText id="session-select-helper-text">
|
||||||
|
Select the exchange session you want to mount this strategy.
|
||||||
|
</FormHelperText>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<FormControl
|
||||||
|
required
|
||||||
|
className={classes.formControl}
|
||||||
|
error={errors.symbol}
|
||||||
|
>
|
||||||
|
<InputLabel id="symbol-select-label">Market</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId="symbol-select-label"
|
||||||
|
id="symbol-select"
|
||||||
|
value={selectedSymbol ? selectedSymbol : ''}
|
||||||
|
onChange={(event) => {
|
||||||
|
setSelectedSymbol(event.target.value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{symbolMenuItems}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormHelperText id="session-select-helper-text">
|
||||||
|
Select the market you want to run this strategy
|
||||||
|
</FormHelperText>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<TextField
|
||||||
|
id="upperPrice"
|
||||||
|
name="upper_price"
|
||||||
|
label="Upper Price"
|
||||||
|
fullWidth
|
||||||
|
required
|
||||||
|
onChange={(event) => {
|
||||||
|
parseFloatCall(event.target.value, setUpperPrice);
|
||||||
|
}}
|
||||||
|
value={upperPrice}
|
||||||
|
InputProps={{
|
||||||
|
inputComponent: PriceNumberFormat,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<TextField
|
||||||
|
id="lowerPrice"
|
||||||
|
name="lower_price"
|
||||||
|
label="Lower Price"
|
||||||
|
fullWidth
|
||||||
|
required
|
||||||
|
onChange={(event) => {
|
||||||
|
parseFloatCall(event.target.value, setLowerPrice);
|
||||||
|
}}
|
||||||
|
value={lowerPrice}
|
||||||
|
InputProps={{
|
||||||
|
inputComponent: PriceNumberFormat,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<TextField
|
||||||
|
id="profitSpread"
|
||||||
|
name="profit_spread"
|
||||||
|
label="Profit Spread"
|
||||||
|
fullWidth
|
||||||
|
required
|
||||||
|
onChange={(event) => {
|
||||||
|
parseFloatCall(event.target.value, setProfitSpread);
|
||||||
|
}}
|
||||||
|
value={profitSpread}
|
||||||
|
InputProps={{
|
||||||
|
inputComponent: StandardNumberFormat,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12} sm={3}>
|
||||||
|
<FormControl component="fieldset">
|
||||||
|
<FormLabel component="legend">Order Quantity By</FormLabel>
|
||||||
|
<RadioGroup
|
||||||
|
name="quantityBy"
|
||||||
|
value={quantityBy}
|
||||||
|
onChange={handleQuantityBy}
|
||||||
|
>
|
||||||
|
<FormControlLabel
|
||||||
|
value="fixedAmount"
|
||||||
|
control={<Radio />}
|
||||||
|
label="Fixed Amount"
|
||||||
|
/>
|
||||||
|
<FormControlLabel
|
||||||
|
value="fixedQuantity"
|
||||||
|
control={<Radio />}
|
||||||
|
label="Fixed Quantity"
|
||||||
|
/>
|
||||||
|
</RadioGroup>
|
||||||
|
</FormControl>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12} sm={9}>
|
||||||
|
{quantityBy === 'fixedQuantity' ? (
|
||||||
|
<TextField
|
||||||
|
id="fixedQuantity"
|
||||||
|
name="order_quantity"
|
||||||
|
label="Fixed Quantity"
|
||||||
|
fullWidth
|
||||||
|
required
|
||||||
|
onChange={(event) => {
|
||||||
|
parseFloatCall(event.target.value, setFixedQuantity);
|
||||||
|
}}
|
||||||
|
value={fixedQuantity}
|
||||||
|
InputProps={{
|
||||||
|
inputComponent: StandardNumberFormat,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{quantityBy === 'fixedAmount' ? (
|
||||||
|
<TextField
|
||||||
|
id="orderAmount"
|
||||||
|
name="order_amount"
|
||||||
|
label="Fixed Amount"
|
||||||
|
fullWidth
|
||||||
|
required
|
||||||
|
onChange={(event) => {
|
||||||
|
parseFloatCall(event.target.value, setFixedAmount);
|
||||||
|
}}
|
||||||
|
value={fixedAmount}
|
||||||
|
InputProps={{
|
||||||
|
inputComponent: PriceNumberFormat,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<TextField
|
||||||
|
id="gridNumber"
|
||||||
|
name="grid_number"
|
||||||
|
label="Number of Grid"
|
||||||
|
fullWidth
|
||||||
|
required
|
||||||
|
onChange={(event) => {
|
||||||
|
parseFloatCall(event.target.value, setGridNumber);
|
||||||
|
}}
|
||||||
|
value={gridNumber}
|
||||||
|
InputProps={{
|
||||||
|
inputComponent: StandardNumberFormat,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<div className={classes.buttons}>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (onBack) {
|
||||||
|
onBack();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button variant="contained" color="primary" onClick={handleAdd}>
|
||||||
|
Add Strategy
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{response ? (
|
||||||
|
response.error ? (
|
||||||
|
<Box m={2}>
|
||||||
|
<Alert severity="error">{response.error}</Alert>
|
||||||
|
</Box>
|
||||||
|
) : response.success ? (
|
||||||
|
<Box m={2}>
|
||||||
|
<Alert severity="success">Strategy Added</Alert>
|
||||||
|
</Box>
|
||||||
|
) : null
|
||||||
|
) : null}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
143
apps/frontend/components/ConnectWallet.js
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { makeStyles } from '@mui/styles';
|
||||||
|
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import ClickAwayListener from '@mui/material/ClickAwayListener';
|
||||||
|
import Grow from '@mui/material/Grow';
|
||||||
|
import Paper from '@mui/material/Paper';
|
||||||
|
import Popper from '@mui/material/Popper';
|
||||||
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
|
import MenuList from '@mui/material/MenuList';
|
||||||
|
import ListItemText from '@mui/material/ListItemText';
|
||||||
|
import PersonIcon from '@mui/icons-material/Person';
|
||||||
|
|
||||||
|
import { useEtherBalance, useTokenBalance, useEthers } from '@usedapp/core';
|
||||||
|
import { formatEther } from '@ethersproject/units';
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
buttons: {
|
||||||
|
margin: theme.spacing(1),
|
||||||
|
padding: theme.spacing(1),
|
||||||
|
},
|
||||||
|
profile: {
|
||||||
|
margin: theme.spacing(1),
|
||||||
|
padding: theme.spacing(1),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const BBG = '0x3Afe98235d680e8d7A52e1458a59D60f45F935C0';
|
||||||
|
|
||||||
|
export default function ConnectWallet() {
|
||||||
|
const classes = useStyles();
|
||||||
|
|
||||||
|
const { activateBrowserWallet, account } = useEthers();
|
||||||
|
const etherBalance = useEtherBalance(account);
|
||||||
|
const tokenBalance = useTokenBalance(BBG, account);
|
||||||
|
|
||||||
|
const [open, setOpen] = React.useState(false);
|
||||||
|
const anchorRef = React.useRef(null);
|
||||||
|
|
||||||
|
const handleToggle = () => {
|
||||||
|
setOpen((prevOpen) => !prevOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = (event) => {
|
||||||
|
if (anchorRef.current && anchorRef.current.contains(event.target)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
function handleListKeyDown(event) {
|
||||||
|
if (event.key === 'Tab') {
|
||||||
|
event.preventDefault();
|
||||||
|
setOpen(false);
|
||||||
|
} else if (event.key === 'Escape') {
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// return focus to the button when we transitioned from !open -> open
|
||||||
|
const prevOpen = React.useRef(open);
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (prevOpen.current === true && open === false) {
|
||||||
|
anchorRef.current.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
prevOpen.current = open;
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{account ? (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
ref={anchorRef}
|
||||||
|
id="composition-button"
|
||||||
|
aria-controls={open ? 'composition-menu' : undefined}
|
||||||
|
aria-expanded={open ? 'true' : undefined}
|
||||||
|
aria-haspopup="true"
|
||||||
|
onClick={handleToggle}
|
||||||
|
>
|
||||||
|
<PersonIcon />
|
||||||
|
<ListItemText primary="Profile" />
|
||||||
|
</Button>
|
||||||
|
<Popper
|
||||||
|
open={open}
|
||||||
|
anchorEl={anchorRef.current}
|
||||||
|
role={undefined}
|
||||||
|
placement="bottom-start"
|
||||||
|
transition
|
||||||
|
disablePortal
|
||||||
|
>
|
||||||
|
{({ TransitionProps, placement }) => (
|
||||||
|
<Grow
|
||||||
|
{...TransitionProps}
|
||||||
|
style={{
|
||||||
|
transformOrigin:
|
||||||
|
placement === 'bottom-start' ? 'left top' : 'left bottom',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Paper>
|
||||||
|
<ClickAwayListener onClickAway={handleClose}>
|
||||||
|
<MenuList
|
||||||
|
autoFocusItem={open}
|
||||||
|
id="composition-menu"
|
||||||
|
aria-labelledby="composition-button"
|
||||||
|
onKeyDown={handleListKeyDown}
|
||||||
|
>
|
||||||
|
<MenuItem onClick={handleClose}>
|
||||||
|
{account && <p>Account: {account}</p>}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={handleClose}>
|
||||||
|
{etherBalance && (
|
||||||
|
<a>ETH Balance: {formatEther(etherBalance)}</a>
|
||||||
|
)}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={handleClose}>
|
||||||
|
{tokenBalance && (
|
||||||
|
<a>BBG Balance: {formatEther(tokenBalance)}</a>
|
||||||
|
)}
|
||||||
|
</MenuItem>
|
||||||
|
</MenuList>
|
||||||
|
</ClickAwayListener>
|
||||||
|
</Paper>
|
||||||
|
</Grow>
|
||||||
|
)}
|
||||||
|
</Popper>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
onClick={() => activateBrowserWallet()}
|
||||||
|
className={classes.buttons}
|
||||||
|
>
|
||||||
|
Connect Wallet
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
56
apps/frontend/components/Detail.tsx
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import { styled } from '@mui/styles';
|
||||||
|
import type { GridStrategy } from '../api/bbgo';
|
||||||
|
|
||||||
|
import RunningTime from './RunningTime';
|
||||||
|
import Summary from './Summary';
|
||||||
|
import Stats from './Stats';
|
||||||
|
|
||||||
|
const StrategyContainer = styled('section')(() => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'space-around',
|
||||||
|
width: '350px',
|
||||||
|
border: '1px solid rgb(248, 149, 35)',
|
||||||
|
borderRadius: '10px',
|
||||||
|
padding: '10px',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const Strategy = styled('div')(() => ({
|
||||||
|
fontSize: '20px',
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const Description = styled('div')(() => ({
|
||||||
|
color: 'rgb(140, 140, 140)',
|
||||||
|
'& .duration': {
|
||||||
|
marginLeft: '3px',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default function Detail({ data }: { data: GridStrategy }) {
|
||||||
|
const { strategy, stats, startTime } = data;
|
||||||
|
const totalProfitsPercentage = (stats.totalProfits / stats.investment) * 100;
|
||||||
|
const gridProfitsPercentage = (stats.gridProfits / stats.investment) * 100;
|
||||||
|
const gridAprPercentage = (stats.gridProfits / 5) * 365;
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const durationMilliseconds = now - startTime;
|
||||||
|
const seconds = durationMilliseconds / 1000;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StrategyContainer>
|
||||||
|
<Strategy>{strategy}</Strategy>
|
||||||
|
<div>{data[strategy].symbol}</div>
|
||||||
|
<RunningTime seconds={seconds} />
|
||||||
|
<Description>
|
||||||
|
0 arbitrages in 24 hours / Total <span>{stats.totalArbs}</span>{' '}
|
||||||
|
arbitrages
|
||||||
|
</Description>
|
||||||
|
<Summary stats={stats} totalProfitsPercentage={totalProfitsPercentage} />
|
||||||
|
<Stats
|
||||||
|
stats={stats}
|
||||||
|
gridProfitsPercentage={gridProfitsPercentage}
|
||||||
|
gridAprPercentage={gridAprPercentage}
|
||||||
|
/>
|
||||||
|
</StrategyContainer>
|
||||||
|
);
|
||||||
|
}
|
49
apps/frontend/components/ExchangeSessionTabPanel.js
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import Paper from '@mui/material/Paper';
|
||||||
|
import Tabs from '@mui/material/Tabs';
|
||||||
|
import Tab from '@mui/material/Tab';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { querySessions } from '../api/bbgo';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import { makeStyles } from '@mui/styles';
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
paper: {
|
||||||
|
margin: theme.spacing(2),
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default function ExchangeSessionTabPanel() {
|
||||||
|
const classes = useStyles();
|
||||||
|
|
||||||
|
const [tabIndex, setTabIndex] = React.useState(0);
|
||||||
|
const handleTabClick = (event, newValue) => {
|
||||||
|
setTabIndex(newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const [sessions, setSessions] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
querySessions((sessions) => {
|
||||||
|
setSessions(sessions);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper className={classes.paper}>
|
||||||
|
<Typography variant="h4" gutterBottom>
|
||||||
|
Sessions
|
||||||
|
</Typography>
|
||||||
|
<Tabs
|
||||||
|
value={tabIndex}
|
||||||
|
onChange={handleTabClick}
|
||||||
|
indicatorColor="primary"
|
||||||
|
textColor="primary"
|
||||||
|
>
|
||||||
|
{sessions.map((session) => {
|
||||||
|
return <Tab key={session.name} label={session.name} />;
|
||||||
|
})}
|
||||||
|
</Tabs>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
88
apps/frontend/components/ReviewSessions.js
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
import React from 'react';
|
||||||
|
import Grid from '@mui/material/Grid';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import List from '@mui/material/List';
|
||||||
|
import ListItem from '@mui/material/ListItem';
|
||||||
|
import ListItemText from '@mui/material/ListItemText';
|
||||||
|
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||||
|
import PowerIcon from '@mui/icons-material/Power';
|
||||||
|
|
||||||
|
import { makeStyles } from '@mui/styles';
|
||||||
|
import { querySessions } from '../api/bbgo';
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
formControl: {
|
||||||
|
marginTop: theme.spacing(1),
|
||||||
|
marginBottom: theme.spacing(1),
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
buttons: {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
marginTop: theme.spacing(2),
|
||||||
|
paddingTop: theme.spacing(2),
|
||||||
|
paddingBottom: theme.spacing(2),
|
||||||
|
'& > *': {
|
||||||
|
marginLeft: theme.spacing(1),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default function ReviewSessions({ onBack, onNext }) {
|
||||||
|
const classes = useStyles();
|
||||||
|
|
||||||
|
const [sessions, setSessions] = React.useState([]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
querySessions((sessions) => {
|
||||||
|
setSessions(sessions);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const items = sessions.map((session, i) => {
|
||||||
|
console.log(session);
|
||||||
|
return (
|
||||||
|
<ListItem key={session.name}>
|
||||||
|
<ListItemIcon>
|
||||||
|
<PowerIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={session.name} secondary={session.exchange} />
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
Review Sessions
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<List component="nav">{items}</List>
|
||||||
|
|
||||||
|
<div className={classes.buttons}>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (onBack) {
|
||||||
|
onBack();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => {
|
||||||
|
if (onNext) {
|
||||||
|
onNext();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
157
apps/frontend/components/ReviewStrategies.js
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
import React from 'react';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import List from '@mui/material/List';
|
||||||
|
import Card from '@mui/material/Card';
|
||||||
|
import CardHeader from '@mui/material/CardHeader';
|
||||||
|
import CardContent from '@mui/material/CardContent';
|
||||||
|
import Avatar from '@mui/material/Avatar';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||||
|
import Table from '@mui/material/Table';
|
||||||
|
import TableBody from '@mui/material/TableBody';
|
||||||
|
import TableCell from '@mui/material/TableCell';
|
||||||
|
import TableContainer from '@mui/material/TableContainer';
|
||||||
|
import TableHead from '@mui/material/TableHead';
|
||||||
|
import TableRow from '@mui/material/TableRow';
|
||||||
|
|
||||||
|
import { makeStyles } from '@mui/styles';
|
||||||
|
import { queryStrategies } from '../api/bbgo';
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
strategyCard: {
|
||||||
|
margin: theme.spacing(1),
|
||||||
|
},
|
||||||
|
formControl: {
|
||||||
|
marginTop: theme.spacing(1),
|
||||||
|
marginBottom: theme.spacing(1),
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
buttons: {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
marginTop: theme.spacing(2),
|
||||||
|
paddingTop: theme.spacing(2),
|
||||||
|
paddingBottom: theme.spacing(2),
|
||||||
|
'& > *': {
|
||||||
|
marginLeft: theme.spacing(1),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
function configToTable(config) {
|
||||||
|
const rows = Object.getOwnPropertyNames(config).map((k) => {
|
||||||
|
return {
|
||||||
|
key: k,
|
||||||
|
val: config[k],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableContainer>
|
||||||
|
<Table aria-label="strategy attributes">
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>Field</TableCell>
|
||||||
|
<TableCell align="right">Value</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{rows.map((row) => (
|
||||||
|
<TableRow key={row.key}>
|
||||||
|
<TableCell component="th" scope="row">
|
||||||
|
{row.key}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="right">{row.val}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ReviewStrategies({ onBack, onNext }) {
|
||||||
|
const classes = useStyles();
|
||||||
|
|
||||||
|
const [strategies, setStrategies] = React.useState([]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
queryStrategies((strategies) => {
|
||||||
|
setStrategies(strategies || []);
|
||||||
|
}).catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const items = strategies.map((o, i) => {
|
||||||
|
const mounts = o.on || [];
|
||||||
|
delete o.on;
|
||||||
|
|
||||||
|
const config = o[o.strategy];
|
||||||
|
|
||||||
|
const titleComps = [o.strategy.toUpperCase()];
|
||||||
|
if (config.symbol) {
|
||||||
|
titleComps.push(config.symbol);
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = titleComps.join(' ');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card key={i} className={classes.strategyCard}>
|
||||||
|
<CardHeader
|
||||||
|
avatar={<Avatar aria-label="strategy">G</Avatar>}
|
||||||
|
action={
|
||||||
|
<IconButton aria-label="settings">
|
||||||
|
<MoreVertIcon />
|
||||||
|
</IconButton>
|
||||||
|
}
|
||||||
|
title={title}
|
||||||
|
subheader={`Exchange ${mounts.map((m) => m.toUpperCase())}`}
|
||||||
|
/>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="body2" color="textSecondary" component="p">
|
||||||
|
Strategy will be executed on session {mounts.join(',')} with the
|
||||||
|
following configuration:
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{configToTable(config)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
Review Strategies
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<List component="nav">{items}</List>
|
||||||
|
|
||||||
|
<div className={classes.buttons}>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (onBack) {
|
||||||
|
onBack();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add New Strategy
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => {
|
||||||
|
if (onNext) {
|
||||||
|
onNext();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
34
apps/frontend/components/RunningTime.tsx
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { styled } from '@mui/styles';
|
||||||
|
import { Description } from './Detail';
|
||||||
|
|
||||||
|
const RunningTimeSection = styled('div')(() => ({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StatusSign = styled('span')(() => ({
|
||||||
|
width: '10px',
|
||||||
|
height: '10px',
|
||||||
|
display: 'block',
|
||||||
|
backgroundColor: 'rgb(113, 218, 113)',
|
||||||
|
borderRadius: '50%',
|
||||||
|
marginRight: '5px',
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default function RunningTime({ seconds }: { seconds: number }) {
|
||||||
|
const day = Math.floor(seconds / (60 * 60 * 24));
|
||||||
|
const hour = Math.floor((seconds % (60 * 60 * 24)) / 3600);
|
||||||
|
const min = Math.floor(((seconds % (60 * 60 * 24)) % 3600) / 60);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RunningTimeSection>
|
||||||
|
<StatusSign />
|
||||||
|
<Description>
|
||||||
|
Running for
|
||||||
|
<span className="duration">{day}</span>D
|
||||||
|
<span className="duration">{hour}</span>H
|
||||||
|
<span className="duration">{min}</span>M
|
||||||
|
</Description>
|
||||||
|
</RunningTimeSection>
|
||||||
|
);
|
||||||
|
}
|
105
apps/frontend/components/SaveConfigAndRestart.js
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
|
||||||
|
import { makeStyles } from '@mui/styles';
|
||||||
|
|
||||||
|
import { ping, saveConfig, setupRestart } from '../api/bbgo';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Alert from '@mui/lab/Alert';
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
strategyCard: {
|
||||||
|
margin: theme.spacing(1),
|
||||||
|
},
|
||||||
|
formControl: {
|
||||||
|
marginTop: theme.spacing(1),
|
||||||
|
marginBottom: theme.spacing(1),
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
buttons: {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
marginTop: theme.spacing(2),
|
||||||
|
paddingTop: theme.spacing(2),
|
||||||
|
paddingBottom: theme.spacing(2),
|
||||||
|
'& > *': {
|
||||||
|
marginLeft: theme.spacing(1),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default function SaveConfigAndRestart({ onBack, onRestarted }) {
|
||||||
|
const classes = useStyles();
|
||||||
|
|
||||||
|
const { push } = useRouter();
|
||||||
|
const [response, setResponse] = React.useState({});
|
||||||
|
|
||||||
|
const handleRestart = () => {
|
||||||
|
saveConfig((resp) => {
|
||||||
|
setResponse(resp);
|
||||||
|
|
||||||
|
setupRestart((resp) => {
|
||||||
|
let t;
|
||||||
|
t = setInterval(() => {
|
||||||
|
ping(() => {
|
||||||
|
clearInterval(t);
|
||||||
|
push('/');
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
}).catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
setResponse(err.response.data);
|
||||||
|
});
|
||||||
|
|
||||||
|
// call restart here
|
||||||
|
}).catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
setResponse(err.response.data);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
Save Config and Restart
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography variant="body1" gutterBottom>
|
||||||
|
Click "Save and Restart" to save the configurations to the config file{' '}
|
||||||
|
<code>bbgo.yaml</code>, and save the exchange session credentials to the
|
||||||
|
dotenv file <code>.env.local</code>.
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<div className={classes.buttons}>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (onBack) {
|
||||||
|
onBack();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button variant="contained" color="primary" onClick={handleRestart}>
|
||||||
|
Save and Restart
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{response ? (
|
||||||
|
response.error ? (
|
||||||
|
<Box m={2}>
|
||||||
|
<Alert severity="error">{response.error}</Alert>
|
||||||
|
</Box>
|
||||||
|
) : response.success ? (
|
||||||
|
<Box m={2}>
|
||||||
|
<Alert severity="success">Config Saved</Alert>
|
||||||
|
</Box>
|
||||||
|
) : null
|
||||||
|
) : null}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
103
apps/frontend/components/SideBar.js
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
import Drawer from '@mui/material/Drawer';
|
||||||
|
import Divider from '@mui/material/Divider';
|
||||||
|
import List from '@mui/material/List';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import ListItem from '@mui/material/ListItem';
|
||||||
|
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||||
|
import DashboardIcon from '@mui/icons-material/Dashboard';
|
||||||
|
import ListItemText from '@mui/material/ListItemText';
|
||||||
|
import ListIcon from '@mui/icons-material/List';
|
||||||
|
import TrendingUpIcon from '@mui/icons-material/TrendingUp';
|
||||||
|
import React from 'react';
|
||||||
|
import { makeStyles } from '@mui/styles';
|
||||||
|
|
||||||
|
const drawerWidth = 240;
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
root: {
|
||||||
|
flexGrow: 1,
|
||||||
|
display: 'flex',
|
||||||
|
},
|
||||||
|
toolbar: {
|
||||||
|
paddingRight: 24, // keep right padding when drawer closed
|
||||||
|
},
|
||||||
|
toolbarIcon: {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
padding: '0 8px',
|
||||||
|
...theme.mixins.toolbar,
|
||||||
|
},
|
||||||
|
appBarSpacer: theme.mixins.toolbar,
|
||||||
|
drawerPaper: {
|
||||||
|
[theme.breakpoints.up('sm')]: {
|
||||||
|
width: drawerWidth,
|
||||||
|
flexShrink: 0,
|
||||||
|
},
|
||||||
|
position: 'relative',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
transition: theme.transitions.create('width', {
|
||||||
|
easing: theme.transitions.easing.sharp,
|
||||||
|
duration: theme.transitions.duration.enteringScreen,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
drawer: {
|
||||||
|
width: drawerWidth,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default function SideBar() {
|
||||||
|
const classes = useStyles();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
variant="permanent"
|
||||||
|
className={classes.drawer}
|
||||||
|
PaperProps={{
|
||||||
|
className: classes.drawerPaper,
|
||||||
|
}}
|
||||||
|
anchor={'left'}
|
||||||
|
open={true}
|
||||||
|
>
|
||||||
|
<div className={classes.appBarSpacer} />
|
||||||
|
|
||||||
|
<List>
|
||||||
|
<Link href={'/'}>
|
||||||
|
<ListItem button>
|
||||||
|
<ListItemIcon>
|
||||||
|
<DashboardIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary="Dashboard" />
|
||||||
|
</ListItem>
|
||||||
|
</Link>
|
||||||
|
</List>
|
||||||
|
<Divider />
|
||||||
|
<List>
|
||||||
|
<Link href={'/orders'}>
|
||||||
|
<ListItem button>
|
||||||
|
<ListItemIcon>
|
||||||
|
<ListIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary="Orders" />
|
||||||
|
</ListItem>
|
||||||
|
</Link>
|
||||||
|
<Link href={'/trades'}>
|
||||||
|
<ListItem button>
|
||||||
|
<ListItemIcon>
|
||||||
|
<ListIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary="Trades" />
|
||||||
|
</ListItem>
|
||||||
|
</Link>
|
||||||
|
<Link href={'/strategies'}>
|
||||||
|
<ListItem button>
|
||||||
|
<ListItemIcon>
|
||||||
|
<TrendingUpIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary="Strategies" />
|
||||||
|
</ListItem>
|
||||||
|
</Link>
|
||||||
|
</List>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}
|
51
apps/frontend/components/Stats.tsx
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import { styled } from '@mui/styles';
|
||||||
|
import { StatsTitle, StatsValue, Percentage } from './Summary';
|
||||||
|
import { GridStats } from '../api/bbgo';
|
||||||
|
|
||||||
|
const StatsSection = styled('div')(() => ({
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: '1fr 1fr 1fr',
|
||||||
|
gap: '10px',
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default function Stats({
|
||||||
|
stats,
|
||||||
|
gridProfitsPercentage,
|
||||||
|
gridAprPercentage,
|
||||||
|
}: {
|
||||||
|
stats: GridStats;
|
||||||
|
gridProfitsPercentage: number;
|
||||||
|
gridAprPercentage: number;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<StatsSection>
|
||||||
|
<div>
|
||||||
|
<StatsTitle>Grid Profits</StatsTitle>
|
||||||
|
<StatsValue>{stats.gridProfits}</StatsValue>
|
||||||
|
<Percentage>{gridProfitsPercentage}%</Percentage>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<StatsTitle>Floating PNL</StatsTitle>
|
||||||
|
<StatsValue>{stats.floatingPNL}</StatsValue>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<StatsTitle>Grid APR</StatsTitle>
|
||||||
|
<Percentage>{gridAprPercentage}%</Percentage>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<StatsTitle>Current Price</StatsTitle>
|
||||||
|
<div>{stats.currentPrice}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<StatsTitle>Price Range</StatsTitle>
|
||||||
|
<div>
|
||||||
|
{stats.lowestPrice}~{stats.highestPrice}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</StatsSection>
|
||||||
|
);
|
||||||
|
}
|
50
apps/frontend/components/Summary.tsx
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import { styled } from '@mui/styles';
|
||||||
|
import { GridStats } from '../api/bbgo';
|
||||||
|
|
||||||
|
const SummarySection = styled('div')(() => ({
|
||||||
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-around',
|
||||||
|
backgroundColor: 'rgb(255, 245, 232)',
|
||||||
|
margin: '10px 0',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const SummaryBlock = styled('div')(() => ({
|
||||||
|
padding: '5px 0 5px 0',
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const StatsTitle = styled('div')(() => ({
|
||||||
|
margin: '0 0 10px 0',
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const StatsValue = styled('div')(() => ({
|
||||||
|
marginBottom: '10px',
|
||||||
|
color: 'rgb(123, 169, 90)',
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const Percentage = styled('div')(() => ({
|
||||||
|
color: 'rgb(123, 169, 90)',
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default function Summary({
|
||||||
|
stats,
|
||||||
|
totalProfitsPercentage,
|
||||||
|
}: {
|
||||||
|
stats: GridStats;
|
||||||
|
totalProfitsPercentage: number;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<SummarySection>
|
||||||
|
<SummaryBlock>
|
||||||
|
<StatsTitle>Investment USDT</StatsTitle>
|
||||||
|
<div>{stats.investment}</div>
|
||||||
|
</SummaryBlock>
|
||||||
|
|
||||||
|
<SummaryBlock>
|
||||||
|
<StatsTitle>Total Profit USDT</StatsTitle>
|
||||||
|
<StatsValue>{stats.totalProfits}</StatsValue>
|
||||||
|
<Percentage>{totalProfitsPercentage}%</Percentage>
|
||||||
|
</SummaryBlock>
|
||||||
|
</SummarySection>
|
||||||
|
);
|
||||||
|
}
|
39
apps/frontend/components/SyncButton.tsx
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import { styled } from '@mui/styles';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { querySyncStatus, SyncStatus, triggerSync } from '../api/bbgo';
|
||||||
|
import useInterval from '../hooks/useInterval';
|
||||||
|
|
||||||
|
const ToolbarButton = styled('button')(({ theme }) => ({
|
||||||
|
padding: theme.spacing(1),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default function SyncButton() {
|
||||||
|
const [syncing, setSyncing] = useState(false);
|
||||||
|
|
||||||
|
const sync = async () => {
|
||||||
|
try {
|
||||||
|
setSyncing(true);
|
||||||
|
await triggerSync();
|
||||||
|
} catch {
|
||||||
|
setSyncing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
sync();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useInterval(() => {
|
||||||
|
querySyncStatus().then((s) => {
|
||||||
|
if (s !== SyncStatus.Syncing) {
|
||||||
|
setSyncing(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToolbarButton disabled={syncing} onClick={sync}>
|
||||||
|
{syncing ? 'Syncing...' : 'Sync'}
|
||||||
|
</ToolbarButton>
|
||||||
|
);
|
||||||
|
}
|
87
apps/frontend/components/TotalAssetsDetails.js
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
import React from 'react';
|
||||||
|
import CardContent from '@mui/material/CardContent';
|
||||||
|
import Card from '@mui/material/Card';
|
||||||
|
import { makeStyles } from '@mui/styles';
|
||||||
|
import List from '@mui/material/List';
|
||||||
|
import ListItem from '@mui/material/ListItem';
|
||||||
|
import ListItemText from '@mui/material/ListItemText';
|
||||||
|
import ListItemAvatar from '@mui/material/ListItemAvatar';
|
||||||
|
import Avatar from '@mui/material/Avatar';
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
root: {
|
||||||
|
margin: theme.spacing(1),
|
||||||
|
},
|
||||||
|
cardContent: {},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const logoCurrencies = {
|
||||||
|
BTC: true,
|
||||||
|
ETH: true,
|
||||||
|
BCH: true,
|
||||||
|
LTC: true,
|
||||||
|
USDT: true,
|
||||||
|
BNB: true,
|
||||||
|
COMP: true,
|
||||||
|
XRP: true,
|
||||||
|
LINK: true,
|
||||||
|
DOT: true,
|
||||||
|
SXP: true,
|
||||||
|
DAI: true,
|
||||||
|
MAX: true,
|
||||||
|
TWD: true,
|
||||||
|
SNT: true,
|
||||||
|
YFI: true,
|
||||||
|
GRT: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function TotalAssetsDetails({ assets }) {
|
||||||
|
const classes = useStyles();
|
||||||
|
|
||||||
|
const sortedAssets = [];
|
||||||
|
for (let k in assets) {
|
||||||
|
sortedAssets.push(assets[k]);
|
||||||
|
}
|
||||||
|
sortedAssets.sort((a, b) => {
|
||||||
|
if (a.inUSD > b.inUSD) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a.inUSD < b.inUSD) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
const items = sortedAssets.map((a) => {
|
||||||
|
return (
|
||||||
|
<ListItem key={a.currency} dense>
|
||||||
|
{a.currency in logoCurrencies ? (
|
||||||
|
<ListItemAvatar>
|
||||||
|
<Avatar
|
||||||
|
alt={a.currency}
|
||||||
|
src={`/images/${a.currency.toLowerCase()}-logo.svg`}
|
||||||
|
/>
|
||||||
|
</ListItemAvatar>
|
||||||
|
) : (
|
||||||
|
<ListItemAvatar>
|
||||||
|
<Avatar alt={a.currency} />
|
||||||
|
</ListItemAvatar>
|
||||||
|
)}
|
||||||
|
<ListItemText
|
||||||
|
primary={`${a.currency} ${a.total}`}
|
||||||
|
secondary={`=~ ${Math.round(a.inUSD)} USD`}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className={classes.root} variant="outlined">
|
||||||
|
<CardContent className={classes.cardContent}>
|
||||||
|
<List dense>{items}</List>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
94
apps/frontend/components/TotalAssetsPie.js
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { ResponsivePie } from '@nivo/pie';
|
||||||
|
import { queryAssets } from '../api/bbgo';
|
||||||
|
import { currencyColor } from '../src/utils';
|
||||||
|
import CardContent from '@mui/material/CardContent';
|
||||||
|
import Card from '@mui/material/Card';
|
||||||
|
import { makeStyles } from '@mui/styles';
|
||||||
|
|
||||||
|
function reduceAssetsBy(assets, field, minimum) {
|
||||||
|
let as = [];
|
||||||
|
|
||||||
|
let others = { id: 'others', labels: 'others', value: 0.0 };
|
||||||
|
for (let key in assets) {
|
||||||
|
if (assets[key]) {
|
||||||
|
let a = assets[key];
|
||||||
|
let value = a[field];
|
||||||
|
|
||||||
|
if (value < minimum) {
|
||||||
|
others.value += value;
|
||||||
|
} else {
|
||||||
|
as.push({
|
||||||
|
id: a.currency,
|
||||||
|
label: a.currency,
|
||||||
|
color: currencyColor(a.currency),
|
||||||
|
value: Math.round(value, 1),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return as;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
root: {
|
||||||
|
margin: theme.spacing(1),
|
||||||
|
},
|
||||||
|
cardContent: {
|
||||||
|
height: 350,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default function TotalAssetsPie({ assets }) {
|
||||||
|
const classes = useStyles();
|
||||||
|
return (
|
||||||
|
<Card className={classes.root} variant="outlined">
|
||||||
|
<CardContent className={classes.cardContent}>
|
||||||
|
<ResponsivePie
|
||||||
|
data={reduceAssetsBy(assets, 'inUSD', 2)}
|
||||||
|
margin={{ top: 20, right: 80, bottom: 10, left: 0 }}
|
||||||
|
padding={0.1}
|
||||||
|
innerRadius={0.8}
|
||||||
|
padAngle={1.0}
|
||||||
|
valueFormat=" >-$f"
|
||||||
|
colors={{ datum: 'data.color' }}
|
||||||
|
// colors={{scheme: 'nivo'}}
|
||||||
|
cornerRadius={0.1}
|
||||||
|
borderWidth={1}
|
||||||
|
borderColor={{ from: 'color', modifiers: [['darker', 0.2]] }}
|
||||||
|
radialLabelsSkipAngle={10}
|
||||||
|
radialLabelsTextColor="#333333"
|
||||||
|
radialLabelsLinkColor={{ from: 'color' }}
|
||||||
|
sliceLabelsSkipAngle={30}
|
||||||
|
sliceLabelsTextColor="#fff"
|
||||||
|
legends={[
|
||||||
|
{
|
||||||
|
anchor: 'right',
|
||||||
|
direction: 'column',
|
||||||
|
justify: false,
|
||||||
|
translateX: 70,
|
||||||
|
translateY: 0,
|
||||||
|
itemsSpacing: 5,
|
||||||
|
itemWidth: 80,
|
||||||
|
itemHeight: 24,
|
||||||
|
itemTextColor: '#999',
|
||||||
|
itemOpacity: 1,
|
||||||
|
symbolSize: 18,
|
||||||
|
symbolShape: 'circle',
|
||||||
|
effects: [
|
||||||
|
{
|
||||||
|
on: 'hover',
|
||||||
|
style: {
|
||||||
|
itemTextColor: '#000',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
60
apps/frontend/components/TotalAssetsSummary.js
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import Card from '@mui/material/Card';
|
||||||
|
import CardContent from '@mui/material/CardContent';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import { makeStyles } from '@mui/styles';
|
||||||
|
|
||||||
|
function aggregateAssetsBy(assets, field) {
|
||||||
|
let total = 0.0;
|
||||||
|
for (let key in assets) {
|
||||||
|
if (assets[key]) {
|
||||||
|
let a = assets[key];
|
||||||
|
let value = a[field];
|
||||||
|
total += value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
root: {
|
||||||
|
margin: theme.spacing(1),
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 14,
|
||||||
|
},
|
||||||
|
pos: {
|
||||||
|
marginTop: 12,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default function TotalAssetSummary({ assets }) {
|
||||||
|
const classes = useStyles();
|
||||||
|
return (
|
||||||
|
<Card className={classes.root} variant="outlined">
|
||||||
|
<CardContent>
|
||||||
|
<Typography
|
||||||
|
className={classes.title}
|
||||||
|
color="textSecondary"
|
||||||
|
gutterBottom
|
||||||
|
>
|
||||||
|
Total Account Balance
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h5" component="h2">
|
||||||
|
{Math.round(aggregateAssetsBy(assets, 'inBTC') * 1e8) / 1e8}{' '}
|
||||||
|
<span>BTC</span>
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography className={classes.pos} color="textSecondary">
|
||||||
|
Estimated Value
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography variant="h5" component="h3">
|
||||||
|
{Math.round(aggregateAssetsBy(assets, 'inUSD') * 100) / 100}{' '}
|
||||||
|
<span>USD</span>
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
161
apps/frontend/components/TradingVolumeBar.js
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
import { ResponsiveBar } from '@nivo/bar';
|
||||||
|
import { queryTradingVolume } from '../api/bbgo';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
function toPeriodDateString(time, period) {
|
||||||
|
switch (period) {
|
||||||
|
case 'day':
|
||||||
|
return (
|
||||||
|
time.getFullYear() + '-' + (time.getMonth() + 1) + '-' + time.getDate()
|
||||||
|
);
|
||||||
|
case 'month':
|
||||||
|
return time.getFullYear() + '-' + (time.getMonth() + 1);
|
||||||
|
case 'year':
|
||||||
|
return time.getFullYear();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
time.getFullYear() + '-' + (time.getMonth() + 1) + '-' + time.getDate()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupData(rows, period, segment) {
|
||||||
|
let dateIndex = {};
|
||||||
|
let startTime = null;
|
||||||
|
let endTime = null;
|
||||||
|
let keys = {};
|
||||||
|
|
||||||
|
rows.forEach((v) => {
|
||||||
|
const time = new Date(v.time);
|
||||||
|
if (!startTime) {
|
||||||
|
startTime = time;
|
||||||
|
}
|
||||||
|
|
||||||
|
endTime = time;
|
||||||
|
|
||||||
|
const dateStr = toPeriodDateString(time, period);
|
||||||
|
const key = v[segment];
|
||||||
|
|
||||||
|
keys[key] = true;
|
||||||
|
|
||||||
|
const k = key ? key : 'total';
|
||||||
|
const quoteVolume = Math.round(v.quoteVolume * 100) / 100;
|
||||||
|
|
||||||
|
if (dateIndex[dateStr]) {
|
||||||
|
dateIndex[dateStr][k] = quoteVolume;
|
||||||
|
} else {
|
||||||
|
dateIndex[dateStr] = {
|
||||||
|
date: dateStr,
|
||||||
|
year: time.getFullYear(),
|
||||||
|
month: time.getMonth() + 1,
|
||||||
|
day: time.getDate(),
|
||||||
|
[k]: quoteVolume,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let data = [];
|
||||||
|
while (startTime < endTime) {
|
||||||
|
const dateStr = toPeriodDateString(startTime, period);
|
||||||
|
const groupData = dateIndex[dateStr];
|
||||||
|
if (groupData) {
|
||||||
|
data.push(groupData);
|
||||||
|
} else {
|
||||||
|
data.push({
|
||||||
|
date: dateStr,
|
||||||
|
year: startTime.getFullYear(),
|
||||||
|
month: startTime.getMonth() + 1,
|
||||||
|
day: startTime.getDate(),
|
||||||
|
total: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (period) {
|
||||||
|
case 'day':
|
||||||
|
startTime.setDate(startTime.getDate() + 1);
|
||||||
|
break;
|
||||||
|
case 'month':
|
||||||
|
startTime.setMonth(startTime.getMonth() + 1);
|
||||||
|
break;
|
||||||
|
case 'year':
|
||||||
|
startTime.setFullYear(startTime.getFullYear() + 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [data, Object.keys(keys)];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TradingVolumeBar(props) {
|
||||||
|
const [tradingVolumes, setTradingVolumes] = useState([]);
|
||||||
|
const [period, setPeriod] = useState(props.period);
|
||||||
|
const [segment, setSegment] = useState(props.segment);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.period !== period) {
|
||||||
|
setPeriod(props.period);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.segment !== segment) {
|
||||||
|
setSegment(props.segment);
|
||||||
|
}
|
||||||
|
|
||||||
|
queryTradingVolume(
|
||||||
|
{ period: props.period, segment: props.segment },
|
||||||
|
(tradingVolumes) => {
|
||||||
|
setTradingVolumes(tradingVolumes);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}, [props.period, props.segment]);
|
||||||
|
|
||||||
|
const [data, keys] = groupData(tradingVolumes, period, segment);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ResponsiveBar
|
||||||
|
keys={keys}
|
||||||
|
data={data}
|
||||||
|
indexBy={'date'}
|
||||||
|
margin={{ top: 50, right: 160, bottom: 100, left: 60 }}
|
||||||
|
padding={0.3}
|
||||||
|
valueScale={{ type: 'linear' }}
|
||||||
|
indexScale={{ type: 'band', round: true }}
|
||||||
|
labelSkipWidth={30}
|
||||||
|
labelSkipHeight={20}
|
||||||
|
enableGridY={true}
|
||||||
|
colors={{ scheme: 'paired' }}
|
||||||
|
axisBottom={{
|
||||||
|
tickRotation: -90,
|
||||||
|
legend: period,
|
||||||
|
legendPosition: 'middle',
|
||||||
|
legendOffset: 80,
|
||||||
|
}}
|
||||||
|
legends={[
|
||||||
|
{
|
||||||
|
dataFrom: 'keys',
|
||||||
|
anchor: 'right',
|
||||||
|
direction: 'column',
|
||||||
|
justify: false,
|
||||||
|
translateX: 120,
|
||||||
|
translateY: 0,
|
||||||
|
itemsSpacing: 2,
|
||||||
|
itemWidth: 100,
|
||||||
|
itemHeight: 20,
|
||||||
|
itemDirection: 'left-to-right',
|
||||||
|
itemOpacity: 0.85,
|
||||||
|
symbolSize: 20,
|
||||||
|
effects: [
|
||||||
|
{
|
||||||
|
on: 'hover',
|
||||||
|
style: {
|
||||||
|
itemOpacity: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
animate={true}
|
||||||
|
motionStiffness={90}
|
||||||
|
motionDamping={15}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
72
apps/frontend/components/TradingVolumePanel.js
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
import Paper from '@mui/material/Paper';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Tabs from '@mui/material/Tabs';
|
||||||
|
import Tab from '@mui/material/Tab';
|
||||||
|
import React from 'react';
|
||||||
|
import TradingVolumeBar from './TradingVolumeBar';
|
||||||
|
import { makeStyles } from '@mui/styles';
|
||||||
|
import Grid from '@mui/material/Grid';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
tradingVolumeBarBox: {
|
||||||
|
height: 400,
|
||||||
|
},
|
||||||
|
paper: {
|
||||||
|
margin: theme.spacing(2),
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default function TradingVolumePanel() {
|
||||||
|
const [period, setPeriod] = React.useState('day');
|
||||||
|
const [segment, setSegment] = React.useState('exchange');
|
||||||
|
const classes = useStyles();
|
||||||
|
const handlePeriodChange = (event, newValue) => {
|
||||||
|
setPeriod(newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSegmentChange = (event, newValue) => {
|
||||||
|
setSegment(newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper className={classes.paper}>
|
||||||
|
<Typography variant="h4" gutterBottom>
|
||||||
|
Trading Volume
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Grid container spacing={0}>
|
||||||
|
<Grid item xs={12} md={6}>
|
||||||
|
<Tabs
|
||||||
|
value={period}
|
||||||
|
onChange={handlePeriodChange}
|
||||||
|
indicatorColor="primary"
|
||||||
|
textColor="primary"
|
||||||
|
>
|
||||||
|
<Tab label="Day" value={'day'} />
|
||||||
|
<Tab label="Month" value={'month'} />
|
||||||
|
<Tab label="Year" value={'year'} />
|
||||||
|
</Tabs>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} md={6}>
|
||||||
|
<Grid container justifyContent={'flex-end'}>
|
||||||
|
<Tabs
|
||||||
|
value={segment}
|
||||||
|
onChange={handleSegmentChange}
|
||||||
|
indicatorColor="primary"
|
||||||
|
textColor="primary"
|
||||||
|
>
|
||||||
|
<Tab label="By Exchange" value={'exchange'} />
|
||||||
|
<Tab label="By Symbol" value={'symbol'} />
|
||||||
|
</Tabs>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Box className={classes.tradingVolumeBarBox}>
|
||||||
|
<TradingVolumeBar period={period} segment={segment} />
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
20
apps/frontend/hooks/useInterval.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
export default function useInterval(cb: Function, delayMs: number | null) {
|
||||||
|
const savedCallback = useRef<Function>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
savedCallback.current = cb;
|
||||||
|
}, [cb]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function tick() {
|
||||||
|
savedCallback.current();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delayMs !== null) {
|
||||||
|
let timerId = setInterval(tick, delayMs);
|
||||||
|
return () => clearInterval(timerId);
|
||||||
|
}
|
||||||
|
}, [delayMs]);
|
||||||
|
}
|
65
apps/frontend/layouts/DashboardLayout.js
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { makeStyles } from '@mui/styles';
|
||||||
|
import AppBar from '@mui/material/AppBar';
|
||||||
|
import Toolbar from '@mui/material/Toolbar';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import Container from '@mui/material/Container';
|
||||||
|
|
||||||
|
import SideBar from '../components/SideBar';
|
||||||
|
import SyncButton from '../components/SyncButton';
|
||||||
|
|
||||||
|
import ConnectWallet from '../components/ConnectWallet';
|
||||||
|
import { Box } from '@mui/material';
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
root: {
|
||||||
|
flexGrow: 1,
|
||||||
|
display: 'flex',
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
flexGrow: 1,
|
||||||
|
height: '100vh',
|
||||||
|
overflow: 'auto',
|
||||||
|
},
|
||||||
|
appBar: {
|
||||||
|
zIndex: theme.zIndex.drawer + 1,
|
||||||
|
},
|
||||||
|
appBarSpacer: theme.mixins.toolbar,
|
||||||
|
container: {},
|
||||||
|
toolbar: {
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default function DashboardLayout({ children }) {
|
||||||
|
const classes = useStyles();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.root}>
|
||||||
|
<AppBar className={classes.appBar}>
|
||||||
|
<Toolbar className={classes.toolbar}>
|
||||||
|
<Typography variant="h6" className={classes.title}>
|
||||||
|
BBGO
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ flexGrow: 1 }} />
|
||||||
|
<SyncButton />
|
||||||
|
<ConnectWallet />
|
||||||
|
</Toolbar>
|
||||||
|
</AppBar>
|
||||||
|
|
||||||
|
<SideBar />
|
||||||
|
|
||||||
|
<main className={classes.content}>
|
||||||
|
<div className={classes.appBarSpacer} />
|
||||||
|
<Container
|
||||||
|
className={classes.container}
|
||||||
|
maxWidth={false}
|
||||||
|
disableGutters={true}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Container>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
43
apps/frontend/layouts/PlainLayout.js
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { makeStyles } from '@mui/styles';
|
||||||
|
import AppBar from '@mui/material/AppBar';
|
||||||
|
import Toolbar from '@mui/material/Toolbar';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import Container from '@mui/material/Container';
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
root: {
|
||||||
|
// flexGrow: 1,
|
||||||
|
display: 'flex',
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
flexGrow: 1,
|
||||||
|
height: '100vh',
|
||||||
|
overflow: 'auto',
|
||||||
|
},
|
||||||
|
appBar: {
|
||||||
|
zIndex: theme.zIndex.drawer + 1,
|
||||||
|
},
|
||||||
|
appBarSpacer: theme.mixins.toolbar,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default function PlainLayout(props) {
|
||||||
|
const classes = useStyles();
|
||||||
|
return (
|
||||||
|
<div className={classes.root}>
|
||||||
|
<AppBar className={classes.appBar}>
|
||||||
|
<Toolbar>
|
||||||
|
<Typography variant="h6" className={classes.title}>
|
||||||
|
{props && props.title ? props.title : 'BBGO Setup Wizard'}
|
||||||
|
</Typography>
|
||||||
|
</Toolbar>
|
||||||
|
</AppBar>
|
||||||
|
|
||||||
|
<main className={classes.content}>
|
||||||
|
<div className={classes.appBarSpacer} />
|
||||||
|
<Container>{props.children}</Container>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
5
apps/frontend/next-env.d.ts
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
/// <reference types="next" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited
|
||||||
|
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
9
apps/frontend/next.config.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
module.exports = async (phase, { defaultConfig }) => {
|
||||||
|
/**
|
||||||
|
* @type {import('next').NextConfig}
|
||||||
|
*/
|
||||||
|
const nextConfig = {
|
||||||
|
/* config options here */
|
||||||
|
}
|
||||||
|
return nextConfig
|
||||||
|
}
|
42
apps/frontend/package.json
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "yarn run next dev",
|
||||||
|
"build": "yarn run next build",
|
||||||
|
"start": "yarn run next start",
|
||||||
|
"export": "yarn run next build && yarn run next export",
|
||||||
|
"prettier": "prettier --write ."
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@emotion/react": "^11.9.3",
|
||||||
|
"@emotion/styled": "^11.9.3",
|
||||||
|
"@ethersproject/units": "^5.6.1",
|
||||||
|
"@mui/icons-material": "^5.8.3",
|
||||||
|
"@mui/lab": "^5.0.0-alpha.85",
|
||||||
|
"@mui/material": "^5.8.3",
|
||||||
|
"@mui/styles": "^5.8.3",
|
||||||
|
"@mui/x-data-grid": "^5.12.1",
|
||||||
|
"@nivo/bar": "^0.79.1",
|
||||||
|
"@nivo/core": "^0.79.0",
|
||||||
|
"@nivo/pie": "^0.79.1",
|
||||||
|
"@usedapp/core": "1.0.9",
|
||||||
|
"axios": "^0.27.2",
|
||||||
|
"classnames": "^2.2.6",
|
||||||
|
"ethers": "^5.6.9",
|
||||||
|
"isomorphic-fetch": "^3.0.0",
|
||||||
|
"next": "12",
|
||||||
|
"qrcode.react": "^3.0.1",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-number-format": "^4.4.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^18.0.0",
|
||||||
|
"@types/react": "^18.0.14",
|
||||||
|
"next-transpile-modules": "^9.0.0",
|
||||||
|
"prettier": "^2.6.2",
|
||||||
|
"typescript": "^4.1.3"
|
||||||
|
}
|
||||||
|
}
|
43
apps/frontend/pages/_app.tsx
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Head from 'next/head';
|
||||||
|
|
||||||
|
import { ThemeProvider } from '@mui/material/styles';
|
||||||
|
|
||||||
|
import CssBaseline from '@mui/material/CssBaseline';
|
||||||
|
import theme from '../src/theme';
|
||||||
|
import '../styles/globals.css';
|
||||||
|
|
||||||
|
export default function MyApp(props) {
|
||||||
|
const { Component, pageProps } = props;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Remove the server-side injected CSS.
|
||||||
|
const jssStyles = document.querySelector('#jss-server-side');
|
||||||
|
if (jssStyles) {
|
||||||
|
jssStyles.parentElement.removeChild(jssStyles);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Head>
|
||||||
|
<title>BBGO</title>
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="minimum-scale=1, initial-scale=1, width=device-width"
|
||||||
|
/>
|
||||||
|
</Head>
|
||||||
|
<ThemeProvider theme={theme}>
|
||||||
|
{/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
|
||||||
|
<CssBaseline />
|
||||||
|
<Component {...pageProps} />
|
||||||
|
</ThemeProvider>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
MyApp.propTypes = {
|
||||||
|
Component: PropTypes.elementType.isRequired,
|
||||||
|
pageProps: PropTypes.object.isRequired,
|
||||||
|
};
|
72
apps/frontend/pages/_document.js
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
/* eslint-disable react/jsx-filename-extension */
|
||||||
|
import React from 'react';
|
||||||
|
import Document, { Html, Head, Main, NextScript } from 'next/document';
|
||||||
|
import { ServerStyleSheets } from '@mui/styles';
|
||||||
|
import theme from '../src/theme';
|
||||||
|
|
||||||
|
export default class MyDocument extends Document {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Html lang="en">
|
||||||
|
<Head>
|
||||||
|
{/* PWA primary color */}
|
||||||
|
<meta name="theme-color" content={theme.palette.primary.main} />
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
|
||||||
|
/>
|
||||||
|
</Head>
|
||||||
|
<body>
|
||||||
|
<Main />
|
||||||
|
<NextScript />
|
||||||
|
</body>
|
||||||
|
</Html>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// `getInitialProps` belongs to `_document` (instead of `_app`),
|
||||||
|
// it's compatible with server-side generation (SSG).
|
||||||
|
MyDocument.getInitialProps = async (ctx) => {
|
||||||
|
// Resolution order
|
||||||
|
//
|
||||||
|
// On the server:
|
||||||
|
// 1. app.getInitialProps
|
||||||
|
// 2. page.getInitialProps
|
||||||
|
// 3. document.getInitialProps
|
||||||
|
// 4. app.render
|
||||||
|
// 5. page.render
|
||||||
|
// 6. document.render
|
||||||
|
//
|
||||||
|
// On the server with error:
|
||||||
|
// 1. document.getInitialProps
|
||||||
|
// 2. app.render
|
||||||
|
// 3. page.render
|
||||||
|
// 4. document.render
|
||||||
|
//
|
||||||
|
// On the client
|
||||||
|
// 1. app.getInitialProps
|
||||||
|
// 2. page.getInitialProps
|
||||||
|
// 3. app.render
|
||||||
|
// 4. page.render
|
||||||
|
|
||||||
|
// Render app and page and get the context of the page with collected side effects.
|
||||||
|
const sheets = new ServerStyleSheets();
|
||||||
|
const originalRenderPage = ctx.renderPage;
|
||||||
|
|
||||||
|
ctx.renderPage = () =>
|
||||||
|
originalRenderPage({
|
||||||
|
enhanceApp: (App) => (props) => sheets.collect(<App {...props} />),
|
||||||
|
});
|
||||||
|
|
||||||
|
const initialProps = await Document.getInitialProps(ctx);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...initialProps,
|
||||||
|
// Styles fragment is rendered after the app and page rendering finish.
|
||||||
|
styles: [
|
||||||
|
...React.Children.toArray(initialProps.styles),
|
||||||
|
sheets.getStyleElement(),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
};
|
6
apps/frontend/pages/api/hello.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||||
|
|
||||||
|
export default (req, res) => {
|
||||||
|
res.statusCode = 200;
|
||||||
|
res.json({ name: 'John Doe' });
|
||||||
|
};
|
55
apps/frontend/pages/connect/index.js
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { makeStyles } from '@mui/styles';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import Paper from '@mui/material/Paper';
|
||||||
|
import PlainLayout from '../../layouts/PlainLayout';
|
||||||
|
import { QRCodeSVG } from 'qrcode.react';
|
||||||
|
import { queryOutboundIP } from '../../api/bbgo';
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
paper: {
|
||||||
|
margin: theme.spacing(2),
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
},
|
||||||
|
dataGridContainer: {
|
||||||
|
display: 'flex',
|
||||||
|
textAlign: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
alignContent: 'center',
|
||||||
|
height: 320,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
function fetchConnectUrl(cb) {
|
||||||
|
return queryOutboundIP((outboundIP) => {
|
||||||
|
cb(
|
||||||
|
window.location.protocol + '//' + outboundIP + ':' + window.location.port
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Connect() {
|
||||||
|
const classes = useStyles();
|
||||||
|
|
||||||
|
const [connectUrl, setConnectUrl] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchConnectUrl(function (url) {
|
||||||
|
setConnectUrl(url);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PlainLayout title={'Connect'}>
|
||||||
|
<Paper className={classes.paper}>
|
||||||
|
<Typography variant="h4" gutterBottom>
|
||||||
|
Sign In Using QR Codes
|
||||||
|
</Typography>
|
||||||
|
<div className={classes.dataGridContainer}>
|
||||||
|
<QRCodeSVG size={160} style={{ flexGrow: 1 }} value={connectUrl} />
|
||||||
|
</div>
|
||||||
|
</Paper>
|
||||||
|
</PlainLayout>
|
||||||
|
);
|
||||||
|
}
|
121
apps/frontend/pages/index.tsx
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
|
||||||
|
import { makeStyles } from '@mui/styles';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Grid from '@mui/material/Grid';
|
||||||
|
import Paper from '@mui/material/Paper';
|
||||||
|
|
||||||
|
import TotalAssetsPie from '../components/TotalAssetsPie';
|
||||||
|
import TotalAssetSummary from '../components/TotalAssetsSummary';
|
||||||
|
import TotalAssetDetails from '../components/TotalAssetsDetails';
|
||||||
|
|
||||||
|
import TradingVolumePanel from '../components/TradingVolumePanel';
|
||||||
|
import ExchangeSessionTabPanel from '../components/ExchangeSessionTabPanel';
|
||||||
|
|
||||||
|
import DashboardLayout from '../layouts/DashboardLayout';
|
||||||
|
|
||||||
|
import { queryAssets, querySessions } from '../api/bbgo';
|
||||||
|
|
||||||
|
import { ChainId, Config, DAppProvider } from '@usedapp/core';
|
||||||
|
import { Theme } from '@mui/material/styles';
|
||||||
|
|
||||||
|
// fix the `theme.spacing` missing error
|
||||||
|
// https://stackoverflow.com/a/70707121/3897950
|
||||||
|
declare module '@mui/styles/defaultTheme' {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-interface (remove this line if you don't have the rule enabled)
|
||||||
|
interface DefaultTheme extends Theme {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
totalAssetsSummary: {
|
||||||
|
margin: theme.spacing(2),
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
flexGrow: 1,
|
||||||
|
},
|
||||||
|
control: {
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const config: Config = {
|
||||||
|
readOnlyChainId: ChainId.Mainnet,
|
||||||
|
readOnlyUrls: {
|
||||||
|
[ChainId.Mainnet]:
|
||||||
|
'https://mainnet.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// props are pageProps passed from _app.tsx
|
||||||
|
export default function Home() {
|
||||||
|
const classes = useStyles();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const [assets, setAssets] = useState({});
|
||||||
|
const [sessions, setSessions] = React.useState([]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
querySessions((sessions) => {
|
||||||
|
if (sessions && sessions.length > 0) {
|
||||||
|
setSessions(sessions);
|
||||||
|
queryAssets(setAssets);
|
||||||
|
} else {
|
||||||
|
router.push('/setup');
|
||||||
|
}
|
||||||
|
}).catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
}, [router]);
|
||||||
|
|
||||||
|
if (sessions.length == 0) {
|
||||||
|
return (
|
||||||
|
<DashboardLayout>
|
||||||
|
<Box m={4}>
|
||||||
|
<Typography variant="h4" gutterBottom>
|
||||||
|
Loading
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</DashboardLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('index: assets', assets);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DAppProvider config={config}>
|
||||||
|
<DashboardLayout>
|
||||||
|
<Paper className={classes.totalAssetsSummary}>
|
||||||
|
<Typography variant="h4" gutterBottom>
|
||||||
|
Total Assets
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<div className={classes.grid}>
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
direction="row"
|
||||||
|
justifyContent="space-around"
|
||||||
|
alignItems="flex-start"
|
||||||
|
spacing={1}
|
||||||
|
>
|
||||||
|
<Grid item xs={12} md={8}>
|
||||||
|
<TotalAssetSummary assets={assets} />
|
||||||
|
<TotalAssetsPie assets={assets} />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12} md={4}>
|
||||||
|
<TotalAssetDetails assets={assets} />
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</div>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
<TradingVolumePanel />
|
||||||
|
|
||||||
|
<ExchangeSessionTabPanel />
|
||||||
|
</DashboardLayout>
|
||||||
|
</DAppProvider>
|
||||||
|
);
|
||||||
|
}
|
81
apps/frontend/pages/orders.js
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { makeStyles } from '@mui/styles';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import Paper from '@mui/material/Paper';
|
||||||
|
import { queryClosedOrders } from '../api/bbgo';
|
||||||
|
import { DataGrid } from '@mui/x-data-grid';
|
||||||
|
import DashboardLayout from '../layouts/DashboardLayout';
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ field: 'gid', headerName: 'GID', width: 80, type: 'number' },
|
||||||
|
{ field: 'clientOrderID', headerName: 'Client Order ID', width: 130 },
|
||||||
|
{ field: 'exchange', headerName: 'Exchange' },
|
||||||
|
{ field: 'symbol', headerName: 'Symbol' },
|
||||||
|
{ field: 'orderType', headerName: 'Type' },
|
||||||
|
{ field: 'side', headerName: 'Side', width: 90 },
|
||||||
|
{
|
||||||
|
field: 'averagePrice',
|
||||||
|
headerName: 'Average Price',
|
||||||
|
type: 'number',
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{ field: 'quantity', headerName: 'Quantity', type: 'number' },
|
||||||
|
{
|
||||||
|
field: 'executedQuantity',
|
||||||
|
headerName: 'Executed Quantity',
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
{ field: 'status', headerName: 'Status' },
|
||||||
|
{ field: 'isMargin', headerName: 'Margin' },
|
||||||
|
{ field: 'isIsolated', headerName: 'Isolated' },
|
||||||
|
{ field: 'creationTime', headerName: 'Create Time', width: 200 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
paper: {
|
||||||
|
margin: theme.spacing(2),
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
},
|
||||||
|
dataGridContainer: {
|
||||||
|
display: 'flex',
|
||||||
|
height: 'calc(100vh - 64px - 120px)',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default function Orders() {
|
||||||
|
const classes = useStyles();
|
||||||
|
|
||||||
|
const [orders, setOrders] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
queryClosedOrders({}, (orders) => {
|
||||||
|
setOrders(
|
||||||
|
orders.map((o) => {
|
||||||
|
o.id = o.gid;
|
||||||
|
return o;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DashboardLayout>
|
||||||
|
<Paper className={classes.paper}>
|
||||||
|
<Typography variant="h4" gutterBottom>
|
||||||
|
Orders
|
||||||
|
</Typography>
|
||||||
|
<div className={classes.dataGridContainer}>
|
||||||
|
<div style={{ flexGrow: 1 }}>
|
||||||
|
<DataGrid
|
||||||
|
rows={orders}
|
||||||
|
columns={columns}
|
||||||
|
pageSize={50}
|
||||||
|
autoPageSize={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Paper>
|
||||||
|
</DashboardLayout>
|
||||||
|
);
|
||||||
|
}
|
132
apps/frontend/pages/setup/index.js
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { makeStyles } from '@mui/styles';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Paper from '@mui/material/Paper';
|
||||||
|
import Stepper from '@mui/material/Stepper';
|
||||||
|
import Step from '@mui/material/Step';
|
||||||
|
import StepLabel from '@mui/material/StepLabel';
|
||||||
|
|
||||||
|
import ConfigureDatabaseForm from '../../components/ConfigureDatabaseForm';
|
||||||
|
import AddExchangeSessionForm from '../../components/AddExchangeSessionForm';
|
||||||
|
import ReviewSessions from '../../components/ReviewSessions';
|
||||||
|
import ConfigureGridStrategyForm from '../../components/ConfigureGridStrategyForm';
|
||||||
|
import ReviewStrategies from '../../components/ReviewStrategies';
|
||||||
|
import SaveConfigAndRestart from '../../components/SaveConfigAndRestart';
|
||||||
|
|
||||||
|
import PlainLayout from '../../layouts/PlainLayout';
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
paper: {
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const steps = [
|
||||||
|
'Configure Database',
|
||||||
|
'Add Exchange Session',
|
||||||
|
'Review Sessions',
|
||||||
|
'Configure Strategy',
|
||||||
|
'Review Strategies',
|
||||||
|
'Save Config and Restart',
|
||||||
|
];
|
||||||
|
|
||||||
|
function getStepContent(step, setActiveStep) {
|
||||||
|
switch (step) {
|
||||||
|
case 0:
|
||||||
|
return (
|
||||||
|
<ConfigureDatabaseForm
|
||||||
|
onConfigured={() => {
|
||||||
|
setActiveStep(1);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 1:
|
||||||
|
return (
|
||||||
|
<AddExchangeSessionForm
|
||||||
|
onBack={() => {
|
||||||
|
setActiveStep(0);
|
||||||
|
}}
|
||||||
|
onAdded={() => {
|
||||||
|
setActiveStep(2);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 2:
|
||||||
|
return (
|
||||||
|
<ReviewSessions
|
||||||
|
onBack={() => {
|
||||||
|
setActiveStep(1);
|
||||||
|
}}
|
||||||
|
onNext={() => {
|
||||||
|
setActiveStep(3);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 3:
|
||||||
|
return (
|
||||||
|
<ConfigureGridStrategyForm
|
||||||
|
onBack={() => {
|
||||||
|
setActiveStep(2);
|
||||||
|
}}
|
||||||
|
onAdded={() => {
|
||||||
|
setActiveStep(4);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 4:
|
||||||
|
return (
|
||||||
|
<ReviewStrategies
|
||||||
|
onBack={() => {
|
||||||
|
setActiveStep(3);
|
||||||
|
}}
|
||||||
|
onNext={() => {
|
||||||
|
setActiveStep(5);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 5:
|
||||||
|
return (
|
||||||
|
<SaveConfigAndRestart
|
||||||
|
onBack={() => {
|
||||||
|
setActiveStep(4);
|
||||||
|
}}
|
||||||
|
onRestarted={() => {}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error('Unknown step');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Setup() {
|
||||||
|
const classes = useStyles();
|
||||||
|
const [activeStep, setActiveStep] = React.useState(0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PlainLayout>
|
||||||
|
<Box m={4}>
|
||||||
|
<Paper className={classes.paper}>
|
||||||
|
<Typography variant="h4" component="h2" gutterBottom>
|
||||||
|
Setup Session
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Stepper activeStep={activeStep} className={classes.stepper}>
|
||||||
|
{steps.map((label) => (
|
||||||
|
<Step key={label}>
|
||||||
|
<StepLabel>{label}</StepLabel>
|
||||||
|
</Step>
|
||||||
|
))}
|
||||||
|
</Stepper>
|
||||||
|
|
||||||
|
<React.Fragment>
|
||||||
|
{getStepContent(activeStep, setActiveStep)}
|
||||||
|
</React.Fragment>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
</PlainLayout>
|
||||||
|
);
|
||||||
|
}
|
43
apps/frontend/pages/strategies.tsx
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import { styled } from '@mui/styles';
|
||||||
|
import DashboardLayout from '../layouts/DashboardLayout';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { queryStrategiesMetrics } from '../api/bbgo';
|
||||||
|
import type { GridStrategy } from '../api/bbgo';
|
||||||
|
|
||||||
|
import Detail from '../components/Detail';
|
||||||
|
|
||||||
|
const StrategiesContainer = styled('div')(() => ({
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
padding: '40px 20px',
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'repeat(3, 350px);',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: '30px',
|
||||||
|
'@media(max-width: 1400px)': {
|
||||||
|
gridTemplateColumns: 'repeat(2, 350px)',
|
||||||
|
},
|
||||||
|
'@media(max-width: 1000px)': {
|
||||||
|
gridTemplateColumns: '350px',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default function Strategies() {
|
||||||
|
const [details, setDetails] = useState<GridStrategy[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
queryStrategiesMetrics().then((value) => {
|
||||||
|
setDetails(value);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DashboardLayout>
|
||||||
|
<StrategiesContainer>
|
||||||
|
{details.map((element) => {
|
||||||
|
return <Detail key={element.id} data={element} />;
|
||||||
|
})}
|
||||||
|
</StrategiesContainer>
|
||||||
|
</DashboardLayout>
|
||||||
|
);
|
||||||
|
}
|
68
apps/frontend/pages/trades.js
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { makeStyles } from '@mui/styles';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import Paper from '@mui/material/Paper';
|
||||||
|
import { queryTrades } from '../api/bbgo';
|
||||||
|
import { DataGrid } from '@mui/x-data-grid';
|
||||||
|
import DashboardLayout from '../layouts/DashboardLayout';
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ field: 'gid', headerName: 'GID', width: 80, type: 'number' },
|
||||||
|
{ field: 'exchange', headerName: 'Exchange' },
|
||||||
|
{ field: 'symbol', headerName: 'Symbol' },
|
||||||
|
{ field: 'side', headerName: 'Side', width: 90 },
|
||||||
|
{ field: 'price', headerName: 'Price', type: 'number', width: 120 },
|
||||||
|
{ field: 'quantity', headerName: 'Quantity', type: 'number' },
|
||||||
|
{ field: 'isMargin', headerName: 'Margin' },
|
||||||
|
{ field: 'isIsolated', headerName: 'Isolated' },
|
||||||
|
{ field: 'tradedAt', headerName: 'Trade Time', width: 200 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
paper: {
|
||||||
|
margin: theme.spacing(2),
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
},
|
||||||
|
dataGridContainer: {
|
||||||
|
display: 'flex',
|
||||||
|
height: 'calc(100vh - 64px - 120px)',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default function Trades() {
|
||||||
|
const classes = useStyles();
|
||||||
|
|
||||||
|
const [trades, setTrades] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
queryTrades({}, (trades) => {
|
||||||
|
setTrades(
|
||||||
|
trades.map((o) => {
|
||||||
|
o.id = o.gid;
|
||||||
|
return o;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DashboardLayout>
|
||||||
|
<Paper className={classes.paper}>
|
||||||
|
<Typography variant="h4" gutterBottom>
|
||||||
|
Trades
|
||||||
|
</Typography>
|
||||||
|
<div className={classes.dataGridContainer}>
|
||||||
|
<div style={{ flexGrow: 1 }}>
|
||||||
|
<DataGrid
|
||||||
|
rows={trades}
|
||||||
|
columns={columns}
|
||||||
|
showToolbar={true}
|
||||||
|
autoPageSize={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Paper>
|
||||||
|
</DashboardLayout>
|
||||||
|
);
|
||||||
|
}
|
BIN
apps/frontend/public/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
18
apps/frontend/public/images/bch-logo.svg
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 24.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#8DC351;}
|
||||||
|
.st1{fill:#FFFFFF;}
|
||||||
|
</style>
|
||||||
|
<g>
|
||||||
|
<circle class="st0" cx="16" cy="16" r="16"/>
|
||||||
|
<path class="st1" d="M21.2,10.5c-0.8-2-2.7-2.1-5-1.7L15.4,6l-1.7,0.5l0.8,2.7c-0.4,0.1-0.9,0.3-1.4,0.4l-0.8-2.8l-1.7,0.5l0.8,2.8
|
||||||
|
c-0.4,0.1-0.7,0.2-1.1,0.3l0,0L8,11.2L8.5,13c0,0,1.3-0.4,1.2-0.4c0.7-0.2,1,0.1,1.2,0.5l0.9,3.2c0,0,0.1,0,0.2,0l-0.2,0.1l1.3,4.5
|
||||||
|
c0,0.2,0,0.6-0.5,0.8c0,0-1.2,0.4-1.2,0.4l0.2,2.1l2.2-0.6c0.4-0.1,0.8-0.2,1.2-0.3l0.8,2.8l1.7-0.5l-0.8-2.8
|
||||||
|
c0.5-0.1,0.9-0.2,1.4-0.4l0.8,2.8l1.7-0.5l-0.8-2.8c2.8-1,4.6-2.3,4.1-5.1c-0.4-2.2-1.7-2.9-3.5-2.8C21.4,13,21.8,12,21.2,10.5
|
||||||
|
L21.2,10.5z M20.6,17.3c0.6,2.1-3.1,2.9-4.3,3.3l-1.1-3.8C16.4,16.5,19.9,15.1,20.6,17.3L20.6,17.3z M18.2,12.2
|
||||||
|
c0.6,1.9-2.5,2.6-3.5,2.9l-1-3.4C14.7,11.4,17.7,10.2,18.2,12.2L18.2,12.2z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
1
apps/frontend/public/images/bnb-logo.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2500.01 2500"><defs><style>.cls-1{fill:#f3ba2f;}</style></defs><title>bi</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M764.48,1050.52,1250,565l485.75,485.73,282.5-282.5L1250,0,482,768l282.49,282.5M0,1250,282.51,967.45,565,1249.94,282.49,1532.45Zm764.48,199.51L1250,1935l485.74-485.72,282.65,282.35-.14.15L1250,2500,482,1732l-.4-.4,282.91-282.12M1935,1250.12l282.51-282.51L2500,1250.1,2217.5,1532.61Z"/><path class="cls-1" d="M1536.52,1249.85h.12L1250,963.19,1038.13,1175h0l-24.34,24.35-50.2,50.21-.4.39.4.41L1250,1536.81l286.66-286.66.14-.16-.26-.14"/></g></g></svg>
|
After Width: | Height: | Size: 678 B |
15
apps/frontend/public/images/btc-logo.svg
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<!-- Creator: CorelDRAW 2019 (64-Bit) -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="100%" height="100%" version="1.1" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd" clip-rule="evenodd"
|
||||||
|
viewBox="0 0 4091.27 4091.73"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
xmlns:xodm="http://www.corel.com/coreldraw/odm/2003">
|
||||||
|
<g id="Layer_x0020_1">
|
||||||
|
<metadata id="CorelCorpID_0Corel-Layer"/>
|
||||||
|
<g id="_1421344023328">
|
||||||
|
<path fill="#F7931A" fill-rule="nonzero" d="M4030.06 2540.77c-273.24,1096.01 -1383.32,1763.02 -2479.46,1489.71 -1095.68,-273.24 -1762.69,-1383.39 -1489.33,-2479.31 273.12,-1096.13 1383.2,-1763.19 2479,-1489.95 1096.06,273.24 1763.03,1383.51 1489.76,2479.57l0.02 -0.02z"/>
|
||||||
|
<path fill="white" fill-rule="nonzero" d="M2947.77 1754.38c40.72,-272.26 -166.56,-418.61 -450,-516.24l91.95 -368.8 -224.5 -55.94 -89.51 359.09c-59.02,-14.72 -119.63,-28.59 -179.87,-42.34l90.16 -361.46 -224.36 -55.94 -92 368.68c-48.84,-11.12 -96.81,-22.11 -143.35,-33.69l0.26 -1.16 -309.59 -77.31 -59.72 239.78c0,0 166.56,38.18 163.05,40.53 90.91,22.69 107.35,82.87 104.62,130.57l-104.74 420.15c6.26,1.59 14.38,3.89 23.34,7.49 -7.49,-1.86 -15.46,-3.89 -23.73,-5.87l-146.81 588.57c-11.11,27.62 -39.31,69.07 -102.87,53.33 2.25,3.26 -163.17,-40.72 -163.17,-40.72l-111.46 256.98 292.15 72.83c54.35,13.63 107.61,27.89 160.06,41.3l-92.9 373.03 224.24 55.94 92 -369.07c61.26,16.63 120.71,31.97 178.91,46.43l-91.69 367.33 224.51 55.94 92.89 -372.33c382.82,72.45 670.67,43.24 791.83,-303.02 97.63,-278.78 -4.86,-439.58 -206.26,-544.44 146.69,-33.83 257.18,-130.31 286.64,-329.61l-0.07 -0.05zm-512.93 719.26c-69.38,278.78 -538.76,128.08 -690.94,90.29l123.28 -494.2c152.17,37.99 640.17,113.17 567.67,403.91zm69.43 -723.3c-63.29,253.58 -453.96,124.75 -580.69,93.16l111.77 -448.21c126.73,31.59 534.85,90.55 468.94,355.05l-0.02 0z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.0 KiB |
16
apps/frontend/public/images/comp-logo.svg
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 24.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 2000 2000" style="enable-background:new 0 0 2000 2000;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#070A0E;}
|
||||||
|
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:#00D395;}
|
||||||
|
</style>
|
||||||
|
<path class="st0" d="M1000,2000c552.3,0,1000-447.7,1000-1000S1552.3,0,1000,0S0,447.7,0,1000S447.7,2000,1000,2000z"/>
|
||||||
|
<path class="st1" d="M577.7,1335.3c-29.9-18.3-48.2-50.8-48.2-85.8v-195.4c0-23.2,18.9-42,42.1-41.9c7.4,0,14.7,2,21.1,5.7
|
||||||
|
l440.9,257.1c25.8,15,41.7,42.6,41.7,72.5v202.4c0.1,27.8-22.4,50.4-50.2,50.4c-9.3,0-18.5-2.6-26.4-7.4L577.7,1335.3z
|
||||||
|
M1234.9,964.4c25.8,15,41.6,42.7,41.7,72.5v410.8c0,12.1-6.5,23.3-17.1,29.2l-96.5,54.3c-1.2,0.7-2.5,1.2-3.9,1.6v-228.1
|
||||||
|
c0-29.5-15.5-56.9-40.9-72.1L731,1001V743.5c0-23.2,18.9-42,42.1-41.9c7.4,0,14.7,2,21.1,5.7L1234.9,964.4z M1427.9,661
|
||||||
|
c25.9,15,41.8,42.7,41.8,72.6v600c-0.1,12.3-6.9,23.6-17.7,29.5l-91.5,49.4V994.8c0-29.5-15.5-56.8-40.7-72L924,685.4V441.2
|
||||||
|
c0-7.4,2-14.7,5.6-21.1c11.7-20,37.4-26.8,57.4-15.2L1427.9,661z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
13
apps/frontend/public/images/dai-logo.svg
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<!-- Creator: CorelDRAW 2019 (64-Bit) -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="100%" height="100%" version="1.1" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd" clip-rule="evenodd"
|
||||||
|
viewBox="0 0 444.44 444.44"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
xmlns:xodm="http://www.corel.com/coreldraw/odm/2003">
|
||||||
|
<g id="Layer_x0020_1">
|
||||||
|
<metadata id="CorelCorpID_0Corel-Layer"/>
|
||||||
|
<path fill="#F5AC37" fill-rule="nonzero" d="M222.22 0c122.74,0 222.22,99.5 222.22,222.22 0,122.74 -99.48,222.22 -222.22,222.22 -122.72,0 -222.22,-99.49 -222.22,-222.22 0,-122.72 99.5,-222.22 222.22,-222.22z"/>
|
||||||
|
<path fill="#FEFEFD" fill-rule="nonzero" d="M230.41 237.91l84.44 0c1.8,0 2.65,0 2.78,-2.36 0.69,-8.59 0.69,-17.23 0,-25.83 0,-1.67 -0.83,-2.36 -2.64,-2.36l-168.05 0c-2.08,0 -2.64,0.69 -2.64,2.64l0 24.72c0,3.19 0,3.19 3.33,3.19l82.78 0zm77.79 -59.44c0.24,-0.63 0.24,-1.32 0,-1.94 -1.41,-3.07 -3.08,-6 -5.02,-8.75 -2.92,-4.7 -6.36,-9.03 -10.28,-12.92 -1.85,-2.35 -3.99,-4.46 -6.39,-6.25 -12.02,-10.23 -26.31,-17.47 -41.67,-21.11 -7.75,-1.74 -15.67,-2.57 -23.61,-2.5l-74.58 0c-2.08,0 -2.36,0.83 -2.36,2.64l0 49.3c0,2.08 0,2.64 2.64,2.64l160.27 0c0,0 1.39,-0.28 1.67,-1.11l-0.68 0zm0 88.33c-2.36,-0.26 -4.74,-0.26 -7.1,0l-154.02 0c-2.08,0 -2.78,0 -2.78,2.78l0 48.2c0,2.22 0,2.78 2.78,2.78l71.11 0c3.4,0.26 6.8,0.02 10.13,-0.69 10.32,-0.74 20.47,-2.98 30.15,-6.67 3.52,-1.22 6.92,-2.81 10.13,-4.72l0.97 0c16.67,-8.67 30.21,-22.29 38.75,-39.01 0,0 0.97,-2.1 -0.12,-2.65zm-191.81 78.75l0 -0.83 0 -32.36 0 -10.97 0 -32.64c0,-1.81 0,-2.08 -2.22,-2.08l-30.14 0c-1.67,0 -2.36,0 -2.36,-2.22l0 -26.39 32.22 0c1.8,0 2.5,0 2.5,-2.36l0 -26.11c0,-1.67 0,-2.08 -2.22,-2.08l-30.14 0c-1.67,0 -2.36,0 -2.36,-2.22l0 -24.44c0,-1.53 0,-1.94 2.22,-1.94l29.86 0c2.08,0 2.64,0 2.64,-2.64l0 -74.86c0,-2.22 0,-2.78 2.78,-2.78l104.16 0c7.56,0.3 15.07,1.13 22.5,2.5 15.31,2.83 30.02,8.3 43.47,16.11 8.92,5.25 17.13,11.59 24.44,18.89 5.5,5.71 10.46,11.89 14.86,18.47 4.37,6.67 8,13.8 10.85,21.25 0.35,1.94 2.21,3.25 4.15,2.92l24.86 0c3.19,0 3.19,0 3.33,3.06l0 22.78c0,2.22 -0.83,2.78 -3.06,2.78l-19.17 0c-1.94,0 -2.5,0 -2.36,2.5 0.76,8.46 0.76,16.95 0,25.41 0,2.36 0,2.64 2.65,2.64l21.93 0c0.97,1.25 0,2.5 0,3.76 0.14,1.61 0.14,3.24 0,4.85l0 16.81c0,2.36 -0.69,3.06 -2.78,3.06l-26.25 0c-1.83,-0.35 -3.61,0.82 -4.03,2.64 -6.25,16.25 -16.25,30.82 -29.17,42.5 -4.72,4.25 -9.68,8.25 -14.86,11.94 -5.56,3.2 -10.97,6.53 -16.67,9.17 -10.49,4.72 -21.49,8.2 -32.78,10.41 -10.72,1.92 -21.59,2.79 -32.5,2.64l-96.39 0 0 -0.14z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.7 KiB |
20
apps/frontend/public/images/dot-logo.svg
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 24.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 1871.3 2503" style="enable-background:new 0 0 1871.3 2503;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#1E1E1E;}
|
||||||
|
.st1{fill:#E6007A;}
|
||||||
|
</style>
|
||||||
|
<title>polkadot</title>
|
||||||
|
<path class="st0" d="M948.2,0C425.4,2.1,2.1,425.4,0,948.2c0,104.7,16.9,208.7,50,308c24.7,67.6,98,104,166.8,83.1
|
||||||
|
c66.5-25.5,101.8-98.2,80.6-166.2c-28.1-77.4-40.6-159.5-36.9-241.7c11.4-377.6,326.7-674.5,704.3-663.1s674.5,326.7,663.1,704.3
|
||||||
|
c-10.7,353.5-289.1,640.6-642.2,662.1c0,0-133.1,8.1-199.3,16.2c-24.4,3.5-48.6,8.3-72.5,14.4c-3.4,3.5-8.9,3.5-12.4,0.1
|
||||||
|
c0,0-0.1-0.1-0.1-0.1c-2.4-3.1-2.4-7.5,0-10.6l20.6-112.4L847,980.1c15-70.2-29.7-139.3-99.9-154.3c-70.2-15-139.3,29.7-154.3,99.9
|
||||||
|
c0,0-297.3,1376.1-297.3,1388.6c-17,66.9,23.4,134.9,90.3,151.9c0.7,0.2,1.5,0.4,2.2,0.5h6.9c66.8,17.3,135-22.9,152.2-89.7
|
||||||
|
c0.3-1.1,0.6-2.2,0.8-3.4c-0.2-2.1-0.2-4.2,0-6.2c3.7-16.2,41.2-199.3,41.2-199.3c28.4-138.2,139.8-244.1,279.2-265.5
|
||||||
|
c28.7-4.4,149.3-12.5,149.3-12.5c520.8-51.9,900.9-516.2,848.9-1037C1819.1,378.1,1425.6,12.4,948.2,0z"/>
|
||||||
|
<path class="st1" d="M1005.7,2186.3c-85.5-17.8-169.1,37.1-186.9,122.5c-0.2,0.8-0.3,1.6-0.5,2.4c-18.5,84.9,35.3,168.8,120.3,187.3
|
||||||
|
c0.1,0,0.2,0,0.3,0.1h4.4c83.2,20.1,166.9-31.1,186.9-114.2c0.2-0.6,0.3-1.3,0.5-1.9v-8.7C1145.4,2288,1090.6,2205.7,1005.7,2186.3z
|
||||||
|
"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
21
apps/frontend/public/images/eth-logo.svg
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<!-- Creator: CorelDRAW 2019 (64-Bit) -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="100%" height="100%" version="1.1" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd" clip-rule="evenodd"
|
||||||
|
viewBox="0 0 784.37 1277.39"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
xmlns:xodm="http://www.corel.com/coreldraw/odm/2003">
|
||||||
|
<g id="Layer_x0020_1">
|
||||||
|
<metadata id="CorelCorpID_0Corel-Layer"/>
|
||||||
|
<g id="_1421394342400">
|
||||||
|
<g>
|
||||||
|
<polygon fill="#343434" fill-rule="nonzero" points="392.07,0 383.5,29.11 383.5,873.74 392.07,882.29 784.13,650.54 "/>
|
||||||
|
<polygon fill="#8C8C8C" fill-rule="nonzero" points="392.07,0 -0,650.54 392.07,882.29 392.07,472.33 "/>
|
||||||
|
<polygon fill="#3C3C3B" fill-rule="nonzero" points="392.07,956.52 387.24,962.41 387.24,1263.28 392.07,1277.38 784.37,724.89 "/>
|
||||||
|
<polygon fill="#8C8C8C" fill-rule="nonzero" points="392.07,1277.38 392.07,956.52 -0,724.89 "/>
|
||||||
|
<polygon fill="#141414" fill-rule="nonzero" points="392.07,882.29 784.13,650.54 392.07,472.33 "/>
|
||||||
|
<polygon fill="#393939" fill-rule="nonzero" points="0,650.54 392.07,882.29 392.07,472.33 "/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
19
apps/frontend/public/images/grt-logo.svg
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="GRT" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 96 96" style="enable-background:new 0 0 96 96;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#6747ED;}
|
||||||
|
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;}
|
||||||
|
</style>
|
||||||
|
<circle class="st0" cx="48" cy="48" r="48"/>
|
||||||
|
<g id="Symbols">
|
||||||
|
<g transform="translate(-88.000000, -52.000000)">
|
||||||
|
<path id="Fill-19" class="st1" d="M135.3,106.2c-7.1,0-12.8-5.7-12.8-12.8c0-7.1,5.7-12.8,12.8-12.8c7.1,0,12.8,5.7,12.8,12.8
|
||||||
|
C148.1,100.5,142.4,106.2,135.3,106.2 M135.3,74.2c10.6,0,19.2,8.6,19.2,19.2s-8.6,19.2-19.2,19.2c-10.6,0-19.2-8.6-19.2-19.2
|
||||||
|
S124.7,74.2,135.3,74.2z M153.6,113.6c1.3,1.3,1.3,3.3,0,4.5l-12.8,12.8c-1.3,1.3-3.3,1.3-4.5,0c-1.3-1.3-1.3-3.3,0-4.5l12.8-12.8
|
||||||
|
C150.3,112.3,152.4,112.3,153.6,113.6z M161,77.4c0,1.8-1.4,3.2-3.2,3.2c-1.8,0-3.2-1.4-3.2-3.2s1.4-3.2,3.2-3.2
|
||||||
|
C159.5,74.2,161,75.6,161,77.4z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.1 KiB |