初始化仓库,b87213827e08344908cdd718039f3120d047b44e

This commit is contained in:
lychiyu 2024-08-30 22:55:17 +08:00
commit 89562094b7
1816 changed files with 242863 additions and 0 deletions

7
.dockerignore Normal file
View File

@ -0,0 +1,7 @@
/contracts
.env.*
/linode
/frontend
/desktop
/data
/output

13
.env.local.example Normal file
View 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
View File

@ -0,0 +1,3 @@
[default]
protoFile = ["pkg/pb/bbgo.proto"]
package = "bbgo"

0
.gitattributes vendored Normal file
View File

68
.gitignore vendored Normal file
View 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
View 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
View File

@ -0,0 +1,5 @@
default: true
extends: null
MD033: false
MD010: false
MD013: false

14
.pre-commit-config.yaml Normal file
View 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
View 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
View File

@ -0,0 +1,2 @@
pkg/strategy/grid2 @kbearXD @gx578007
python @narumiruna

127
CODE_OF_CONDUCT.md Normal file
View 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
View 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 dont 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
View 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
View 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
View 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

1
Procfile Normal file
View File

@ -0,0 +1 @@
worker: bin/bbgo run --config config/bbgo.yaml

667
README.md Normal file
View 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
View 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
# 對於 okexorder 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
View 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"
]
}

View File

@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}

35
apps/backtest-report/.gitignore vendored Normal file
View 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

View 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.

View 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;

View 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;

View 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;

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View 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

View 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;
}
}

View 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
View 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.

View 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);

View 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"
}
}

View 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

View 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;

View 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' })
}

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View 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
View 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;

View File

@ -0,0 +1,3 @@
import { tsvParse, csvParse } from "d3-dsv";
import { timeParse } from "d3-time-format";

View 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;
}
}

View 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;
}

View 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"]
}

View File

@ -0,0 +1,3 @@
export * from './report';
export * from './order';
export * from './market';

View 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;
}

View 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;
}

View 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;
}

File diff suppressed because it is too large Load Diff

34
apps/frontend/.gitignore vendored Normal file
View 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

View File

@ -0,0 +1,3 @@
.next/
out/
node_modules/

View File

@ -0,0 +1,3 @@
{
"singleQuote": true
}

34
apps/frontend/README.md Normal file
View 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
View 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;
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
)}
</>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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}
/>
);
}

View 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>
);
}

View 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]);
}

View 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>
);
}

View 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
View 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.

View File

@ -0,0 +1,9 @@
module.exports = async (phase, { defaultConfig }) => {
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {
/* config options here */
}
return nextConfig
}

View 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"
}
}

View 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,
};

View 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(),
],
};
};

View 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' });
};

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

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