diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index ee1e02e6b0..196516304c 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -7,7 +7,6 @@ on: branches: [ main ] jobs: - build: runs-on: ubuntu-latest @@ -15,8 +14,16 @@ jobs: matrix: redis-version: - 6.2 + go-version: + - 1.17 + - 1.18 + env: + MYSQL_DATABASE: bbgo + MYSQL_USER: "root" + MYSQL_PASSWORD: "root" # pragma: allowlist secret steps: + - uses: actions/checkout@v2 - uses: actions/cache@v2 @@ -28,22 +35,59 @@ jobs: restore-keys: | ${{ runner.os }}-go- - - name: Setup redis + - name: Set up MySQL + run: | + sudo /etc/init.d/mysql start + mysql -e 'CREATE DATABASE ${{ env.MYSQL_DATABASE }};' -u${{ env.MYSQL_USER }} -p${{ env.MYSQL_PASSWORD }} + + - name: Set up redis uses: shogo82148/actions-setup-redis@v1 with: redis-version: ${{ matrix.redis-version }} # auto-start: "false" - name: Set up Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v3 with: - go-version: 1.17 + go-version: ${{ matrix.go-version }} + + - name: Install Migration Tool + run: go install github.com/c9s/rockhopper/cmd/rockhopper@v1.2.1 + + - name: Test Migration SQL Files For MySQL + run: | + rockhopper --config rockhopper_mysql.yaml up + + - name: Test Migration SQL Files For SQLite + run: | + rockhopper --config rockhopper_sqlite.yaml up - name: Build run: go build -v ./cmd/bbgo - name: Test - run: go test -v ./pkg/... + run: | + go test -race -coverprofile coverage.txt -covermode atomic ./pkg/... + sed -i -e '/_requestgen.go/d' coverage.txt - name: TestDnum - run: go test -tags dnum -v ./pkg/... + run: | + go test -race -coverprofile coverage_dnum.txt -covermode atomic -tags dnum ./pkg/... + sed -i -e '/_requestgen.go/d' coverage_dnum.txt + + - name: Revive Check + uses: morphy2k/revive-action@v2 + with: + reporter: github-pr-review + fail_on_error: true + + - name: Upload Coverage Report + uses: codecov/codecov-action@v3 + with: + files: ./coverage.txt,./coverage_dnum.txt + + - name: Create dotenv file + run: | + echo "DB_DRIVER=mysql" >> .env.local + echo "DB_DSN=root:root@/bbgo" >> .env.local + diff --git a/.github/workflows/golang-lint.yml b/.github/workflows/golang-lint.yml new file mode 100644 index 0000000000..420724cb71 --- /dev/null +++ b/.github/workflows/golang-lint.yml @@ -0,0 +1,22 @@ +name: golang-lint +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] +permissions: + contents: read + +jobs: + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/setup-go@v3 + with: + go-version: 1.18 + - uses: actions/checkout@v3 + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + version: v1.46.2 diff --git a/.github/workflows/node.yml b/.github/workflows/node.yml new file mode 100644 index 0000000000..d523b2a8a6 --- /dev/null +++ b/.github/workflows/node.yml @@ -0,0 +1,40 @@ +name: Node.js CI + +on: + push: + branches: [ main ] + paths: + - apps/backtest-report + - frontend + pull_request: + branches: [ main ] + paths: + - apps/backtest-report + - frontend + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [ 16.x ] + + steps: + - uses: actions/checkout@v3 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + - run: npm install -g yarn + - name: Install + run: yarn install + working-directory: "apps/backtest-report" + - name: Build + run: yarn run next build + working-directory: "apps/backtest-report" + - name: Export + run: yarn run next export + working-directory: "apps/backtest-report" + - run: yarn export + working-directory: "frontend" diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 22b15048bc..dde156d4e3 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -3,8 +3,13 @@ name: Python on: push: branches: [ main ] + paths: + - python + pull_request: branches: [ main ] + paths: + - python jobs: @@ -13,31 +18,31 @@ jobs: strategy: matrix: - python-version: [3.8] + python-version: [ 3.8 ] steps: - - name: Checkout code - uses: actions/checkout@v2 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - - name: Install poetry - run: pip install poetry==1.1.13 - - - name: Install package - run: | - cd python - poetry install - - - name: Test - run: | - cd python - poetry run pytest -v -s tests - - - name: Lint - run: | - cd python - poetry run flake8 . + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Install poetry + run: pip install poetry==1.1.13 + + - name: Install package + run: | + cd python + poetry install + + - name: Test + run: | + cd python + poetry run pytest -v -s tests + + - name: Lint + run: | + cd python + poetry run flake8 . diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 65b4a8366f..a23ef5ade7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: - name: Install Go uses: actions/setup-go@v2 with: - go-version: 1.17.5 + go-version: 1.18 - name: Install Node uses: actions/setup-node@v2 with: diff --git a/.gitignore b/.gitignore index 25feb7f168..5e9314db07 100644 --- a/.gitignore +++ b/.gitignore @@ -32,13 +32,35 @@ /config/bbgo.yaml +/localconfig + /pkg/server/assets.go -bbgo.sqlite3 +*.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/ diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000000..aba75da15a --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,9 @@ +run: + issues-exit-code: 0 + tests: true + timeout: 5m +linters: + disable-all: true + enable: + - gofmt + - gosimple diff --git a/.markdownlint.yaml b/.markdownlint.yaml new file mode 100644 index 0000000000..cd599ae329 --- /dev/null +++ b/.markdownlint.yaml @@ -0,0 +1,5 @@ +default: true +extends: null +MD033: false +MD010: false +MD013: false diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000000..cbb0418291 --- /dev/null +++ b/.pre-commit-config.yaml @@ -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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3dfe10936c..c92962cdcb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,6 +22,15 @@ 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 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. diff --git a/Dockerfile b/Dockerfile index 5c3379e4c1..acc49db3d3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # First stage container -FROM golang:1.17.6-alpine3.15 AS builder +FROM golang:1.17.11-alpine3.16 AS builder RUN apk add --no-cache git ca-certificates gcc libc-dev pkgconfig # gcc is for github.com/mattn/go-sqlite3 # ADD . $GOPATH/src/github.com/c9s/bbgo @@ -11,19 +11,21 @@ ENV GOPATH_ORIG=$GOPATH ENV GOPATH=${GO_MOD_CACHE:+$WORKDIR/$GO_MOD_CACHE} ENV GOPATH=${GOPATH:-$GOPATH_ORIG} ENV CGO_ENABLED=1 -RUN go get github.com/mattn/go-sqlite3 +RUN cd $WORKDIR && go get github.com/mattn/go-sqlite3 ADD . . RUN go build -o $GOPATH_ORIG/bin/bbgo ./cmd/bbgo # Second stage container -FROM alpine:3.15 +FROM alpine:3.16 -# RUN apk add --no-cache ca-certificates -RUN mkdir /app +# Create the default user 'bbgo' and assign to env 'USER' +ENV USER=bbgo +RUN adduser -D -G wheel "$USER" +USER ${USER} -WORKDIR /app 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: diff --git a/Makefile b/Makefile index a15a6ade7a..bdd1b9efe0 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,10 @@ OSX_APP_CODESIGN_IDENTITY ?= # OSX_APP_GUI ?= lorca OSX_APP_GUI ?= webview -FRONTEND_EXPORT_DIR = frontend/out +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 @@ -187,6 +190,7 @@ pkg/version/version.go: .FORCE 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 $< @@ -220,19 +224,27 @@ docker-push: docker push yoanlin/bbgo bash -c "[[ -n $(DOCKER_TAG) ]] && docker push yoanlin/bbgo:$(DOCKER_TAG)" -frontend/node_modules: - cd frontend && yarn install +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) -frontend/out/index.html: frontend/node_modules - cd frontend && yarn export +$(BACKTEST_REPORT_APP_DIR)/node_modules: + cd $(BACKTEST_REPORT_APP_DIR) && yarn install -pkg/server/assets.go: frontend/out/index.html - go run ./util/embed -package server -output $@ $(FRONTEND_EXPORT_DIR) +$(BACKTEST_REPORT_APP_DIR)/out/index.html: .FORCE $(BACKTEST_REPORT_APP_DIR)/node_modules + cd $(BACKTEST_REPORT_APP_DIR) && yarn build && yarn export -embed: pkg/server/assets.go +pkg/backtest/assets.go: $(BACKTEST_REPORT_APP_DIR)/out/index.html + go run ./utils/embed -package backtest -tag web -output $@ $(BACKTEST_REPORT_EXPORT_DIR) -static: frontend/out/index.html pkg/server/assets.go +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) @@ -260,6 +272,6 @@ grpc-py: $(PWD)/pkg/pb/bbgo.proto clean: - rm -rf $(BUILD_DIR) $(DIST_DIR) $(FRONTEND_EXPORT_DIR) $(GRPC_GO_DEPS) pkg/pb/*.pb.go + 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 diff --git a/README.md b/README.md index c0d9786b64..00fa0bcae9 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,14 @@ # BBGO -A trading bot framework written in Go. The name bbgo comes from the BB8 bot in the Star Wars movie. +A crypto trading bot framework written in Go. The name bbgo comes from the BB8 bot in the Star Wars movie. ## 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) open collective badge open collective badge @@ -17,17 +20,18 @@ A trading bot framework written in Go. The name bbgo comes from the BB8 bot in t ## What You Can Do With BBGO -### Trading Bot Users +### Trading Bot Users đŸ’â€â™€ïž đŸ’â€â™‚ïž You can use BBGO to run the built-in strategies. -### Strategy Developers +### Strategy Developers đŸ„· You can use BBGO's trading unit and back-test unit to implement your own strategies. -### Trading Unit Developers +### 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. +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 @@ -38,9 +42,42 @@ You can use BBGO's underlying common exchange API, currently it supports 4+ majo - 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. -- Standard indicators, e.g., SMA, EMA, BOLL, VMA, MACD... +- 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. @@ -49,7 +86,9 @@ You can use BBGO's underlying common exchange API, currently it supports 4+ majo ## Screenshots -![bbgo dashboard](assets/screenshots/dashboard.jpeg) +![bbgo dashboard](assets/screenshots/dashboard.jpeg) + +![bbgo backtest report](assets/screenshots/backtest-report.jpg) ## Supported Exchanges @@ -59,26 +98,22 @@ You can use BBGO's underlying common exchange API, currently it supports 4+ majo - Kucoin Spot Exchange - MAX Spot Exchange (located in Taiwan) - ## Documentation and General Topics -- Check the [documentation index](doc/README.md) - -## 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) +- Check the [documentation index](doc/README.md) ## Requirements Get your exchange API key and secret after you register the accounts (you can choose one or more exchanges): - MAX: -- Binance: +- Binance: - FTX: - OKEx: - Kucoin: -This project is maintained and supported by a small group of team. If you would like to support this project, please register on the exchanges using the provided links with referral codes above. +This project is maintained and supported by a small group of team. If you would like to support this project, please +register on the exchanges using the provided links with referral codes above. ## Installation @@ -101,23 +136,27 @@ bash <(curl -s https://raw.githubusercontent.com/c9s/bbgo/main/scripts/setup-bol ``` 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 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) +Since v2, we've added 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: +### One-click Linode StackScript -- BBGO Grid Trading on Binance -- BBGO USDT/TWD Grid Trading on MAX -- BBGO USDC/TWD Grid Trading on MAX -- BBGO LINK/TWD Grid Trading on MAX -- BBGO USDC/USDT Grid Trading on MAX -- BBGO Standard Grid Trading on MAX +StackScript allows you to one-click deploy a lightweight instance with bbgo. + +- BBGO grid on Binance +- BBGO grid USDT/TWD on MAX +- BBGO grid USDC/TWD on MAX +- BBGO grid LINK/TWD on MAX +- BBGO grid USDC/USDT on MAX +- BBGO grid on MAX +- BBGO bollmaker on Binance ### Build from source @@ -161,14 +200,12 @@ 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 @@ -181,33 +218,33 @@ To start bbgo with the frontend dashboard: bbgo run --enable-webserver ``` - If you want to switch to other 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" ``` + ## Advanced Configuration ### 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: +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 @@ -226,6 +263,10 @@ 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: @@ -233,6 +274,9 @@ To use MySQL database for data syncing, first you need to install your mysql ser ```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) @@ -252,7 +296,7 @@ DB_DSN="user:password@tcp(127.0.0.1:3306)/bbgo" #### Configure Sqlite3 Database -Just put these environment variables in your `.env.local` file: +To use SQLite3 instead of MySQL, simply put these environment variables in your `.env.local` file: ```sh DB_DRIVER=sqlite3 @@ -272,6 +316,9 @@ 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`: @@ -298,9 +345,14 @@ Check out the strategy directory [strategy](pkg/strategy) for all built-in strat indicator [bollgrid](pkg/strategy/bollgrid) - `grid` strategy implements the fixed price band grid strategy [grid](pkg/strategy/grid). See [document](./doc/strategy/grid.md). -- `support` strategy implements the fixed price band grid strategy [support](pkg/strategy/support). See +- `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). 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 @@ -317,80 +369,11 @@ bbgo run --config config/buyandhold.yaml See [Back-testing](./doc/topics/back-testing.md) -## Adding New Built-in Strategy - -Fork and clone this repository, Create a directory under `pkg/strategy/newstrategy`, write your strategy -at `pkg/strategy/newstrategy/strategy.go`. - -Define a strategy struct: - -```go -package newstrategy - -import ( - "github.com/c9s/bbgo/pkg/fixedpoint" -) - -type Strategy struct { - Symbol string `json:"symbol"` - Param1 int `json:"param1"` - Param2 int `json:"param2"` - Param3 fixedpoint.Value `json:"param3"` -} -``` - -Register your strategy: - -```go -package newstrategy +## Adding Strategy -const ID = "newstrategy" +See [Developing Strategy](./doc/topics/developing-strategy.md) -const stateKey = "state-v1" - -var log = logrus.WithField("strategy", ID) - -func init() { - bbgo.RegisterStrategy(ID, &Strategy{}) -} -``` - -Implement the strategy methods: - -```go -package newstrategy - -func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { - session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: "2m"}) -} - -func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { - // .... - return nil -} -``` - -Edit `pkg/cmd/builtin.go`, and import the package, like this: - -```go -package cmd - -// import built-in strategies -import ( - _ "github.com/c9s/bbgo/pkg/strategy/bollgrid" - _ "github.com/c9s/bbgo/pkg/strategy/buyandhold" - _ "github.com/c9s/bbgo/pkg/strategy/flashcrash" - _ "github.com/c9s/bbgo/pkg/strategy/grid" - _ "github.com/c9s/bbgo/pkg/strategy/pricealert" - _ "github.com/c9s/bbgo/pkg/strategy/support" - _ "github.com/c9s/bbgo/pkg/strategy/swing" - _ "github.com/c9s/bbgo/pkg/strategy/trailingstop" - _ "github.com/c9s/bbgo/pkg/strategy/xmaker" - _ "github.com/c9s/bbgo/pkg/strategy/xpuremaker" -) -``` - -## Write your own strategy +## Write your own private strategy Create your go package, and initialize the repository with `go mod` and add bbgo as a dependency: @@ -443,6 +426,13 @@ Or you can build your own wrapper binary via: bbgo build --config config/bbgo.yaml ``` +See also: + +- +- +- +- + ## Command Usages ### Submitting Orders to a specific exchagne session @@ -487,21 +477,19 @@ that is using bbgo component. for example: ```go type Strategy struct { -*bbgo.Notifiability + Symbol string `json:"symbol" + Market types.Market } ``` -And then, in your code, you can call the methods of Notifiability. - Supported components (single exchange strategy only for now): -- `*bbgo.Notifiability` +- `*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: -- `*bbgo.ExchangeSession` - `types.Market` ## Strategy Execution Phases @@ -557,13 +545,6 @@ streambook.BindStream(stream) 6. Push your changes to your fork. 7. Send a pull request. -### Setup frontend development environment - -```sh -cd frontend -yarn install -``` - ### Testing Desktop App for webview @@ -580,11 +561,28 @@ make embed && go run -tags web ./cmd/bbgo-lorca ## FAQ -What's Position? +### What's Position? - Base Currency & Quote Currency - How to calculate average cost? +### Looking For A New Strategy? + +You can write an article about BBGO in any topic, in 750-1500 words for exchange, and I can implement the strategy for +you (depends on the complexity and efforts). If you're interested in, DM me in telegram or +twitter , 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 . + +## Community + +- Telegram Group +- Telegram Group (Taiwan) +- Twitter + ## Contributing See [Contributing](./CONTRIBUTING.md) @@ -593,6 +591,11 @@ See [Contributing](./CONTRIBUTING.md) +## 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 diff --git a/apps/backtest-report/.eslintrc.json b/apps/backtest-report/.eslintrc.json new file mode 100644 index 0000000000..bffb357a71 --- /dev/null +++ b/apps/backtest-report/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/apps/backtest-report/.gitignore b/apps/backtest-report/.gitignore new file mode 100644 index 0000000000..737d872109 --- /dev/null +++ b/apps/backtest-report/.gitignore @@ -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 diff --git a/apps/backtest-report/README.md b/apps/backtest-report/README.md new file mode 100644 index 0000000000..332787c08d --- /dev/null +++ b/apps/backtest-report/README.md @@ -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 --session binance --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. diff --git a/apps/backtest-report/components/OrderListTable.tsx b/apps/backtest-report/components/OrderListTable.tsx new file mode 100644 index 0000000000..4e075b490b --- /dev/null +++ b/apps/backtest-report/components/OrderListTable.tsx @@ -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) => ( + { + 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") + }}> + {order.order_id} + {order.symbol} + {order.side} + {order.order_type} + {order.price} + {order.quantity} + {order.status} + {formatDate(order.creation_time)} + {order.tag} + + )); + + return
+ + setShowCanceledOrders(event.currentTarget.checked)}/> + + + + + + + + + + + + + + + + + {rows} +
Order IDSymbolSideOrder TypePriceQuantityStatusCreation TimeTag
+
+} + +const formatDate = (d : Date) : string => { + return moment(d).format("MMM Do YY hh:mm:ss A Z"); +} + + +export default OrderListTable; diff --git a/apps/backtest-report/components/ReportDetails.tsx b/apps/backtest-report/components/ReportDetails.tsx new file mode 100644 index 0000000000..81fa24377f --- /dev/null +++ b/apps/backtest-report/components/ReportDetails.tsx @@ -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 ( + + +
+ + {stat.title} + {stat.dir ? + ({color: stat.dir == "up" ? theme.colors.teal[6] : theme.colors.red[6]})} + size={16} + radius="xs" + > + + + : null} + + + {stat.value} + +
+ + + {stat.diff ? + ({color: stat.diff && stat.diff > 0 ? theme.colors.teal[6] : theme.colors.red[6]})} + size={38} + radius="md" + > + + + : null} +
+ + {stat.diff ? + + 0 ? 'teal' : 'red'} weight={700}> + {stat.diff}% + {' '} + {stat.diff && stat.diff > 0 ? 'increase' : 'decrease'} compared to last month + : null} + + {stat.desc ? ( + + {stat.desc} + + ) : null} + +
+ ); + }); + + return ( + + {stats} + + ); +} + + +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 = ; + + +interface BalanceDetailsProps { + balances: BalanceMap; +} + +const BalanceDetails = (props: BalanceDetailsProps) => { + const rows = Object.entries(props.balances).map(([k, v]) => { + return + {k} + {v.available} + ; + }); + + return + + + + + + + {rows} +
CurrencyBalance
; +}; + +const ReportDetails = (props: ReportDetailsProps) => { + const [reportSummary, setReportSummary] = useState() + useEffect(() => { + fetchReportSummary(props.basePath, props.runID).then((summary: ReportSummary) => { + console.log("summary", props.runID, summary); + setReportSummary(summary) + }) + }, [props.runID]) + + if (!reportSummary) { + return
+

Loading {props.runID}

+
; + } + + 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 +
+ Strategy: {strategyName} + {reportSummary.sessions.map((session) => Exchange: {session})} + {reportSummary.symbols.map((symbol) => Symbol: {symbol})} + + {reportSummary.startTime.toString()} — {reportSummary.endTime.toString()} ~ { + moment.duration((new Date(reportSummary.endTime)).getTime() - (new Date(reportSummary.startTime)).getTime()).humanize() + } + Run ID: {runID} +
+ = 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}`}, + ]}/> + + + + Initial Total Balances + + + + Final Total Balances + + + + + { + /* + + + + + {skeleton} + + */ + } +
+ { + reportSummary.symbols.map((symbol: string, i: number) => { + return + }) + } +
+ +
; +}; + + +export default ReportDetails; diff --git a/apps/backtest-report/components/ReportNavigator.tsx b/apps/backtest-report/components/ReportNavigator.tsx new file mode 100644 index 0000000000..55e16d3bcd --- /dev/null +++ b/apps/backtest-report/components/ReportNavigator.tsx @@ -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({runs: []}); + + useEffect(() => { + setLoading(true) + fetchIndex('/output', setReportIndex).then(() => { + setLoading(false); + }) + }, []); + + if (isLoading) { + return
Loading...
; + } + + if (reportIndex.runs.length == 0) { + return
No back-test report data
+ } + + return
+ + + + } + > + { + reportIndex.runs.map((entry) => { + return { + if (props.onSelect) { + props.onSelect(entry); + } + }}> +
+ {entry.id} +
+
+ }) + } +
+
; + + +}; + +export default ReportNavigator; diff --git a/apps/backtest-report/components/TimeRangeSlider/components/Handle.js b/apps/backtest-report/components/TimeRangeSlider/components/Handle.js new file mode 100644 index 0000000000..338fb0133b --- /dev/null +++ b/apps/backtest-report/components/TimeRangeSlider/components/Handle.js @@ -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 ( + <> +
+
+
+
+ + ) +} + +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 diff --git a/apps/backtest-report/components/TimeRangeSlider/components/KeyboardHandle.js b/apps/backtest-report/components/TimeRangeSlider/components/KeyboardHandle.js new file mode 100644 index 0000000000..077312bdaa --- /dev/null +++ b/apps/backtest-report/components/TimeRangeSlider/components/KeyboardHandle.js @@ -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 }) => ( + + + + + +
+ + {testResponse ? ( + testResponse.error ? ( + + {testResponse.error} + + ) : testResponse.success ? ( + + Connection Test Succeeded + + ) : null + ) : null} + + {response ? ( + response.error ? ( + + {response.error} + + ) : response.success ? ( + + Exchange Session Added + + ) : null + ) : null} + + ); +} diff --git a/apps/frontend/components/ConfigureDatabaseForm.js b/apps/frontend/components/ConfigureDatabaseForm.js new file mode 100644 index 0000000000..1b3d7ce35c --- /dev/null +++ b/apps/frontend/components/ConfigureDatabaseForm.js @@ -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 ( + + + Configure Database + + + + 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. + + + + + + + Database Driver + { + setDriver(event.target.value); + }} + > + } + label="Standard (Default)" + /> + } + label="MySQL" + /> + + + + + + + {driver === 'mysql' ? ( + + { + setMysqlURL(event.target.value); + resetTestResponse(); + }} + /> + MySQL DSN + + + If you have database installed on your machine, you can enter the + DSN string like the following format: +
+
+                root:password@tcp(127.0.0.1:3306)/bbgo
+              
+
+ Be sure to create your database before using it. You need to + execute the following statement to create a database: +
+
+                CREATE DATABASE bbgo CHARSET utf8;
+              
+
+
+ ) : ( + + + + If you don't know what to choose, just pick the standard driver + (sqlite3). +
+ For professionals, you can pick MySQL driver, BBGO works best + with MySQL, especially for larger data scale. +
+
+
+ )} +
+ +
+ + + +
+ + {testResponse ? ( + testResponse.error ? ( + + {testResponse.error} + + ) : testResponse.success ? ( + + Connection Test Succeeded + + ) : null + ) : null} +
+ ); +} diff --git a/apps/frontend/components/ConfigureGridStrategyForm.js b/apps/frontend/components/ConfigureGridStrategyForm.js new file mode 100644 index 0000000000..2305671e52 --- /dev/null +++ b/apps/frontend/components/ConfigureGridStrategyForm.js @@ -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 ( + { + 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 ( + { + 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 ( + + {session.name} + + ); + }); + + const symbolMenuItems = activeSessionSymbols.map((symbol, index) => { + return ( + + {symbol} + + ); + }); + + return ( + + + Add Grid Strategy + + + + 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. + + + + + + Session + + + + Select the exchange session you want to mount this strategy. + + + + + + Market + + + + Select the market you want to run this strategy + + + + + { + parseFloatCall(event.target.value, setUpperPrice); + }} + value={upperPrice} + InputProps={{ + inputComponent: PriceNumberFormat, + }} + /> + + + + { + parseFloatCall(event.target.value, setLowerPrice); + }} + value={lowerPrice} + InputProps={{ + inputComponent: PriceNumberFormat, + }} + /> + + + + { + parseFloatCall(event.target.value, setProfitSpread); + }} + value={profitSpread} + InputProps={{ + inputComponent: StandardNumberFormat, + }} + /> + + + + + Order Quantity By + + } + label="Fixed Amount" + /> + } + label="Fixed Quantity" + /> + + + + + + {quantityBy === 'fixedQuantity' ? ( + { + parseFloatCall(event.target.value, setFixedQuantity); + }} + value={fixedQuantity} + InputProps={{ + inputComponent: StandardNumberFormat, + }} + /> + ) : null} + + {quantityBy === 'fixedAmount' ? ( + { + parseFloatCall(event.target.value, setFixedAmount); + }} + value={fixedAmount} + InputProps={{ + inputComponent: PriceNumberFormat, + }} + /> + ) : null} + + + + { + parseFloatCall(event.target.value, setGridNumber); + }} + value={gridNumber} + InputProps={{ + inputComponent: StandardNumberFormat, + }} + /> + + + +
+ + + +
+ + {response ? ( + response.error ? ( + + {response.error} + + ) : response.success ? ( + + Strategy Added + + ) : null + ) : null} +
+ ); +} diff --git a/apps/frontend/components/ConnectWallet.js b/apps/frontend/components/ConnectWallet.js new file mode 100644 index 0000000000..e68eaf799b --- /dev/null +++ b/apps/frontend/components/ConnectWallet.js @@ -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 ? ( + <> + + + {({ TransitionProps, placement }) => ( + + + + + + {account &&

Account: {account}

} +
+ + {etherBalance && ( + ETH Balance: {formatEther(etherBalance)} + )} + + + {tokenBalance && ( + BBG Balance: {formatEther(tokenBalance)} + )} + +
+
+
+
+ )} +
+ + ) : ( +
+ +
+ )} + + ); +} diff --git a/apps/frontend/components/Detail.tsx b/apps/frontend/components/Detail.tsx new file mode 100644 index 0000000000..79f4ed739a --- /dev/null +++ b/apps/frontend/components/Detail.tsx @@ -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 ( + + {strategy} +
{data[strategy].symbol}
+ + + 0 arbitrages in 24 hours / Total {stats.totalArbs}{' '} + arbitrages + + + + + ); +} diff --git a/apps/frontend/components/ExchangeSessionTabPanel.js b/apps/frontend/components/ExchangeSessionTabPanel.js new file mode 100644 index 0000000000..fb76f447c6 --- /dev/null +++ b/apps/frontend/components/ExchangeSessionTabPanel.js @@ -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 ( + + + Sessions + + + {sessions.map((session) => { + return ; + })} + + + ); +} diff --git a/apps/frontend/components/ReviewSessions.js b/apps/frontend/components/ReviewSessions.js new file mode 100644 index 0000000000..6eb49c7318 --- /dev/null +++ b/apps/frontend/components/ReviewSessions.js @@ -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 ( + + + + + + + ); + }); + + return ( + + + Review Sessions + + + {items} + +
+ + + +
+
+ ); +} diff --git a/apps/frontend/components/ReviewStrategies.js b/apps/frontend/components/ReviewStrategies.js new file mode 100644 index 0000000000..085fabefc8 --- /dev/null +++ b/apps/frontend/components/ReviewStrategies.js @@ -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 ( + + + + + Field + Value + + + + {rows.map((row) => ( + + + {row.key} + + {row.val} + + ))} + +
+
+ ); +} + +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 ( + + G} + action={ + + + + } + title={title} + subheader={`Exchange ${mounts.map((m) => m.toUpperCase())}`} + /> + + + Strategy will be executed on session {mounts.join(',')} with the + following configuration: + + + {configToTable(config)} + + + ); + }); + + return ( + + + Review Strategies + + + {items} + +
+ + + +
+
+ ); +} diff --git a/apps/frontend/components/RunningTime.tsx b/apps/frontend/components/RunningTime.tsx new file mode 100644 index 0000000000..07e21c1d6d --- /dev/null +++ b/apps/frontend/components/RunningTime.tsx @@ -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 ( + + + + Running for + {day}D + {hour}H + {min}M + + + ); +} diff --git a/apps/frontend/components/SaveConfigAndRestart.js b/apps/frontend/components/SaveConfigAndRestart.js new file mode 100644 index 0000000000..86350ab564 --- /dev/null +++ b/apps/frontend/components/SaveConfigAndRestart.js @@ -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 ( + + + Save Config and Restart + + + + Click "Save and Restart" to save the configurations to the config file{' '} + bbgo.yaml, and save the exchange session credentials to the + dotenv file .env.local. + + +
+ + + +
+ + {response ? ( + response.error ? ( + + {response.error} + + ) : response.success ? ( + + Config Saved + + ) : null + ) : null} +
+ ); +} diff --git a/apps/frontend/components/SideBar.js b/apps/frontend/components/SideBar.js new file mode 100644 index 0000000000..e491099650 --- /dev/null +++ b/apps/frontend/components/SideBar.js @@ -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 ( + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/apps/frontend/components/Stats.tsx b/apps/frontend/components/Stats.tsx new file mode 100644 index 0000000000..bd34bde169 --- /dev/null +++ b/apps/frontend/components/Stats.tsx @@ -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 ( + +
+ Grid Profits + {stats.gridProfits} + {gridProfitsPercentage}% +
+ +
+ Floating PNL + {stats.floatingPNL} +
+ +
+ Grid APR + {gridAprPercentage}% +
+ +
+ Current Price +
{stats.currentPrice}
+
+ +
+ Price Range +
+ {stats.lowestPrice}~{stats.highestPrice} +
+
+
+ ); +} diff --git a/apps/frontend/components/Summary.tsx b/apps/frontend/components/Summary.tsx new file mode 100644 index 0000000000..a91435fab1 --- /dev/null +++ b/apps/frontend/components/Summary.tsx @@ -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 ( + + + Investment USDT +
{stats.investment}
+
+ + + Total Profit USDT + {stats.totalProfits} + {totalProfitsPercentage}% + +
+ ); +} diff --git a/apps/frontend/components/SyncButton.tsx b/apps/frontend/components/SyncButton.tsx new file mode 100644 index 0000000000..de00aacb52 --- /dev/null +++ b/apps/frontend/components/SyncButton.tsx @@ -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 ( + + {syncing ? 'Syncing...' : 'Sync'} + + ); +} diff --git a/apps/frontend/components/TotalAssetsDetails.js b/apps/frontend/components/TotalAssetsDetails.js new file mode 100644 index 0000000000..b587b72ffc --- /dev/null +++ b/apps/frontend/components/TotalAssetsDetails.js @@ -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 ( + + {a.currency in logoCurrencies ? ( + + + + ) : ( + + + + )} + + + ); + }); + + return ( + + + {items} + + + ); +} diff --git a/apps/frontend/components/TotalAssetsPie.js b/apps/frontend/components/TotalAssetsPie.js new file mode 100644 index 0000000000..695b8daadd --- /dev/null +++ b/apps/frontend/components/TotalAssetsPie.js @@ -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 ( + + + + + + ); +} diff --git a/apps/frontend/components/TotalAssetsSummary.js b/apps/frontend/components/TotalAssetsSummary.js new file mode 100644 index 0000000000..f0d4110299 --- /dev/null +++ b/apps/frontend/components/TotalAssetsSummary.js @@ -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 ( + + + + Total Account Balance + + + {Math.round(aggregateAssetsBy(assets, 'inBTC') * 1e8) / 1e8}{' '} + BTC + + + + Estimated Value + + + + {Math.round(aggregateAssetsBy(assets, 'inUSD') * 100) / 100}{' '} + USD + + + + ); +} diff --git a/apps/frontend/components/TradingVolumeBar.js b/apps/frontend/components/TradingVolumeBar.js new file mode 100644 index 0000000000..b9b0773b2c --- /dev/null +++ b/apps/frontend/components/TradingVolumeBar.js @@ -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 ( + + ); +} diff --git a/apps/frontend/components/TradingVolumePanel.js b/apps/frontend/components/TradingVolumePanel.js new file mode 100644 index 0000000000..165ebccff1 --- /dev/null +++ b/apps/frontend/components/TradingVolumePanel.js @@ -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 ( + + + Trading Volume + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/apps/frontend/hooks/useInterval.ts b/apps/frontend/hooks/useInterval.ts new file mode 100644 index 0000000000..9a54d776a6 --- /dev/null +++ b/apps/frontend/hooks/useInterval.ts @@ -0,0 +1,20 @@ +import { useEffect, useRef } from 'react'; + +export default function useInterval(cb: Function, delayMs: number | null) { + const savedCallback = useRef(); + + useEffect(() => { + savedCallback.current = cb; + }, [cb]); + + useEffect(() => { + function tick() { + savedCallback.current(); + } + + if (delayMs !== null) { + let timerId = setInterval(tick, delayMs); + return () => clearInterval(timerId); + } + }, [delayMs]); +} diff --git a/apps/frontend/layouts/DashboardLayout.js b/apps/frontend/layouts/DashboardLayout.js new file mode 100644 index 0000000000..1f9ffd8f0a --- /dev/null +++ b/apps/frontend/layouts/DashboardLayout.js @@ -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 ( +
+ + + + BBGO + + + + + + + + + +
+
+ + {children} + +
+
+ ); +} diff --git a/apps/frontend/layouts/PlainLayout.js b/apps/frontend/layouts/PlainLayout.js new file mode 100644 index 0000000000..8bbca9d773 --- /dev/null +++ b/apps/frontend/layouts/PlainLayout.js @@ -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 ( +
+ + + + {props && props.title ? props.title : 'BBGO Setup Wizard'} + + + + +
+
+ {props.children} +
+
+ ); +} diff --git a/apps/frontend/next-env.d.ts b/apps/frontend/next-env.d.ts new file mode 100644 index 0000000000..4f11a03dc6 --- /dev/null +++ b/apps/frontend/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/apps/frontend/next.config.js b/apps/frontend/next.config.js new file mode 100644 index 0000000000..c9fac5d498 --- /dev/null +++ b/apps/frontend/next.config.js @@ -0,0 +1,9 @@ +module.exports = async (phase, { defaultConfig }) => { + /** + * @type {import('next').NextConfig} + */ + const nextConfig = { + /* config options here */ + } + return nextConfig +} \ No newline at end of file diff --git a/apps/frontend/package.json b/apps/frontend/package.json new file mode 100644 index 0000000000..0096838400 --- /dev/null +++ b/apps/frontend/package.json @@ -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" + } +} diff --git a/apps/frontend/pages/_app.tsx b/apps/frontend/pages/_app.tsx new file mode 100644 index 0000000000..78178c8016 --- /dev/null +++ b/apps/frontend/pages/_app.tsx @@ -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 ( + + + BBGO + + + + {/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */} + + + + + ); +} + +MyApp.propTypes = { + Component: PropTypes.elementType.isRequired, + pageProps: PropTypes.object.isRequired, +}; diff --git a/frontend/pages/_document.js b/apps/frontend/pages/_document.js similarity index 88% rename from frontend/pages/_document.js rename to apps/frontend/pages/_document.js index 23a9e5e0ac..72d1a1fdd5 100644 --- a/frontend/pages/_document.js +++ b/apps/frontend/pages/_document.js @@ -1,9 +1,7 @@ /* eslint-disable react/jsx-filename-extension */ import React from 'react'; -import Document, { - Html, Head, Main, NextScript, -} from 'next/document'; -import { ServerStyleSheets } from '@material-ui/styles'; +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 { @@ -66,6 +64,9 @@ MyDocument.getInitialProps = async (ctx) => { return { ...initialProps, // Styles fragment is rendered after the app and page rendering finish. - styles: [...React.Children.toArray(initialProps.styles), sheets.getStyleElement()], + styles: [ + ...React.Children.toArray(initialProps.styles), + sheets.getStyleElement(), + ], }; }; diff --git a/frontend/pages/api/hello.js b/apps/frontend/pages/api/hello.js similarity index 64% rename from frontend/pages/api/hello.js rename to apps/frontend/pages/api/hello.js index 5b77ec0d11..07d9d9ba2b 100644 --- a/frontend/pages/api/hello.js +++ b/apps/frontend/pages/api/hello.js @@ -1,6 +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' }) -} + res.statusCode = 200; + res.json({ name: 'John Doe' }); +}; diff --git a/apps/frontend/pages/connect/index.js b/apps/frontend/pages/connect/index.js new file mode 100644 index 0000000000..0e2eb47dc7 --- /dev/null +++ b/apps/frontend/pages/connect/index.js @@ -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 ( + + + + Sign In Using QR Codes + +
+ +
+
+
+ ); +} diff --git a/apps/frontend/pages/index.tsx b/apps/frontend/pages/index.tsx new file mode 100644 index 0000000000..3185b8164d --- /dev/null +++ b/apps/frontend/pages/index.tsx @@ -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 ( + + + + Loading + + + + ); + } + + console.log('index: assets', assets); + + return ( + + + + + Total Assets + + +
+ + + + + + + + + + +
+
+ + + + +
+
+ ); +} diff --git a/apps/frontend/pages/orders.js b/apps/frontend/pages/orders.js new file mode 100644 index 0000000000..744b67e486 --- /dev/null +++ b/apps/frontend/pages/orders.js @@ -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 ( + + + + Orders + +
+
+ +
+
+
+
+ ); +} diff --git a/apps/frontend/pages/setup/index.js b/apps/frontend/pages/setup/index.js new file mode 100644 index 0000000000..664e479805 --- /dev/null +++ b/apps/frontend/pages/setup/index.js @@ -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 ( + { + setActiveStep(1); + }} + /> + ); + case 1: + return ( + { + setActiveStep(0); + }} + onAdded={() => { + setActiveStep(2); + }} + /> + ); + case 2: + return ( + { + setActiveStep(1); + }} + onNext={() => { + setActiveStep(3); + }} + /> + ); + case 3: + return ( + { + setActiveStep(2); + }} + onAdded={() => { + setActiveStep(4); + }} + /> + ); + case 4: + return ( + { + setActiveStep(3); + }} + onNext={() => { + setActiveStep(5); + }} + /> + ); + + case 5: + return ( + { + setActiveStep(4); + }} + onRestarted={() => {}} + /> + ); + + default: + throw new Error('Unknown step'); + } +} + +export default function Setup() { + const classes = useStyles(); + const [activeStep, setActiveStep] = React.useState(0); + + return ( + + + + + Setup Session + + + + {steps.map((label) => ( + + {label} + + ))} + + + + {getStepContent(activeStep, setActiveStep)} + + + + + ); +} diff --git a/apps/frontend/pages/strategies.tsx b/apps/frontend/pages/strategies.tsx new file mode 100644 index 0000000000..1ff03af142 --- /dev/null +++ b/apps/frontend/pages/strategies.tsx @@ -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([]); + + useEffect(() => { + queryStrategiesMetrics().then((value) => { + setDetails(value); + }); + }, []); + + return ( + + + {details.map((element) => { + return ; + })} + + + ); +} diff --git a/apps/frontend/pages/trades.js b/apps/frontend/pages/trades.js new file mode 100644 index 0000000000..d300f95de4 --- /dev/null +++ b/apps/frontend/pages/trades.js @@ -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 ( + + + + Trades + +
+
+ +
+
+
+
+ ); +} diff --git a/frontend/public/favicon.ico b/apps/frontend/public/favicon.ico similarity index 100% rename from frontend/public/favicon.ico rename to apps/frontend/public/favicon.ico diff --git a/frontend/public/images/bch-logo.svg b/apps/frontend/public/images/bch-logo.svg similarity index 100% rename from frontend/public/images/bch-logo.svg rename to apps/frontend/public/images/bch-logo.svg diff --git a/frontend/public/images/bnb-logo.svg b/apps/frontend/public/images/bnb-logo.svg similarity index 100% rename from frontend/public/images/bnb-logo.svg rename to apps/frontend/public/images/bnb-logo.svg diff --git a/frontend/public/images/btc-logo.svg b/apps/frontend/public/images/btc-logo.svg similarity index 100% rename from frontend/public/images/btc-logo.svg rename to apps/frontend/public/images/btc-logo.svg diff --git a/frontend/public/images/comp-logo.svg b/apps/frontend/public/images/comp-logo.svg similarity index 100% rename from frontend/public/images/comp-logo.svg rename to apps/frontend/public/images/comp-logo.svg diff --git a/frontend/public/images/dai-logo.svg b/apps/frontend/public/images/dai-logo.svg similarity index 100% rename from frontend/public/images/dai-logo.svg rename to apps/frontend/public/images/dai-logo.svg diff --git a/frontend/public/images/dot-logo.svg b/apps/frontend/public/images/dot-logo.svg similarity index 100% rename from frontend/public/images/dot-logo.svg rename to apps/frontend/public/images/dot-logo.svg diff --git a/frontend/public/images/eth-logo.svg b/apps/frontend/public/images/eth-logo.svg similarity index 100% rename from frontend/public/images/eth-logo.svg rename to apps/frontend/public/images/eth-logo.svg diff --git a/frontend/public/images/grt-logo.svg b/apps/frontend/public/images/grt-logo.svg similarity index 100% rename from frontend/public/images/grt-logo.svg rename to apps/frontend/public/images/grt-logo.svg diff --git a/frontend/public/images/link-logo.svg b/apps/frontend/public/images/link-logo.svg similarity index 100% rename from frontend/public/images/link-logo.svg rename to apps/frontend/public/images/link-logo.svg diff --git a/frontend/public/images/ltc-logo.svg b/apps/frontend/public/images/ltc-logo.svg similarity index 100% rename from frontend/public/images/ltc-logo.svg rename to apps/frontend/public/images/ltc-logo.svg diff --git a/frontend/public/images/max-logo.svg b/apps/frontend/public/images/max-logo.svg similarity index 100% rename from frontend/public/images/max-logo.svg rename to apps/frontend/public/images/max-logo.svg diff --git a/frontend/public/images/snt-logo.svg b/apps/frontend/public/images/snt-logo.svg similarity index 100% rename from frontend/public/images/snt-logo.svg rename to apps/frontend/public/images/snt-logo.svg diff --git a/frontend/public/images/sxp-logo.svg b/apps/frontend/public/images/sxp-logo.svg similarity index 100% rename from frontend/public/images/sxp-logo.svg rename to apps/frontend/public/images/sxp-logo.svg diff --git a/frontend/public/images/twd-logo.svg b/apps/frontend/public/images/twd-logo.svg similarity index 100% rename from frontend/public/images/twd-logo.svg rename to apps/frontend/public/images/twd-logo.svg diff --git a/frontend/public/images/usdt-logo.svg b/apps/frontend/public/images/usdt-logo.svg similarity index 100% rename from frontend/public/images/usdt-logo.svg rename to apps/frontend/public/images/usdt-logo.svg diff --git a/frontend/public/images/xrp-logo.svg b/apps/frontend/public/images/xrp-logo.svg similarity index 100% rename from frontend/public/images/xrp-logo.svg rename to apps/frontend/public/images/xrp-logo.svg diff --git a/frontend/public/images/yfi-logo.svg b/apps/frontend/public/images/yfi-logo.svg similarity index 100% rename from frontend/public/images/yfi-logo.svg rename to apps/frontend/public/images/yfi-logo.svg diff --git a/apps/frontend/public/vercel.svg b/apps/frontend/public/vercel.svg new file mode 100644 index 0000000000..fbf0e25a65 --- /dev/null +++ b/apps/frontend/public/vercel.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/frontend/src/theme.js b/apps/frontend/src/theme.js similarity index 76% rename from frontend/src/theme.js rename to apps/frontend/src/theme.js index cf3475595d..ec5e14d587 100644 --- a/frontend/src/theme.js +++ b/apps/frontend/src/theme.js @@ -1,5 +1,5 @@ -import { createTheme } from '@material-ui/core/styles'; -import { red } from '@material-ui/core/colors'; +import { createTheme } from '@mui/material/styles'; +import { red } from '@mui/material/colors'; // Create a theme instance. const theme = createTheme({ diff --git a/apps/frontend/src/utils.js b/apps/frontend/src/utils.js new file mode 100644 index 0000000000..54b09ce7a3 --- /dev/null +++ b/apps/frontend/src/utils.js @@ -0,0 +1,37 @@ +export function currencyColor(currency) { + switch (currency) { + case 'BTC': + return '#f69c3d'; + case 'ETH': + return '#497493'; + case 'MCO': + return '#032144'; + case 'OMG': + return '#2159ec'; + case 'LTC': + return '#949494'; + case 'USDT': + return '#2ea07b'; + case 'SAND': + return '#2E9AD0'; + case 'XRP': + return '#00AAE4'; + case 'BCH': + return '#8DC351'; + case 'MAX': + return '#2D4692'; + case 'TWD': + return '#4A7DED'; + } +} + +export function throttle(fn, delayMillis) { + let permitted = true; + return () => { + if (permitted) { + fn.apply(this, arguments); + permitted = false; + setTimeout(() => (permitted = true), delayMillis); + } + }; +} diff --git a/frontend/styles/Home.module.css b/apps/frontend/styles/Home.module.css similarity index 100% rename from frontend/styles/Home.module.css rename to apps/frontend/styles/Home.module.css diff --git a/apps/frontend/styles/globals.css b/apps/frontend/styles/globals.css new file mode 100644 index 0000000000..e5e2dcc23b --- /dev/null +++ b/apps/frontend/styles/globals.css @@ -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; +} diff --git a/frontend/tsconfig.json b/apps/frontend/tsconfig.json similarity index 91% rename from frontend/tsconfig.json rename to apps/frontend/tsconfig.json index 35d51eac90..5bee8c4d57 100644 --- a/frontend/tsconfig.json +++ b/apps/frontend/tsconfig.json @@ -16,7 +16,8 @@ "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "preserve" + "jsx": "preserve", + "incremental": true }, "include": [ "next-env.d.ts", diff --git a/apps/frontend/yarn.lock b/apps/frontend/yarn.lock new file mode 100644 index 0000000000..8537ee4f1f --- /dev/null +++ b/apps/frontend/yarn.lock @@ -0,0 +1,2136 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@ampproject/remapping@^2.1.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d" + integrity sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w== + dependencies: + "@jridgewell/gen-mapping" "^0.1.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.7.tgz#44416b6bd7624b998f5b1af5d470856c40138789" + integrity sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg== + dependencies: + "@babel/highlight" "^7.16.7" + +"@babel/compat-data@^7.17.10": + version "7.18.5" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.18.5.tgz#acac0c839e317038c73137fbb6ef71a1d6238471" + integrity sha512-BxhE40PVCBxVEJsSBhB6UWyAuqJRxGsAw8BdHMJ3AKGydcwuWW4kOO3HmqBQAdcq/OP+/DlTVxLvsCzRTnZuGg== + +"@babel/core@^7.0.0": + version "7.18.5" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.18.5.tgz#c597fa680e58d571c28dda9827669c78cdd7f000" + integrity sha512-MGY8vg3DxMnctw0LdvSEojOsumc70g0t18gNyUdAZqB1Rpd1Bqo/svHGvt+UJ6JcGX+DIekGFDxxIWofBxLCnQ== + dependencies: + "@ampproject/remapping" "^2.1.0" + "@babel/code-frame" "^7.16.7" + "@babel/generator" "^7.18.2" + "@babel/helper-compilation-targets" "^7.18.2" + "@babel/helper-module-transforms" "^7.18.0" + "@babel/helpers" "^7.18.2" + "@babel/parser" "^7.18.5" + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.18.5" + "@babel/types" "^7.18.4" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.1" + semver "^6.3.0" + +"@babel/generator@^7.18.2": + version "7.18.2" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.18.2.tgz#33873d6f89b21efe2da63fe554460f3df1c5880d" + integrity sha512-W1lG5vUwFvfMd8HVXqdfbuG7RuaSrTCCD8cl8fP8wOivdbtbIg2Db3IWUcgvfxKbbn6ZBGYRW/Zk1MIwK49mgw== + dependencies: + "@babel/types" "^7.18.2" + "@jridgewell/gen-mapping" "^0.3.0" + jsesc "^2.5.1" + +"@babel/helper-compilation-targets@^7.18.2": + version "7.18.2" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.18.2.tgz#67a85a10cbd5fc7f1457fec2e7f45441dc6c754b" + integrity sha512-s1jnPotJS9uQnzFtiZVBUxe67CuBa679oWFHpxYYnTpRL/1ffhyX44R9uYiXoa/pLXcY9H2moJta0iaanlk/rQ== + dependencies: + "@babel/compat-data" "^7.17.10" + "@babel/helper-validator-option" "^7.16.7" + browserslist "^4.20.2" + semver "^6.3.0" + +"@babel/helper-environment-visitor@^7.16.7", "@babel/helper-environment-visitor@^7.18.2": + version "7.18.2" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.2.tgz#8a6d2dedb53f6bf248e31b4baf38739ee4a637bd" + integrity sha512-14GQKWkX9oJzPiQQ7/J36FTXcD4kSp8egKjO9nINlSKiHITRA9q/R74qu8S9xlc/b/yjsJItQUeeh3xnGN0voQ== + +"@babel/helper-function-name@^7.17.9": + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz#136fcd54bc1da82fcb47565cf16fd8e444b1ff12" + integrity sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg== + dependencies: + "@babel/template" "^7.16.7" + "@babel/types" "^7.17.0" + +"@babel/helper-hoist-variables@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz#86bcb19a77a509c7b77d0e22323ef588fa58c246" + integrity sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz#25612a8091a999704461c8a222d0efec5d091437" + integrity sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-module-transforms@^7.18.0": + version "7.18.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.18.0.tgz#baf05dec7a5875fb9235bd34ca18bad4e21221cd" + integrity sha512-kclUYSUBIjlvnzN2++K9f2qzYKFgjmnmjwL4zlmU5f8ZtzgWe8s0rUPSTGy2HmK4P8T52MQsS+HTQAgZd3dMEA== + dependencies: + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-module-imports" "^7.16.7" + "@babel/helper-simple-access" "^7.17.7" + "@babel/helper-split-export-declaration" "^7.16.7" + "@babel/helper-validator-identifier" "^7.16.7" + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.18.0" + "@babel/types" "^7.18.0" + +"@babel/helper-plugin-utils@^7.17.12": + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.17.12.tgz#86c2347da5acbf5583ba0a10aed4c9bf9da9cf96" + integrity sha512-JDkf04mqtN3y4iAbO1hv9U2ARpPyPL1zqyWs/2WG1pgSq9llHFjStX5jdxb84himgJm+8Ng+x0oiWF/nw/XQKA== + +"@babel/helper-simple-access@^7.17.7": + version "7.18.2" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.18.2.tgz#4dc473c2169ac3a1c9f4a51cfcd091d1c36fcff9" + integrity sha512-7LIrjYzndorDY88MycupkpQLKS1AFfsVRm2k/9PtKScSy5tZq0McZTj+DiMRynboZfIqOKvo03pmhTaUgiD6fQ== + dependencies: + "@babel/types" "^7.18.2" + +"@babel/helper-split-export-declaration@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz#0b648c0c42da9d3920d85ad585f2778620b8726b" + integrity sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-validator-identifier@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz#e8c602438c4a8195751243da9031d1607d247cad" + integrity sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw== + +"@babel/helper-validator-option@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz#b203ce62ce5fe153899b617c08957de860de4d23" + integrity sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ== + +"@babel/helpers@^7.18.2": + version "7.18.2" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.18.2.tgz#970d74f0deadc3f5a938bfa250738eb4ac889384" + integrity sha512-j+d+u5xT5utcQSzrh9p+PaJX94h++KN+ng9b9WEJq7pkUPAd61FGqhjuUEdfknb3E/uDBb7ruwEeKkIxNJPIrg== + dependencies: + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.18.2" + "@babel/types" "^7.18.2" + +"@babel/highlight@^7.16.7": + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.17.12.tgz#257de56ee5afbd20451ac0a75686b6b404257351" + integrity sha512-7yykMVF3hfZY2jsHZEEgLc+3x4o1O+fYyULu11GynEUQNwB6lua+IIQn1FiJxNucd5UlyJryrwsOh8PL9Sn8Qg== + dependencies: + "@babel/helper-validator-identifier" "^7.16.7" + chalk "^2.0.0" + js-tokens "^4.0.0" + +"@babel/parser@^7.16.7", "@babel/parser@^7.18.5": + version "7.18.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.5.tgz#337062363436a893a2d22faa60be5bb37091c83c" + integrity sha512-YZWVaglMiplo7v8f1oMQ5ZPQr0vn7HPeZXxXWsxXJRjGVrzUFn9OxFQl1sb5wzfootjA/yChhW84BV+383FSOw== + +"@babel/plugin-syntax-jsx@^7.12.13": + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.17.12.tgz#834035b45061983a491f60096f61a2e7c5674a47" + integrity sha512-spyY3E3AURfxh/RHtjx5j6hs8am5NbUBGfcZ2vB3uShSpZdQyXSf5rR5Mk76vbtlAZOelyVQ71Fg0x9SG4fsog== + dependencies: + "@babel/helper-plugin-utils" "^7.17.12" + +"@babel/runtime@^7.0.0", "@babel/runtime@^7.13.10", "@babel/runtime@^7.17.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.7": + version "7.18.3" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.3.tgz#c7b654b57f6f63cf7f8b418ac9ca04408c4579f4" + integrity sha512-38Y8f7YUhce/K7RMwTp7m0uCumpv9hZkitCbBClqQIow1qSbCvGkcegKOXpEWCQLfWmevgRiWokZ1GkpfhbZug== + dependencies: + regenerator-runtime "^0.13.4" + +"@babel/template@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155" + integrity sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w== + dependencies: + "@babel/code-frame" "^7.16.7" + "@babel/parser" "^7.16.7" + "@babel/types" "^7.16.7" + +"@babel/traverse@^7.18.0", "@babel/traverse@^7.18.2", "@babel/traverse@^7.18.5": + version "7.18.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.5.tgz#94a8195ad9642801837988ab77f36e992d9a20cd" + integrity sha512-aKXj1KT66sBj0vVzk6rEeAO6Z9aiiQ68wfDgge3nHhA/my6xMM/7HGQUNumKZaoa2qUPQ5whJG9aAifsxUKfLA== + dependencies: + "@babel/code-frame" "^7.16.7" + "@babel/generator" "^7.18.2" + "@babel/helper-environment-visitor" "^7.18.2" + "@babel/helper-function-name" "^7.17.9" + "@babel/helper-hoist-variables" "^7.16.7" + "@babel/helper-split-export-declaration" "^7.16.7" + "@babel/parser" "^7.18.5" + "@babel/types" "^7.18.4" + debug "^4.1.0" + globals "^11.1.0" + +"@babel/types@^7.16.7", "@babel/types@^7.17.0", "@babel/types@^7.18.0", "@babel/types@^7.18.2", "@babel/types@^7.18.4": + version "7.18.4" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.4.tgz#27eae9b9fd18e9dccc3f9d6ad051336f307be354" + integrity sha512-ThN1mBcMq5pG/Vm2IcBmPPfyPXbd8S02rS+OBIDENdufvqC7Z/jHPCv9IcP01277aKtDI8g/2XysBN4hA8niiw== + dependencies: + "@babel/helper-validator-identifier" "^7.16.7" + to-fast-properties "^2.0.0" + +"@date-io/core@^2.14.0": + version "2.14.0" + resolved "https://registry.yarnpkg.com/@date-io/core/-/core-2.14.0.tgz#03e9b9b9fc8e4d561c32dd324df0f3ccd967ef14" + integrity sha512-qFN64hiFjmlDHJhu+9xMkdfDG2jLsggNxKXglnekUpXSq8faiqZgtHm2lsHCUuaPDTV6wuXHcCl8J1GQ5wLmPw== + +"@date-io/date-fns@^2.11.0": + version "2.14.0" + resolved "https://registry.yarnpkg.com/@date-io/date-fns/-/date-fns-2.14.0.tgz#92ab150f488f294c135c873350d154803cebdbea" + integrity sha512-4fJctdVyOd5cKIKGaWUM+s3MUXMuzkZaHuTY15PH70kU1YTMrCoauA7hgQVx9qj0ZEbGrH9VSPYJYnYro7nKiA== + dependencies: + "@date-io/core" "^2.14.0" + +"@date-io/dayjs@^2.11.0": + version "2.14.0" + resolved "https://registry.yarnpkg.com/@date-io/dayjs/-/dayjs-2.14.0.tgz#8d4e93e1d473bb5f25210866204dc33384ca4c20" + integrity sha512-4fRvNWaOh7AjvOyJ4h6FYMS7VHLQnIEeAV5ahv6sKYWx+1g1UwYup8h7+gPuoF+sW2hTScxi7PVaba2Jk/U8Og== + dependencies: + "@date-io/core" "^2.14.0" + +"@date-io/luxon@^2.11.1": + version "2.14.0" + resolved "https://registry.yarnpkg.com/@date-io/luxon/-/luxon-2.14.0.tgz#cd1641229e00a899625895de3a31e3aaaf66629f" + integrity sha512-KmpBKkQFJ/YwZgVd0T3h+br/O0uL9ZdE7mn903VPAG2ZZncEmaUfUdYKFT7v7GyIKJ4KzCp379CRthEbxevEVg== + dependencies: + "@date-io/core" "^2.14.0" + +"@date-io/moment@^2.11.0": + version "2.14.0" + resolved "https://registry.yarnpkg.com/@date-io/moment/-/moment-2.14.0.tgz#8300abd6ae8c55d8edee90d118db3cef0b1d4f58" + integrity sha512-VsoLXs94GsZ49ecWuvFbsa081zEv2xxG7d+izJsqGa2L8RPZLlwk27ANh87+SNnOUpp+qy2AoCAf0mx4XXhioA== + dependencies: + "@date-io/core" "^2.14.0" + +"@emotion/babel-plugin@^11.7.1": + version "11.9.2" + resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.9.2.tgz#723b6d394c89fb2ef782229d92ba95a740576e95" + integrity sha512-Pr/7HGH6H6yKgnVFNEj2MVlreu3ADqftqjqwUvDy/OJzKFgxKeTQ+eeUf20FOTuHVkDON2iNa25rAXVYtWJCjw== + dependencies: + "@babel/helper-module-imports" "^7.12.13" + "@babel/plugin-syntax-jsx" "^7.12.13" + "@babel/runtime" "^7.13.10" + "@emotion/hash" "^0.8.0" + "@emotion/memoize" "^0.7.5" + "@emotion/serialize" "^1.0.2" + babel-plugin-macros "^2.6.1" + convert-source-map "^1.5.0" + escape-string-regexp "^4.0.0" + find-root "^1.1.0" + source-map "^0.5.7" + stylis "4.0.13" + +"@emotion/cache@^11.7.1", "@emotion/cache@^11.9.3": + version "11.9.3" + resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.9.3.tgz#96638449f6929fd18062cfe04d79b29b44c0d6cb" + integrity sha512-0dgkI/JKlCXa+lEXviaMtGBL0ynpx4osh7rjOXE71q9bIF8G+XhJgvi+wDu0B0IdCVx37BffiwXlN9I3UuzFvg== + dependencies: + "@emotion/memoize" "^0.7.4" + "@emotion/sheet" "^1.1.1" + "@emotion/utils" "^1.0.0" + "@emotion/weak-memoize" "^0.2.5" + stylis "4.0.13" + +"@emotion/hash@^0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413" + integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow== + +"@emotion/is-prop-valid@^1.1.2", "@emotion/is-prop-valid@^1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.1.3.tgz#f0907a416368cf8df9e410117068e20fe87c0a3a" + integrity sha512-RFg04p6C+1uO19uG8N+vqanzKqiM9eeV1LDOG3bmkYmuOj7NbKNlFC/4EZq5gnwAIlcC/jOT24f8Td0iax2SXA== + dependencies: + "@emotion/memoize" "^0.7.4" + +"@emotion/memoize@^0.7.4", "@emotion/memoize@^0.7.5": + version "0.7.5" + resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.5.tgz#2c40f81449a4e554e9fc6396910ed4843ec2be50" + integrity sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ== + +"@emotion/react@^11.9.3": + version "11.9.3" + resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.9.3.tgz#f4f4f34444f6654a2e550f5dab4f2d360c101df9" + integrity sha512-g9Q1GcTOlzOEjqwuLF/Zd9LC+4FljjPjDfxSM7KmEakm+hsHXk+bYZ2q+/hTJzr0OUNkujo72pXLQvXj6H+GJQ== + dependencies: + "@babel/runtime" "^7.13.10" + "@emotion/babel-plugin" "^11.7.1" + "@emotion/cache" "^11.9.3" + "@emotion/serialize" "^1.0.4" + "@emotion/utils" "^1.1.0" + "@emotion/weak-memoize" "^0.2.5" + hoist-non-react-statics "^3.3.1" + +"@emotion/serialize@^1.0.2", "@emotion/serialize@^1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.0.4.tgz#ff31fd11bb07999611199c2229e152faadc21a3c" + integrity sha512-1JHamSpH8PIfFwAMryO2bNka+y8+KA5yga5Ocf2d7ZEiJjb7xlLW7aknBGZqJLajuLOvJ+72vN+IBSwPlXD1Pg== + dependencies: + "@emotion/hash" "^0.8.0" + "@emotion/memoize" "^0.7.4" + "@emotion/unitless" "^0.7.5" + "@emotion/utils" "^1.0.0" + csstype "^3.0.2" + +"@emotion/sheet@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.1.1.tgz#015756e2a9a3c7c5f11d8ec22966a8dbfbfac787" + integrity sha512-J3YPccVRMiTZxYAY0IOq3kd+hUP8idY8Kz6B/Cyo+JuXq52Ek+zbPbSQUrVQp95aJ+lsAW7DPL1P2Z+U1jGkKA== + +"@emotion/styled@^11.9.3": + version "11.9.3" + resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-11.9.3.tgz#47f0c71137fec7c57035bf3659b52fb536792340" + integrity sha512-o3sBNwbtoVz9v7WB1/Y/AmXl69YHmei2mrVnK7JgyBJ//Rst5yqPZCecEJlMlJrFeWHp+ki/54uN265V2pEcXA== + dependencies: + "@babel/runtime" "^7.13.10" + "@emotion/babel-plugin" "^11.7.1" + "@emotion/is-prop-valid" "^1.1.3" + "@emotion/serialize" "^1.0.4" + "@emotion/utils" "^1.1.0" + +"@emotion/unitless@^0.7.5": + version "0.7.5" + resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed" + integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg== + +"@emotion/utils@^1.0.0", "@emotion/utils@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.1.0.tgz#86b0b297f3f1a0f2bdb08eeac9a2f49afd40d0cf" + integrity sha512-iRLa/Y4Rs5H/f2nimczYmS5kFJEbpiVvgN3XVfZ022IYhuNA1IRSHEizcof88LtCTXtl9S2Cxt32KgaXEu72JQ== + +"@emotion/weak-memoize@^0.2.5": + version "0.2.5" + resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46" + integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA== + +"@ethersproject/abi@5.6.4", "@ethersproject/abi@^5.6.3": + version "5.6.4" + resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.6.4.tgz#f6e01b6ed391a505932698ecc0d9e7a99ee60362" + integrity sha512-TTeZUlCeIHG6527/2goZA6gW5F8Emoc7MrZDC7hhP84aRGvW3TEdTnZR08Ls88YXM1m2SuK42Osw/jSi3uO8gg== + dependencies: + "@ethersproject/address" "^5.6.1" + "@ethersproject/bignumber" "^5.6.2" + "@ethersproject/bytes" "^5.6.1" + "@ethersproject/constants" "^5.6.1" + "@ethersproject/hash" "^5.6.1" + "@ethersproject/keccak256" "^5.6.1" + "@ethersproject/logger" "^5.6.0" + "@ethersproject/properties" "^5.6.0" + "@ethersproject/strings" "^5.6.1" + +"@ethersproject/abstract-provider@5.6.1", "@ethersproject/abstract-provider@^5.6.1": + version "5.6.1" + resolved "https://registry.yarnpkg.com/@ethersproject/abstract-provider/-/abstract-provider-5.6.1.tgz#02ddce150785caf0c77fe036a0ebfcee61878c59" + integrity sha512-BxlIgogYJtp1FS8Muvj8YfdClk3unZH0vRMVX791Z9INBNT/kuACZ9GzaY1Y4yFq+YSy6/w4gzj3HCRKrK9hsQ== + dependencies: + "@ethersproject/bignumber" "^5.6.2" + "@ethersproject/bytes" "^5.6.1" + "@ethersproject/logger" "^5.6.0" + "@ethersproject/networks" "^5.6.3" + "@ethersproject/properties" "^5.6.0" + "@ethersproject/transactions" "^5.6.2" + "@ethersproject/web" "^5.6.1" + +"@ethersproject/abstract-signer@5.6.2", "@ethersproject/abstract-signer@^5.6.2": + version "5.6.2" + resolved "https://registry.yarnpkg.com/@ethersproject/abstract-signer/-/abstract-signer-5.6.2.tgz#491f07fc2cbd5da258f46ec539664713950b0b33" + integrity sha512-n1r6lttFBG0t2vNiI3HoWaS/KdOt8xyDjzlP2cuevlWLG6EX0OwcKLyG/Kp/cuwNxdy/ous+R/DEMdTUwWQIjQ== + dependencies: + "@ethersproject/abstract-provider" "^5.6.1" + "@ethersproject/bignumber" "^5.6.2" + "@ethersproject/bytes" "^5.6.1" + "@ethersproject/logger" "^5.6.0" + "@ethersproject/properties" "^5.6.0" + +"@ethersproject/address@5.6.1", "@ethersproject/address@^5.6.1": + version "5.6.1" + resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.6.1.tgz#ab57818d9aefee919c5721d28cd31fd95eff413d" + integrity sha512-uOgF0kS5MJv9ZvCz7x6T2EXJSzotiybApn4XlOgoTX0xdtyVIJ7pF+6cGPxiEq/dpBiTfMiw7Yc81JcwhSYA0Q== + dependencies: + "@ethersproject/bignumber" "^5.6.2" + "@ethersproject/bytes" "^5.6.1" + "@ethersproject/keccak256" "^5.6.1" + "@ethersproject/logger" "^5.6.0" + "@ethersproject/rlp" "^5.6.1" + +"@ethersproject/base64@5.6.1", "@ethersproject/base64@^5.6.1": + version "5.6.1" + resolved "https://registry.yarnpkg.com/@ethersproject/base64/-/base64-5.6.1.tgz#2c40d8a0310c9d1606c2c37ae3092634b41d87cb" + integrity sha512-qB76rjop6a0RIYYMiB4Eh/8n+Hxu2NIZm8S/Q7kNo5pmZfXhHGHmS4MinUainiBC54SCyRnwzL+KZjj8zbsSsw== + dependencies: + "@ethersproject/bytes" "^5.6.1" + +"@ethersproject/basex@5.6.1", "@ethersproject/basex@^5.6.1": + version "5.6.1" + resolved "https://registry.yarnpkg.com/@ethersproject/basex/-/basex-5.6.1.tgz#badbb2f1d4a6f52ce41c9064f01eab19cc4c5305" + integrity sha512-a52MkVz4vuBXR06nvflPMotld1FJWSj2QT0985v7P/emPZO00PucFAkbcmq2vpVU7Ts7umKiSI6SppiLykVWsA== + dependencies: + "@ethersproject/bytes" "^5.6.1" + "@ethersproject/properties" "^5.6.0" + +"@ethersproject/bignumber@5.6.2", "@ethersproject/bignumber@^5.6.2": + version "5.6.2" + resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.6.2.tgz#72a0717d6163fab44c47bcc82e0c550ac0315d66" + integrity sha512-v7+EEUbhGqT3XJ9LMPsKvXYHFc8eHxTowFCG/HgJErmq4XHJ2WR7aeyICg3uTOAQ7Icn0GFHAohXEhxQHq4Ubw== + dependencies: + "@ethersproject/bytes" "^5.6.1" + "@ethersproject/logger" "^5.6.0" + bn.js "^5.2.1" + +"@ethersproject/bytes@5.6.1", "@ethersproject/bytes@^5.6.1": + version "5.6.1" + resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.6.1.tgz#24f916e411f82a8a60412344bf4a813b917eefe7" + integrity sha512-NwQt7cKn5+ZE4uDn+X5RAXLp46E1chXoaMmrxAyA0rblpxz8t58lVkrHXoRIn0lz1joQElQ8410GqhTqMOwc6g== + dependencies: + "@ethersproject/logger" "^5.6.0" + +"@ethersproject/constants@5.6.1", "@ethersproject/constants@^5.6.1": + version "5.6.1" + resolved "https://registry.yarnpkg.com/@ethersproject/constants/-/constants-5.6.1.tgz#e2e974cac160dd101cf79fdf879d7d18e8cb1370" + integrity sha512-QSq9WVnZbxXYFftrjSjZDUshp6/eKp6qrtdBtUCm0QxCV5z1fG/w3kdlcsjMCQuQHUnAclKoK7XpXMezhRDOLg== + dependencies: + "@ethersproject/bignumber" "^5.6.2" + +"@ethersproject/contracts@5.6.2": + version "5.6.2" + resolved "https://registry.yarnpkg.com/@ethersproject/contracts/-/contracts-5.6.2.tgz#20b52e69ebc1b74274ff8e3d4e508de971c287bc" + integrity sha512-hguUA57BIKi6WY0kHvZp6PwPlWF87MCeB4B7Z7AbUpTxfFXFdn/3b0GmjZPagIHS+3yhcBJDnuEfU4Xz+Ks/8g== + dependencies: + "@ethersproject/abi" "^5.6.3" + "@ethersproject/abstract-provider" "^5.6.1" + "@ethersproject/abstract-signer" "^5.6.2" + "@ethersproject/address" "^5.6.1" + "@ethersproject/bignumber" "^5.6.2" + "@ethersproject/bytes" "^5.6.1" + "@ethersproject/constants" "^5.6.1" + "@ethersproject/logger" "^5.6.0" + "@ethersproject/properties" "^5.6.0" + "@ethersproject/transactions" "^5.6.2" + +"@ethersproject/hash@5.6.1", "@ethersproject/hash@^5.6.1": + version "5.6.1" + resolved "https://registry.yarnpkg.com/@ethersproject/hash/-/hash-5.6.1.tgz#224572ea4de257f05b4abf8ae58b03a67e99b0f4" + integrity sha512-L1xAHurbaxG8VVul4ankNX5HgQ8PNCTrnVXEiFnE9xoRnaUcgfD12tZINtDinSllxPLCtGwguQxJ5E6keE84pA== + dependencies: + "@ethersproject/abstract-signer" "^5.6.2" + "@ethersproject/address" "^5.6.1" + "@ethersproject/bignumber" "^5.6.2" + "@ethersproject/bytes" "^5.6.1" + "@ethersproject/keccak256" "^5.6.1" + "@ethersproject/logger" "^5.6.0" + "@ethersproject/properties" "^5.6.0" + "@ethersproject/strings" "^5.6.1" + +"@ethersproject/hdnode@5.6.2", "@ethersproject/hdnode@^5.6.2": + version "5.6.2" + resolved "https://registry.yarnpkg.com/@ethersproject/hdnode/-/hdnode-5.6.2.tgz#26f3c83a3e8f1b7985c15d1db50dc2903418b2d2" + integrity sha512-tERxW8Ccf9CxW2db3WsN01Qao3wFeRsfYY9TCuhmG0xNpl2IO8wgXU3HtWIZ49gUWPggRy4Yg5axU0ACaEKf1Q== + dependencies: + "@ethersproject/abstract-signer" "^5.6.2" + "@ethersproject/basex" "^5.6.1" + "@ethersproject/bignumber" "^5.6.2" + "@ethersproject/bytes" "^5.6.1" + "@ethersproject/logger" "^5.6.0" + "@ethersproject/pbkdf2" "^5.6.1" + "@ethersproject/properties" "^5.6.0" + "@ethersproject/sha2" "^5.6.1" + "@ethersproject/signing-key" "^5.6.2" + "@ethersproject/strings" "^5.6.1" + "@ethersproject/transactions" "^5.6.2" + "@ethersproject/wordlists" "^5.6.1" + +"@ethersproject/json-wallets@5.6.1", "@ethersproject/json-wallets@^5.6.1": + version "5.6.1" + resolved "https://registry.yarnpkg.com/@ethersproject/json-wallets/-/json-wallets-5.6.1.tgz#3f06ba555c9c0d7da46756a12ac53483fe18dd91" + integrity sha512-KfyJ6Zwz3kGeX25nLihPwZYlDqamO6pfGKNnVMWWfEVVp42lTfCZVXXy5Ie8IZTN0HKwAngpIPi7gk4IJzgmqQ== + dependencies: + "@ethersproject/abstract-signer" "^5.6.2" + "@ethersproject/address" "^5.6.1" + "@ethersproject/bytes" "^5.6.1" + "@ethersproject/hdnode" "^5.6.2" + "@ethersproject/keccak256" "^5.6.1" + "@ethersproject/logger" "^5.6.0" + "@ethersproject/pbkdf2" "^5.6.1" + "@ethersproject/properties" "^5.6.0" + "@ethersproject/random" "^5.6.1" + "@ethersproject/strings" "^5.6.1" + "@ethersproject/transactions" "^5.6.2" + aes-js "3.0.0" + scrypt-js "3.0.1" + +"@ethersproject/keccak256@5.6.1", "@ethersproject/keccak256@^5.6.1": + version "5.6.1" + resolved "https://registry.yarnpkg.com/@ethersproject/keccak256/-/keccak256-5.6.1.tgz#b867167c9b50ba1b1a92bccdd4f2d6bd168a91cc" + integrity sha512-bB7DQHCTRDooZZdL3lk9wpL0+XuG3XLGHLh3cePnybsO3V0rdCAOQGpn/0R3aODmnTOOkCATJiD2hnL+5bwthA== + dependencies: + "@ethersproject/bytes" "^5.6.1" + js-sha3 "0.8.0" + +"@ethersproject/logger@5.6.0", "@ethersproject/logger@^5.6.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.6.0.tgz#d7db1bfcc22fd2e4ab574cba0bb6ad779a9a3e7a" + integrity sha512-BiBWllUROH9w+P21RzoxJKzqoqpkyM1pRnEKG69bulE9TSQD8SAIvTQqIMZmmCO8pUNkgLP1wndX1gKghSpBmg== + +"@ethersproject/networks@5.6.4", "@ethersproject/networks@^5.6.3": + version "5.6.4" + resolved "https://registry.yarnpkg.com/@ethersproject/networks/-/networks-5.6.4.tgz#51296d8fec59e9627554f5a8a9c7791248c8dc07" + integrity sha512-KShHeHPahHI2UlWdtDMn2lJETcbtaJge4k7XSjDR9h79QTd6yQJmv6Cp2ZA4JdqWnhszAOLSuJEd9C0PRw7hSQ== + dependencies: + "@ethersproject/logger" "^5.6.0" + +"@ethersproject/pbkdf2@5.6.1", "@ethersproject/pbkdf2@^5.6.1": + version "5.6.1" + resolved "https://registry.yarnpkg.com/@ethersproject/pbkdf2/-/pbkdf2-5.6.1.tgz#f462fe320b22c0d6b1d72a9920a3963b09eb82d1" + integrity sha512-k4gRQ+D93zDRPNUfmduNKq065uadC2YjMP/CqwwX5qG6R05f47boq6pLZtV/RnC4NZAYOPH1Cyo54q0c9sshRQ== + dependencies: + "@ethersproject/bytes" "^5.6.1" + "@ethersproject/sha2" "^5.6.1" + +"@ethersproject/properties@5.6.0", "@ethersproject/properties@^5.6.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@ethersproject/properties/-/properties-5.6.0.tgz#38904651713bc6bdd5bdd1b0a4287ecda920fa04" + integrity sha512-szoOkHskajKePTJSZ46uHUWWkbv7TzP2ypdEK6jGMqJaEt2sb0jCgfBo0gH0m2HBpRixMuJ6TBRaQCF7a9DoCg== + dependencies: + "@ethersproject/logger" "^5.6.0" + +"@ethersproject/providers@5.6.8": + version "5.6.8" + resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.6.8.tgz#22e6c57be215ba5545d3a46cf759d265bb4e879d" + integrity sha512-Wf+CseT/iOJjrGtAOf3ck9zS7AgPmr2fZ3N97r4+YXN3mBePTG2/bJ8DApl9mVwYL+RpYbNxMEkEp4mPGdwG/w== + dependencies: + "@ethersproject/abstract-provider" "^5.6.1" + "@ethersproject/abstract-signer" "^5.6.2" + "@ethersproject/address" "^5.6.1" + "@ethersproject/base64" "^5.6.1" + "@ethersproject/basex" "^5.6.1" + "@ethersproject/bignumber" "^5.6.2" + "@ethersproject/bytes" "^5.6.1" + "@ethersproject/constants" "^5.6.1" + "@ethersproject/hash" "^5.6.1" + "@ethersproject/logger" "^5.6.0" + "@ethersproject/networks" "^5.6.3" + "@ethersproject/properties" "^5.6.0" + "@ethersproject/random" "^5.6.1" + "@ethersproject/rlp" "^5.6.1" + "@ethersproject/sha2" "^5.6.1" + "@ethersproject/strings" "^5.6.1" + "@ethersproject/transactions" "^5.6.2" + "@ethersproject/web" "^5.6.1" + bech32 "1.1.4" + ws "7.4.6" + +"@ethersproject/random@5.6.1", "@ethersproject/random@^5.6.1": + version "5.6.1" + resolved "https://registry.yarnpkg.com/@ethersproject/random/-/random-5.6.1.tgz#66915943981bcd3e11bbd43733f5c3ba5a790255" + integrity sha512-/wtPNHwbmng+5yi3fkipA8YBT59DdkGRoC2vWk09Dci/q5DlgnMkhIycjHlavrvrjJBkFjO/ueLyT+aUDfc4lA== + dependencies: + "@ethersproject/bytes" "^5.6.1" + "@ethersproject/logger" "^5.6.0" + +"@ethersproject/rlp@5.6.1", "@ethersproject/rlp@^5.6.1": + version "5.6.1" + resolved "https://registry.yarnpkg.com/@ethersproject/rlp/-/rlp-5.6.1.tgz#df8311e6f9f24dcb03d59a2bac457a28a4fe2bd8" + integrity sha512-uYjmcZx+DKlFUk7a5/W9aQVaoEC7+1MOBgNtvNg13+RnuUwT4F0zTovC0tmay5SmRslb29V1B7Y5KCri46WhuQ== + dependencies: + "@ethersproject/bytes" "^5.6.1" + "@ethersproject/logger" "^5.6.0" + +"@ethersproject/sha2@5.6.1", "@ethersproject/sha2@^5.6.1": + version "5.6.1" + resolved "https://registry.yarnpkg.com/@ethersproject/sha2/-/sha2-5.6.1.tgz#211f14d3f5da5301c8972a8827770b6fd3e51656" + integrity sha512-5K2GyqcW7G4Yo3uenHegbXRPDgARpWUiXc6RiF7b6i/HXUoWlb7uCARh7BAHg7/qT/Q5ydofNwiZcim9qpjB6g== + dependencies: + "@ethersproject/bytes" "^5.6.1" + "@ethersproject/logger" "^5.6.0" + hash.js "1.1.7" + +"@ethersproject/signing-key@5.6.2", "@ethersproject/signing-key@^5.6.2": + version "5.6.2" + resolved "https://registry.yarnpkg.com/@ethersproject/signing-key/-/signing-key-5.6.2.tgz#8a51b111e4d62e5a62aee1da1e088d12de0614a3" + integrity sha512-jVbu0RuP7EFpw82vHcL+GP35+KaNruVAZM90GxgQnGqB6crhBqW/ozBfFvdeImtmb4qPko0uxXjn8l9jpn0cwQ== + dependencies: + "@ethersproject/bytes" "^5.6.1" + "@ethersproject/logger" "^5.6.0" + "@ethersproject/properties" "^5.6.0" + bn.js "^5.2.1" + elliptic "6.5.4" + hash.js "1.1.7" + +"@ethersproject/solidity@5.6.1": + version "5.6.1" + resolved "https://registry.yarnpkg.com/@ethersproject/solidity/-/solidity-5.6.1.tgz#5845e71182c66d32e6ec5eefd041fca091a473e2" + integrity sha512-KWqVLkUUoLBfL1iwdzUVlkNqAUIFMpbbeH0rgCfKmJp0vFtY4AsaN91gHKo9ZZLkC4UOm3cI3BmMV4N53BOq4g== + dependencies: + "@ethersproject/bignumber" "^5.6.2" + "@ethersproject/bytes" "^5.6.1" + "@ethersproject/keccak256" "^5.6.1" + "@ethersproject/logger" "^5.6.0" + "@ethersproject/sha2" "^5.6.1" + "@ethersproject/strings" "^5.6.1" + +"@ethersproject/strings@5.6.1", "@ethersproject/strings@^5.6.1": + version "5.6.1" + resolved "https://registry.yarnpkg.com/@ethersproject/strings/-/strings-5.6.1.tgz#dbc1b7f901db822b5cafd4ebf01ca93c373f8952" + integrity sha512-2X1Lgk6Jyfg26MUnsHiT456U9ijxKUybz8IM1Vih+NJxYtXhmvKBcHOmvGqpFSVJ0nQ4ZCoIViR8XlRw1v/+Cw== + dependencies: + "@ethersproject/bytes" "^5.6.1" + "@ethersproject/constants" "^5.6.1" + "@ethersproject/logger" "^5.6.0" + +"@ethersproject/transactions@5.6.2", "@ethersproject/transactions@^5.6.2": + version "5.6.2" + resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.6.2.tgz#793a774c01ced9fe7073985bb95a4b4e57a6370b" + integrity sha512-BuV63IRPHmJvthNkkt9G70Ullx6AcM+SDc+a8Aw/8Yew6YwT51TcBKEp1P4oOQ/bP25I18JJr7rcFRgFtU9B2Q== + dependencies: + "@ethersproject/address" "^5.6.1" + "@ethersproject/bignumber" "^5.6.2" + "@ethersproject/bytes" "^5.6.1" + "@ethersproject/constants" "^5.6.1" + "@ethersproject/keccak256" "^5.6.1" + "@ethersproject/logger" "^5.6.0" + "@ethersproject/properties" "^5.6.0" + "@ethersproject/rlp" "^5.6.1" + "@ethersproject/signing-key" "^5.6.2" + +"@ethersproject/units@5.6.1", "@ethersproject/units@^5.6.1": + version "5.6.1" + resolved "https://registry.yarnpkg.com/@ethersproject/units/-/units-5.6.1.tgz#ecc590d16d37c8f9ef4e89e2005bda7ddc6a4e6f" + integrity sha512-rEfSEvMQ7obcx3KWD5EWWx77gqv54K6BKiZzKxkQJqtpriVsICrktIQmKl8ReNToPeIYPnFHpXvKpi068YFZXw== + dependencies: + "@ethersproject/bignumber" "^5.6.2" + "@ethersproject/constants" "^5.6.1" + "@ethersproject/logger" "^5.6.0" + +"@ethersproject/wallet@5.6.2": + version "5.6.2" + resolved "https://registry.yarnpkg.com/@ethersproject/wallet/-/wallet-5.6.2.tgz#cd61429d1e934681e413f4bc847a5f2f87e3a03c" + integrity sha512-lrgh0FDQPuOnHcF80Q3gHYsSUODp6aJLAdDmDV0xKCN/T7D99ta1jGVhulg3PY8wiXEngD0DfM0I2XKXlrqJfg== + dependencies: + "@ethersproject/abstract-provider" "^5.6.1" + "@ethersproject/abstract-signer" "^5.6.2" + "@ethersproject/address" "^5.6.1" + "@ethersproject/bignumber" "^5.6.2" + "@ethersproject/bytes" "^5.6.1" + "@ethersproject/hash" "^5.6.1" + "@ethersproject/hdnode" "^5.6.2" + "@ethersproject/json-wallets" "^5.6.1" + "@ethersproject/keccak256" "^5.6.1" + "@ethersproject/logger" "^5.6.0" + "@ethersproject/properties" "^5.6.0" + "@ethersproject/random" "^5.6.1" + "@ethersproject/signing-key" "^5.6.2" + "@ethersproject/transactions" "^5.6.2" + "@ethersproject/wordlists" "^5.6.1" + +"@ethersproject/web@5.6.1", "@ethersproject/web@^5.6.1": + version "5.6.1" + resolved "https://registry.yarnpkg.com/@ethersproject/web/-/web-5.6.1.tgz#6e2bd3ebadd033e6fe57d072db2b69ad2c9bdf5d" + integrity sha512-/vSyzaQlNXkO1WV+RneYKqCJwualcUdx/Z3gseVovZP0wIlOFcCE1hkRhKBH8ImKbGQbMl9EAAyJFrJu7V0aqA== + dependencies: + "@ethersproject/base64" "^5.6.1" + "@ethersproject/bytes" "^5.6.1" + "@ethersproject/logger" "^5.6.0" + "@ethersproject/properties" "^5.6.0" + "@ethersproject/strings" "^5.6.1" + +"@ethersproject/wordlists@5.6.1", "@ethersproject/wordlists@^5.6.1": + version "5.6.1" + resolved "https://registry.yarnpkg.com/@ethersproject/wordlists/-/wordlists-5.6.1.tgz#1e78e2740a8a21e9e99947e47979d72e130aeda1" + integrity sha512-wiPRgBpNbNwCQFoCr8bcWO8o5I810cqO6mkdtKfLKFlLxeCWcnzDi4Alu8iyNzlhYuS9npCwivMbRWF19dyblw== + dependencies: + "@ethersproject/bytes" "^5.6.1" + "@ethersproject/hash" "^5.6.1" + "@ethersproject/logger" "^5.6.0" + "@ethersproject/properties" "^5.6.0" + "@ethersproject/strings" "^5.6.1" + +"@jridgewell/gen-mapping@^0.1.0": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz#e5d2e450306a9491e3bd77e323e38d7aff315996" + integrity sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w== + dependencies: + "@jridgewell/set-array" "^1.0.0" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@jridgewell/gen-mapping@^0.3.0": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.1.tgz#cf92a983c83466b8c0ce9124fadeaf09f7c66ea9" + integrity sha512-GcHwniMlA2z+WFPWuY8lp3fsza0I8xPFMWL5+n8LYyP6PSvPrXf4+n8stDHZY2DM0zy9sVkRDy1jDI4XGzYVqg== + dependencies: + "@jridgewell/set-array" "^1.0.0" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/resolve-uri@^3.0.3": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.0.7.tgz#30cd49820a962aff48c8fffc5cd760151fca61fe" + integrity sha512-8cXDaBBHOr2pQ7j77Y6Vp5VDT2sIqWyWQ56TjEq4ih/a4iST3dItRe8Q9fp0rrIl9DoKhWQtUQz/YpOxLkXbNA== + +"@jridgewell/set-array@^1.0.0": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.1.tgz#36a6acc93987adcf0ba50c66908bd0b70de8afea" + integrity sha512-Ct5MqZkLGEXTVmQYbGtx9SVqD2fqwvdubdps5D3djjAkgkKwT918VNOz65pEHFaYTeWcukmJmH5SwsA9Tn2ObQ== + +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.13" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.13.tgz#b6461fb0c2964356c469e115f504c95ad97ab88c" + integrity sha512-GryiOJmNcWbovBxTfZSF71V/mXbgcV3MewDe3kIMCLyIh5e7SKAeUZs+rMnJ8jkMolZ/4/VsdBmMrw3l+VdZ3w== + +"@jridgewell/trace-mapping@^0.3.9": + version "0.3.13" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.13.tgz#dcfe3e95f224c8fe97a87a5235defec999aa92ea" + integrity sha512-o1xbKhp9qnIAoHJSWd6KlCZfqslL4valSF81H8ImioOAxluWYWOpWkpyktY2vnt4tbrX9XYaxovq6cgowaJp2w== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@metamask/detect-provider@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@metamask/detect-provider/-/detect-provider-1.2.0.tgz#3667a7531f2a682e3c3a43eaf3a1958bdb42a696" + integrity sha512-ocA76vt+8D0thgXZ7LxFPyqw3H7988qblgzddTDA6B8a/yU0uKV42QR/DhA+Jh11rJjxW0jKvwb5htA6krNZDQ== + +"@mui/base@5.0.0-alpha.85": + version "5.0.0-alpha.85" + resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-alpha.85.tgz#e9e19678bf72dae228d0f25d33dfe20462aac833" + integrity sha512-ONlQJOmQrxmR+pYF9AqH69FOG4ofwzVzNltwb2xKAQIW3VbsNZahcHIpzhFd70W6EIU+QHzB9TzamSM+Fg/U7w== + dependencies: + "@babel/runtime" "^7.17.2" + "@emotion/is-prop-valid" "^1.1.2" + "@mui/types" "^7.1.4" + "@mui/utils" "^5.8.4" + "@popperjs/core" "^2.11.5" + clsx "^1.1.1" + prop-types "^15.8.1" + react-is "^17.0.2" + +"@mui/icons-material@^5.8.3": + version "5.8.4" + resolved "https://registry.yarnpkg.com/@mui/icons-material/-/icons-material-5.8.4.tgz#3f2907c9f8f5ce4d754cb8fb4b68b5a1abf4d095" + integrity sha512-9Z/vyj2szvEhGWDvb+gG875bOGm8b8rlHBKOD1+nA3PcgC3fV6W1AU6pfOorPeBfH2X4mb9Boe97vHvaSndQvA== + dependencies: + "@babel/runtime" "^7.17.2" + +"@mui/lab@^5.0.0-alpha.85": + version "5.0.0-alpha.86" + resolved "https://registry.yarnpkg.com/@mui/lab/-/lab-5.0.0-alpha.86.tgz#83323e0ff17fdea641fa1d93be024413bf407ec3" + integrity sha512-5dx9/vHldiE5KFu99YUtEGKyUgwTiq8wM+IhEnNKkU+YjEMULVYV+mgS9nvnf6laKtgqy2hOE4JivqRPIuOGdA== + dependencies: + "@babel/runtime" "^7.17.2" + "@mui/base" "5.0.0-alpha.85" + "@mui/system" "^5.8.4" + "@mui/utils" "^5.8.4" + "@mui/x-date-pickers" "5.0.0-alpha.1" + clsx "^1.1.1" + prop-types "^15.8.1" + react-is "^17.0.2" + react-transition-group "^4.4.2" + rifm "^0.12.1" + +"@mui/material@^5.8.3": + version "5.8.4" + resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.8.4.tgz#b9cdae0c79ea770bc9cc3aafb7f750ed8ebe1b5d" + integrity sha512-KlOJS1JGhwuhdoF4fulmz41h/YxyMdZSc+ncz+HAah0GKn8ovAs5774f1w0lIasxbtI1Ziunwvmnu9PvvUKdMw== + dependencies: + "@babel/runtime" "^7.17.2" + "@mui/base" "5.0.0-alpha.85" + "@mui/system" "^5.8.4" + "@mui/types" "^7.1.4" + "@mui/utils" "^5.8.4" + "@types/react-transition-group" "^4.4.4" + clsx "^1.1.1" + csstype "^3.1.0" + prop-types "^15.8.1" + react-is "^17.0.2" + react-transition-group "^4.4.2" + +"@mui/private-theming@^5.8.4": + version "5.8.4" + resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-5.8.4.tgz#8ff896601cf84eb9f8394db7674ee4dd2a3343f7" + integrity sha512-3Lp0VAEjtQygJ70MWEyHkKvg327O6YoBH6ZNEy6fIsrK6gmRIj+YrlvJ7LQCbowY+qDGnbdMrTBd1hfThlI8lg== + dependencies: + "@babel/runtime" "^7.17.2" + "@mui/utils" "^5.8.4" + prop-types "^15.8.1" + +"@mui/styled-engine@^5.8.0": + version "5.8.0" + resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.8.0.tgz#89ed42efe7c8749e5a60af035bc5d3a6bea362bf" + integrity sha512-Q3spibB8/EgeMYHc+/o3RRTnAYkSl7ROCLhXJ830W8HZ2/iDiyYp16UcxKPurkXvLhUaILyofPVrP3Su2uKsAw== + dependencies: + "@babel/runtime" "^7.17.2" + "@emotion/cache" "^11.7.1" + prop-types "^15.8.1" + +"@mui/styles@^5.8.3": + version "5.8.4" + resolved "https://registry.yarnpkg.com/@mui/styles/-/styles-5.8.4.tgz#cc6463df91ad1cc1c035229526f865093bbfc03e" + integrity sha512-Td7dafJDgpdzObT0z5CH/ihOh22MG2vZ7p2tpnrKaq3We50f8l3T69XeTNcy2OH0TWnXJJuASZS/0uMJmVPfag== + dependencies: + "@babel/runtime" "^7.17.2" + "@emotion/hash" "^0.8.0" + "@mui/private-theming" "^5.8.4" + "@mui/types" "^7.1.4" + "@mui/utils" "^5.8.4" + clsx "^1.1.1" + csstype "^3.1.0" + hoist-non-react-statics "^3.3.2" + jss "^10.8.2" + jss-plugin-camel-case "^10.8.2" + jss-plugin-default-unit "^10.8.2" + jss-plugin-global "^10.8.2" + jss-plugin-nested "^10.8.2" + jss-plugin-props-sort "^10.8.2" + jss-plugin-rule-value-function "^10.8.2" + jss-plugin-vendor-prefixer "^10.8.2" + prop-types "^15.8.1" + +"@mui/system@^5.8.4": + version "5.8.4" + resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.8.4.tgz#88306aefcc3a60528f69dcd2d66516831859c328" + integrity sha512-eeYZXlOn4p+tYwqqDlci6wW4knJ68aGx5A24YU9ubYZ5o0IwveoNP3LC9sHAMxigk/mUTqL4bpSMJ2HbTn2aQg== + dependencies: + "@babel/runtime" "^7.17.2" + "@mui/private-theming" "^5.8.4" + "@mui/styled-engine" "^5.8.0" + "@mui/types" "^7.1.4" + "@mui/utils" "^5.8.4" + clsx "^1.1.1" + csstype "^3.1.0" + prop-types "^15.8.1" + +"@mui/types@^7.1.4": + version "7.1.4" + resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.1.4.tgz#4185c05d6df63ec673cda15feab80440abadc764" + integrity sha512-uveM3byMbthO+6tXZ1n2zm0W3uJCQYtwt/v5zV5I77v2v18u0ITkb8xwhsDD2i3V2Kye7SaNR6FFJ6lMuY/WqQ== + +"@mui/utils@^5.4.1", "@mui/utils@^5.6.0", "@mui/utils@^5.8.4": + version "5.8.4" + resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.8.4.tgz#5c46b5900bd2452b3ce54a7a1c94a3e2a8a75c34" + integrity sha512-BHYErfrjqqh76KaDAm8wZlhEip1Uj7Cmco65NcsF3BWrAl3FWngACpaPZeEbTgmaEwyWAQEE6LZhsmy43hfyqQ== + dependencies: + "@babel/runtime" "^7.17.2" + "@types/prop-types" "^15.7.5" + "@types/react-is" "^16.7.1 || ^17.0.0" + prop-types "^15.8.1" + react-is "^17.0.2" + +"@mui/x-data-grid@^5.12.1": + version "5.12.2" + resolved "https://registry.yarnpkg.com/@mui/x-data-grid/-/x-data-grid-5.12.2.tgz#e7bde75549ab592ebdafe2d12a2b7f671d484d22" + integrity sha512-OA5jjSoGPrO742GWNSxUPac6U1m8wF0rzcmqlj5vMuBySkPi0ycPRRlVAlYJWTVhSBPs+UWoHA9QpTE19eMBYg== + dependencies: + "@babel/runtime" "^7.17.2" + "@mui/utils" "^5.4.1" + clsx "^1.1.1" + prop-types "^15.8.1" + reselect "^4.1.5" + +"@mui/x-date-pickers@5.0.0-alpha.1": + version "5.0.0-alpha.1" + resolved "https://registry.yarnpkg.com/@mui/x-date-pickers/-/x-date-pickers-5.0.0-alpha.1.tgz#7450b5544b9ed655db41891c74e2c5f652fbedb7" + integrity sha512-dLPkRiIn2Gr0momblxiOnIwrxn4SijVix+8e08mwAGWhiWcmWep1O9XTRDpZsjB0kjHYCf+kZjlRX4dxnj2acg== + dependencies: + "@date-io/date-fns" "^2.11.0" + "@date-io/dayjs" "^2.11.0" + "@date-io/luxon" "^2.11.1" + "@date-io/moment" "^2.11.0" + "@mui/utils" "^5.6.0" + clsx "^1.1.1" + prop-types "^15.7.2" + react-transition-group "^4.4.2" + rifm "^0.12.1" + +"@next/env@12.1.6": + version "12.1.6" + resolved "https://registry.yarnpkg.com/@next/env/-/env-12.1.6.tgz#5f44823a78335355f00f1687cfc4f1dafa3eca08" + integrity sha512-Te/OBDXFSodPU6jlXYPAXpmZr/AkG6DCATAxttQxqOWaq6eDFX25Db3dK0120GZrSZmv4QCe9KsZmJKDbWs4OA== + +"@next/swc-android-arm-eabi@12.1.6": + version "12.1.6" + resolved "https://registry.yarnpkg.com/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.1.6.tgz#79a35349b98f2f8c038ab6261aa9cd0d121c03f9" + integrity sha512-BxBr3QAAAXWgk/K7EedvzxJr2dE014mghBSA9iOEAv0bMgF+MRq4PoASjuHi15M2zfowpcRG8XQhMFtxftCleQ== + +"@next/swc-android-arm64@12.1.6": + version "12.1.6" + resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-12.1.6.tgz#ec08ea61794f8752c8ebcacbed0aafc5b9407456" + integrity sha512-EboEk3ROYY7U6WA2RrMt/cXXMokUTXXfnxe2+CU+DOahvbrO8QSWhlBl9I9ZbFzJx28AGB9Yo3oQHCvph/4Lew== + +"@next/swc-darwin-arm64@12.1.6": + version "12.1.6" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.1.6.tgz#d1053805615fd0706e9b1667893a72271cd87119" + integrity sha512-P0EXU12BMSdNj1F7vdkP/VrYDuCNwBExtRPDYawgSUakzi6qP0iKJpya2BuLvNzXx+XPU49GFuDC5X+SvY0mOw== + +"@next/swc-darwin-x64@12.1.6": + version "12.1.6" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-12.1.6.tgz#2d1b926a22f4c5230d5b311f9c56cfdcc406afec" + integrity sha512-9FptMnbgHJK3dRDzfTpexs9S2hGpzOQxSQbe8omz6Pcl7rnEp9x4uSEKY51ho85JCjL4d0tDLBcXEJZKKLzxNg== + +"@next/swc-linux-arm-gnueabihf@12.1.6": + version "12.1.6" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.1.6.tgz#c021918d2a94a17f823106a5e069335b8a19724f" + integrity sha512-PvfEa1RR55dsik/IDkCKSFkk6ODNGJqPY3ysVUZqmnWMDSuqFtf7BPWHFa/53znpvVB5XaJ5Z1/6aR5CTIqxPw== + +"@next/swc-linux-arm64-gnu@12.1.6": + version "12.1.6" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.1.6.tgz#ac55c07bfabde378dfa0ce2b8fc1c3b2897e81ae" + integrity sha512-53QOvX1jBbC2ctnmWHyRhMajGq7QZfl974WYlwclXarVV418X7ed7o/EzGY+YVAEKzIVaAB9JFFWGXn8WWo0gQ== + +"@next/swc-linux-arm64-musl@12.1.6": + version "12.1.6" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.1.6.tgz#e429f826279894be9096be6bec13e75e3d6bd671" + integrity sha512-CMWAkYqfGdQCS+uuMA1A2UhOfcUYeoqnTW7msLr2RyYAys15pD960hlDfq7QAi8BCAKk0sQ2rjsl0iqMyziohQ== + +"@next/swc-linux-x64-gnu@12.1.6": + version "12.1.6" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.1.6.tgz#1f276c0784a5ca599bfa34b2fcc0b38f3a738e08" + integrity sha512-AC7jE4Fxpn0s3ujngClIDTiEM/CQiB2N2vkcyWWn6734AmGT03Duq6RYtPMymFobDdAtZGFZd5nR95WjPzbZAQ== + +"@next/swc-linux-x64-musl@12.1.6": + version "12.1.6" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.1.6.tgz#1d9933dd6ba303dcfd8a2acd6ac7c27ed41e2eea" + integrity sha512-c9Vjmi0EVk0Kou2qbrynskVarnFwfYIi+wKufR9Ad7/IKKuP6aEhOdZiIIdKsYWRtK2IWRF3h3YmdnEa2WLUag== + +"@next/swc-win32-arm64-msvc@12.1.6": + version "12.1.6" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.1.6.tgz#2ef9837f12ca652b1783d72ecb86208906042f02" + integrity sha512-3UTOL/5XZSKFelM7qN0it35o3Cegm6LsyuERR3/OoqEExyj3aCk7F025b54/707HTMAnjlvQK3DzLhPu/xxO4g== + +"@next/swc-win32-ia32-msvc@12.1.6": + version "12.1.6" + resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.1.6.tgz#74003d0aa1c59dfa56cb15481a5c607cbc0027b9" + integrity sha512-8ZWoj6nCq6fI1yCzKq6oK0jE6Mxlz4MrEsRyu0TwDztWQWe7rh4XXGLAa2YVPatYcHhMcUL+fQQbqd1MsgaSDA== + +"@next/swc-win32-x64-msvc@12.1.6": + version "12.1.6" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.1.6.tgz#a350caf42975e7197b24b495b8d764eec7e6a36e" + integrity sha512-4ZEwiRuZEicXhXqmhw3+de8Z4EpOLQj/gp+D9fFWo6ii6W1kBkNNvvEx4A90ugppu+74pT1lIJnOuz3A9oQeJA== + +"@nivo/annotations@0.79.1": + version "0.79.1" + resolved "https://registry.yarnpkg.com/@nivo/annotations/-/annotations-0.79.1.tgz#c1b93a1facf55e3f32e2af1b8fb0ba1bebc01910" + integrity sha512-lYso9Luu0maSDtIufwvyVt2+Wue7R9Fh3CIjuRDmNR72UjAgAVEcCar27Fy865UXGsj2hRJZ7KY/1s6kT3gu/w== + dependencies: + "@nivo/colors" "0.79.1" + "@react-spring/web" "9.3.1" + lodash "^4.17.21" + +"@nivo/arcs@0.79.1": + version "0.79.1" + resolved "https://registry.yarnpkg.com/@nivo/arcs/-/arcs-0.79.1.tgz#768d5e91356e94199377fbd0ca762bc364353414" + integrity sha512-owScoElMv5EwDbZKJhns282MnXVM4rq9jYwBnFBx872Igi2r6HwKk1m4jDWGfDktJ7MyECvuVzxRaUImWQdufA== + dependencies: + "@nivo/colors" "0.79.1" + "@react-spring/web" "9.3.1" + d3-shape "^1.3.5" + +"@nivo/axes@0.79.0": + version "0.79.0" + resolved "https://registry.yarnpkg.com/@nivo/axes/-/axes-0.79.0.tgz#6f009819b26f93a4126697152aeab5f979f1ab6c" + integrity sha512-EhSeCPxtWEuxqnifeyF/pIJEzL7pRM3rfygL+MpfT5ypu5NcXYRGQo/Bw0Vh+GF1ML+tNAE0rRvCu2jgLSdVNQ== + dependencies: + "@nivo/scales" "0.79.0" + "@react-spring/web" "9.3.1" + d3-format "^1.4.4" + d3-time-format "^3.0.0" + +"@nivo/bar@^0.79.1": + version "0.79.1" + resolved "https://registry.yarnpkg.com/@nivo/bar/-/bar-0.79.1.tgz#42d28169307e735cb84e57b4b6915195ef1c97fb" + integrity sha512-swJ2FtFeRPWJK9O6aZiqTDi2J6GrU2Z6kIHBBCXBlFmq6+vfd5AqOHytdXPTaN80JsKDBBdtY7tqRjpRPlDZwQ== + dependencies: + "@nivo/annotations" "0.79.1" + "@nivo/axes" "0.79.0" + "@nivo/colors" "0.79.1" + "@nivo/legends" "0.79.1" + "@nivo/scales" "0.79.0" + "@nivo/tooltip" "0.79.0" + "@react-spring/web" "9.3.1" + d3-scale "^3.2.3" + d3-shape "^1.2.2" + lodash "^4.17.21" + +"@nivo/colors@0.79.1": + version "0.79.1" + resolved "https://registry.yarnpkg.com/@nivo/colors/-/colors-0.79.1.tgz#0504c08b6a598bc5cb5a8b823d332a73fdc6ef43" + integrity sha512-45huBmz46OoQtfqzHrnqDJ9msebOBX84fTijyOBi8mn8iTDOK2xWgzT7cCYP3hKE58IclkibkzVyWCeJ+rUlqg== + dependencies: + d3-color "^2.0.0" + d3-scale "^3.2.3" + d3-scale-chromatic "^2.0.0" + lodash "^4.17.21" + +"@nivo/core@^0.79.0": + version "0.79.0" + resolved "https://registry.yarnpkg.com/@nivo/core/-/core-0.79.0.tgz#5755212c2058c20899990e7c8ec0e918ac00e5f5" + integrity sha512-e1iGodmGuXkF+QWAjhHVFc+lUnfBoUwaWqVcBXBfebzNc50tTJrTTMHyQczjgOIfTc8gEu23lAY4mVZCDKscig== + dependencies: + "@nivo/recompose" "0.79.0" + "@react-spring/web" "9.3.1" + d3-color "^2.0.0" + d3-format "^1.4.4" + d3-interpolate "^2.0.1" + d3-scale "^3.2.3" + d3-scale-chromatic "^2.0.0" + d3-shape "^1.3.5" + d3-time-format "^3.0.0" + lodash "^4.17.21" + +"@nivo/legends@0.79.1": + version "0.79.1" + resolved "https://registry.yarnpkg.com/@nivo/legends/-/legends-0.79.1.tgz#60b1806bba547f796e6e5b66943d65153de60c79" + integrity sha512-AoabiLherOAk3/HR/N791fONxNdwNk/gCTJC/6BKUo2nX+JngEYm3nVFmTC1R6RdjwJTeCb9Vtuc4MHA+mcgig== + +"@nivo/pie@^0.79.1": + version "0.79.1" + resolved "https://registry.yarnpkg.com/@nivo/pie/-/pie-0.79.1.tgz#4461e5273adabd0ef52bfcb54fbf6604f676d5a5" + integrity sha512-Cm8I6/nrmcpJLwziUhZ3TtwRV6K/7qWJ6alN6bUh8z7w2nScSnD/PhmAPS89p3jzSUEBPOvCViKwdvyThJ8KCg== + dependencies: + "@nivo/arcs" "0.79.1" + "@nivo/colors" "0.79.1" + "@nivo/legends" "0.79.1" + "@nivo/tooltip" "0.79.0" + d3-shape "^1.3.5" + +"@nivo/recompose@0.79.0": + version "0.79.0" + resolved "https://registry.yarnpkg.com/@nivo/recompose/-/recompose-0.79.0.tgz#c0c54ecabb2300ce672f3c3199f74629df33cc08" + integrity sha512-2GFnOHfA2jzTOA5mdKMwJ6myCRGoXQQbQvFFQ7B/+hnHfU/yrOVpiGt6TPAn3qReC4dyDYrzy1hr9UeQh677ig== + dependencies: + react-lifecycles-compat "^3.0.4" + +"@nivo/scales@0.79.0": + version "0.79.0" + resolved "https://registry.yarnpkg.com/@nivo/scales/-/scales-0.79.0.tgz#553b6910288080fbfbbe4d2aab1dd80e2d172e6e" + integrity sha512-5fAt5Wejp8yzAk6qmA3KU+celCxNYrrBhfvOi2ECDG8KQi+orbDnrO6qjVF6+ebfOn9az8ZVukcSeGA5HceiMg== + dependencies: + d3-scale "^3.2.3" + d3-time "^1.0.11" + d3-time-format "^3.0.0" + lodash "^4.17.21" + +"@nivo/tooltip@0.79.0": + version "0.79.0" + resolved "https://registry.yarnpkg.com/@nivo/tooltip/-/tooltip-0.79.0.tgz#3d46be8734e5d30e5387515db0c83bd1c795f442" + integrity sha512-hsJsvhDVR9P/QqIEDIttaA6aslR3tU9So1s/k2jMdppL7J9ZH/IrVx9TbIP7jDKmnU5AMIP5uSstXj9JiKLhQA== + dependencies: + "@react-spring/web" "9.3.1" + +"@popperjs/core@^2.11.5": + version "2.11.5" + resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.5.tgz#db5a11bf66bdab39569719555b0f76e138d7bd64" + integrity sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw== + +"@react-spring/animated@~9.3.0": + version "9.3.2" + resolved "https://registry.yarnpkg.com/@react-spring/animated/-/animated-9.3.2.tgz#bda85e92e9e9b6861c259f2dacb54270a37b0f39" + integrity sha512-pBvKydRHbTzuyaeHtxGIOvnskZxGo/S5/YK1rtYm88b9NQZuZa95Rgd3O0muFL+99nvBMBL8cvQGD0UJmsqQsg== + dependencies: + "@react-spring/shared" "~9.3.0" + "@react-spring/types" "~9.3.0" + +"@react-spring/core@~9.3.0": + version "9.3.2" + resolved "https://registry.yarnpkg.com/@react-spring/core/-/core-9.3.2.tgz#d1dc5810666ac18550db89c58567f28fbe04fb07" + integrity sha512-kMRjkgdQ6LJ0lmb/wQlONpghaMT83UxglXHJC6m9kZS/GKVmN//TYMEK85xN1rC5Gg+BmjG61DtLCSkkLDTfNw== + dependencies: + "@react-spring/animated" "~9.3.0" + "@react-spring/shared" "~9.3.0" + "@react-spring/types" "~9.3.0" + +"@react-spring/rafz@~9.3.0": + version "9.3.2" + resolved "https://registry.yarnpkg.com/@react-spring/rafz/-/rafz-9.3.2.tgz#0cbd296cd17bbf1e7e49d3b3616884e026d5fb67" + integrity sha512-YtqNnAYp5bl6NdnDOD5TcYS40VJmB+Civ4LPtcWuRPKDAOa/XAf3nep48r0wPTmkK936mpX8aIm7h+luW59u5A== + +"@react-spring/shared@~9.3.0": + version "9.3.2" + resolved "https://registry.yarnpkg.com/@react-spring/shared/-/shared-9.3.2.tgz#967ce1d8a16d820a99e6eeb2a8f7ca9311d9dfa0" + integrity sha512-ypGQQ8w7mWnrELLon4h6mBCBxdd8j1pgLzmHXLpTC/f4ya2wdP+0WIKBWXJymIf+5NiTsXgSJra5SnHP5FBY+A== + dependencies: + "@react-spring/rafz" "~9.3.0" + "@react-spring/types" "~9.3.0" + +"@react-spring/types@~9.3.0": + version "9.3.2" + resolved "https://registry.yarnpkg.com/@react-spring/types/-/types-9.3.2.tgz#0277d436e50d7a824897dd7bb880f4842fbcd0fe" + integrity sha512-u+IK9z9Re4hjNkBYKebZr7xVDYTai2RNBsI4UPL/k0B6lCNSwuqWIXfKZUDVlMOeZHtDqayJn4xz6HcSkTj3FQ== + +"@react-spring/web@9.3.1": + version "9.3.1" + resolved "https://registry.yarnpkg.com/@react-spring/web/-/web-9.3.1.tgz#5b377ba7ad52e746c2b59e2738c021de3f219d0b" + integrity sha512-sisZIgFGva/Z+xKWPSfXpukF0AP3kR9ALTxlHL87fVotMUCJX5vtH/YlVcywToEFwTHKt3MpI5Wy2M+vgVEeaw== + dependencies: + "@react-spring/animated" "~9.3.0" + "@react-spring/core" "~9.3.0" + "@react-spring/shared" "~9.3.0" + "@react-spring/types" "~9.3.0" + +"@types/node@^18.0.0": + version "18.0.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.0.0.tgz#67c7b724e1bcdd7a8821ce0d5ee184d3b4dd525a" + integrity sha512-cHlGmko4gWLVI27cGJntjs/Sj8th9aYwplmZFwmmgYQQvL5NUsgVJG7OddLvNfLqYS31KFN0s3qlaD9qCaxACA== + +"@types/parse-json@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" + integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== + +"@types/prop-types@*", "@types/prop-types@^15.7.5": + version "15.7.5" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf" + integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w== + +"@types/react-is@^16.7.1 || ^17.0.0": + version "17.0.3" + resolved "https://registry.yarnpkg.com/@types/react-is/-/react-is-17.0.3.tgz#2d855ba575f2fc8d17ef9861f084acc4b90a137a" + integrity sha512-aBTIWg1emtu95bLTLx0cpkxwGW3ueZv71nE2YFBpL8k/z5czEW8yYpOo8Dp+UUAFAtKwNaOsh/ioSeQnWlZcfw== + dependencies: + "@types/react" "*" + +"@types/react-transition-group@^4.4.4": + version "4.4.4" + resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.4.tgz#acd4cceaa2be6b757db61ed7b432e103242d163e" + integrity sha512-7gAPz7anVK5xzbeQW9wFBDg7G++aPLAFY0QaSMOou9rJZpbuI58WAuJrgu+qR92l61grlnCUe7AFX8KGahAgug== + dependencies: + "@types/react" "*" + +"@types/react@*", "@types/react@^18.0.14": + version "18.0.14" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.14.tgz#e016616ffff51dba01b04945610fe3671fdbe06d" + integrity sha512-x4gGuASSiWmo0xjDLpm5mPb52syZHJx02VKbqUKdLmKtAwIh63XClGsiTI1K6DO5q7ox4xAsQrU+Gl3+gGXF9Q== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + +"@types/scheduler@*": + version "0.16.2" + resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" + integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== + +"@uniswap/token-lists@^1.0.0-beta.27": + version "1.0.0-beta.30" + resolved "https://registry.yarnpkg.com/@uniswap/token-lists/-/token-lists-1.0.0-beta.30.tgz#2103ca23b8007c59ec71718d34cdc97861c409e5" + integrity sha512-HwY2VvkQ8lNR6ks5NqQfAtg+4IZqz3KV1T8d2DlI8emIn9uMmaoFbIOg0nzjqAVKKnZSbMTRRtUoAh6mmjRvog== + +"@usedapp/core@1.0.9": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@usedapp/core/-/core-1.0.9.tgz#f0f08d75be420d9377b3853a4aa99b4e99761cc3" + integrity sha512-vGugFfm55R99mwuJXh1enpiOgDSWOZ2akZ8E2nFJhXzqK6WlTkP7zZuKatlde10X7dLbVC2FTCx3ZhrtLWilIA== + dependencies: + "@metamask/detect-provider" "^1.2.0" + "@uniswap/token-lists" "^1.0.0-beta.27" + fetch-mock "^9.11.0" + lodash.merge "^4.6.2" + lodash.pickby "^4.6.0" + nanoid "3.1.22" + +aes-js@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-3.0.0.tgz#e21df10ad6c2053295bcbb8dab40b09dbea87e4d" + integrity sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +axios@^0.27.2: + version "0.27.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972" + integrity sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ== + dependencies: + follow-redirects "^1.14.9" + form-data "^4.0.0" + +babel-plugin-macros@^2.6.1: + version "2.8.0" + resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz#0f958a7cc6556b1e65344465d99111a1e5e10138" + integrity sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg== + dependencies: + "@babel/runtime" "^7.7.2" + cosmiconfig "^6.0.0" + resolve "^1.12.0" + +bech32@1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/bech32/-/bech32-1.1.4.tgz#e38c9f37bf179b8eb16ae3a772b40c356d4832e9" + integrity sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ== + +bn.js@^4.11.9: + version "4.12.0" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" + integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== + +bn.js@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" + integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== + +brorand@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" + integrity sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w== + +browserslist@^4.20.2: + version "4.20.4" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.20.4.tgz#98096c9042af689ee1e0271333dbc564b8ce4477" + integrity sha512-ok1d+1WpnU24XYN7oC3QWgTyMhY/avPJ/r9T00xxvUOIparA/gc+UPUMaod3i+G6s+nI2nUb9xZ5k794uIwShw== + dependencies: + caniuse-lite "^1.0.30001349" + electron-to-chromium "^1.4.147" + escalade "^3.1.1" + node-releases "^2.0.5" + picocolors "^1.0.0" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +caniuse-lite@^1.0.30001332, caniuse-lite@^1.0.30001349: + version "1.0.30001357" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001357.tgz#dec7fc4158ef6ad24690d0eec7b91f32b8cb1b5d" + integrity sha512-b+KbWHdHePp+ZpNj+RDHFChZmuN+J5EvuQUlee9jOQIUAdhv9uvAZeEtUeLAknXbkiu1uxjQ9NLp1ie894CuWg== + +chalk@^2.0.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +classnames@^2.2.6: + version "2.3.1" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e" + integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA== + +clsx@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188" + integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA== + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +convert-source-map@^1.5.0, convert-source-map@^1.7.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369" + integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA== + dependencies: + safe-buffer "~5.1.1" + +core-js@^3.0.0: + version "3.23.2" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.23.2.tgz#e07a60ca8b14dd129cabdc3d2551baf5a01c76f0" + integrity sha512-ELJOWxNrJfOH/WK4VJ3Qd+fOqZuOuDNDJz0xG6Bt4mGg2eO/UT9CljCrbqDGovjLKUrGajEEBcoTOc0w+yBYeQ== + +cosmiconfig@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982" + integrity sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.1.0" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.7.2" + +css-vendor@^2.0.8: + version "2.0.8" + resolved "https://registry.yarnpkg.com/css-vendor/-/css-vendor-2.0.8.tgz#e47f91d3bd3117d49180a3c935e62e3d9f7f449d" + integrity sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ== + dependencies: + "@babel/runtime" "^7.8.3" + is-in-browser "^1.0.2" + +csstype@^3.0.2, csstype@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.0.tgz#4ddcac3718d787cf9df0d1b7d15033925c8f29f2" + integrity sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA== + +d3-array@2, d3-array@^2.3.0: + version "2.12.1" + resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-2.12.1.tgz#e20b41aafcdffdf5d50928004ececf815a465e81" + integrity sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ== + dependencies: + internmap "^1.0.0" + +"d3-color@1 - 2", d3-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-2.0.0.tgz#8d625cab42ed9b8f601a1760a389f7ea9189d62e" + integrity sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ== + +"d3-format@1 - 2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-2.0.0.tgz#a10bcc0f986c372b729ba447382413aabf5b0767" + integrity sha512-Ab3S6XuE/Q+flY96HXT0jOXcM4EAClYFnRGY5zsjRGNy6qCYrQsMffs7cV5Q9xejb35zxW5hf/guKw34kvIKsA== + +d3-format@^1.4.4: + version "1.4.5" + resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.4.5.tgz#374f2ba1320e3717eb74a9356c67daee17a7edb4" + integrity sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ== + +"d3-interpolate@1 - 2", "d3-interpolate@1.2.0 - 2", d3-interpolate@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-2.0.1.tgz#98be499cfb8a3b94d4ff616900501a64abc91163" + integrity sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ== + dependencies: + d3-color "1 - 2" + +d3-path@1: + version "1.0.9" + resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.9.tgz#48c050bb1fe8c262493a8caf5524e3e9591701cf" + integrity sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg== + +d3-scale-chromatic@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-2.0.0.tgz#c13f3af86685ff91323dc2f0ebd2dabbd72d8bab" + integrity sha512-LLqy7dJSL8yDy7NRmf6xSlsFZ6zYvJ4BcWFE4zBrOPnQERv9zj24ohnXKRbyi9YHnYV+HN1oEO3iFK971/gkzA== + dependencies: + d3-color "1 - 2" + d3-interpolate "1 - 2" + +d3-scale@^3.2.3: + version "3.3.0" + resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-3.3.0.tgz#28c600b29f47e5b9cd2df9749c206727966203f3" + integrity sha512-1JGp44NQCt5d1g+Yy+GeOnZP7xHo0ii8zsQp6PGzd+C1/dl0KGsp9A7Mxwp+1D1o4unbTTxVdU/ZOIEBoeZPbQ== + dependencies: + d3-array "^2.3.0" + d3-format "1 - 2" + d3-interpolate "1.2.0 - 2" + d3-time "^2.1.1" + d3-time-format "2 - 3" + +d3-shape@^1.2.2, d3-shape@^1.3.5: + version "1.3.7" + resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.3.7.tgz#df63801be07bc986bc54f63789b4fe502992b5d7" + integrity sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw== + dependencies: + d3-path "1" + +"d3-time-format@2 - 3", d3-time-format@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-3.0.0.tgz#df8056c83659e01f20ac5da5fdeae7c08d5f1bb6" + integrity sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag== + dependencies: + d3-time "1 - 2" + +"d3-time@1 - 2", d3-time@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-2.1.1.tgz#e9d8a8a88691f4548e68ca085e5ff956724a6682" + integrity sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ== + dependencies: + d3-array "2" + +d3-time@^1.0.11: + version "1.1.0" + resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.1.0.tgz#b1e19d307dae9c900b7e5b25ffc5dcc249a8a0f1" + integrity sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA== + +debug@^4.1.0, debug@^4.1.1: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +dom-helpers@^5.0.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" + integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== + dependencies: + "@babel/runtime" "^7.8.7" + csstype "^3.0.2" + +electron-to-chromium@^1.4.147: + version "1.4.162" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.162.tgz#69f8b900477208544a6e2a6e9bd3dc9e73163ed8" + integrity sha512-JrMk3tR2rnBojfAipp9nGh/vcWyBHeNsAVBqehtk4vq0o1bE4sVw19ICeidNx3u0i2yg4X8BvyUIM/yo2vO9aA== + +elliptic@6.5.4: + version "6.5.4" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" + integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ== + dependencies: + bn.js "^4.11.9" + brorand "^1.1.0" + hash.js "^1.0.0" + hmac-drbg "^1.0.1" + inherits "^2.0.4" + minimalistic-assert "^1.0.1" + minimalistic-crypto-utils "^1.0.1" + +enhanced-resolve@^5.7.0: + version "5.9.3" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.9.3.tgz#44a342c012cbc473254af5cc6ae20ebd0aae5d88" + integrity sha512-Bq9VSor+kjvW3f9/MiiR4eE3XYgOl7/rS8lnSxbRbF3kS0B2r+Y9w5krBWxZgDxASVZbdYrn5wT4j/Wb0J9qow== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +ethers@^5.6.9: + version "5.6.9" + resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.6.9.tgz#4e12f8dfcb67b88ae7a78a9519b384c23c576a4d" + integrity sha512-lMGC2zv9HC5EC+8r429WaWu3uWJUCgUCt8xxKCFqkrFuBDZXDYIdzDUECxzjf2BMF8IVBByY1EBoGSL3RTm8RA== + dependencies: + "@ethersproject/abi" "5.6.4" + "@ethersproject/abstract-provider" "5.6.1" + "@ethersproject/abstract-signer" "5.6.2" + "@ethersproject/address" "5.6.1" + "@ethersproject/base64" "5.6.1" + "@ethersproject/basex" "5.6.1" + "@ethersproject/bignumber" "5.6.2" + "@ethersproject/bytes" "5.6.1" + "@ethersproject/constants" "5.6.1" + "@ethersproject/contracts" "5.6.2" + "@ethersproject/hash" "5.6.1" + "@ethersproject/hdnode" "5.6.2" + "@ethersproject/json-wallets" "5.6.1" + "@ethersproject/keccak256" "5.6.1" + "@ethersproject/logger" "5.6.0" + "@ethersproject/networks" "5.6.4" + "@ethersproject/pbkdf2" "5.6.1" + "@ethersproject/properties" "5.6.0" + "@ethersproject/providers" "5.6.8" + "@ethersproject/random" "5.6.1" + "@ethersproject/rlp" "5.6.1" + "@ethersproject/sha2" "5.6.1" + "@ethersproject/signing-key" "5.6.2" + "@ethersproject/solidity" "5.6.1" + "@ethersproject/strings" "5.6.1" + "@ethersproject/transactions" "5.6.2" + "@ethersproject/units" "5.6.1" + "@ethersproject/wallet" "5.6.2" + "@ethersproject/web" "5.6.1" + "@ethersproject/wordlists" "5.6.1" + +fetch-mock@^9.11.0: + version "9.11.0" + resolved "https://registry.yarnpkg.com/fetch-mock/-/fetch-mock-9.11.0.tgz#371c6fb7d45584d2ae4a18ee6824e7ad4b637a3f" + integrity sha512-PG1XUv+x7iag5p/iNHD4/jdpxL9FtVSqRMUQhPab4hVDt80T1MH5ehzVrL2IdXO9Q2iBggArFvPqjUbHFuI58Q== + dependencies: + "@babel/core" "^7.0.0" + "@babel/runtime" "^7.0.0" + core-js "^3.0.0" + debug "^4.1.1" + glob-to-regexp "^0.4.0" + is-subset "^0.1.1" + lodash.isequal "^4.5.0" + path-to-regexp "^2.2.1" + querystring "^0.2.0" + whatwg-url "^6.5.0" + +find-root@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" + integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng== + +follow-redirects@^1.14.9: + version "1.15.1" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5" + integrity sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA== + +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +glob-to-regexp@^0.4.0: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +graceful-fs@^4.2.4: + version "4.2.10" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" + integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +hash.js@1.1.7, hash.js@^1.0.0, hash.js@^1.0.3: + version "1.1.7" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" + integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== + dependencies: + inherits "^2.0.3" + minimalistic-assert "^1.0.1" + +hmac-drbg@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" + integrity sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg== + dependencies: + hash.js "^1.0.3" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.1" + +hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== + dependencies: + react-is "^16.7.0" + +hyphenate-style-name@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz#691879af8e220aea5750e8827db4ef62a54e361d" + integrity sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ== + +import-fresh@^3.1.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +inherits@^2.0.3, inherits@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +internmap@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/internmap/-/internmap-1.0.1.tgz#0017cc8a3b99605f0302f2b198d272e015e5df95" + integrity sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw== + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== + +is-core-module@^2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.9.0.tgz#e1c34429cd51c6dd9e09e0799e396e27b19a9c69" + integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A== + dependencies: + has "^1.0.3" + +is-in-browser@^1.0.2, is-in-browser@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/is-in-browser/-/is-in-browser-1.1.3.tgz#56ff4db683a078c6082eb95dad7dc62e1d04f835" + integrity sha512-FeXIBgG/CPGd/WUxuEyvgGTEfwiG9Z4EKGxjNMRqviiIIfsmgrpnHLffEDdwUHqNva1VEW91o3xBT/m8Elgl9g== + +is-subset@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-subset/-/is-subset-0.1.1.tgz#8a59117d932de1de00f245fcdd39ce43f1e939a6" + integrity sha512-6Ybun0IkarhmEqxXCNw/C0bna6Zb/TkfUX9UbwJtK6ObwAVCxmAP308WWTHviM/zAqXk05cdhYsUsZeGQh99iw== + +isomorphic-fetch@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz#0267b005049046d2421207215d45d6a262b8b8b4" + integrity sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA== + dependencies: + node-fetch "^2.6.1" + whatwg-fetch "^3.4.1" + +js-sha3@0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840" + integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q== + +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json5@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" + integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== + +jss-plugin-camel-case@^10.8.2: + version "10.9.0" + resolved "https://registry.yarnpkg.com/jss-plugin-camel-case/-/jss-plugin-camel-case-10.9.0.tgz#4921b568b38d893f39736ee8c4c5f1c64670aaf7" + integrity sha512-UH6uPpnDk413/r/2Olmw4+y54yEF2lRIV8XIZyuYpgPYTITLlPOsq6XB9qeqv+75SQSg3KLocq5jUBXW8qWWww== + dependencies: + "@babel/runtime" "^7.3.1" + hyphenate-style-name "^1.0.3" + jss "10.9.0" + +jss-plugin-default-unit@^10.8.2: + version "10.9.0" + resolved "https://registry.yarnpkg.com/jss-plugin-default-unit/-/jss-plugin-default-unit-10.9.0.tgz#bb23a48f075bc0ce852b4b4d3f7582bc002df991" + integrity sha512-7Ju4Q9wJ/MZPsxfu4T84mzdn7pLHWeqoGd/D8O3eDNNJ93Xc8PxnLmV8s8ZPNRYkLdxZqKtm1nPQ0BM4JRlq2w== + dependencies: + "@babel/runtime" "^7.3.1" + jss "10.9.0" + +jss-plugin-global@^10.8.2: + version "10.9.0" + resolved "https://registry.yarnpkg.com/jss-plugin-global/-/jss-plugin-global-10.9.0.tgz#fc07a0086ac97aca174e37edb480b69277f3931f" + integrity sha512-4G8PHNJ0x6nwAFsEzcuVDiBlyMsj2y3VjmFAx/uHk/R/gzJV+yRHICjT4MKGGu1cJq2hfowFWCyrr/Gg37FbgQ== + dependencies: + "@babel/runtime" "^7.3.1" + jss "10.9.0" + +jss-plugin-nested@^10.8.2: + version "10.9.0" + resolved "https://registry.yarnpkg.com/jss-plugin-nested/-/jss-plugin-nested-10.9.0.tgz#cc1c7d63ad542c3ccc6e2c66c8328c6b6b00f4b3" + integrity sha512-2UJnDrfCZpMYcpPYR16oZB7VAC6b/1QLsRiAutOt7wJaaqwCBvNsosLEu/fUyKNQNGdvg2PPJFDO5AX7dwxtoA== + dependencies: + "@babel/runtime" "^7.3.1" + jss "10.9.0" + tiny-warning "^1.0.2" + +jss-plugin-props-sort@^10.8.2: + version "10.9.0" + resolved "https://registry.yarnpkg.com/jss-plugin-props-sort/-/jss-plugin-props-sort-10.9.0.tgz#30e9567ef9479043feb6e5e59db09b4de687c47d" + integrity sha512-7A76HI8bzwqrsMOJTWKx/uD5v+U8piLnp5bvru7g/3ZEQOu1+PjHvv7bFdNO3DwNPC9oM0a//KwIJsIcDCjDzw== + dependencies: + "@babel/runtime" "^7.3.1" + jss "10.9.0" + +jss-plugin-rule-value-function@^10.8.2: + version "10.9.0" + resolved "https://registry.yarnpkg.com/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.9.0.tgz#379fd2732c0746fe45168011fe25544c1a295d67" + integrity sha512-IHJv6YrEf8pRzkY207cPmdbBstBaE+z8pazhPShfz0tZSDtRdQua5jjg6NMz3IbTasVx9FdnmptxPqSWL5tyJg== + dependencies: + "@babel/runtime" "^7.3.1" + jss "10.9.0" + tiny-warning "^1.0.2" + +jss-plugin-vendor-prefixer@^10.8.2: + version "10.9.0" + resolved "https://registry.yarnpkg.com/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.9.0.tgz#aa9df98abfb3f75f7ed59a3ec50a5452461a206a" + integrity sha512-MbvsaXP7iiVdYVSEoi+blrW+AYnTDvHTW6I6zqi7JcwXdc6I9Kbm234nEblayhF38EftoenbM+5218pidmC5gA== + dependencies: + "@babel/runtime" "^7.3.1" + css-vendor "^2.0.8" + jss "10.9.0" + +jss@10.9.0, jss@^10.8.2: + version "10.9.0" + resolved "https://registry.yarnpkg.com/jss/-/jss-10.9.0.tgz#7583ee2cdc904a83c872ba695d1baab4b59c141b" + integrity sha512-YpzpreB6kUunQBbrlArlsMpXYyndt9JATbt95tajx0t4MTJJcCJdd4hdNpHmOIDiUJrF/oX5wtVFrS3uofWfGw== + dependencies: + "@babel/runtime" "^7.3.1" + csstype "^3.0.2" + is-in-browser "^1.1.3" + tiny-warning "^1.0.2" + +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + +lodash.isequal@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ== + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +lodash.pickby@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.pickby/-/lodash.pickby-4.6.0.tgz#7dea21d8c18d7703a27c704c15d3b84a67e33aff" + integrity sha512-AZV+GsS/6ckvPOVQPXSiFFacKvKB4kOQu6ynt9wz0F3LO4R9Ij4K1ddYsIytDpSgLz88JHd9P+oaLeej5/Sl7Q== + +lodash.sortby@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" + integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA== + +lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +loose-envify@^1.1.0, loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + +minimalistic-crypto-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" + integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +nanoid@3.1.22: + version "3.1.22" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.22.tgz#b35f8fb7d151990a8aebd5aa5015c03cf726f844" + integrity sha512-/2ZUaJX2ANuLtTvqTlgqBQNJoQO398KyJgZloL0PZkC0dpysjncRUPsFe3DUPzz/y3h+u7C46np8RMuvF3jsSQ== + +nanoid@^3.1.30: + version "3.3.4" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" + integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== + +next-transpile-modules@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/next-transpile-modules/-/next-transpile-modules-9.0.0.tgz#133b1742af082e61cc76b02a0f12ffd40ce2bf90" + integrity sha512-VCNFOazIAnXn1hvgYYSTYMnoWgKgwlYh4lm1pKbSfiB3kj5ZYLcKVhfh3jkPOg1cnd9DP+pte9yCUocdPEUBTQ== + dependencies: + enhanced-resolve "^5.7.0" + escalade "^3.1.1" + +next@12: + version "12.1.6" + resolved "https://registry.yarnpkg.com/next/-/next-12.1.6.tgz#eb205e64af1998651f96f9df44556d47d8bbc533" + integrity sha512-cebwKxL3/DhNKfg9tPZDQmbRKjueqykHHbgaoG4VBRH3AHQJ2HO0dbKFiS1hPhe1/qgc2d/hFeadsbPicmLD+A== + dependencies: + "@next/env" "12.1.6" + caniuse-lite "^1.0.30001332" + postcss "8.4.5" + styled-jsx "5.0.2" + optionalDependencies: + "@next/swc-android-arm-eabi" "12.1.6" + "@next/swc-android-arm64" "12.1.6" + "@next/swc-darwin-arm64" "12.1.6" + "@next/swc-darwin-x64" "12.1.6" + "@next/swc-linux-arm-gnueabihf" "12.1.6" + "@next/swc-linux-arm64-gnu" "12.1.6" + "@next/swc-linux-arm64-musl" "12.1.6" + "@next/swc-linux-x64-gnu" "12.1.6" + "@next/swc-linux-x64-musl" "12.1.6" + "@next/swc-win32-arm64-msvc" "12.1.6" + "@next/swc-win32-ia32-msvc" "12.1.6" + "@next/swc-win32-x64-msvc" "12.1.6" + +node-fetch@^2.6.1: + version "2.6.7" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" + integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== + dependencies: + whatwg-url "^5.0.0" + +node-releases@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.5.tgz#280ed5bc3eba0d96ce44897d8aee478bfb3d9666" + integrity sha512-U9h1NLROZTq9uE1SNffn6WuPDg8icmi3ns4rEl/oTfIle4iLjTliCzgTsbaIFMq/Xn078/lfY/BL0GWZ+psK4Q== + +object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-json@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-to-regexp@^2.2.1: + version "2.4.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-2.4.0.tgz#35ce7f333d5616f1c1e1bfe266c3aba2e5b2e704" + integrity sha512-G6zHoVqC6GGTQkZwF4lkuEyMbVOjoBKAEybQUypI1WTkqinCOrq2x6U2+phkJ1XsEMTy4LjtwPI7HW+NVrRR2w== + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +postcss@8.4.5: + version "8.4.5" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.5.tgz#bae665764dfd4c6fcc24dc0fdf7e7aa00cc77f95" + integrity sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg== + dependencies: + nanoid "^3.1.30" + picocolors "^1.0.0" + source-map-js "^1.0.1" + +prettier@^2.6.2: + version "2.7.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.7.1.tgz#e235806850d057f97bb08368a4f7d899f7760c64" + integrity sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g== + +prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: + version "15.8.1" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + +punycode@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +qrcode.react@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/qrcode.react/-/qrcode.react-3.0.2.tgz#7ceaea165aa7066253ef670a25bf238eaec4eb9e" + integrity sha512-8F3SGxSkNb3fMIHdlseqjFjLbsPrF3WvF/1MOboSUUHytT537W8f/FtbdA3XFIHDrc+TrRBjTI/QLmwhAIGWWw== + +querystring@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.1.tgz#40d77615bb09d16902a85c3e38aa8b5ed761c2dd" + integrity sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg== + +react-dom@^18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" + integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== + dependencies: + loose-envify "^1.1.0" + scheduler "^0.23.0" + +react-is@^16.13.1, react-is@^16.7.0: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + +react-is@^17.0.2: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" + integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== + +react-lifecycles-compat@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" + integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== + +react-number-format@^4.4.4: + version "4.9.3" + resolved "https://registry.yarnpkg.com/react-number-format/-/react-number-format-4.9.3.tgz#338500fe9c61b1ac73c8d6dff4ec97dd13fd2b50" + integrity sha512-am1A1xYAbENuKJ+zpM7V+B1oRTSeOHYltqVKExznIVFweBzhLmOBmyb1DfIKjHo90E0bo1p3nzVJ2NgS5xh+sQ== + dependencies: + prop-types "^15.7.2" + +react-transition-group@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.2.tgz#8b59a56f09ced7b55cbd53c36768b922890d5470" + integrity sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg== + dependencies: + "@babel/runtime" "^7.5.5" + dom-helpers "^5.0.1" + loose-envify "^1.4.0" + prop-types "^15.6.2" + +react@^18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" + integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== + dependencies: + loose-envify "^1.1.0" + +regenerator-runtime@^0.13.4: + version "0.13.9" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" + integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== + +reselect@^4.1.5: + version "4.1.6" + resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.1.6.tgz#19ca2d3d0b35373a74dc1c98692cdaffb6602656" + integrity sha512-ZovIuXqto7elwnxyXbBtCPo9YFEr3uJqj2rRbcOOog1bmu2Ag85M4hixSwFWyaBMKXNgvPaJ9OSu9SkBPIeJHQ== + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve@^1.12.0: + version "1.22.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" + integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== + dependencies: + is-core-module "^2.9.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +rifm@^0.12.1: + version "0.12.1" + resolved "https://registry.yarnpkg.com/rifm/-/rifm-0.12.1.tgz#8fa77f45b7f1cda2a0068787ac821f0593967ac4" + integrity sha512-OGA1Bitg/dSJtI/c4dh90svzaUPt228kzFsUkJbtA2c964IqEAwWXeL9ZJi86xWv3j5SMqRvGULl7bA6cK0Bvg== + +safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +scheduler@^0.23.0: + version "0.23.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" + integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw== + dependencies: + loose-envify "^1.1.0" + +scrypt-js@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/scrypt-js/-/scrypt-js-3.0.1.tgz#d314a57c2aef69d1ad98a138a21fe9eafa9ee312" + integrity sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA== + +semver@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +source-map-js@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" + integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== + +source-map@^0.5.7: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== + +styled-jsx@5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.0.2.tgz#ff230fd593b737e9e68b630a694d460425478729" + integrity sha512-LqPQrbBh3egD57NBcHET4qcgshPks+yblyhPlH2GY8oaDgKs8SK4C3dBh3oSJjgzJ3G5t1SYEZGHkP+QEpX9EQ== + +stylis@4.0.13: + version "4.0.13" + resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.0.13.tgz#f5db332e376d13cc84ecfe5dace9a2a51d954c91" + integrity sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag== + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +tapable@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + +tiny-warning@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" + integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== + +tr46@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" + integrity sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA== + dependencies: + punycode "^2.1.0" + +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + +typescript@^4.1.3: + version "4.7.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235" + integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== + +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + +webidl-conversions@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" + integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== + +whatwg-fetch@^3.4.1: + version "3.6.2" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz#dced24f37f2624ed0281725d51d0e2e3fe677f8c" + integrity sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA== + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + +whatwg-url@^6.5.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8" + integrity sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ== + dependencies: + lodash.sortby "^4.7.0" + tr46 "^1.0.1" + webidl-conversions "^4.0.2" + +ws@7.4.6: + version "7.4.6" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" + integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== + +yaml@^1.7.2: + version "1.10.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" + integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== diff --git a/assets/screenshots/backtest-report.jpg b/assets/screenshots/backtest-report.jpg new file mode 100644 index 0000000000..adeeca0720 Binary files /dev/null and b/assets/screenshots/backtest-report.jpg differ diff --git a/charts/bbgo/templates/deployment.yaml b/charts/bbgo/templates/deployment.yaml index 821dc7cf7b..c62d2e4160 100644 --- a/charts/bbgo/templates/deployment.yaml +++ b/charts/bbgo/templates/deployment.yaml @@ -49,6 +49,14 @@ spec: {{- if .Values.webserver.enabled }} - "--enable-webserver" {{- end }} + {{- if .Values.grpc.enabled }} + - "--enable-grpc" + - "--grpc-bind" + - {{ printf ":%d" (.Values.grpc.port | int) | default ":50051" | quote }} + {{- end }} + {{- if .Values.debug.enabled }} + - "--debug" + {{- end }} ports: {{- if .Values.webserver.enabled }} @@ -56,6 +64,11 @@ spec: containerPort: 8080 protocol: TCP {{- end }} + {{- if .Values.grpc.enabled }} + - name: grpc + containerPort: {{ .Values.grpc.port | default 50051 }} + protocol: TCP + {{- end }} {{- if .Values.metrics.enabled }} - name: metrics containerPort: 9090 diff --git a/charts/bbgo/templates/service.yaml b/charts/bbgo/templates/service.yaml index 154fc73c3d..ce4243a7d7 100644 --- a/charts/bbgo/templates/service.yaml +++ b/charts/bbgo/templates/service.yaml @@ -11,5 +11,11 @@ spec: targetPort: http protocol: TCP name: http + {{- if .Values.grpc.enabled }} + - port: {{ .Values.grpc.port | default 50051 }} + targetPort: grpc + protocol: TCP + name: grpc + {{- end }} selector: {{- include "bbgo.selectorLabels" . | nindent 4 }} diff --git a/charts/bbgo/values.yaml b/charts/bbgo/values.yaml index b749c95273..179c317773 100644 --- a/charts/bbgo/values.yaml +++ b/charts/bbgo/values.yaml @@ -74,6 +74,13 @@ metrics: enabled: false port: 9090 +grpc: + enabled: false + port: 50051 + +debug: + enabled: false + resources: # We usually recommend not to specify default resources and to leave this as a conscious # choice for the user. This also increases chances charts run on environments with little diff --git a/cmd/bbgo-lorca/main.go b/cmd/bbgo-lorca/main.go index 3fa818dc36..b44a225d8a 100644 --- a/cmd/bbgo-lorca/main.go +++ b/cmd/bbgo-lorca/main.go @@ -15,7 +15,6 @@ import ( log "github.com/sirupsen/logrus" "github.com/c9s/bbgo/pkg/bbgo" - "github.com/c9s/bbgo/pkg/cmd" "github.com/c9s/bbgo/pkg/server" ) @@ -92,7 +91,7 @@ func main() { // we could initialize the environment from the settings if setup == nil { - if err := cmd.BootstrapEnvironment(ctx, environ, userConfig); err != nil { + if err := bbgo.BootstrapEnvironment(ctx, environ, userConfig); err != nil { log.WithError(err).Error("failed to bootstrap environment") return } @@ -102,6 +101,11 @@ func main() { return } + if err := trader.LoadState(); err != nil { + log.WithError(err).Error("failed to load strategy states") + return + } + // for setup mode, we don't start the trader go func() { if err := trader.Run(ctx); err != nil { diff --git a/cmd/bbgo-webview/main.go b/cmd/bbgo-webview/main.go index c3b1f27d7e..3402370903 100644 --- a/cmd/bbgo-webview/main.go +++ b/cmd/bbgo-webview/main.go @@ -16,7 +16,6 @@ import ( log "github.com/sirupsen/logrus" "github.com/c9s/bbgo/pkg/bbgo" - "github.com/c9s/bbgo/pkg/cmd" "github.com/c9s/bbgo/pkg/server" ) @@ -93,7 +92,7 @@ func main() { // we could initialize the environment from the settings if setup == nil { - if err := cmd.BootstrapEnvironment(ctx, environ, userConfig); err != nil { + if err := bbgo.BootstrapEnvironment(ctx, environ, userConfig); err != nil { log.WithError(err).Error("failed to bootstrap environment") return } @@ -110,6 +109,11 @@ func main() { return } + if err := trader.LoadState(); err != nil { + log.WithError(err).Error("failed to load strategy states") + return + } + // for setup mode, we don't start the trader if err := trader.Run(ctx); err != nil { log.WithError(err).Error("failed to start trader") @@ -118,7 +122,7 @@ func main() { } // find a free port for binding the server - ln, err := net.Listen("tcp", "127.0.0.1:" + strconv.Itoa(portNum)) + ln, err := net.Listen("tcp", "127.0.0.1:"+strconv.Itoa(portNum)) if err != nil { log.WithError(err).Error("can not bind listener") return diff --git a/cmd/update-doc/main.go b/cmd/update-doc/main.go index ae4fcdfcf3..214ff608b0 100644 --- a/cmd/update-doc/main.go +++ b/cmd/update-doc/main.go @@ -1,12 +1,12 @@ package main import ( + "fmt" "github.com/c9s/bbgo/pkg/cmd" "github.com/spf13/cobra/doc" + "log" "path" "runtime" - "fmt" - "log" ) func main() { diff --git a/codecov.yaml b/codecov.yaml new file mode 100644 index 0000000000..fa52faad4a --- /dev/null +++ b/codecov.yaml @@ -0,0 +1,21 @@ +codecov: + require_ci_to_pass: true +comment: + behavior: default + layout: reach,diff,flags,files,footer + require_changes: false +coverage: + precision: 2 + range: + - 18.0 + - 100.0 + round: down +github_checks: + annotations: false +parsers: + gcov: + branch_detection: + conditional: true + loop: true + macro: false + method: false diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000000..c4a76f25a1 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,23 @@ +codecov: + require_ci_to_pass: true + +comment: + behavior: default + layout: "reach,diff,flags,files,footer" + require_changes: no + +parsers: + gcov: + branch_detection: + conditional: yes + loop: yes + method: no + macro: no + +coverage: + precision: 2 + round: down + range: "70...100" + +github_checks: + annotations: false diff --git a/config/audacitymaker.yaml b/config/audacitymaker.yaml new file mode 100644 index 0000000000..36ecaa8870 --- /dev/null +++ b/config/audacitymaker.yaml @@ -0,0 +1,21 @@ +persistence: + json: + directory: var/data + redis: + host: 127.0.0.1 + port: 6379 + db: 0 + +sessions: + binance: + exchange: binance + envVarPrefix: binance +# futures: true + +exchangeStrategies: +- on: binance + audacitymaker: + symbol: ETHBUSD + orderFlow: + interval: 1m + quantity: 0.01 diff --git a/config/backtest.yaml b/config/backtest.yaml new file mode 100644 index 0000000000..d41388b290 --- /dev/null +++ b/config/backtest.yaml @@ -0,0 +1,23 @@ +--- +backtest: + startTime: "2022-01-01" + endTime: "2022-01-02" + symbols: + - BTCUSDT + sessions: + - binance + - ftx + - max + - kucoin + - okex + +exchangeStrategies: +- on: binance + grid: + symbol: BTCUSDT + quantity: 0.001 + gridNumber: 100 + profitSpread: 1000.0 # The profit price spread that you want to add to your sell order when your buy order is executed + upperPrice: 40_000.0 + lowerPrice: 20_000.0 + diff --git a/config/binance-margin.yaml b/config/binance-margin.yaml new file mode 100644 index 0000000000..61cb4aea59 --- /dev/null +++ b/config/binance-margin.yaml @@ -0,0 +1,27 @@ +--- +sessions: + # cross margin + binance_margin: + exchange: binance + margin: true + + # isolated margin + binance_margin_linkusdt: + exchange: binance + margin: true + isolatedMargin: true + isolatedMarginSymbol: LINKUSDT + + binance_margin_dotusdt: + exchange: binance + margin: true + isolatedMargin: true + isolatedMarginSymbol: DOTUSDT + +exchangeStrategies: + +- on: binance_margin_linkusdt + dummy: + symbol: LINKUSDT + interval: 1m + diff --git a/config/bollgrid.yaml b/config/bollgrid.yaml index bef3371124..93c2796cac 100644 --- a/config/bollgrid.yaml +++ b/config/bollgrid.yaml @@ -3,19 +3,10 @@ notifications: slack: defaultChannel: "dev-bbgo" errorChannel: "bbgo-error" - - # if you want to route channel by symbol - symbolChannels: - "^BTC": "btc" - "^ETH": "eth" - "^BNB": "bnb" - - # object routing rules - routing: - trade: "$symbol" - order: "$symbol" - submitOrder: "$session" # not supported yet - pnL: "bbgo-pnl" + switches: + trade: true + orderUpdate: true + submitOrder: true sessions: # binance: @@ -53,8 +44,8 @@ backtest: - BTCUSDT account: max: - makerFeeRate: 15 - takerFeeRate: 15 + makerFeeRate: 0.075% + takerFeeRate: 0.075% balances: BTC: 0.0 USDT: 10000.0 diff --git a/config/bollmaker.yaml b/config/bollmaker.yaml index 1a86fc6f5f..732c23a7fd 100644 --- a/config/bollmaker.yaml +++ b/config/bollmaker.yaml @@ -16,17 +16,17 @@ backtest: # for testing max draw down (MDD) at 03-12 # see here for more details # https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp - startTime: "2021-08-01" - endTime: "2021-08-30" + startTime: "2022-05-01" + endTime: "2022-08-14" sessions: - binance symbols: - ETHUSDT - account: + accounts: binance: balances: - ETH: 1.0 - USDT: 20_000.0 + ETH: 0.0 + USDT: 10_000.0 exchangeStrategies: @@ -40,10 +40,13 @@ exchangeStrategies: # quantity is the base order quantity for your buy/sell order. quantity: 0.05 + # amount is used for fixed-amount order, for example, use fixed 20 USDT order for BTCUSDT market + # amount: 20 + # useTickerPrice use the ticker api to get the mid price instead of the closed kline price. # The back-test engine is kline-based, so the ticker price api is not supported. # Turn this on if you want to do real trading. - useTickerPrice: false + useTickerPrice: true # spread is the price spread from the middle price. # For ask orders, the ask price is ((bestAsk + bestBid) / 2 * (1.0 + spread)) @@ -56,9 +59,93 @@ exchangeStrategies: # For short position, you will only place buy order below the price (= average cost * (1 - minProfitSpread)) minProfitSpread: 0.1% + # trendEMA detects the trend by a given EMA + # when EMA goes up (the last > the previous), allow buy and sell + # when EMA goes down (the last < the previous), disable buy, allow sell + # uncomment this to enable it: + trendEMA: + interval: 1d + window: 7 + maxGradient: 1.5 + minGradient: 1.01 + + # ================================================================== + # Dynamic spread is an experimental feature. it will override the fixed spread settings above. + # + # dynamicSpread enables the automatic adjustment to bid and ask spread. + # Choose one of the scaling strategy to enable dynamicSpread: + # - amplitude: scales by K-line amplitude + # - weightedBollWidth: scales by weighted Bollinger band width (explained below) + # ================================================================== + # + # ========================================= + # dynamicSpread with amplitude + # ========================================= + # dynamicSpread: + # amplitude: # delete other scaling strategy if this is defined + # # window is the window of the SMAs of spreads + # window: 1 + # askSpreadScale: + # byPercentage: + # # exp means we want to use exponential scale, you can replace "exp" with "linear" for linear scale + # exp: + # # from down to up + # domain: [ 0.0001, 0.005 ] + # # when in down band, holds 1.0 by maximum + # # when in up band, holds 0.05 by maximum + # range: [ 0.001, 0.002 ] + # bidSpreadScale: + # byPercentage: + # # exp means we want to use exponential scale, you can replace "exp" with "linear" for linear scale + # exp: + # # from down to up + # domain: [ 0.0001, 0.005 ] + # # when in down band, holds 1.0 by maximum + # # when in up band, holds 0.05 by maximum + # range: [ 0.001, 0.002 ] + # + # ========================================= + # dynamicSpread with weightedBollWidth + # ========================================= + # dynamicSpread: + # # weightedBollWidth scales spread base on weighted Bollinger bandwidth ratio between default and neutral bands. + # # + # # Given the default band: moving average bd_mid, band from bd_lower to bd_upper. + # # And the neutral band: from bn_lower to bn_upper + # # Set the sigmoid weighting function: + # # - to ask spread, the weighting density function d_weight(x) is sigmoid((x - bd_mid) / (bd_upper - bd_lower)) + # # - to bid spread, the weighting density function d_weight(x) is sigmoid((bd_mid - x) / (bd_upper - bd_lower)) + # # Then calculate the weighted band width ratio by taking integral of d_weight(x) from bx_lower to bx_upper: + # # - weighted_ratio = integral(d_weight from bn_lower to bn_upper) / integral(d_weight from bd_lower to bd_upper) + # # - The wider neutral band get greater ratio + # # - To ask spread, the higher neutral band get greater ratio + # # - To bid spread, the lower neutral band get greater ratio + # # The weighted ratio always positive, and may be greater than 1 if neutral band is wider than default band. + # + # weightedBollWidth: # delete other scaling strategy if this is defined + # # sensitivity is a factor of the weighting function: 1 / (1 + exp(-(x - bd_mid) * sensitivity / (bd_upper - bd_lower))) + # # A positive number. The greater factor, the sharper weighting function. Default set to 1.0 . + # sensitivity: 1.0 + # + # askSpreadScale: + # byPercentage: + # # exp means we want to use exponential scale, you can replace "exp" with "linear" for linear scale + # linear: + # # from down to up + # domain: [ 0.1, 0.5 ] + # range: [ 0.001, 0.002 ] + # bidSpreadScale: + # byPercentage: + # # exp means we want to use exponential scale, you can replace "exp" with "linear" for linear scale + # linear: + # # from down to up + # domain: [ 0.1, 0.5 ] + # range: [ 0.001, 0.002 ] + # maxExposurePosition is the maximum position you can hold # +10 means you can hold 10 ETH long position by maximum # -10 means you can hold -10 ETH short position by maximum + # uncomment this if you want a fixed position exposure. # maxExposurePosition: 3.0 maxExposurePosition: 10 @@ -88,6 +175,10 @@ exchangeStrategies: # downtrendSkew, like the strongDowntrendSkew, but the price is still in the default band. downtrendSkew: 1.2 + # defaultBollinger is a long-term time frame bollinger + # this bollinger band is used for controlling your position (how much you can hold) + # when price is near the upper band, it holds less. + # when price is near the lower band, it holds more. defaultBollinger: interval: "1h" window: 21 @@ -101,36 +192,29 @@ exchangeStrategies: bandWidth: 2.0 # tradeInBand: when tradeInBand is set, you will only place orders in the bollinger band. - tradeInBand: false + tradeInBand: true # buyBelowNeutralSMA: when this set, it will only place buy order when the current price is below the SMA line. - buyBelowNeutralSMA: false - - persistence: - type: redis - - # Set up your stop order, this is optional - # sometimes the stop order might decrease your total profit. - # you can setup multiple stop, - stops: - # use trailing stop order - - trailingStop: - # callbackRate: when the price reaches -1% from the previous highest, we trigger the stop - callbackRate: 5.1% + buyBelowNeutralSMA: true - # closePosition is how much position do you want to close - closePosition: 20% + exits: - # minProfit is how much profit you want to take. - # if you set this option, your stop will only be triggered above the average cost. - minProfit: 5% + # roiTakeProfit is used to force taking profit by percentage of the position ROI (currently the price change) + # force to take the profit ROI exceeded the percentage. + - roiTakeProfit: + percentage: 3% - # interval is the time interval for checking your stop - interval: 1m + - protectiveStopLoss: + activationRatio: 1% + stopLossRatio: 0.2% + placeStopOrder: false - # virtual means we don't place a a REAL stop order - # when virtual is on - # the strategy won't place a REAL stop order, instead if watches the close price, - # and if the condition matches, it submits a market order to close your position. - virtual: true + - protectiveStopLoss: + activationRatio: 2% + stopLossRatio: 1% + placeStopOrder: false + - protectiveStopLoss: + activationRatio: 5% + stopLossRatio: 3% + placeStopOrder: false diff --git a/config/bollmaker_optimizer.yaml b/config/bollmaker_optimizer.yaml new file mode 100644 index 0000000000..e7f19b21b6 --- /dev/null +++ b/config/bollmaker_optimizer.yaml @@ -0,0 +1,29 @@ +# usage: +# +# go run ./cmd/bbgo optimize --config bollmaker_ethusdt.yaml --optimizer-config optimizer.yaml --debug +# +--- +executor: + type: local + local: + maxNumberOfProcesses: 10 + +matrix: +- type: iterate + label: interval + path: '/exchangeStrategies/0/bollmaker/interval' + values: [ "1m", "5m", "15m", "30m" ] + +- type: range + path: '/exchangeStrategies/0/bollmaker/amount' + label: amount + min: 20.0 + max: 100.0 + step: 20.0 + +- type: range + label: spread + path: '/exchangeStrategies/0/bollmaker/spread' + min: 0.1% + max: 0.2% + step: 0.01% diff --git a/config/dca.yaml b/config/dca.yaml new file mode 100644 index 0000000000..b8d08e47b5 --- /dev/null +++ b/config/dca.yaml @@ -0,0 +1,23 @@ +--- +backtest: + startTime: "2022-04-01" + endTime: "2022-05-01" + sessions: + - binance + symbols: + - BTCUSDT + accounts: + binance: + balances: + USDT: 20_000.0 + +exchangeStrategies: + +- on: binance + dca: + symbol: BTCUSDT + budgetPeriod: day + investmentInterval: 4h + budget: 1000 + + diff --git a/config/drift.yaml b/config/drift.yaml new file mode 100644 index 0000000000..0ace1ba49c --- /dev/null +++ b/config/drift.yaml @@ -0,0 +1,102 @@ +--- +persistence: + redis: + host: 127.0.0.1 + port: 6379 + db: 0 + +sessions: + binance: + exchange: binance + futures: false + envVarPrefix: binance + heikinAshi: false + + # Drift strategy intends to place buy/sell orders as much value mas it could be. To exchanges that requires to + # calculate fees before placing limit orders (e.g. FTX Pro), make sure the fee rate is configured correctly and + # enable modifyOrderAmountForFee to prevent order rejection. + makerFeeRate: 0.0002 + takerFeeRate: 0.0007 + modifyOrderAmountForFee: false + +exchangeStrategies: + +- on: binance + drift: + canvasPath: "./output.png" + symbol: ETHBUSD + limitOrder: false + quantity: 0.01 + # kline interval for indicators + interval: 1m + window: 1 + useAtr: true + useStopLoss: true + stoploss: 0.23% + source: ohlc4 + predictOffset: 2 + noTrailingStopLoss: false + trailingStopLossType: kline + # stddev on high/low-source + hlVarianceMultiplier: 0.13 + hlRangeWindow: 4 + smootherWindow: 19 + fisherTransformWindow: 73 + atrWindow: 14 + # orders not been traded will be canceled after `pendingMinutes` minutes + pendingMinutes: 5 + noRebalance: true + trendWindow: 12 + rebalanceFilter: 2 + + trailingActivationRatio: [0.0015, 0.002, 0.004, 0.01] + trailingCallbackRate: [0.0001, 0.00012, 0.001, 0.002] + + generateGraph: true + graphPNLDeductFee: true + graphPNLPath: "./pnl.png" + graphCumPNLPath: "./cumpnl.png" + #exits: + #- roiStopLoss: + # percentage: 0.8% + #- roiTakeProfit: + # percentage: 35% + #- protectiveStopLoss: + # activationRatio: 0.6% + # stopLossRatio: 0.1% + # placeStopOrder: false + #- protectiveStopLoss: + # activationRatio: 5% + # stopLossRatio: 1% + # placeStopOrder: false + #- cumulatedVolumeTakeProfit: + # interval: 5m + # window: 2 + # minQuoteVolume: 200_000_000 + #- protectiveStopLoss: + # activationRatio: 2% + # stopLossRatio: 1% + # placeStopOrder: false + +sync: + userDataStream: + trades: true + filledOrders: true + sessions: + - binance + symbols: + - ETHBUSD + +backtest: + startTime: "2022-09-25" + endTime: "2022-09-30" + symbols: + - ETHBUSD + sessions: [binance] + accounts: + binance: + makerFeeRate: 0.0000 + takerFeeRate: 0.0000 + balances: + ETH: 0.03 + BUSD: 0 diff --git a/config/driftBTC.yaml b/config/driftBTC.yaml new file mode 100644 index 0000000000..a05c110a29 --- /dev/null +++ b/config/driftBTC.yaml @@ -0,0 +1,139 @@ +--- +persistence: + redis: + host: 127.0.0.1 + port: 6379 + db: 0 + +sessions: + binance: + exchange: binance + #futures: true + #margin: true + #isolatedMargin: true + #isolatedMarginSymbol: BTCUSDT + envVarPrefix: binance + heikinAshi: false + + # Drift strategy intends to place buy/sell orders as much value mas it could be. To exchanges that requires to + # calculate fees before placing limit orders (e.g. FTX Pro), make sure the fee rate is configured correctly and + # enable modifyOrderAmountForFee to prevent order rejection. + makerFeeRate: 0.02% + takerFeeRate: 0.07% + modifyOrderAmountForFee: false + +exchangeStrategies: + +- on: binance + drift: + limitOrder: true + #quantity: 0.0012 + canvasPath: "./output.png" + symbol: BTCUSDT + # kline interval for indicators + interval: 1m + window: 6 + useAtr: true + useStopLoss: true + stoploss: 0.05% + source: hl2 + predictOffset: 2 + noTrailingStopLoss: false + trailingStopLossType: kline + # stddev on high/low-source + hlVarianceMultiplier: 0.14 + hlRangeWindow: 4 + smootherWindow: 3 + fisherTransformWindow: 125 + #fisherTransformWindow: 117 + atrWindow: 24 + # orders not been traded will be canceled after `pendingMinutes` minutes + pendingMinutes: 10 + noRebalance: true + trendWindow: 15 + rebalanceFilter: -0.1 + + # ActivationRatio should be increasing order + # when farest price from entry goes over that ratio, start using the callback ratio accordingly to do trailingstop + #trailingActivationRatio: [0.01, 0.016, 0.05] + #trailingActivationRatio: [0.001, 0.0081, 0.022] + trailingActivationRatio: [0.0008, 0.002, 0.01] + #trailingActivationRatio: [] + #trailingCallbackRate: [] + #trailingCallbackRate: [0.002, 0.01, 0.1] + #trailingCallbackRate: [0.0004, 0.0009, 0.018] + trailingCallbackRate: [0.00014, 0.0003, 0.0016] + + generateGraph: true + graphPNLDeductFee: false + graphPNLPath: "./pnl.png" + graphCumPNLPath: "./cumpnl.png" + #exits: + # - roiStopLoss: + # percentage: 0.35% + #- roiTakeProfit: + # percentage: 0.7% + #- protectiveStopLoss: + # activationRatio: 0.5% + # stopLossRatio: 0.2% + # placeStopOrder: false + #- trailingStop: + # callbackRate: 0.3% + # activationRatio is relative to the average cost, + # when side is buy, 1% means lower 1% than the average cost. + # when side is sell, 1% means higher 1% than the average cost. + # activationRatio: 0.7% + # minProfit uses the position ROI to calculate the profit ratio + + # minProfit: 1.5% + # interval: 1m + # side: sell + # closePosition: 100% + + #- trailingStop: + # callbackRate: 0.3% + # activationRatio is relative to the average cost, + # when side is buy, 1% means lower 1% than the average cost. + # when side is sell, 1% means higher 1% than the average cost. + # activationRatio: 0.7% + # minProfit uses the position ROI to calculate the profit ratio + + # minProfit: 1.5% + # interval: 1m + # side: buy + # closePosition: 100% + #- protectiveStopLoss: + # activationRatio: 5% + # stopLossRatio: 1% + # placeStopOrder: false + #- cumulatedVolumeTakeProfit: + # interval: 5m + # window: 2 + # minQuoteVolume: 200_000_000 + #- protectiveStopLoss: + # activationRatio: 2% + # stopLossRatio: 1% + # placeStopOrder: false + +sync: + userDataStream: + trades: true + filledOrders: true + sessions: + - binance + symbols: + - BTCUSDT + +backtest: + startTime: "2022-09-25" + endTime: "2022-10-30" + symbols: + - BTCUSDT + sessions: [binance] + accounts: + binance: + makerFeeRate: 0.000 + takerFeeRate: 0.000 + balances: + BTC: 0 + USDT: 49 diff --git a/config/elliottwave.yaml b/config/elliottwave.yaml new file mode 100644 index 0000000000..d3d138f4cb --- /dev/null +++ b/config/elliottwave.yaml @@ -0,0 +1,124 @@ +--- +persistence: + redis: + host: 127.0.0.1 + port: 6379 + db: 0 + +sessions: + binance: + exchange: binance + futures: false + envVarPrefix: binance + heikinAshi: false + + # Drift strategy intends to place buy/sell orders as much value mas it could be. To exchanges that requires to + # calculate fees before placing limit orders (e.g. FTX Pro), make sure the fee rate is configured correctly and + # enable modifyOrderAmountForFee to prevent order rejection. + makerFeeRate: 0.0002 + takerFeeRate: 0.0007 + modifyOrderAmountForFee: false + +exchangeStrategies: + +- on: binance + elliottwave: + symbol: BNBBUSD + limitOrder: true + quantity: 0.16 + # kline interval for indicators + interval: 1m + stoploss: 0.01% + windowATR: 14 + windowQuick: 5 + windowSlow: 9 + source: hl2 + pendingMinutes: 10 + useHeikinAshi: true + + drawGraph: true + graphIndicatorPath: "./indicator.png" + graphPNLPath: "./pnl.png" + graphCumPNLPath: "./cumpnl.png" + + + # ActivationRatio should be increasing order + # when farest price from entry goes over that ratio, start using the callback ratio accordingly to do trailingstop + #trailingActivationRatio: [0.01, 0.016, 0.05] + #trailingActivationRatio: [0.001, 0.0081, 0.022] + trailingActivationRatio: [0.0017, 0.01, 0.015] + #trailingActivationRatio: [] + #trailingCallbackRate: [] + #trailingCallbackRate: [0.002, 0.01, 0.1] + #trailingCallbackRate: [0.0004, 0.0009, 0.018] + trailingCallbackRate: [0.0006, 0.0019, 0.006] + + #exits: + # - roiStopLoss: + # percentage: 0.35% + #- roiTakeProfit: + # percentage: 0.7% + #- protectiveStopLoss: + # activationRatio: 0.5% + # stopLossRatio: 0.2% + # placeStopOrder: false + #- trailingStop: + # callbackRate: 0.3% + # activationRatio is relative to the average cost, + # when side is buy, 1% means lower 1% than the average cost. + # when side is sell, 1% means higher 1% than the average cost. + # activationRatio: 0.7% + # minProfit uses the position ROI to calculate the profit ratio + + # minProfit: 1.5% + # interval: 1m + # side: sell + # closePosition: 100% + + #- trailingStop: + # callbackRate: 0.3% + # activationRatio is relative to the average cost, + # when side is buy, 1% means lower 1% than the average cost. + # when side is sell, 1% means higher 1% than the average cost. + # activationRatio: 0.7% + # minProfit uses the position ROI to calculate the profit ratio + + # minProfit: 1.5% + # interval: 1m + # side: buy + # closePosition: 100% + #- protectiveStopLoss: + # activationRatio: 5% + # stopLossRatio: 1% + # placeStopOrder: false + #- cumulatedVolumeTakeProfit: + # interval: 5m + # window: 2 + # minQuoteVolume: 200_000_000 + #- protectiveStopLoss: + # activationRatio: 2% + # stopLossRatio: 1% + # placeStopOrder: false + +sync: + userDataStream: + trades: true + filledOrders: true + sessions: + - binance + symbols: + - BNBBUSD + +backtest: + startTime: "2022-09-01" + endTime: "2022-09-30" + symbols: + - BNBBUSD + sessions: [binance] + accounts: + binance: + makerFeeRate: 0.000 + takerFeeRate: 0.000 + balances: + BNB: 0 + BUSD: 100 diff --git a/config/ewo_dgtrd.yaml b/config/ewo_dgtrd.yaml index 2b2eec339d..605c45e14c 100644 --- a/config/ewo_dgtrd.yaml +++ b/config/ewo_dgtrd.yaml @@ -4,47 +4,62 @@ sessions: exchange: binance futures: true envVarPrefix: binance + heikinAshi: false exchangeStrategies: - on: binance ewo_dgtrd: symbol: MATICUSDT - interval: 30m + # kline interval for indicators + interval: 15m + # use ema as MA useEma: false + # use sma as MA, used when ema is false + # if both sma and ema are false, use EVMA useSma: false - sigWin: 3 + # ewo signal line window size + sigWin: 5 + # SL percentage from entry price stoploss: 2% + # use HeikinAshi klines instead of normal OHLC useHeikinAshi: true - disableShortStop: true - #stops: - #- trailingStop: - # callbackRate: 5.1% - # closePosition: 20% - # minProfit: 1% - # interval: 1m - # virtual: true + # disable SL when short + disableShortStop: false + # disable SL when long + disableLongStop: false + # CCI Stochastic Indicator high filter + cciStochFilterHigh: 80 + # CCI Stochastic Indicator low filter + cciStochFilterLow: 20 + # ewo change rate histogram's upperbound filter + # set to 1 would intend to let all ewo pass + ewoChangeFilterHigh: 1. + # ewo change rate histogram's lowerbound filter + # set to 0 would intend to let all ewo pass + ewoChangeFilterLow: 0.0 + # print record exit point in log messages + record: false sync: userDataStream: trades: true filledOrders: true - since: 2020-12-01 - sessions: - - binance - symbols: - - MATICUSDT + sessions: + - binance + symbols: + - MATICUSDT backtest: - startTime: "2022-04-14" - endTime: "2022-04-28" + startTime: "2022-05-01" + endTime: "2022-05-27" symbols: - MATICUSDT sessions: [binance] - account: + accounts: binance: - makerFeeRate: 0 - takerFeeRate: 0 + #makerFeeRate: 0 + #takerFeeRate: 15 balances: - MATIC: 500 - USDT: 10000 + MATIC: 000.0 + USDT: 15000.0 diff --git a/config/factorzoo.yaml b/config/factorzoo.yaml index 40647e74d8..1de7a576e1 100644 --- a/config/factorzoo.yaml +++ b/config/factorzoo.yaml @@ -2,29 +2,43 @@ sessions: binance: exchange: binance envVarPrefix: binance -# futures: true - exchangeStrategies: - on: binance factorzoo: - symbol: BTCUSDT - interval: 12h # T:20/12h - quantity: 0.95 + symbol: BTCBUSD + linear: + enabled: true + interval: 1d + quantity: 1.0 + window: 5 + + exits: + - trailingStop: + callbackRate: 1% + activationRatio: 1% + closePosition: 100% + minProfit: 15% + interval: 1m + side: buy + - trailingStop: + callbackRate: 1% + activationRatio: 1% + closePosition: 100% + minProfit: 15% + interval: 1m + side: sell backtest: sessions: - binance - # for testing max draw down (MDD) at 03-12 - # see here for more details - # https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp - startTime: "2022-03-15" - endTime: "2022-04-13" + startTime: "2021-01-01" + endTime: "2022-08-31" symbols: - - BTCUSDT - account: + - BTCBUSD + accounts: binance: balances: BTC: 1.0 - USDT: 45_000.0 + BUSD: 40_000.0 diff --git a/config/fmaker.yaml b/config/fmaker.yaml new file mode 100644 index 0000000000..e1993fab36 --- /dev/null +++ b/config/fmaker.yaml @@ -0,0 +1,26 @@ +sessions: + binance: + exchange: binance + envVarPrefix: binance + + +exchangeStrategies: +- on: binance + fmaker: + symbol: BTCUSDT + interval: 1m + spread: 0.15% + amount: 300 # 11 + +backtest: + sessions: + - binance + startTime: "2022-01-01" + endTime: "2022-05-31" + symbols: + - BTCUSDT + account: + binance: + balances: + BTC: 1 # 1 + USDT: 45_000 # 30_000 diff --git a/config/funding.yaml b/config/funding.yaml index 0fcd433ed5..9f7e7352b9 100644 --- a/config/funding.yaml +++ b/config/funding.yaml @@ -4,17 +4,10 @@ notifications: defaultChannel: "dev-bbgo" errorChannel: "bbgo-error" - # if you want to route channel by symbol - symbolChannels: - "^BTC": "btc" - "^ETH": "eth" - - # object routing rules - routing: - trade: "$symbol" - order: "$symbol" - submitOrder: "$session" # not supported yet - pnL: "bbgo-pnl" + switches: + trade: true + orderUpdate: true + submitOrder: true sessions: binance: diff --git a/config/grid-usdttwd.yaml b/config/grid-usdttwd.yaml index 29eb06c8a8..4b4e73ef40 100644 --- a/config/grid-usdttwd.yaml +++ b/config/grid-usdttwd.yaml @@ -27,12 +27,15 @@ backtest: # https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp startTime: "2021-01-01" endTime: "2021-01-30" + sessions: + - max symbols: - USDTTWD - account: + feeMode: quote + accounts: max: - makerFeeRate: 15 - takerFeeRate: 15 + makerFeeRate: 0.0125% + takerFeeRate: 0.075% balances: BTC: 0.0 USDT: 10_000.0 diff --git a/config/grid.yaml b/config/grid.yaml index 9430e85550..4704c811b8 100644 --- a/config/grid.yaml +++ b/config/grid.yaml @@ -31,11 +31,12 @@ backtest: # for testing max draw down (MDD) at 03-12 # see here for more details # https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp - startTime: "2022-01-10" - endTime: "2022-01-11" + startTime: "2022-05-09" + endTime: "2022-05-20" symbols: - BTCUSDT - account: + sessions: [binance] + accounts: binance: balances: BTC: 0.0 @@ -52,9 +53,9 @@ exchangeStrategies: # exp: # domain: [20_000, 30_000] # range: [0.2, 0.001] - gridNumber: 100 + gridNumber: 20 profitSpread: 1000.0 # The profit price spread that you want to add to your sell order when your buy order is executed - upperPrice: 50_000.0 - lowerPrice: 20_000.0 - long: true # The sell order is submitted in the same order amount as the filled corresponding buy order, rather than the same quantity. + upperPrice: 30_000.0 + lowerPrice: 28_000.0 + # long: true # The sell order is submitted in the same order amount as the filled corresponding buy order, rather than the same quantity. diff --git a/config/harmonic.yaml b/config/harmonic.yaml new file mode 100644 index 0000000000..be52b01b05 --- /dev/null +++ b/config/harmonic.yaml @@ -0,0 +1,37 @@ +persistence: +json: + directory: var/data +redis: + host: 127.0.0.1 + port: 6379 + db: 0 + +sessions: + binance: + exchange: binance + envVarPrefix: binance + +exchangeStrategies: + - on: binance + harmonic: + symbol: BTCBUSD + interval: 1s + window: 500 + quantity: 0.05 + # Draw pnl + drawGraph: true + graphPNLPath: "./pnl.png" + graphCumPNLPath: "./cumpnl.png" + +backtest: + sessions: + - binance + startTime: "2022-09-30" + endTime: "2022-10-01" + symbols: + - BTCBUSD + accounts: + binance: + balances: + BTC: 1.0 + BUSD: 40_000.0 \ No newline at end of file diff --git a/config/irr.yaml b/config/irr.yaml new file mode 100644 index 0000000000..5aa782bc50 --- /dev/null +++ b/config/irr.yaml @@ -0,0 +1,37 @@ +persistence: + json: + directory: var/data + redis: + host: 127.0.0.1 + port: 6379 + db: 0 + +sessions: + binance: + exchange: binance + envVarPrefix: binance + +exchangeStrategies: +- on: binance + irr: + symbol: BTCBUSD + interval: 1m + window: 120 + amount: 5_000.0 + # Draw pnl + drawGraph: true + graphPNLPath: "./pnl.png" + graphCumPNLPath: "./cumpnl.png" + +backtest: + sessions: + - binance + startTime: "2022-09-01" + endTime: "2022-10-04" + symbols: + - BTCBUSD + accounts: + binance: + takerFeeRate: 0.0 + balances: + BUSD: 5_000.0 diff --git a/config/marketcap.yaml b/config/marketcap.yaml new file mode 100644 index 0000000000..d1f8b41bf2 --- /dev/null +++ b/config/marketcap.yaml @@ -0,0 +1,26 @@ +--- +notifications: + slack: + defaultChannel: "bbgo" + errorChannel: "bbgo-error" + switches: + trade: true + orderUpdate: true + submitOrder: true + + +exchangeStrategies: + - on: max + marketcap: + interval: 1m + quoteCurrency: TWD + quoteCurrencyWeight: 0% + baseCurrencies: + - BTC + - ETH + - MATIC + threshold: 1% + # max amount to buy or sell per order + maxAmount: 1_000 + dryRun: true + queryInterval: 1h diff --git a/config/max-margin.yaml b/config/max-margin.yaml new file mode 100644 index 0000000000..ad7cc7588c --- /dev/null +++ b/config/max-margin.yaml @@ -0,0 +1,35 @@ +--- +sessions: + max_margin: + exchange: max + margin: true + +sync: + # userDataStream is used to sync the trading data in real-time + # it uses the websocket connection to insert the trades + userDataStream: + trades: false + filledOrders: false + + # since is the start date of your trading data + since: 2019-11-01 + + # sessions is the list of session names you want to sync + # by default, BBGO sync all your available sessions. + sessions: + - max_margin + + # symbols is the list of symbols you want to sync + # by default, BBGO try to guess your symbols by your existing account balances. + symbols: + - BTCUSDT + - ETHUSDT + + +exchangeStrategies: + +- on: max_margin + pricealert: + symbol: LINKUSDT + interval: 1m + diff --git a/config/optimizer-hyperparam-search.yaml b/config/optimizer-hyperparam-search.yaml new file mode 100644 index 0000000000..909483c272 --- /dev/null +++ b/config/optimizer-hyperparam-search.yaml @@ -0,0 +1,52 @@ +# usage: +# +# go run ./cmd/bbgo hoptimize --config bollmaker_ethusdt.yaml --optimizer-config optimizer-hyperparam-search.yaml +# +--- +# The search algorithm. Supports the following algorithms: +# - tpe: (default) Tree-structured Parzen Estimators +# - cmaes: Covariance Matrix Adaptation Evolution Strategy +# - sobol: Quasi-monte carlo sampling based on Sobol sequence +# - random: random search +# Reference: https://c-bata.medium.com/practical-bayesian-optimization-in-go-using-goptuna-edf97195fcb5 +algorithm: tpe + +# The objective function to be maximized. Possible options are: +# - profit: by trading profit +# - volume: by trading volume +# - equity: by equity difference +objectiveBy: equity + +# Maximum number of search evaluations. +maxEvaluation: 1000 + +executor: + type: local + local: + maxNumberOfProcesses: 10 + +matrix: +- type: string # alias: iterate + path: '/exchangeStrategies/0/bollmaker/interval' + values: ["1m", "5m"] + +- type: rangeInt + label: window + path: '/exchangeStrategies/0/bollmaker/defaultBollinger/window' + min: 12 + max: 240 + +- type: rangeFloat # alias: range + path: '/exchangeStrategies/0/bollmaker/spread' + min: 0.001 + max: 0.002 + +- type: rangeFloat + path: '/exchangeStrategies/0/bollmaker/quantity' + min: 0.001 + max: 0.070 + # Most markets defines the minimum order amount. "step" is useful in such case. + step: 0.001 + +- type: bool + path: '/exchangeStrategies/0/bollmaker/buyBelowNeutralSMA' \ No newline at end of file diff --git a/config/optimizer.yaml b/config/optimizer.yaml new file mode 100644 index 0000000000..ad3621c1d3 --- /dev/null +++ b/config/optimizer.yaml @@ -0,0 +1,26 @@ +# usage: +# +# go run ./cmd/bbgo optimize --config bollmaker_ethusdt.yaml --optimizer-config optimizer.yaml --debug +# +--- +executor: + type: local + local: + maxNumberOfProcesses: 10 + +matrix: +- type: iterate + path: '/exchangeStrategies/0/bollmaker/interval' + values: ["1m", "5m"] + +- type: range + path: '/exchangeStrategies/0/bollmaker/amount' + min: 20.0 + max: 40.0 + step: 20.0 + +- type: range + path: '/exchangeStrategies/0/bollmaker/spread' + min: 0.1% + max: 0.2% + step: 0.02% diff --git a/config/pivotshort-GMTBUSD.yaml b/config/pivotshort-GMTBUSD.yaml new file mode 100644 index 0000000000..d2d62021e0 --- /dev/null +++ b/config/pivotshort-GMTBUSD.yaml @@ -0,0 +1,63 @@ +--- +sessions: + binance: + exchange: binance + envVarPrefix: binance + margin: true + isolatedMargin: true + isolatedMarginSymbol: GMTBUSD + # futures: true + +exchangeStrategies: +- on: binance + pivotshort: + symbol: GMTBUSD + interval: 5m + window: 120 + + entry: + immediate: true + catBounceRatio: 1% + quantity: 20 + numLayers: 3 + marginOrderSideEffect: borrow + + exits: + # roiStopLoss is the stop loss percentage of the position ROI (currently the price change) + - roiStopLoss: + percentage: 2% + + # roiTakeProfit is used to force taking profit by percentage of the position ROI (currently the price change) + # force to take the profit ROI exceeded the percentage. + - roiTakeProfit: + percentage: 30% + + - protectiveStopLoss: + activationRatio: 1% + stopLossRatio: 0.2% + placeStopOrder: true + + # lowerShadowTakeProfit is used to taking profit when the (lower shadow height / low price) > lowerShadowRatio + # you can grab a simple stats by the following SQL: + # SELECT ((close - low) / close) AS shadow_ratio FROM binance_klines WHERE symbol = 'ETHUSDT' AND `interval` = '5m' AND start_time > '2022-01-01' ORDER BY shadow_ratio DESC LIMIT 20; + - lowerShadowTakeProfit: + ratio: 3% + + # cumulatedVolumeTakeProfit is used to take profit when the cumulated quote volume from the klines exceeded a threshold + - cumulatedVolumeTakeProfit: + minQuoteVolume: 90_000_000 + window: 5 + + +backtest: + sessions: + - binance + startTime: "2022-05-25" + endTime: "2022-06-03" + symbols: + - GMTBUSD + accounts: + binance: + balances: + GMT: 3_000.0 + USDT: 3_000.0 diff --git a/config/pivotshort.yaml b/config/pivotshort.yaml new file mode 100644 index 0000000000..414a0acb7f --- /dev/null +++ b/config/pivotshort.yaml @@ -0,0 +1,153 @@ +--- +sessions: + binance: + exchange: binance + envVarPrefix: binance + + # uncomment this to enable cross margin + margin: true + + # uncomment this to enable isolated margin + # isolatedMargin: true + # isolatedMarginSymbol: ETHUSDT + +exchangeStrategies: +- on: binance + pivotshort: + symbol: ETHUSDT + + # interval is the main pivot interval + interval: 5m + + # window is the main pivot window + window: 200 + + quantity: 10.0 + + # when quantity is not given, leverage will be used. + # leverage: 10.0 + + # breakLow settings are used for shorting when the current price break the previous low + breakLow: + # ratio is how much the price breaks the previous low to trigger the short. + ratio: 0% + + # quantity is used for submitting the sell order + # if quantity is not set, all base balance will be used for selling the short. + quantity: 10.0 + + # marketOrder submits the market sell order when the closed price is lower than the previous pivot low. + # by default we will use market order + marketOrder: true + + # limitOrder place limit order to open the short position instead of using market order + # this is useful when your quantity or leverage is quiet large. + limitOrder: false + + # limitOrderTakerRatio is the price ratio to adjust your limit order as a taker order. e.g., 0.1% + # for sell order, 0.1% ratio means your final price = price * (1 - 0.1%) + # for buy order, 0.1% ratio means your final price = price * (1 + 0.1%) + # this is only enabled when the limitOrder option set to true + limitOrderTakerRatio: 0 + + # bounceRatio is used for calculating the price of the limit sell order. + # it's ratio of pivot low bounce when a new pivot low is detected. + # Sometimes when the price breaks the previous low, the price might be pulled back to a higher price. + # The bounceRatio is useful for such case, however, you might also miss the chance to short at the price if there is no pull back. + # Notice: When marketOrder is set, bounceRatio will not be used. + # bounceRatio: 0.1% + + # stopEMA is the price range we allow short. + # Short-allowed price range = [current price] > [EMA] * (1 - [stopEMARange]) + # Higher the stopEMARange than higher the chance to open a short + stopEMA: + interval: 1h + window: 99 + range: 2% + + trendEMA: + interval: 1d + window: 7 + + resistanceShort: + enabled: true + interval: 5m + window: 80 + + quantity: 10.0 + + # minDistance is used to ignore the place that is too near to the current price + minDistance: 5% + groupDistance: 1% + + # ratio is the ratio of the resistance price, + # higher the ratio, higher the sell price + # first_layer_price = resistance_price * (1 + ratio) + # second_layer_price = (resistance_price * (1 + ratio)) * (2 * layerSpread) + ratio: 1.5% + numOfLayers: 3 + layerSpread: 0.4% + + exits: + # (0) roiStopLoss is the stop loss percentage of the position ROI (currently the price change) + - roiStopLoss: + percentage: 0.8% + + # (1) roiTakeProfit is used to force taking profit by percentage of the position ROI (currently the price change) + # force to take the profit ROI exceeded the percentage. + - roiTakeProfit: + percentage: 35% + + # (2) protective stop loss -- short term + - protectiveStopLoss: + activationRatio: 0.6% + stopLossRatio: 0.1% + placeStopOrder: false + + # (3) protective stop loss -- long term + - protectiveStopLoss: + activationRatio: 5% + stopLossRatio: 1% + placeStopOrder: false + + # (4) lowerShadowTakeProfit is used to taking profit when the (lower shadow height / low price) > lowerShadowRatio + # you can grab a simple stats by the following SQL: + # SELECT ((close - low) / close) AS shadow_ratio FROM binance_klines WHERE symbol = 'ETHUSDT' AND `interval` = '5m' AND start_time > '2022-01-01' ORDER BY shadow_ratio DESC LIMIT 20; + - lowerShadowTakeProfit: + interval: 30m + window: 99 + ratio: 3% + + # (5) cumulatedVolumeTakeProfit is used to take profit when the cumulated quote volume from the klines exceeded a threshold + - cumulatedVolumeTakeProfit: + interval: 5m + window: 2 + minQuoteVolume: 200_000_000 + + - trailingStop: + callbackRate: 3% + + # activationRatio is relative to the average cost, + # when side is buy, 1% means lower 1% than the average cost. + # when side is sell, 1% means higher 1% than the average cost. + activationRatio: 40% + + # minProfit uses the position ROI to calculate the profit ratio + # minProfit: 1% + + interval: 1m + side: buy + closePosition: 100% + +backtest: + sessions: + - binance + startTime: "2022-01-01" + endTime: "2022-06-18" + symbols: + - ETHUSDT + accounts: + binance: + balances: + ETH: 10.0 + USDT: 5000.0 diff --git a/config/pivotshort_optimizer.yaml b/config/pivotshort_optimizer.yaml new file mode 100644 index 0000000000..207c8f5a26 --- /dev/null +++ b/config/pivotshort_optimizer.yaml @@ -0,0 +1,65 @@ +# usage: +# +# go run ./cmd/bbgo optimize --config config/pivotshort.yaml --optimizer-config config/pivotshort_optimizer.yaml --debug +# +--- +executor: + type: local + local: + maxNumberOfProcesses: 10 + +matrix: + +- type: iterate + label: interval + path: '/exchangeStrategies/0/pivotshort/interval' + values: [ "1m", "5m", "30m" ] + +- type: range + path: '/exchangeStrategies/0/pivotshort/window' + label: window + min: 100.0 + max: 200.0 + step: 20.0 + +- type: range + path: '/exchangeStrategies/0/pivotshort/breakLow/stopEMARange' + label: stopEMARange + min: 0% + max: 10% + step: 1% + +- type: range + path: '/exchangeStrategies/0/pivotshort/exits/0/roiStopLoss/percentage' + label: roiStopLossPercentage + min: 0.5% + max: 2% + step: 0.1% + +- type: range + path: '/exchangeStrategies/0/pivotshort/exits/1/roiTakeProfit/percentage' + label: roiTakeProfit + min: 10% + max: 40% + step: 5% + +- type: range + path: '/exchangeStrategies/0/pivotshort/exits/2/protectiveStopLoss/activationRatio' + label: protectiveStopLoss_activationRatio + min: 0.5% + max: 3% + step: 0.1% + +- type: range + path: '/exchangeStrategies/0/pivotshort/exits/4/lowerShadowTakeProfit/ratio' + label: lowerShadowTakeProfit_ratio + min: 1% + max: 10% + step: 1% + +- type: range + path: '/exchangeStrategies/0/pivotshort/exits/5/cumulatedVolumeTakeProfit/minQuoteVolume' + label: cumulatedVolumeTakeProfit_minQuoteVolume + min: 3_000_000 + max: 20_000_000 + step: 100_000 diff --git a/config/pricealert.yaml b/config/pricealert.yaml index 2b7180285b..1662c50d51 100644 --- a/config/pricealert.yaml +++ b/config/pricealert.yaml @@ -4,17 +4,10 @@ notifications: defaultChannel: "dev-bbgo" errorChannel: "bbgo-error" - # if you want to route channel by symbol - symbolChannels: - "^BTC": "btc" - "^ETH": "eth" - - # object routing rules - routing: - trade: "$symbol" - order: "$symbol" - submitOrder: "$session" # not supported yet - pnL: "bbgo-pnl" + switches: + trade: true + orderUpdate: true + submitOrder: true sessions: binance: diff --git a/config/pricedrop.yaml b/config/pricedrop.yaml index e650a981cf..c3bb1d61a9 100644 --- a/config/pricedrop.yaml +++ b/config/pricedrop.yaml @@ -3,16 +3,10 @@ notifications: slack: defaultChannel: "bbgo" errorChannel: "bbgo-error" - -reportPnL: -- averageCostBySymbols: - - "BTCUSDT" - - "ETHUSDT" - - "BNBUSDT" - of: binance - when: - - "@daily" - - "@hourly" + switches: + trade: true + orderUpdate: true + submitOrder: true sessions: binance: @@ -46,8 +40,8 @@ backtest: - BTCUSDT account: binance: - makerFeeRate: 15 - takerFeeRate: 15 + makerFeeRate: 0.075% + takerFeeRate: 0.075% balances: BTC: 0.1 USDT: 10000.0 diff --git a/config/rebalance.yaml b/config/rebalance.yaml index c30fd2c446..d18ce1819c 100644 --- a/config/rebalance.yaml +++ b/config/rebalance.yaml @@ -3,13 +3,16 @@ notifications: slack: defaultChannel: "bbgo" errorChannel: "bbgo-error" + switches: + trade: true + orderUpdate: true + submitOrder: true exchangeStrategies: - on: max rebalance: interval: 1d - baseCurrency: TWD - ignoreLocked: true + quoteCurrency: TWD targetWeights: BTC: 40% ETH: 20% @@ -19,5 +22,4 @@ exchangeStrategies: threshold: 2% # max amount to buy or sell per order maxAmount: 10_000 - verbose: true dryRun: false diff --git a/config/rsmaker.yaml b/config/rsmaker.yaml new file mode 100644 index 0000000000..a19941d18e --- /dev/null +++ b/config/rsmaker.yaml @@ -0,0 +1,101 @@ +--- +persistence: + redis: + host: 127.0.0.1 + port: 6379 + db: 0 + +sync: + # userDataStream is used to sync the trading data in real-time + # it uses the websocket connection to insert the trades + userDataStream: + trades: true + filledOrders: true + + # since is the start date of your trading data + since: 2021-08-01 + + # sessions is the list of session names you want to sync + # by default, BBGO sync all your available sessions. + sessions: + - binance + + # symbols is the list of symbols you want to sync + # by default, BBGO try to guess your symbols by your existing account balances. + symbols: + - NEARBUSD + - BTCUSDT + - ETHUSDT + - LINKUSDT + - BNBUSDT + - DOTUSDT + - DOTBUSD + + +sessions: + binance: + exchange: binance + envVarPrefix: binance +# futures: true + + +exchangeStrategies: +- on: binance + rsmaker: + symbol: BTCBUSD + interval: 1m +# quantity: 40 + amount: 20 + minProfitSpread: 0.1% + +# uptrendSkew: 0.7 + + # downtrendSkew, like the strongDowntrendSkew, but the price is still in the default band. +# downtrendSkew: 1.3 + + # tradeInBand: when tradeInBand is set, you will only place orders in the bollinger band. +# tradeInBand: true + + # buyBelowNeutralSMA: when this set, it will only place buy order when the current price is below the SMA line. +# buyBelowNeutralSMA: true + + defaultBollinger: + interval: "1h" + window: 21 + bandWidth: 2.0 + + # neutralBollinger is the smaller range of the bollinger band + # If price is in this band, it usually means the price is oscillating. + neutralBollinger: + interval: "5m" + window: 21 + bandWidth: 2.0 + + dynamicExposurePositionScale: + byPercentage: + # exp means we want to use exponential scale, you can replace "exp" with "linear" for linear scale + exp: + # from lower band -100% (-1) to upper band 100% (+1) + domain: [ -2, 2 ] + # when in down band, holds 1.0 by maximum + # when in up band, holds 0.05 by maximum + range: [ 1, 0.01 ] + + + +backtest: + sessions: + - binance + # for testing max draw down (MDD) at 03-12 + # see here for more details + # https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp + startTime: "2022-03-26" + endTime: "2022-04-12" + symbols: + - BTCBUSD + account: + binance: + makerFeeRate: 0.0 + balances: + BTC: 1 + BUSD: 45_000.0 diff --git a/config/schedule-USDTTWD.yaml b/config/schedule-USDTTWD.yaml new file mode 100644 index 0000000000..068557f015 --- /dev/null +++ b/config/schedule-USDTTWD.yaml @@ -0,0 +1,33 @@ +--- +backtest: + sessions: + - max + startTime: "2022-01-01" + endTime: "2022-06-18" + symbols: + - USDTTWD + accounts: + binance: + balances: + TWD: 280_000.0 + +exchangeStrategies: +- on: max + schedule: + interval: 1m + symbol: USDTTWD + side: buy + amount: 500 + + aboveMovingAverage: + type: EWMA + interval: 1h + window: 99 + side: sell + + belowMovingAverage: + type: EWMA + interval: 1h + window: 99 + side: buy + diff --git a/config/schedule.yaml b/config/schedule.yaml index fa231846e7..28754cfab9 100644 --- a/config/schedule.yaml +++ b/config/schedule.yaml @@ -1,29 +1,23 @@ --- -riskControls: - # This is the session-based risk controller, which let you configure different risk controller by session. - sessionBased: - # "max" is the session name that you want to configure the risk control - max: - # orderExecutor is one of the risk control - orderExecutor: - # symbol-routed order executor - bySymbol: - USDTTWD: - # basic risk control order executor - basic: - minQuoteBalance: 100.0 - maxBaseAssetBalance: 30_000.0 - minBaseAssetBalance: 0.0 - maxOrderAmount: 1_000.0 +backtest: + sessions: + - binance + startTime: "2022-01-01" + endTime: "2022-06-18" + symbols: + - ETHUSDT + accounts: + binance: + balances: + USDT: 20_000.0 exchangeStrategies: - -- on: max +- on: binance schedule: - interval: 1m - symbol: USDTTWD + interval: 1h + symbol: ETHUSDT side: buy - quantity: 10 + amount: 20 aboveMovingAverage: type: EWMA diff --git a/config/skeleton.yaml b/config/skeleton.yaml index f0db312450..d801df353f 100644 --- a/config/skeleton.yaml +++ b/config/skeleton.yaml @@ -2,6 +2,7 @@ sessions: binance: exchange: binance + heikinAshi: true envVarPrefix: binance exchangeStrategies: @@ -11,10 +12,11 @@ exchangeStrategies: symbol: BNBBUSD backtest: - startTime: "2022-01-02" - endTime: "2022-01-19" + startTime: "2022-06-14" + endTime: "2022-06-15" symbols: - BNBBUSD + sessions: [binance] account: binance: balances: diff --git a/config/supertrend.yaml b/config/supertrend.yaml new file mode 100644 index 0000000000..7045004193 --- /dev/null +++ b/config/supertrend.yaml @@ -0,0 +1,98 @@ +--- +persistence: + redis: + host: 127.0.0.1 + port: 6379 + db: 0 + +sessions: + binance: + exchange: binance + envVarPrefix: binance + margin: true + isolatedMargin: true + isolatedMarginSymbol: BTCUSDT + +backtest: + sessions: [binance] + # for testing max draw down (MDD) at 03-12 + # see here for more details + # https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp + startTime: "2022-01-01" + endTime: "2022-06-30" + symbols: + - BTCUSDT + accounts: + binance: + makerCommission: 10 # 0.15% + takerCommission: 15 # 0.15% + balances: + BTC: 1.0 + USDT: 10000.0 + +exchangeStrategies: +- on: binance + supertrend: + symbol: BTCUSDT + + # interval is how long do you want to update your order price and quantity + interval: 1m + + # ATR window used by Supertrend + window: 34 + # ATR Multiplier for calculating super trend prices, the higher, the stronger the trends are + supertrendMultiplier: 4 + + # leverage uses the account net value to calculate the order qty + leverage: 1.0 + # quantity sets the fixed order qty, takes precedence over Leverage + #quantity: 0.5 + + # fastDEMAWindow and slowDEMAWindow are for filtering super trend noise + fastDEMAWindow: 40 + slowDEMAWindow: 49 + + # Use linear regression as trend confirmation + linearRegression: + interval: 1m + window: 74 + + # TP according to ATR multiple, 0 to disable this + TakeProfitAtrMultiplier: 0 + + # Set SL price to the low of the triggering Kline + stopLossByTriggeringK: false + + # TP/SL by reversed supertrend signal + stopByReversedSupertrend: false + + # TP/SL by reversed DEMA signal + stopByReversedDema: false + + # TP/SL by reversed linear regression signal + stopByReversedLinGre: false + + # Draw pnl + drawGraph: true + graphPNLPath: "./pnl.png" + graphCumPNLPath: "./cumpnl.png" + + exits: + # roiStopLoss is the stop loss percentage of the position ROI (currently the price change) + - roiStopLoss: + percentage: 4.5% + - protectiveStopLoss: + activationRatio: 3% + stopLossRatio: 2.5% + placeStopOrder: false + - protectiveStopLoss: + activationRatio: 7% + stopLossRatio: 3.5% + placeStopOrder: false + - trailingStop: + callbackRate: 4.5% + #activationRatio: 40% + minProfit: 9.5% + interval: 1m + side: both + closePosition: 100% diff --git a/config/support-margin.yaml b/config/support-margin.yaml index bc28cea49f..7c0207cd6d 100644 --- a/config/support-margin.yaml +++ b/config/support-margin.yaml @@ -4,18 +4,10 @@ notifications: defaultChannel: "dev-bbgo" errorChannel: "bbgo-error" - # if you want to route channel by symbol - symbolChannels: - "^BTC": "btc" - "^ETH": "eth" - "^BNB": "bnb" - - # object routing rules - routing: - trade: "$symbol" - order: "$symbol" - submitOrder: "$session" # not supported yet - pnL: "bbgo-pnl" + switches: + trade: true + orderUpdate: true + submitOrder: true sessions: binance_margin_linkusdt: @@ -47,8 +39,8 @@ backtest: - LINKUSDT account: binance: - makerFeeRate: 15 - takerFeeRate: 15 + makerFeeRate: 0.075% + takerFeeRate: 0.075% balances: LINK: 0.0 USDT: 10000.0 diff --git a/config/support.yaml b/config/support.yaml index 744c70ccf8..221ea574f2 100644 --- a/config/support.yaml +++ b/config/support.yaml @@ -3,50 +3,25 @@ notifications: slack: defaultChannel: "dev-bbgo" errorChannel: "bbgo-error" - - # if you want to route channel by symbol - symbolChannels: - "^BTC": "btc" - "^ETH": "eth" - "^BNB": "bnb" - - # object routing rules - routing: - trade: "$symbol" - order: "$symbol" - submitOrder: "$session" # not supported yet - pnL: "bbgo-pnl" + switches: + trade: true + orderUpdate: true + submitOrder: true sessions: binance: exchange: binance -riskControls: - # This is the session-based risk controller, which let you configure different risk controller by session. - sessionBased: - max: - orderExecutor: - bySymbol: - BTCUSDT: - basic: - minQuoteBalance: 100.0 - maxBaseAssetBalance: 3.0 - minBaseAssetBalance: 0.0 - maxOrderAmount: 1000.0 - backtest: - # for testing max draw down (MDD) at 03-12 - # see here for more details - # https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp - startTime: "2020-09-04" - endTime: "2020-09-14" + startTime: "2021-09-01" + endTime: "2021-11-01" + sessions: + - binance symbols: - - BTCUSDT - - ETHUSDT + - LINKUSDT account: binance: balances: - BTC: 0.0 USDT: 10000.0 exchangeStrategies: @@ -54,8 +29,8 @@ exchangeStrategies: - on: binance support: symbol: LINKUSDT - interval: 1m - minVolume: 1_000 + interval: 5m + minVolume: 80_000 triggerMovingAverage: interval: 5m window: 99 @@ -66,17 +41,16 @@ exchangeStrategies: scaleQuantity: byVolume: exp: - domain: [ 1_000, 200_000 ] + domain: [ 10_000, 200_000 ] range: [ 0.5, 1.0 ] maxBaseAssetBalance: 1000.0 minQuoteAssetBalance: 2000.0 - #trailingStopTarget: - # callbackRatio: 0.015 - # minimumProfitPercentage: 0.02 + trailingStopTarget: + callbackRatio: 1.5% + minimumProfitPercentage: 2% targets: - profitPercentage: 0.02 quantityPercentage: 0.5 - diff --git a/config/swing.yaml b/config/swing.yaml index 764fa5a12b..45625eda2d 100644 --- a/config/swing.yaml +++ b/config/swing.yaml @@ -4,18 +4,10 @@ notifications: defaultChannel: "dev-bbgo" errorChannel: "bbgo-error" - # if you want to route channel by symbol - symbolChannels: - "^BTC": "btc" - "^ETH": "eth" - "^BNB": "bnb" - - # object routing rules - routing: - trade: "$symbol" - order: "$symbol" - submitOrder: "$session" # not supported yet - pnL: "bbgo-pnl" + switches: + trade: true + orderUpdate: true + submitOrder: true sessions: binance: diff --git a/config/sync.yaml b/config/sync.yaml index b1cba855cc..3a24c9844d 100644 --- a/config/sync.yaml +++ b/config/sync.yaml @@ -4,10 +4,25 @@ sessions: exchange: binance envVarPrefix: binance + binance_margin_dotusdt: + exchange: binance + envVarPrefix: binance + margin: true + isolatedMargin: true + isolatedMarginSymbol: DOTUSDT + max: exchange: max envVarPrefix: max + kucoin: + exchange: kucoin + envVarPrefix: kucoin + + okex: + exchange: okex + envVarPrefix: okex + sync: # userDataStream is used to sync the trading data in real-time # it uses the websocket connection to insert the trades @@ -16,17 +31,34 @@ sync: filledOrders: true # since is the start date of your trading data - since: 2019-11-01 + since: 2019-01-01 # sessions is the list of session names you want to sync # by default, BBGO sync all your available sessions. sessions: - binance + - binance_margin_dotusdt - max + - okex + - kucoin # symbols is the list of symbols you want to sync # by default, BBGO try to guess your symbols by your existing account balances. symbols: - BTCUSDT - ETHUSDT - - LINKUSDT + - DOTUSDT + - binance:BNBUSDT + - max:USDTTWD + + # marginHistory enables the margin history sync + marginHistory: true + + # marginAssets lists the assets that are used in the margin. + # including loan, repay, interest and liquidation + marginAssets: + - USDT + + depositHistory: true + rewardHistory: true + withdrawHistory: true diff --git a/config/trendtrader.yaml b/config/trendtrader.yaml new file mode 100644 index 0000000000..30a166c0c2 --- /dev/null +++ b/config/trendtrader.yaml @@ -0,0 +1,50 @@ +persistence: + json: + directory: var/data + redis: + host: 127.0.0.1 + port: 6379 + db: 0 + +sessions: + binance: + exchange: binance + envVarPrefix: binance +# futures: true + +exchangeStrategies: +- on: binance + trendtrader: + symbol: BTCBUSD + trendLine: + interval: 30m + pivotRightWindow: 40 + quantity: 1 + exits: + - trailingStop: + callbackRate: 1% + activationRatio: 1% + closePosition: 100% + minProfit: 15% + interval: 1m + side: buy + - trailingStop: + callbackRate: 1% + activationRatio: 1% + closePosition: 100% + minProfit: 15% + interval: 1m + side: sell + +backtest: + sessions: + - binance + startTime: "2021-01-01" + endTime: "2022-08-31" + symbols: + - BTCBUSD + accounts: + binance: + balances: + BTC: 1 + BUSD: 50_000.0 \ No newline at end of file diff --git a/config/wall.yaml b/config/wall.yaml new file mode 100644 index 0000000000..6280882812 --- /dev/null +++ b/config/wall.yaml @@ -0,0 +1,38 @@ +--- +persistence: + redis: + host: 127.0.0.1 + port: 6379 + db: 0 + +sessions: + max: + exchange: max + envVarPrefix: MAX + +exchangeStrategies: + +- on: max + wall: + symbol: DOTUSDT + + # interval is how long do you want to update your order price and quantity + interval: 1m + + fixedPrice: 2.0 + + side: buy + + # quantity is the base order quantity for your buy/sell order. + # quantity: 0.05 + + numLayers: 3 + layerSpread: 0.1 + + quantityScale: + byLayer: + linear: + domain: [ 1, 3 ] + range: [ 10.0, 30.0 ] + + diff --git a/config/xbalance.yaml b/config/xbalance.yaml index a437fc2ec9..e3f88005e1 100644 --- a/config/xbalance.yaml +++ b/config/xbalance.yaml @@ -4,22 +4,10 @@ notifications: defaultChannel: "dev-bbgo" errorChannel: "bbgo-error" - # if you want to route channel by symbol - symbolChannels: - "^BTC": "btc" - "^ETH": "eth" - - # if you want to route channel by exchange session - sessionChannels: - max: "bbgo-max" - binance: "bbgo-binance" - - # routing rules - routing: - trade: "$symbol" - order: "$slient" - submitOrder: "$slient" - pnL: "bbgo-pnl" + switches: + trade: true + orderUpdate: true + submitOrder: true sessions: max: diff --git a/config/xgap.yaml b/config/xgap.yaml index 89d7329869..03b12d6d5f 100644 --- a/config/xgap.yaml +++ b/config/xgap.yaml @@ -4,22 +4,10 @@ notifications: defaultChannel: "dev-bbgo" errorChannel: "bbgo-error" - # if you want to route channel by symbol - symbolChannels: - "^BTC": "btc" - "^ETH": "eth" - - # if you want to route channel by exchange session - sessionChannels: - max: "bbgo-max" - binance: "bbgo-binance" - - # routing rules - routing: - trade: "$silent" - order: "$silent" - submitOrder: "$silent" - pnL: "bbgo-pnl" + switches: + trade: true + orderUpdate: true + submitOrder: true persistence: json: diff --git a/config/xmaker-btcusdt.yaml b/config/xmaker-btcusdt.yaml index 6beb4bd52b..f67094a119 100644 --- a/config/xmaker-btcusdt.yaml +++ b/config/xmaker-btcusdt.yaml @@ -4,31 +4,10 @@ notifications: defaultChannel: "dev-bbgo" errorChannel: "bbgo-error" - # if you want to route channel by symbol - symbolChannels: - "^BTC": "btc" - "^ETH": "eth" - - # if you want to route channel by exchange session - sessionChannels: - max: "bbgo-max" - binance: "bbgo-binance" - - # routing rules - routing: - trade: "$symbol" - order: "$silent" - submitOrder: "$silent" - pnL: "bbgo-pnl" - -reportPnL: -- averageCostBySymbols: - - "BTCUSDT" - - "BNBUSDT" - of: binance - when: - - "@daily" - - "@hourly" + switches: + trade: true + orderUpdate: true + submitOrder: true persistence: json: diff --git a/config/xmaker-ethusdt.yaml b/config/xmaker-ethusdt.yaml index b7faef1f4e..f4a0181d95 100644 --- a/config/xmaker-ethusdt.yaml +++ b/config/xmaker-ethusdt.yaml @@ -4,22 +4,10 @@ notifications: defaultChannel: "dev-bbgo" errorChannel: "bbgo-error" - # if you want to route channel by symbol - symbolChannels: - "^BTC": "btc" - "^ETH": "eth" - - # if you want to route channel by exchange session - sessionChannels: - max: "bbgo-max" - binance: "bbgo-binance" - - # routing rules - routing: - trade: "$symbol" - order: "$silent" - submitOrder: "$silent" - pnL: "bbgo-pnl" + switches: + trade: true + orderUpdate: false + submitOrder: false persistence: json: diff --git a/config/xmaker.yaml b/config/xmaker.yaml index 293b58247b..323bf130d0 100644 --- a/config/xmaker.yaml +++ b/config/xmaker.yaml @@ -4,11 +4,10 @@ notifications: defaultChannel: "dev-bbgo" errorChannel: "bbgo-error" - # routing rules - routing: - trade: "$silent" - order: "$silent" - submitOrder: "$silent" + switches: + trade: true + orderUpdate: false + submitOrder: false persistence: json: diff --git a/config/xnav.yaml b/config/xnav.yaml new file mode 100644 index 0000000000..2e59eef842 --- /dev/null +++ b/config/xnav.yaml @@ -0,0 +1,35 @@ +--- +notifications: + slack: + defaultChannel: "dev-bbgo" + errorChannel: "bbgo-error" + + switches: + trade: true + orderUpdate: true + submitOrder: true + +sessions: + max: + exchange: max + envVarPrefix: max + + binance: + exchange: binance + envVarPrefix: binance + +persistence: + json: + directory: var/data + redis: + host: 127.0.0.1 + port: 6379 + db: 0 + +crossExchangeStrategies: + +- xnav: + interval: 1h + reportOnStart: true + ignoreDusts: true + diff --git a/config/xpuremaker.yaml b/config/xpuremaker.yaml index b089a1adbe..697fdfeb72 100644 --- a/config/xpuremaker.yaml +++ b/config/xpuremaker.yaml @@ -4,14 +4,10 @@ notifications: defaultChannel: "bbgo" errorChannel: "bbgo-error" -reportPnL: -- averageCostBySymbols: - - "BTCUSDT" - - "BNBUSDT" - of: binance - when: - - "@daily" - - "@hourly" + switches: + trade: true + orderUpdate: true + submitOrder: true sessions: max: diff --git a/contracts/package-lock.json b/contracts/package-lock.json index 34ec9674cd..878822bca7 100644 --- a/contracts/package-lock.json +++ b/contracts/package-lock.json @@ -15692,9 +15692,9 @@ } }, "node_modules/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" }, "node_modules/mkdirp": { "version": "0.5.5", @@ -30446,9 +30446,9 @@ } }, "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" }, "mkdirp": { "version": "0.5.5", diff --git a/deploy.sh b/deploy.sh index e8c08b8b56..df8838387f 100755 --- a/deploy.sh +++ b/deploy.sh @@ -1,4 +1,28 @@ #!/bin/bash +# +# Setup: +# +# 1) Make sure that you have the SSH host called "bbgo" and your linux system has systemd installed. +# +# 2) Use ssh to connect the bbgo host +# +# $ ssh bbgo +# +# 3) On the REMOTE server, create directory, setup the dotenv file and the bbgo.yaml config file +# $ mkdir bbgo +# $ vim bbgo/.env.local +# $ vim bbgo/bbgo.yaml +# +# 4) Make sure your REMOTE user can use SUDO WITHOUT PASSWORD. +# +# 5) Run the following command to setup systemd from LOCAL: +# +# $ SETUP_SYSTEMD=yes sh deploy.sh bbgo +# +# 6) To update your existing deployment, simply run deploy.sh again: +# +# $ sh deploy.sh bbgo +# set -e target=$1 @@ -13,8 +37,8 @@ bin_type=bbgo-slim host_bin_dir=bin host=bbgo -host_user=root -host_home=/root +# host_user=ubuntu +# host_home=/root host_systemd_service_dir=/etc/systemd/system host_os=linux @@ -31,6 +55,13 @@ if [[ -n $SETUP_SYSTEMD ]] ; then fi +use_dnum=no +if [[ -n $USE_DNUM ]] ; then + use_dnum=yes +fi + + + # use the git describe as the binary version, you may override this with something else. tag=$(git describe --tags) @@ -92,6 +123,11 @@ if [[ $(remote_test "-e $host_systemd_service_dir/$target.service") != "yes" ]]; host_home=$(remote_eval "\$HOME") fi + if [[ -z $host_user ]]; then + host_user=$(remote_eval "\$USER") + fi + + cat <".systemd.$target.service" [Unit] After=network-online.target @@ -110,27 +146,42 @@ RestartSec=30 END info "uploading systemd service file..." - scp ".systemd.$target.service" "$host:$host_systemd_service_dir/$target.service" + scp ".systemd.$target.service" "$host:$target.service" + remote_run "sudo mv -v $target.service $host_systemd_service_dir/$target.service" + # scp ".systemd.$target.service" "$host:$host_systemd_service_dir/$target.service" info "reloading systemd daemon..." - remote_run "sudo systemctl daemon-reload && systemctl enable $target" + remote_run "sudo systemctl daemon-reload && sudo systemctl enable $target" fi -info "building binary: $bin_type-$host_os-$host_arch..." -make $bin_type-$host_os-$host_arch + +bin_target=$bin_type-$host_os-$host_arch + +if [[ "$use_dnum" == "yes" ]]; then + bin_target=$bin_type-dnum-$host_os-$host_arch +fi + + +info "building binary: $bin_target..." +make $bin_target # copy the binary to the server info "deploying..." info "copying binary to host $host..." if [[ $(remote_test "-e $host_bin_dir/bbgo-$tag") != "yes" ]] ; then - scp build/bbgo/$bin_type-$host_os-$host_arch $host:$host_bin_dir/bbgo-$tag + scp build/bbgo/$bin_target $host:$host_bin_dir/bbgo-$tag else info "binary $host_bin_dir/bbgo-$tag already exists, we will use the existing one" fi -# link binary and restart the systemd service -info "linking binary and restarting..." -ssh $host "(cd $target && ln -sf \$HOME/$host_bin_dir/bbgo-$tag bbgo && sudo systemctl restart $target.service)" +if [[ -n "$UPLOAD_ONLY" ]] ; then + # link binary and restart the systemd service + info "linking binary" + ssh $host "(cd $target && ln -sf \$HOME/$host_bin_dir/bbgo-$tag bbgo)" +else + info "linking binary and restarting..." + ssh $host "(cd $target && ln -sf \$HOME/$host_bin_dir/bbgo-$tag bbgo && sudo systemctl restart $target.service)" +fi info "deployed successfully!" diff --git a/doc/README.md b/doc/README.md index bac675a10e..869a0a2c52 100644 --- a/doc/README.md +++ b/doc/README.md @@ -22,9 +22,11 @@ * [Grid](strategy/grid.md) - Grid Strategy Explanation * [Interaction](strategy/interaction.md) - Interaction registration for strategies * [Price Alert](strategy/pricealert.md) - Send price alert notification on price changes +* [Supertrend](strategy/supertrend.md) - Supertrend strategy uses Supertrend indicator as trend, and DEMA indicator as noise filter * [Support](strategy/support.md) - Support strategy that buys on high volume support ### Development +* [Developing Strategy](topics/developing-strategy.md) - developing strategy * [Adding New Exchange](development/adding-new-exchange.md) - Check lists for adding new exchanges * [KuCoin Command-line Test Tool](development/kucoin-cli.md) - Kucoin command-line tools * [SQL Migration](development/migration.md) - Adding new SQL migration scripts diff --git a/doc/commands/bbgo.md b/doc/commands/bbgo.md index 9b84814a5b..a3663da90e 100644 --- a/doc/commands/bbgo.md +++ b/doc/commands/bbgo.md @@ -12,6 +12,7 @@ bbgo [flags] --binance-api-key string binance api key --binance-api-secret string binance api secret --config string config file (default "bbgo.yaml") + --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") --ftx-api-key string ftx api key @@ -33,19 +34,22 @@ bbgo [flags] ### SEE ALSO * [bbgo account](bbgo_account.md) - show user account details (ex: balance) -* [bbgo backtest](bbgo_backtest.md) - backtest your strategies +* [bbgo backtest](bbgo_backtest.md) - run backtest with strategies * [bbgo balances](bbgo_balances.md) - Show user account balances * [bbgo build](bbgo_build.md) - build cross-platform binary * [bbgo cancel-order](bbgo_cancel-order.md) - cancel orders * [bbgo deposits](bbgo_deposits.md) - A testing utility that will query deposition history in last 7 days * [bbgo execute-order](bbgo_execute-order.md) - execute buy/sell on the balance/position you have on specific symbol * [bbgo get-order](bbgo_get-order.md) - Get order status +* [bbgo hoptimize](bbgo_hoptimize.md) - run hyperparameter optimizer (experimental) * [bbgo kline](bbgo_kline.md) - connect to the kline market data streaming service of an exchange * [bbgo list-orders](bbgo_list-orders.md) - list user's open orders in exchange of a specific trading pair +* [bbgo margin](bbgo_margin.md) - margin related history * [bbgo market](bbgo_market.md) - List the symbols that the are available to be traded in the exchange +* [bbgo optimize](bbgo_optimize.md) - run optimizer * [bbgo orderbook](bbgo_orderbook.md) - connect to the order book market data streaming service of an exchange * [bbgo orderupdate](bbgo_orderupdate.md) - Listen to order update events -* [bbgo pnl](bbgo_pnl.md) - pnl calculator +* [bbgo pnl](bbgo_pnl.md) - Average Cost Based PnL Calculator * [bbgo run](bbgo_run.md) - run strategies from config file * [bbgo submit-order](bbgo_submit-order.md) - place order to the exchange * [bbgo sync](bbgo_sync.md) - sync trades and orders history @@ -55,4 +59,4 @@ bbgo [flags] * [bbgo userdatastream](bbgo_userdatastream.md) - Listen to session events (orderUpdate, tradeUpdate, balanceUpdate, balanceSnapshot) * [bbgo version](bbgo_version.md) - show version name -###### Auto generated by spf13/cobra on 2-May-2022 +###### Auto generated by spf13/cobra on 12-Oct-2022 diff --git a/doc/commands/bbgo_account.md b/doc/commands/bbgo_account.md index 5860e5104b..355a3898cd 100644 --- a/doc/commands/bbgo_account.md +++ b/doc/commands/bbgo_account.md @@ -20,6 +20,7 @@ bbgo account [--session SESSION] [flags] --binance-api-key string binance api key --binance-api-secret string binance api secret --config string config file (default "bbgo.yaml") + --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") --ftx-api-key string ftx api key @@ -41,4 +42,4 @@ bbgo account [--session SESSION] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 2-May-2022 +###### Auto generated by spf13/cobra on 12-Oct-2022 diff --git a/doc/commands/bbgo_backtest.md b/doc/commands/bbgo_backtest.md index 683cde97de..7eb6727d55 100644 --- a/doc/commands/bbgo_backtest.md +++ b/doc/commands/bbgo_backtest.md @@ -1,6 +1,6 @@ ## bbgo backtest -backtest your strategies +run backtest with strategies ``` bbgo backtest [flags] @@ -9,15 +9,18 @@ bbgo backtest [flags] ### Options ``` - --base-asset-baseline use base asset performance as the competitive baseline performance - --force force execution without confirm - -h, --help help for backtest - --output string the report output directory - --sync sync backtest data - --sync-from string sync backtest data from the given time, which will override the time range in the backtest config - --sync-only sync backtest data only, do not run backtest - -v, --verbose count verbose level - --verify verify the kline back-test data + --base-asset-baseline use base asset performance as the competitive baseline performance + --force force execution without confirm + -h, --help help for backtest + --output string the report output directory + --session string specify only one exchange session to run backtest + --subdir generate report in the sub-directory of the output directory + --sync sync backtest data + --sync-exchange string specify only one exchange to sync backtest data + --sync-from string sync backtest data from the given time, which will override the time range in the backtest config + --sync-only sync backtest data only, do not run backtest + -v, --verbose count verbose level + --verify verify the kline back-test data ``` ### Options inherited from parent commands @@ -26,6 +29,7 @@ bbgo backtest [flags] --binance-api-key string binance api key --binance-api-secret string binance api secret --config string config file (default "bbgo.yaml") + --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") --ftx-api-key string ftx api key @@ -47,4 +51,4 @@ bbgo backtest [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 2-May-2022 +###### Auto generated by spf13/cobra on 12-Oct-2022 diff --git a/doc/commands/bbgo_balances.md b/doc/commands/bbgo_balances.md index 8951b782a4..9eda2ef7c1 100644 --- a/doc/commands/bbgo_balances.md +++ b/doc/commands/bbgo_balances.md @@ -19,6 +19,7 @@ bbgo balances [--session SESSION] [flags] --binance-api-key string binance api key --binance-api-secret string binance api secret --config string config file (default "bbgo.yaml") + --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") --ftx-api-key string ftx api key @@ -40,4 +41,4 @@ bbgo balances [--session SESSION] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 2-May-2022 +###### Auto generated by spf13/cobra on 12-Oct-2022 diff --git a/doc/commands/bbgo_build.md b/doc/commands/bbgo_build.md index 00bed1272a..50ce89e1ef 100644 --- a/doc/commands/bbgo_build.md +++ b/doc/commands/bbgo_build.md @@ -18,6 +18,7 @@ bbgo build [flags] --binance-api-key string binance api key --binance-api-secret string binance api secret --config string config file (default "bbgo.yaml") + --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") --ftx-api-key string ftx api key @@ -39,4 +40,4 @@ bbgo build [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 2-May-2022 +###### Auto generated by spf13/cobra on 12-Oct-2022 diff --git a/doc/commands/bbgo_cancel-order.md b/doc/commands/bbgo_cancel-order.md index 18f47e40b8..198539955d 100644 --- a/doc/commands/bbgo_cancel-order.md +++ b/doc/commands/bbgo_cancel-order.md @@ -28,6 +28,7 @@ bbgo cancel-order [flags] --binance-api-key string binance api key --binance-api-secret string binance api secret --config string config file (default "bbgo.yaml") + --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") --ftx-api-key string ftx api key @@ -49,4 +50,4 @@ bbgo cancel-order [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 2-May-2022 +###### Auto generated by spf13/cobra on 12-Oct-2022 diff --git a/doc/commands/bbgo_deposits.md b/doc/commands/bbgo_deposits.md index 90d34cd715..b29d4fe06e 100644 --- a/doc/commands/bbgo_deposits.md +++ b/doc/commands/bbgo_deposits.md @@ -20,6 +20,7 @@ bbgo deposits [flags] --binance-api-key string binance api key --binance-api-secret string binance api secret --config string config file (default "bbgo.yaml") + --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") --ftx-api-key string ftx api key @@ -41,4 +42,4 @@ bbgo deposits [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 2-May-2022 +###### Auto generated by spf13/cobra on 12-Oct-2022 diff --git a/doc/commands/bbgo_execute-order.md b/doc/commands/bbgo_execute-order.md index fee489c9e8..aa565a429d 100644 --- a/doc/commands/bbgo_execute-order.md +++ b/doc/commands/bbgo_execute-order.md @@ -27,6 +27,7 @@ bbgo execute-order --session SESSION --symbol SYMBOL --side SIDE --target-quanti --binance-api-key string binance api key --binance-api-secret string binance api secret --config string config file (default "bbgo.yaml") + --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") --ftx-api-key string ftx api key @@ -48,4 +49,4 @@ bbgo execute-order --session SESSION --symbol SYMBOL --side SIDE --target-quanti * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 2-May-2022 +###### Auto generated by spf13/cobra on 12-Oct-2022 diff --git a/doc/commands/bbgo_get-order.md b/doc/commands/bbgo_get-order.md index c51dfea3e9..e222bc8985 100644 --- a/doc/commands/bbgo_get-order.md +++ b/doc/commands/bbgo_get-order.md @@ -21,6 +21,7 @@ bbgo get-order --session SESSION --order-id ORDER_ID [flags] --binance-api-key string binance api key --binance-api-secret string binance api secret --config string config file (default "bbgo.yaml") + --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") --ftx-api-key string ftx api key @@ -42,4 +43,4 @@ bbgo get-order --session SESSION --order-id ORDER_ID [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 2-May-2022 +###### Auto generated by spf13/cobra on 12-Oct-2022 diff --git a/doc/commands/bbgo_hoptimize.md b/doc/commands/bbgo_hoptimize.md new file mode 100644 index 0000000000..44f3cd46f9 --- /dev/null +++ b/doc/commands/bbgo_hoptimize.md @@ -0,0 +1,49 @@ +## bbgo hoptimize + +run hyperparameter optimizer (experimental) + +``` +bbgo hoptimize [flags] +``` + +### Options + +``` + -h, --help help for hoptimize + --json print optimizer metrics in json format + --json-keep-all keep all results of trials + --name string assign an optimization session name + --optimizer-config string config file (default "optimizer.yaml") + --output string backtest report output directory (default "output") + --tsv print optimizer metrics in csv format +``` + +### Options inherited from parent commands + +``` + --binance-api-key string binance api key + --binance-api-secret string binance api secret + --config string config file (default "bbgo.yaml") + --cpu-profile string cpu profile + --debug debug mode + --dotenv string the dotenv file you want to load (default ".env.local") + --ftx-api-key string ftx api key + --ftx-api-secret string ftx api secret + --ftx-subaccount string subaccount name. Specify it if the credential is for subaccount. + --max-api-key string max api key + --max-api-secret string max api secret + --metrics enable prometheus metrics + --metrics-port string prometheus http server port (default "9090") + --no-dotenv disable built-in dotenv + --slack-channel string slack trading channel (default "dev-bbgo") + --slack-error-channel string slack error channel (default "bbgo-error") + --slack-token string slack token + --telegram-bot-auth-token string telegram auth token + --telegram-bot-token string telegram bot token from bot father +``` + +### SEE ALSO + +* [bbgo](bbgo.md) - bbgo is a crypto trading bot + +###### Auto generated by spf13/cobra on 12-Oct-2022 diff --git a/doc/commands/bbgo_kline.md b/doc/commands/bbgo_kline.md index 891fb1909a..b984f122d3 100644 --- a/doc/commands/bbgo_kline.md +++ b/doc/commands/bbgo_kline.md @@ -21,6 +21,7 @@ bbgo kline [flags] --binance-api-key string binance api key --binance-api-secret string binance api secret --config string config file (default "bbgo.yaml") + --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") --ftx-api-key string ftx api key @@ -42,4 +43,4 @@ bbgo kline [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 2-May-2022 +###### Auto generated by spf13/cobra on 12-Oct-2022 diff --git a/doc/commands/bbgo_list-orders.md b/doc/commands/bbgo_list-orders.md index baa435218c..fd92f31224 100644 --- a/doc/commands/bbgo_list-orders.md +++ b/doc/commands/bbgo_list-orders.md @@ -20,6 +20,7 @@ bbgo list-orders open|closed --session SESSION --symbol SYMBOL [flags] --binance-api-key string binance api key --binance-api-secret string binance api secret --config string config file (default "bbgo.yaml") + --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") --ftx-api-key string ftx api key @@ -41,4 +42,4 @@ bbgo list-orders open|closed --session SESSION --symbol SYMBOL [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 2-May-2022 +###### Auto generated by spf13/cobra on 12-Oct-2022 diff --git a/doc/commands/bbgo_margin.md b/doc/commands/bbgo_margin.md new file mode 100644 index 0000000000..30566f3ff2 --- /dev/null +++ b/doc/commands/bbgo_margin.md @@ -0,0 +1,42 @@ +## bbgo margin + +margin related history + +### Options + +``` + -h, --help help for margin +``` + +### Options inherited from parent commands + +``` + --binance-api-key string binance api key + --binance-api-secret string binance api secret + --config string config file (default "bbgo.yaml") + --cpu-profile string cpu profile + --debug debug mode + --dotenv string the dotenv file you want to load (default ".env.local") + --ftx-api-key string ftx api key + --ftx-api-secret string ftx api secret + --ftx-subaccount string subaccount name. Specify it if the credential is for subaccount. + --max-api-key string max api key + --max-api-secret string max api secret + --metrics enable prometheus metrics + --metrics-port string prometheus http server port (default "9090") + --no-dotenv disable built-in dotenv + --slack-channel string slack trading channel (default "dev-bbgo") + --slack-error-channel string slack error channel (default "bbgo-error") + --slack-token string slack token + --telegram-bot-auth-token string telegram auth token + --telegram-bot-token string telegram bot token from bot father +``` + +### SEE ALSO + +* [bbgo](bbgo.md) - bbgo is a crypto trading bot +* [bbgo margin interests](bbgo_margin_interests.md) - query interests history +* [bbgo margin loans](bbgo_margin_loans.md) - query loans history +* [bbgo margin repays](bbgo_margin_repays.md) - query repay history + +###### Auto generated by spf13/cobra on 12-Oct-2022 diff --git a/doc/commands/bbgo_margin_interests.md b/doc/commands/bbgo_margin_interests.md new file mode 100644 index 0000000000..d5cce499d4 --- /dev/null +++ b/doc/commands/bbgo_margin_interests.md @@ -0,0 +1,45 @@ +## bbgo margin interests + +query interests history + +``` +bbgo margin interests --session=SESSION_NAME --asset=ASSET [flags] +``` + +### Options + +``` + --asset string asset + -h, --help help for interests + --session string exchange session name +``` + +### Options inherited from parent commands + +``` + --binance-api-key string binance api key + --binance-api-secret string binance api secret + --config string config file (default "bbgo.yaml") + --cpu-profile string cpu profile + --debug debug mode + --dotenv string the dotenv file you want to load (default ".env.local") + --ftx-api-key string ftx api key + --ftx-api-secret string ftx api secret + --ftx-subaccount string subaccount name. Specify it if the credential is for subaccount. + --max-api-key string max api key + --max-api-secret string max api secret + --metrics enable prometheus metrics + --metrics-port string prometheus http server port (default "9090") + --no-dotenv disable built-in dotenv + --slack-channel string slack trading channel (default "dev-bbgo") + --slack-error-channel string slack error channel (default "bbgo-error") + --slack-token string slack token + --telegram-bot-auth-token string telegram auth token + --telegram-bot-token string telegram bot token from bot father +``` + +### SEE ALSO + +* [bbgo margin](bbgo_margin.md) - margin related history + +###### Auto generated by spf13/cobra on 12-Oct-2022 diff --git a/doc/commands/bbgo_margin_loans.md b/doc/commands/bbgo_margin_loans.md new file mode 100644 index 0000000000..5e28f3ebdd --- /dev/null +++ b/doc/commands/bbgo_margin_loans.md @@ -0,0 +1,45 @@ +## bbgo margin loans + +query loans history + +``` +bbgo margin loans --session=SESSION_NAME --asset=ASSET [flags] +``` + +### Options + +``` + --asset string asset + -h, --help help for loans + --session string exchange session name +``` + +### Options inherited from parent commands + +``` + --binance-api-key string binance api key + --binance-api-secret string binance api secret + --config string config file (default "bbgo.yaml") + --cpu-profile string cpu profile + --debug debug mode + --dotenv string the dotenv file you want to load (default ".env.local") + --ftx-api-key string ftx api key + --ftx-api-secret string ftx api secret + --ftx-subaccount string subaccount name. Specify it if the credential is for subaccount. + --max-api-key string max api key + --max-api-secret string max api secret + --metrics enable prometheus metrics + --metrics-port string prometheus http server port (default "9090") + --no-dotenv disable built-in dotenv + --slack-channel string slack trading channel (default "dev-bbgo") + --slack-error-channel string slack error channel (default "bbgo-error") + --slack-token string slack token + --telegram-bot-auth-token string telegram auth token + --telegram-bot-token string telegram bot token from bot father +``` + +### SEE ALSO + +* [bbgo margin](bbgo_margin.md) - margin related history + +###### Auto generated by spf13/cobra on 12-Oct-2022 diff --git a/doc/commands/bbgo_margin_repays.md b/doc/commands/bbgo_margin_repays.md new file mode 100644 index 0000000000..f0cb88ed97 --- /dev/null +++ b/doc/commands/bbgo_margin_repays.md @@ -0,0 +1,45 @@ +## bbgo margin repays + +query repay history + +``` +bbgo margin repays --session=SESSION_NAME --asset=ASSET [flags] +``` + +### Options + +``` + --asset string asset + -h, --help help for repays + --session string exchange session name +``` + +### Options inherited from parent commands + +``` + --binance-api-key string binance api key + --binance-api-secret string binance api secret + --config string config file (default "bbgo.yaml") + --cpu-profile string cpu profile + --debug debug mode + --dotenv string the dotenv file you want to load (default ".env.local") + --ftx-api-key string ftx api key + --ftx-api-secret string ftx api secret + --ftx-subaccount string subaccount name. Specify it if the credential is for subaccount. + --max-api-key string max api key + --max-api-secret string max api secret + --metrics enable prometheus metrics + --metrics-port string prometheus http server port (default "9090") + --no-dotenv disable built-in dotenv + --slack-channel string slack trading channel (default "dev-bbgo") + --slack-error-channel string slack error channel (default "bbgo-error") + --slack-token string slack token + --telegram-bot-auth-token string telegram auth token + --telegram-bot-token string telegram bot token from bot father +``` + +### SEE ALSO + +* [bbgo margin](bbgo_margin.md) - margin related history + +###### Auto generated by spf13/cobra on 12-Oct-2022 diff --git a/doc/commands/bbgo_market.md b/doc/commands/bbgo_market.md index 0c9be4d935..c6758b56eb 100644 --- a/doc/commands/bbgo_market.md +++ b/doc/commands/bbgo_market.md @@ -19,6 +19,7 @@ bbgo market [flags] --binance-api-key string binance api key --binance-api-secret string binance api secret --config string config file (default "bbgo.yaml") + --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") --ftx-api-key string ftx api key @@ -40,4 +41,4 @@ bbgo market [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 2-May-2022 +###### Auto generated by spf13/cobra on 12-Oct-2022 diff --git a/doc/commands/bbgo_optimize.md b/doc/commands/bbgo_optimize.md new file mode 100644 index 0000000000..6fbec07c07 --- /dev/null +++ b/doc/commands/bbgo_optimize.md @@ -0,0 +1,48 @@ +## bbgo optimize + +run optimizer + +``` +bbgo optimize [flags] +``` + +### Options + +``` + -h, --help help for optimize + --json print optimizer metrics in json format + --limit int limit how many results to print pr metric (default 50) + --optimizer-config string config file (default "optimizer.yaml") + --output string backtest report output directory (default "output") + --tsv print optimizer metrics in csv format +``` + +### Options inherited from parent commands + +``` + --binance-api-key string binance api key + --binance-api-secret string binance api secret + --config string config file (default "bbgo.yaml") + --cpu-profile string cpu profile + --debug debug mode + --dotenv string the dotenv file you want to load (default ".env.local") + --ftx-api-key string ftx api key + --ftx-api-secret string ftx api secret + --ftx-subaccount string subaccount name. Specify it if the credential is for subaccount. + --max-api-key string max api key + --max-api-secret string max api secret + --metrics enable prometheus metrics + --metrics-port string prometheus http server port (default "9090") + --no-dotenv disable built-in dotenv + --slack-channel string slack trading channel (default "dev-bbgo") + --slack-error-channel string slack error channel (default "bbgo-error") + --slack-token string slack token + --telegram-bot-auth-token string telegram auth token + --telegram-bot-token string telegram bot token from bot father +``` + +### SEE ALSO + +* [bbgo](bbgo.md) - bbgo is a crypto trading bot + +###### Auto generated by spf13/cobra on 12-Oct-2022 diff --git a/doc/commands/bbgo_orderbook.md b/doc/commands/bbgo_orderbook.md index 7019e2dbe0..ce4bdb6c1b 100644 --- a/doc/commands/bbgo_orderbook.md +++ b/doc/commands/bbgo_orderbook.md @@ -21,6 +21,7 @@ bbgo orderbook --session=[exchange_name] --symbol=[pair_name] [flags] --binance-api-key string binance api key --binance-api-secret string binance api secret --config string config file (default "bbgo.yaml") + --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") --ftx-api-key string ftx api key @@ -42,4 +43,4 @@ bbgo orderbook --session=[exchange_name] --symbol=[pair_name] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 2-May-2022 +###### Auto generated by spf13/cobra on 12-Oct-2022 diff --git a/doc/commands/bbgo_orderupdate.md b/doc/commands/bbgo_orderupdate.md index e1e22d5577..b0f7fcd15b 100644 --- a/doc/commands/bbgo_orderupdate.md +++ b/doc/commands/bbgo_orderupdate.md @@ -19,6 +19,7 @@ bbgo orderupdate [flags] --binance-api-key string binance api key --binance-api-secret string binance api secret --config string config file (default "bbgo.yaml") + --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") --ftx-api-key string ftx api key @@ -40,4 +41,4 @@ bbgo orderupdate [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 2-May-2022 +###### Auto generated by spf13/cobra on 12-Oct-2022 diff --git a/doc/commands/bbgo_pnl.md b/doc/commands/bbgo_pnl.md index 06771e973a..679837d710 100644 --- a/doc/commands/bbgo_pnl.md +++ b/doc/commands/bbgo_pnl.md @@ -1,6 +1,10 @@ ## bbgo pnl -pnl calculator +Average Cost Based PnL Calculator + +### Synopsis + +This command calculates the average cost-based profit from your total trades ``` bbgo pnl [flags] @@ -9,11 +13,13 @@ bbgo pnl [flags] ### Options ``` - -h, --help help for pnl - --include-transfer convert transfer records into trades - --limit int number of trades (default 500) - --session string target exchange - --symbol string trading symbol + -h, --help help for pnl + --include-transfer convert transfer records into trades + --limit uint number of trades + --session stringArray target exchange sessions + --since string query trades from a time point + --symbol string trading symbol + --sync sync before loading trades ``` ### Options inherited from parent commands @@ -22,6 +28,7 @@ bbgo pnl [flags] --binance-api-key string binance api key --binance-api-secret string binance api secret --config string config file (default "bbgo.yaml") + --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") --ftx-api-key string ftx api key @@ -43,4 +50,4 @@ bbgo pnl [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 2-May-2022 +###### Auto generated by spf13/cobra on 12-Oct-2022 diff --git a/doc/commands/bbgo_run.md b/doc/commands/bbgo_run.md index 2742577141..7c06a00b7b 100644 --- a/doc/commands/bbgo_run.md +++ b/doc/commands/bbgo_run.md @@ -9,12 +9,12 @@ bbgo run [flags] ### Options ``` - --cpu-profile string cpu profile --enable-grpc enable grpc server --enable-web-server legacy option, this is renamed to --enable-webserver --enable-webserver enable webserver --grpc-bind string grpc server binding (default ":50051") -h, --help help for run + --lightweight lightweight mode --no-compile do not compile wrapper binary --no-sync do not sync on startup --setup use setup mode @@ -30,6 +30,7 @@ bbgo run [flags] --binance-api-key string binance api key --binance-api-secret string binance api secret --config string config file (default "bbgo.yaml") + --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") --ftx-api-key string ftx api key @@ -51,4 +52,4 @@ bbgo run [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 2-May-2022 +###### Auto generated by spf13/cobra on 12-Oct-2022 diff --git a/doc/commands/bbgo_submit-order.md b/doc/commands/bbgo_submit-order.md index 20b9fbb0b8..e66dda886b 100644 --- a/doc/commands/bbgo_submit-order.md +++ b/doc/commands/bbgo_submit-order.md @@ -9,12 +9,14 @@ bbgo submit-order --session SESSION --symbol SYMBOL --side SIDE --quantity QUANT ### Options ``` - -h, --help help for submit-order - --price string the trading price - --quantity string the trading quantity - --session string the exchange session name for sync - --side string the trading side: buy or sell - --symbol string the trading pair, like btcusdt + -h, --help help for submit-order + --margin-side-effect string margin order side effect + --market submit order as a market order + --price string the trading price + --quantity string the trading quantity + --session string the exchange session name for sync + --side string the trading side: buy or sell + --symbol string the trading pair, like btcusdt ``` ### Options inherited from parent commands @@ -23,6 +25,7 @@ bbgo submit-order --session SESSION --symbol SYMBOL --side SIDE --quantity QUANT --binance-api-key string binance api key --binance-api-secret string binance api secret --config string config file (default "bbgo.yaml") + --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") --ftx-api-key string ftx api key @@ -44,4 +47,4 @@ bbgo submit-order --session SESSION --symbol SYMBOL --side SIDE --quantity QUANT * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 2-May-2022 +###### Auto generated by spf13/cobra on 12-Oct-2022 diff --git a/doc/commands/bbgo_sync.md b/doc/commands/bbgo_sync.md index 77f31dc45a..f677b28f94 100644 --- a/doc/commands/bbgo_sync.md +++ b/doc/commands/bbgo_sync.md @@ -3,16 +3,16 @@ sync trades and orders history ``` -bbgo sync --session=[exchange_name] --symbol=[pair_name] [--since=yyyy/mm/dd] [flags] +bbgo sync [--session=[exchange_name]] [--symbol=[pair_name]] [[--since=yyyy/mm/dd]] [flags] ``` ### Options ``` - -h, --help help for sync - --session string the exchange session name for sync - --since string sync from time - --symbol string symbol of market for syncing + -h, --help help for sync + --session stringArray the exchange session name for sync + --since string sync from time + --symbol string symbol of market for syncing ``` ### Options inherited from parent commands @@ -21,6 +21,7 @@ bbgo sync --session=[exchange_name] --symbol=[pair_name] [--since=yyyy/mm/dd] [f --binance-api-key string binance api key --binance-api-secret string binance api secret --config string config file (default "bbgo.yaml") + --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") --ftx-api-key string ftx api key @@ -42,4 +43,4 @@ bbgo sync --session=[exchange_name] --symbol=[pair_name] [--since=yyyy/mm/dd] [f * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 2-May-2022 +###### Auto generated by spf13/cobra on 12-Oct-2022 diff --git a/doc/commands/bbgo_trades.md b/doc/commands/bbgo_trades.md index 0e0197b05a..5da8e79b9d 100644 --- a/doc/commands/bbgo_trades.md +++ b/doc/commands/bbgo_trades.md @@ -21,6 +21,7 @@ bbgo trades --session=[exchange_name] --symbol=[pair_name] [flags] --binance-api-key string binance api key --binance-api-secret string binance api secret --config string config file (default "bbgo.yaml") + --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") --ftx-api-key string ftx api key @@ -42,4 +43,4 @@ bbgo trades --session=[exchange_name] --symbol=[pair_name] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 2-May-2022 +###### Auto generated by spf13/cobra on 12-Oct-2022 diff --git a/doc/commands/bbgo_tradeupdate.md b/doc/commands/bbgo_tradeupdate.md index 071ac5e57f..a631f44f55 100644 --- a/doc/commands/bbgo_tradeupdate.md +++ b/doc/commands/bbgo_tradeupdate.md @@ -19,6 +19,7 @@ bbgo tradeupdate --session=[exchange_name] [flags] --binance-api-key string binance api key --binance-api-secret string binance api secret --config string config file (default "bbgo.yaml") + --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") --ftx-api-key string ftx api key @@ -40,4 +41,4 @@ bbgo tradeupdate --session=[exchange_name] [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 2-May-2022 +###### Auto generated by spf13/cobra on 12-Oct-2022 diff --git a/doc/commands/bbgo_transfer-history.md b/doc/commands/bbgo_transfer-history.md index ebd32f53af..374830eb17 100644 --- a/doc/commands/bbgo_transfer-history.md +++ b/doc/commands/bbgo_transfer-history.md @@ -21,6 +21,7 @@ bbgo transfer-history [flags] --binance-api-key string binance api key --binance-api-secret string binance api secret --config string config file (default "bbgo.yaml") + --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") --ftx-api-key string ftx api key @@ -42,4 +43,4 @@ bbgo transfer-history [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 2-May-2022 +###### Auto generated by spf13/cobra on 12-Oct-2022 diff --git a/doc/commands/bbgo_userdatastream.md b/doc/commands/bbgo_userdatastream.md index edf6bd039a..f84fca3b6c 100644 --- a/doc/commands/bbgo_userdatastream.md +++ b/doc/commands/bbgo_userdatastream.md @@ -19,6 +19,7 @@ bbgo userdatastream [flags] --binance-api-key string binance api key --binance-api-secret string binance api secret --config string config file (default "bbgo.yaml") + --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") --ftx-api-key string ftx api key @@ -40,4 +41,4 @@ bbgo userdatastream [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 2-May-2022 +###### Auto generated by spf13/cobra on 12-Oct-2022 diff --git a/doc/commands/bbgo_version.md b/doc/commands/bbgo_version.md index ed7ed82562..9e2872a5d5 100644 --- a/doc/commands/bbgo_version.md +++ b/doc/commands/bbgo_version.md @@ -18,6 +18,7 @@ bbgo version [flags] --binance-api-key string binance api key --binance-api-secret string binance api secret --config string config file (default "bbgo.yaml") + --cpu-profile string cpu profile --debug debug mode --dotenv string the dotenv file you want to load (default ".env.local") --ftx-api-key string ftx api key @@ -39,4 +40,4 @@ bbgo version [flags] * [bbgo](bbgo.md) - bbgo is a crypto trading bot -###### Auto generated by spf13/cobra on 2-May-2022 +###### Auto generated by spf13/cobra on 12-Oct-2022 diff --git a/doc/configuration/slack.md b/doc/configuration/slack.md index 881114b292..edad0d7d5b 100644 --- a/doc/configuration/slack.md +++ b/doc/configuration/slack.md @@ -4,9 +4,9 @@ Go to the Slack apps page to create your own slack app: -Click "Install your app" -> "Install to Workspace" +Click "Install your app" -> "Install to Workspace" in *Settings/Basic Information*. -Copy the *Bot User OAuth Token*. +Copy the *Bot User OAuth Token* in *Features/OAuth & Permissions*. Put your slack bot token in the `.env.local` file: @@ -23,11 +23,10 @@ notifications: defaultChannel: "bbgo-xarb" errorChannel: "bbgo-error" - # routing rules - routing: - trade: "$silent" - order: "$slient" - submitOrder: "$slient" + switches: + trade: true + orderUpdate: true + submitOrder: true ``` Besure to add your bot to the public channel by clicking "Add slack app to channel". diff --git a/doc/development/adding-new-exchange.md b/doc/development/adding-new-exchange.md index d3299d92de..6f0ae80754 100644 --- a/doc/development/adding-new-exchange.md +++ b/doc/development/adding-new-exchange.md @@ -6,9 +6,12 @@ You should send multiple small pull request to implement them. **Please avoid sending a pull request with huge changes** +**Important** -- for the underlying http API please use `requestgen` to generate the +requests. + ## Checklist -Exchange Interface - the minimum requirement for spot trading +Exchange Interface - (required) the minimum requirement for spot trading - [ ] QueryMarkets - [ ] QueryTickers @@ -26,49 +29,55 @@ Order Query Service Interface - (optional) used for querying order status - [ ] QueryOrder -Back-testing service - kline data is used for back-testing +Back-testing service - (optional, required by backtesting) kline data is used for back-testing - [ ] QueryKLines -Convert functions: +Convert functions (required): - [ ] MarketData convert functions - - [ ] toGlobalMarket - - [ ] toGlobalTicker - - [ ] toGlobalKLine + - [ ] toGlobalMarket + - [ ] toGlobalTicker + - [ ] toGlobalKLine - [ ] UserData convert functions - - [ ] toGlobalOrder - - [ ] toGlobalTrade - - [ ] toGlobalAccount - - [ ] toGlobalBalance + - [ ] toGlobalOrder + - [ ] toGlobalTrade + - [ ] toGlobalAccount + - [ ] toGlobalBalance Stream - [ ] UserDataStream - - [ ] Trade message parser - - [ ] Order message parser - - [ ] Account message parser - - [ ] Balance message parser + - [ ] Trade message parser + - [ ] Order message parser + - [ ] Account message parser + - [ ] Balance message parser - [ ] MarketDataStream - - [ ] OrderBook message parser (or depth) - - [ ] KLine message parser (required for backtesting) - - [ ] Public trade message parser (optional) - - [ ] Ticker message parser (optional) -- [ ] ping/pong handling. -- [ ] heart-beat hanlding or keep-alive handling. -- [ ] handling reconnect + - [ ] OrderBook message parser (or depth) + - [ ] KLine message parser (required for backtesting and strategy) + - [ ] Public trade message parser (optional) + - [ ] Ticker message parser (optional) +- [ ] ping/pong handling. (you can reuse the existing types.StandardStream) +- [ ] heart-beat hanlding or keep-alive handling. (already included in types.StandardStream) +- [ ] handling reconnect. (already included in types.StandardStream) Database -- [ ] Add a new kline table for the exchange (this is required for back-testing) - - [ ] Add MySQL migration SQL - - [ ] Add SQLite migration SQL +- [ ] Add a new kline table for the exchange (required for back-testing) + - [ ] Add MySQL migration SQL + - [ ] Add SQLite migration SQL Exchange Factory - [ ] Add the exchange constructor to the exchange instance factory function. - [ ] Add extended fields to the ExchangeSession struct. (optional) +# Tools + +- Use a tool to convert JSON response to Go struct +- Use requestgen to generate request builders +- Use callbackgen to generate callbacks + # Implementation Go to `pkg/types/exchange.go` and add your exchange type: @@ -79,8 +88,8 @@ const ( ExchangeBinance = ExchangeName("binance") ExchangeFTX = ExchangeName("ftx") ExchangeOKEx = ExchangeName("okex") - ExchangeKucoin = ExchangeName("kucoin") - ExchangeBacktest = ExchangeName("backtest") + ExchangeKucoin = ExchangeName("kucoin") + ExchangeBacktest = ExchangeName("backtest") ) ``` @@ -104,6 +113,66 @@ func NewExchangeStandard(n types.ExchangeName, key, secret, passphrase, subAccou } ``` +## Using requestgen + +### Alias + +You can put the go:generate alias on the top of the file: + +``` +//go:generate -command GetRequest requestgen -method GET +//go:generate -command PostRequest requestgen -method POST +//go:generate -command DeleteRequest requestgen -method DELETE +``` + +Please note that the alias only works in the same file. + +### Defining Request Type Names + +Please define request type name in the following format: + +``` +{Verb}{Service}{Resource}Request +``` + +for example: + +``` +type GetMarginMarketsRequest struct { + client requestgen.APIClient +} +``` + +then you can attach the go:generate command on that type: + +``` + +//go:generate GetRequest -url "/api/v3/wallet/m/limits" -type GetMarginBorrowingLimitsRequest -responseType .MarginBorrowingLimitMap +``` + +## Un-marshalling Timestamps + +For millisecond timestamps, you can use `types.MillisecondTimestamp`, it will automatically convert the timestamp into +time.Time: + +``` +type MarginInterestRecord struct { + Currency string `json:"currency"` + CreatedAt types.MillisecondTimestamp `json:"created_at"` +} +``` + +## Un-marshalling numbers + +For number fields, especially floating numbers, please use `fixedpoint.Value`, it can parse int, float64, float64 in +string: + +``` +type A struct { + Amount fixedpoint.Value `json:"amount"` +} +``` + ## Test Market Data Stream ### Test order book stream @@ -118,7 +187,6 @@ godotenv -f .env.local -- go run ./cmd/bbgo orderbook --config config/bbgo.yaml godotenv -f .env.local -- go run ./cmd/bbgo --config config/bbgo.yaml userdatastream --session kucoin ``` - ## Test Restful Endpoints You can choose the session name to set-up for testing: diff --git a/doc/development/frontend.md b/doc/development/frontend.md new file mode 100644 index 0000000000..22f0a54474 --- /dev/null +++ b/doc/development/frontend.md @@ -0,0 +1,40 @@ +### Setup frontend development environment + +You will need yarn to install the dependencies: + +```shell +npm install -g yarn +``` + +And you need next.js: + +```shell +npm install -g next@11 +``` + +The frontend files are in the `frontend/` directory: + +```sh +cd frontend +``` + +Run `yarn install` to install the dependencies: + +```shell +yarn install +``` + +Build and compile the frontend static files: + +```shell +yarn export +``` + +To start development, use: + +```shell +yarn dev +``` + + + diff --git a/doc/development/indicator.md b/doc/development/indicator.md index b6c1c72e2f..d3469943e3 100644 --- a/doc/development/indicator.md +++ b/doc/development/indicator.md @@ -69,23 +69,43 @@ try to create new indicators in `pkg/indicator/` folder, and add compilation hin // go:generate callbackgen -type StructName type StructName struct { ... - UpdateCallbacks []func(value float64) + updateCallbacks []func(value float64) } ``` And implement required interface methods: ```go + +func (inc *StructName) Update(value float64) { + // indicator calculation here... + // push value... +} + +func (inc *StructName) PushK(k types.KLine) { + inc.Update(k.Close.Float64()) +} + // custom function -func (inc *StructName) calculateAndUpdate(kLines []types.KLine) { - // calculation... - // assign the result to calculatedValue - inc.EmitUpdate(calculatedValue) // produce data, broadcast to the subscribers +func (inc *StructName) CalculateAndUpdate(kLines []types.KLine) { + if len(inc.Values) == 0 { + // preload or initialization + for _, k := range allKLines { + inc.PushK(k) + } + + inc.EmitUpdate(inc.Last()) + } else { + // update new value only + k := allKLines[len(allKLines)-1] + inc.PushK(k) + inc.EmitUpdate(calculatedValue) // produce data, broadcast to the subscribers + } } // custom function func (inc *StructName) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { // filter on interval - inc.calculateAndUpdate(window) + inc.CalculateAndUpdate(window) } // required @@ -98,3 +118,7 @@ The `KLineWindowUpdater` interface is currently defined in `pkg/indicator/ewma.g Once the implementation is done, run `go generate` to generate the callback functions of the indicator. You should be able to implement your strategy and use the new indicator in the same way as `AD`. + +#### Generalize + +In order to provide indicator users a lower learning curve, we've designed the `types.Series` interface. We recommend indicator developers to also implement the `types.Series` interface to provide richer functionality on the computed result. To have deeper understanding how `types.Series` works, please refer to [doc/development/series.md](./series.md) diff --git a/doc/development/migration.md b/doc/development/migration.md index 50070dc511..f24fb8af85 100644 --- a/doc/development/migration.md +++ b/doc/development/migration.md @@ -23,7 +23,7 @@ rockhopper --config rockhopper_mysql.yaml create --type sql add_pnl_column ``` -Be sure to edit both sqlite3 and mysql migration files. ( [Sample](migrations/mysql/20210531234123_add_kline_taker_buy_columns.sql) ) +Be sure to edit both sqlite3 and mysql migration files. ( [Sample](.../../migrations/mysql/20210531234123_add_kline_taker_buy_columns.sql) ) To test the drivers, you have to update the rockhopper_mysql.yaml file to connect your database, then do: diff --git a/doc/development/release-process.md b/doc/development/release-process.md index 004277dbd5..89efb1b351 100644 --- a/doc/development/release-process.md +++ b/doc/development/release-process.md @@ -1,6 +1,18 @@ # Release Process -## 1. Prepare the release note +Create a new branch for the new release: + +```shell +git checkout -b release/v1.39.0 origin/main +``` + +## 1. Run the release test script + +```shell +bash scripts/release-test.sh +``` + +## 2. Prepare the release note You need to prepare the release note for your next release version. @@ -15,12 +27,12 @@ doc/release/v1.20.2.md Run changelog script to generate a changelog template: ```sh -bash utils/changelog.sh +bash utils/changelog.sh > doc/release/v1.20.2.md ``` Edit your changelog. -## 2. Make the release +## 3. Make the release Run the following command to create the release: @@ -35,5 +47,4 @@ The above command wilL: - Run git tag to create the tag. - Run git push to push the created tag. - You can go to to modify the changelog diff --git a/doc/development/series.md b/doc/development/series.md new file mode 100644 index 0000000000..08eec47112 --- /dev/null +++ b/doc/development/series.md @@ -0,0 +1,43 @@ +Indicator Interface +----------------------------------- + +In bbgo, we've added several interfaces to standardize the indicator protocol. +The new interfaces will allow strategy developers switching similar indicators without checking the code. +Signal contributors or indicator developers were also able to be benefit from the existing interface functions, such as `Add`, `Mul`, `Minus`, and `Div`, without rebuilding the wheels. + +The series interface in bbgo borrows the concept of `series` type in pinescript that allow us to query data in time-based reverse order (data that created later will be the former object in series). Right now, based on the return type, we have two interfaces been defined in [pkg/types/indicator.go](../../pkg/types/indicator.go): + +```go +type Series interface { + Last() float64 // newest element + Index(i int) float64 // i >= 0, query float64 data in reverse order using i as index + Length() int // length of data piped in array +} +``` + +and + +```go +type BoolSeries interface { + Last() bool // newest element + Index(i int) bool // i >= 0, query bool data in reverse order using i as index + Length() int // length of data piped in array +} +``` + +Series were used almost everywhere in indicators to return the calculated numeric results, but the use of BoolSeries is quite limited. At this moment, we only use BoolSeries to check if some condition is fullfilled at some timepoint. For example, in `CrossOver` and `CrossUnder` functions if `Last()` returns true, then there might be a cross event happend on the curves at the moment. + +#### Expected Implementation + +The calculation could either be done during invoke time (lazy init, for example), or pre-calculated everytime when event happens(ex: kline close). If it's done during invoke time and the computation is CPU intensive, better to cache the result somewhere inside the struct. Also remember to always implement the Series interface on indicator's struct pointer, so that access to the indicator would always point to the same memory space. + +#### Compile Time Check + +We recommend developers to add the following line inside the indicator source: + +```go +var _ types.Series = &INDICATOR_TYPENAME{} +// Change INDICATOR_TYPENAME to the struct name that implements types.Series +``` + +and if any of the method in the interface not been implemented, this would generate compile time error messages. diff --git a/doc/release/v1.31.4.md b/doc/release/v1.31.4.md new file mode 100644 index 0000000000..b0e4f4a6bb --- /dev/null +++ b/doc/release/v1.31.4.md @@ -0,0 +1,10 @@ +[Full Changelog](https://github.com/c9s/bbgo/compare/v1.31.3...main) + +## Improves + + - [#582](https://github.com/c9s/bbgo/pull/582): improve: backtest: rename backtest.account to backtest.accounts + +## Fixes + + - [#580](https://github.com/c9s/bbgo/pull/580): fix: fix okex rate limit + - [#579](https://github.com/c9s/bbgo/pull/579): fix: fix kucoin rate limit diff --git a/doc/release/v1.32.0.md b/doc/release/v1.32.0.md new file mode 100644 index 0000000000..c2b19719a8 --- /dev/null +++ b/doc/release/v1.32.0.md @@ -0,0 +1,20 @@ +[Full Changelog](https://github.com/c9s/bbgo/compare/v1.31.4...main) + +## Fixes +- [#590](https://github.com/c9s/bbgo/pull/590): fix: fix net asset field +- [#589](https://github.com/c9s/bbgo/pull/589): fix: use net asset to calculate inUSD +- [#588](https://github.com/c9s/bbgo/pull/588): fix: add interest and fix net asset column +- [#584](https://github.com/c9s/bbgo/pull/584): feature: record nav values into db + +## Features +- [#587](https://github.com/c9s/bbgo/pull/587): feature: add grpc port and config in helm chart +- [#586](https://github.com/c9s/bbgo/pull/586): add grpc value in helm chart +- [#581](https://github.com/c9s/bbgo/pull/581): feature: add --sync-exchange option to override backtest sync exchanges + +## Tests +- [#585](https://github.com/c9s/bbgo/pull/585): indicator: add test case for boll + + +## Doc +- [#583](https://github.com/c9s/bbgo/pull/583): doc: add frontend development setup doc + diff --git a/doc/release/v1.33.0.md b/doc/release/v1.33.0.md new file mode 100644 index 0000000000..ab45b0ef3d --- /dev/null +++ b/doc/release/v1.33.0.md @@ -0,0 +1,106 @@ +## Fixes + +- backtest: fixed duplicated order update trigger for market order filled status. +- backtest: fixed the kline sync and rewrote the back-filling logic. (faster sync) +- sync: fixed the binance withdraw history sync with the new API. (implemented with requestgen) +- fixed profits table: data too long for profits column 'symbol' error. +- fixed binance bookTicker typename. +- fixed helm chart grpc binding string. +- fixed duplicated kline sync issue and add unique index for kline tables. +- interact: fixed missing make(). +- fixed incorrect binance futures position parsing. +- fixed SMA indicator. +- fixed and improve the sqlite support for back-testing. + +## Features + +- added more binance margin API support +- added binance loan history, repay history, interest history sync. +- added CoinMarketCap API. +- backtest: added web-based backtest report with kline chart and position information. +- backtest: added strategy parameter optimizer (grid search). +- indicator: added cci indicator +- improved and redesigned the strategy persistence API. +- indicator: added emv indicator + +## New Strategies + +- added `supertrend` strategy. +- added `pivotshort` strategy. +- added `dca` strategy. +- added `fmaker` strategy. +- added `autoborrow` strategy. +- added `wall` strategy. + +## Strategy Updates + +- `bollmaker`: added dynamic spread support. +- `bollmaker`: added exchange fee to position. +- `ewo`: fixed entry backtest. +- `rebalance`: use limit orders + +[Full Changelog](https://github.com/c9s/bbgo/compare/v1.32.0...main) + + - [#682](https://github.com/c9s/bbgo/pull/682): fix: fix duplicated filled order update callbacks in backtest + - [#681](https://github.com/c9s/bbgo/pull/681): Indicator/supertrend + - [#653](https://github.com/c9s/bbgo/pull/653): strategy: add supertrend strategy + - [#678](https://github.com/c9s/bbgo/pull/678): interact: fix missing make() + - [#638](https://github.com/c9s/bbgo/pull/638): strategy: add fmaker + - [#679](https://github.com/c9s/bbgo/pull/679): fix: close / rollback queries/transactions on error + - [#676](https://github.com/c9s/bbgo/pull/676): fix: rewrite kline verifying function + - [#674](https://github.com/c9s/bbgo/pull/674): rename LocalActiveOrderBook to just ActiveOrderBook + - [#672](https://github.com/c9s/bbgo/pull/672): fix and simplify LocalActiveOrderBook + - [#671](https://github.com/c9s/bbgo/pull/671): Fix futures position incorrect + - [#670](https://github.com/c9s/bbgo/pull/670): Improve backtest report ui + - [#669](https://github.com/c9s/bbgo/pull/669): fix: fix partial kline sync + - [#667](https://github.com/c9s/bbgo/pull/667): strategy: pivotshort refactor + - [#660](https://github.com/c9s/bbgo/pull/660): pivotshort: clean up strategy + - [#666](https://github.com/c9s/bbgo/pull/666): improve: apply default exchange fee rate + - [#664](https://github.com/c9s/bbgo/pull/664): fix: use the correct id for state loading + - [#663](https://github.com/c9s/bbgo/pull/663): test: add more test on Test_loadPersistenceFields + - [#661](https://github.com/c9s/bbgo/pull/661): fix: drop IsZero + - [#656](https://github.com/c9s/bbgo/pull/656): refactor: drop unused function + - [#657](https://github.com/c9s/bbgo/pull/657): fix: bollmaker: fix short position order + - [#655](https://github.com/c9s/bbgo/pull/655): fix: improve and fix kline sync + - [#654](https://github.com/c9s/bbgo/pull/654): fix: change from local timezone to UTC when do kline synchronization + - [#652](https://github.com/c9s/bbgo/pull/652): refactor/fix: withdraw sync + - [#650](https://github.com/c9s/bbgo/pull/650): Fix: Persistence Reflect IsZero + - [#649](https://github.com/c9s/bbgo/pull/649): fix: max: fix QueryAccount for margin wallet + - [#648](https://github.com/c9s/bbgo/pull/648): feature: binance margin history sync support + - [#644](https://github.com/c9s/bbgo/pull/644): feature: sync binance margin history into db + - [#645](https://github.com/c9s/bbgo/pull/645): feature: add emv indicator, fix: sma + - [#633](https://github.com/c9s/bbgo/pull/633): Fix/ewo entry, backtest + - [#637](https://github.com/c9s/bbgo/pull/637): feature: binance margin loan/interest/repay history + - [#636](https://github.com/c9s/bbgo/pull/636): fix: max: fix trades/orders parsing + - [#635](https://github.com/c9s/bbgo/pull/635): feature: max margin wallet + - [#617](https://github.com/c9s/bbgo/pull/617): feature: bollmaker dynamic spread + - [#634](https://github.com/c9s/bbgo/pull/634): rebalance: place limit orders + - [#632](https://github.com/c9s/bbgo/pull/632): fix: setup-bollgrid.sh: respect exchange name from command line argument + - [#630](https://github.com/c9s/bbgo/pull/630): fix: fix duplicated kline sync issue and add unique index for kline tables + - [#628](https://github.com/c9s/bbgo/pull/628): fix: fix summary report intervals + - [#627](https://github.com/c9s/bbgo/pull/627): feature: add grid optimizer + - [#626](https://github.com/c9s/bbgo/pull/626): use types.Interval instead of string + - [#625](https://github.com/c9s/bbgo/pull/625): feature: web-based back-test report - add mantine UI framework + - [#622](https://github.com/c9s/bbgo/pull/622): fix: back-test report: load position from the manifest + - [#605](https://github.com/c9s/bbgo/pull/605): feature: add web-based back-test report + - [#620](https://github.com/c9s/bbgo/pull/620): fix: sqlite3 compilation + - [#619](https://github.com/c9s/bbgo/pull/619): fix dockerfile. + - [#618](https://github.com/c9s/bbgo/pull/618): fix: golang version in Dockerfile + - [#610](https://github.com/c9s/bbgo/pull/610): feature: SLTP from bookticker. fix: bookTicker typename, depth buffer
 + - [#615](https://github.com/c9s/bbgo/pull/615): python: parse balance borrowed + - [#614](https://github.com/c9s/bbgo/pull/614): ftx: Let FTX support 4hr interval + - [#592](https://github.com/c9s/bbgo/pull/592): feature: add CoinMarketCap API + - [#613](https://github.com/c9s/bbgo/pull/613): bollmaker: set exchange fee to position + - [#609](https://github.com/c9s/bbgo/pull/609): Fix error: Data too long for profits column 'symbol' + - [#612](https://github.com/c9s/bbgo/pull/612): python sdk: use decimal. + - [#611](https://github.com/c9s/bbgo/pull/611): feature: add wall strategy + - [#603](https://github.com/c9s/bbgo/pull/603): feature: backtest report - #2 state recorder + - [#599](https://github.com/c9s/bbgo/pull/599): feature: add cci indicator + - [#601](https://github.com/c9s/bbgo/pull/601): feature: backtest report + - [#600](https://github.com/c9s/bbgo/pull/600): fix helm chart grpc binding string + - [#562](https://github.com/c9s/bbgo/pull/562): add Series documentation + - [#598](https://github.com/c9s/bbgo/pull/598): fix: binance data sync + - [#593](https://github.com/c9s/bbgo/pull/593): glassnode: simplify NewAuthenticatedRequest + - [#597](https://github.com/c9s/bbgo/pull/597): strategy: update bollmaker to support new strategy controller + - [#575](https://github.com/c9s/bbgo/pull/575): feature: binance: add get deposit address request API + - [#596](https://github.com/c9s/bbgo/pull/596): improve persistence api diff --git a/doc/release/v1.33.1.md b/doc/release/v1.33.1.md new file mode 100644 index 0000000000..fad27137f4 --- /dev/null +++ b/doc/release/v1.33.1.md @@ -0,0 +1,14 @@ +## Fixes + +- fixed sync since time field check (nil pointer error). +- fixed reflect insert for sqlite. +- fixed drift window indicator. + +[Full Changelog](https://github.com/c9s/bbgo/compare/v1.33.0...main) + + - [#691](https://github.com/c9s/bbgo/pull/691): fix: fix sync since time field check + - [#690](https://github.com/c9s/bbgo/pull/690): config: add dca config + - [#685](https://github.com/c9s/bbgo/pull/685): ci: add node workflow + - [#689](https://github.com/c9s/bbgo/pull/689): fix: fix reflect insert (remove gid field) + - [#688](https://github.com/c9s/bbgo/pull/688): fix: drift window in factorzoo, order_execution print order, refactor
 + - [#687](https://github.com/c9s/bbgo/pull/687): fix: check for div zero in drift indicator diff --git a/doc/release/v1.33.2.md b/doc/release/v1.33.2.md new file mode 100644 index 0000000000..0cb536c59b --- /dev/null +++ b/doc/release/v1.33.2.md @@ -0,0 +1,12 @@ +## Fixes + +- fixed net profit for zero fee. +- fixed and rewrite binance deposit history sync. +- refactored and fixed the deposity batch query. +- fixed the pnl command and add warning logs. + +[Full Changelog](https://github.com/c9s/bbgo/compare/v1.33.1...main) + + - [#693](https://github.com/c9s/bbgo/pull/693): fix: fix and rewrite binance deposit history sync + - [#695](https://github.com/c9s/bbgo/pull/695): fix: calcualte fee in quote only when fee is not zero + - [#692](https://github.com/c9s/bbgo/pull/692): fix: fix pnl command calculation and add warning logs diff --git a/doc/release/v1.33.3.md b/doc/release/v1.33.3.md new file mode 100644 index 0000000000..d20dea2b8a --- /dev/null +++ b/doc/release/v1.33.3.md @@ -0,0 +1,12 @@ +## Fixes + +- Fixed MAX v3 order cancel api. +- Fixed active order book order cancel wait time. +- Fixed and refined pivotshort position close code. + +[Full Changelog](https://github.com/c9s/bbgo/compare/v1.33.2...main) + + - [#699](https://github.com/c9s/bbgo/pull/699): pivotshort: add roiMinTakeProfitPercentage option and cumulatedVolume
 + - [#697](https://github.com/c9s/bbgo/pull/697): strategy: remove redundant code + - [#698](https://github.com/c9s/bbgo/pull/698): strategy pivotshort: refactor and add stop EMA + - [#677](https://github.com/c9s/bbgo/pull/677): strategy: pivotshort: improve short position trigger diff --git a/doc/release/v1.33.4.md b/doc/release/v1.33.4.md new file mode 100644 index 0000000000..20775ba679 --- /dev/null +++ b/doc/release/v1.33.4.md @@ -0,0 +1,23 @@ +## Fixes + +- Fixed fixedpoint percentage boundary check. +- Fixed syncing goroutine leak +- Removed kline debug log +- Fixed telegram notifier args filtering. +- Fixed message format args filtering. +- Fixed RecordPosition profit pointer checking. + +## Strategy Updates + +- pivotshort: add bounce short support. + +[Full Changelog](https://github.com/c9s/bbgo/compare/v1.33.3...main) + + - [#712](https://github.com/c9s/bbgo/pull/712): fix: fixedpoint percentage bound check + - [#710](https://github.com/c9s/bbgo/pull/710): strategy: pivot: add bounce short + - [#708](https://github.com/c9s/bbgo/pull/708): format js code by prettier + - [#706](https://github.com/c9s/bbgo/pull/706): add prettier to format the typescript code + - [#703](https://github.com/c9s/bbgo/pull/703): fix: syncing goroutine leak + - [#705](https://github.com/c9s/bbgo/pull/705): add codecoverage badge + - [#704](https://github.com/c9s/bbgo/pull/704): ci: codecoverage + - [#700](https://github.com/c9s/bbgo/pull/700): pivotshort: add breakLow.bounceRatio option diff --git a/doc/release/v1.34.0.md b/doc/release/v1.34.0.md new file mode 100644 index 0000000000..962ea8ac0c --- /dev/null +++ b/doc/release/v1.34.0.md @@ -0,0 +1,43 @@ +## Fixes + +- Fixed futures kline data and ticker data. +- Fixed frontend data sync blocking issue. +- Fixed xmaker bollinger band value checking. + +## Improvments + +- Sharing backtest report kline data. +- Upgraded frontend material UI from v4 to v5.8.3. +- Added sync session symbol support. + +## Features + +- Added bool parameter support for optimizer. +- Added ALMA indicator. +- Added frontend sync button. +- Added Ehler's super smoother filter. +- Added frontend grid stats panel. + +## Strategies + +- Added EWO histogram +- Refactored and updated rebalance strategy. + + +[Full Changelog](https://github.com/c9s/bbgo/compare/v1.33.4...main) + + - [#723](https://github.com/c9s/bbgo/pull/723): feature: add Ehler's Super smoother filter + - [#724](https://github.com/c9s/bbgo/pull/724): Grid panel draft + - [#726](https://github.com/c9s/bbgo/pull/726): rebalance: replace float slice by string-value map + - [#725](https://github.com/c9s/bbgo/pull/725): rebalance: simplify code + - [#713](https://github.com/c9s/bbgo/pull/713): improve: share klines tsv + - [#722](https://github.com/c9s/bbgo/pull/722): fix futures mode not use futures kline data. + - [#719](https://github.com/c9s/bbgo/pull/719): optimizer: bool type parameter + - [#718](https://github.com/c9s/bbgo/pull/718): fix: sync api guard condition + - [#716](https://github.com/c9s/bbgo/pull/716): fix: frontend: do not block whole page while syncing + - [#707](https://github.com/c9s/bbgo/pull/707): feature: add basic implementation of alma indicator + - [#717](https://github.com/c9s/bbgo/pull/717): strategy: fix xmaker bollinger band value checking and value updating + - [#715](https://github.com/c9s/bbgo/pull/715): feature: on demand sync button + - [#714](https://github.com/c9s/bbgo/pull/714): improve: support specifying session in the sync symbol + - [#647](https://github.com/c9s/bbgo/pull/647): strategy: ewo: add histogram + - [#711](https://github.com/c9s/bbgo/pull/711): upgrade material UI from v4 to v5.8.3 diff --git a/doc/release/v1.35.0.md b/doc/release/v1.35.0.md new file mode 100644 index 0000000000..4534bca4f7 --- /dev/null +++ b/doc/release/v1.35.0.md @@ -0,0 +1,20 @@ +## Fixes + +- Avoid doing truncate table in the mysql migration script. +- Fixed supertrend strategy. +- Fixed rma with adjust setting. + +## Features + +- Added heikinashi kline support +- Added DMI indicator +- Added marketcap strategy + +[Full Changelog](https://github.com/c9s/bbgo/compare/v1.34.0...main) + + - [#721](https://github.com/c9s/bbgo/pull/721): feature: add heikinashi support + - [#720](https://github.com/c9s/bbgo/pull/720): fix: fix strategy supertrend + - [#728](https://github.com/c9s/bbgo/pull/728): feature: add dmi indicator + - [#727](https://github.com/c9s/bbgo/pull/727): strategy: add marketcap strategy + - [#730](https://github.com/c9s/bbgo/pull/730): refactor: clean up unused max v2 api + - [#729](https://github.com/c9s/bbgo/pull/729): refactor: re-arrange maxapi files diff --git a/doc/release/v1.36.0.md b/doc/release/v1.36.0.md new file mode 100644 index 0000000000..9aea34457c --- /dev/null +++ b/doc/release/v1.36.0.md @@ -0,0 +1,83 @@ +## Fixes + +- Fixed backtest stop limit order / stop market order emulation. +- Fixed optimizer panic issue (when report is nil) +- Fixed pnl command market settings. +- Fixed MAX API endpoints. + +## Improvements + +- Improved supertrend strategy. +- Improved pivotshort strategy. +- Refactor reward service sync. + +## Features + +- Added tearsheet backend api +- Added seriesExtend for indicators. +- Added progressbar to optimizer. + +## New API + +- Added ExitMethodSet (trailing stop, roi stop loss, roi take profit, ... etc) +- Added new graceful shutdown API, persistence API, notification API, order executor API. + +[Full Changelog](https://github.com/c9s/bbgo/compare/v1.35.0...main) + + - [#799](https://github.com/c9s/bbgo/pull/799): Improve supertrend strategy + - [#801](https://github.com/c9s/bbgo/pull/801): feature: optimizer: support --tsv option and render tsv output + - [#800](https://github.com/c9s/bbgo/pull/800): fix: fix exit method for trailing stop + - [#798](https://github.com/c9s/bbgo/pull/798): fix: fix trailingstop and add long position test case + - [#797](https://github.com/c9s/bbgo/pull/797): feature: re-implement trailing stop and add mock test + - [#796](https://github.com/c9s/bbgo/pull/796): strategy/pivotshort: add supportTakeProfit method + - [#793](https://github.com/c9s/bbgo/pull/793): Fix pnl command + - [#795](https://github.com/c9s/bbgo/pull/795): optimizer/fix: prevent from crashing if missing SummaryReport + - [#794](https://github.com/c9s/bbgo/pull/794): strategy/pivotshort: fix resistance updater + - [#792](https://github.com/c9s/bbgo/pull/792): strategy/pivotshort: fix findNextResistancePriceAndPlaceOrders + - [#791](https://github.com/c9s/bbgo/pull/791): strategy: pivotshort: refactor breaklow logics + - [#790](https://github.com/c9s/bbgo/pull/790): fix: strategy: pivoshort: cancel order when shutdown + - [#789](https://github.com/c9s/bbgo/pull/789): strategy: pivotshort: add more improvements for margin + - [#787](https://github.com/c9s/bbgo/pull/787): strategy: pivotshort: use active orderbook to maintain the resistance orders + - [#786](https://github.com/c9s/bbgo/pull/786): strategy: pivotshort: resistance short + - [#731](https://github.com/c9s/bbgo/pull/731): add tearsheet backend api (Sharpe) + - [#784](https://github.com/c9s/bbgo/pull/784): strategy: pivotshort: fix stopEMA + - [#785](https://github.com/c9s/bbgo/pull/785): optimizer: add progressbar + - [#778](https://github.com/c9s/bbgo/pull/778): feature: add seriesExtend + - [#783](https://github.com/c9s/bbgo/pull/783): fix: pivotshort: fix kline history loading + - [#782](https://github.com/c9s/bbgo/pull/782): refactor: moving exit methods from pivotshort to the core + - [#781](https://github.com/c9s/bbgo/pull/781): strategy: pivotshort: optimize and update config + - [#775](https://github.com/c9s/bbgo/pull/775): test: backtest: add order cancel test case + - [#773](https://github.com/c9s/bbgo/pull/773): fix: fix backtest taker order execution + - [#772](https://github.com/c9s/bbgo/pull/772): fix: backtest: fix stop order backtest, add more test cases and assertions + - [#770](https://github.com/c9s/bbgo/pull/770): fix: fix backtest stop limit order matching and add test cases + - [#769](https://github.com/c9s/bbgo/pull/769): backtest-report: sort intervals + - [#768](https://github.com/c9s/bbgo/pull/768): feature: backtest: add ohlc legend + - [#766](https://github.com/c9s/bbgo/pull/766): backtest-report: add time range slider + - [#765](https://github.com/c9s/bbgo/pull/765): improve: backtest-report layout improvements, EMA indicators and fixed the clean up issue + - [#764](https://github.com/c9s/bbgo/pull/764): strategy/pivotshort: refactor exit methods and add protection stop exit method + - [#761](https://github.com/c9s/bbgo/pull/761): datasource: refactor glassnodeapi + - [#760](https://github.com/c9s/bbgo/pull/760): doc: fix link + - [#758](https://github.com/c9s/bbgo/pull/758): improve: add pnl cmd options and fix trade query + - [#757](https://github.com/c9s/bbgo/pull/757): totp-user: add default user 'bbgo' + - [#756](https://github.com/c9s/bbgo/pull/756): refactor: clean up rsmaker, xbalance, dca, pivotshort strategies + - [#755](https://github.com/c9s/bbgo/pull/755): improve: bbgo: call global persistence facade to sync data + - [#754](https://github.com/c9s/bbgo/pull/754): optimizer: refactor max num of process in optimizer configs + - [#750](https://github.com/c9s/bbgo/pull/750): refactor: persistence singleton and improve backtest cancel performance + - [#753](https://github.com/c9s/bbgo/pull/753): optimizer: add max num of thread in config + - [#752](https://github.com/c9s/bbgo/pull/752): Upgrade nextjs from 11 to 12 + - [#751](https://github.com/c9s/bbgo/pull/751): fix: reformat go code + - [#746](https://github.com/c9s/bbgo/pull/746): pivotshort: add strategy controller + - [#747](https://github.com/c9s/bbgo/pull/747): strategy/supertrend: use new order executor api + - [#748](https://github.com/c9s/bbgo/pull/748): bollmaker: remove redundant code for adapting new order executor api + - [#749](https://github.com/c9s/bbgo/pull/749): improve: add parallel local process executor for optimizer + - [#639](https://github.com/c9s/bbgo/pull/639): strategy: rsmaker: initial idea prototype + - [#745](https://github.com/c9s/bbgo/pull/745): fix: depth: do not test depth buffer when race is on + - [#744](https://github.com/c9s/bbgo/pull/744): refactor: refactor and update the support strategy + - [#743](https://github.com/c9s/bbgo/pull/743): strategy/bollmaker: refactor and clean up + - [#742](https://github.com/c9s/bbgo/pull/742): refactor: clean up bbgo.Notifiability + - [#739](https://github.com/c9s/bbgo/pull/739): refactor: redesign order executor api + - [#738](https://github.com/c9s/bbgo/pull/738): feature: binance: add binance spot rebate history support + - [#736](https://github.com/c9s/bbgo/pull/736): fix: gosimple alert + - [#737](https://github.com/c9s/bbgo/pull/737): refactor: refactor reward service sync + - [#732](https://github.com/c9s/bbgo/pull/732): Refactor grid panel + - [#734](https://github.com/c9s/bbgo/pull/734): fix: apply gofmt on all files, add revive action diff --git a/doc/release/v1.37.0.md b/doc/release/v1.37.0.md new file mode 100644 index 0000000000..f7713f4979 --- /dev/null +++ b/doc/release/v1.37.0.md @@ -0,0 +1,36 @@ + +## Fixes + +- Fixed backtest file lock race issue when running multiple back-test processes by the optimizer. +- Fixed optimizer limitation. +- Fixed backtest final asset calculation. +- Fixed exit method stop market data subscription. + +## Improvements and Refactoring + +- Refactored and cleaned up indicators. +- Added risk related functions for futures and margin. +- Prepare database before executing backtest. + +## Strategies + +- autoborrow: fixed repay amount calculation. +- pivotshort: updated pivot low on stream start. +- pivotshort: added leverage settings. +- pivotshort: improved quantity calculation. +- pivotshort: fixed resistance order placement. +- supertrend: fixed double dema initialization. +- supertrend: fixed types.TradeStats usage. + + +[Full Changelog](https://github.com/c9s/bbgo/compare/v1.36.0...main) + + - [#830](https://github.com/c9s/bbgo/pull/830): backtest: resolve data race on index.json + - [#829](https://github.com/c9s/bbgo/pull/829): optimizer: eliminate limitation of number of grid point + - [#827](https://github.com/c9s/bbgo/pull/827): strategy/pivotshort: improve quantity calculation for margin and futures + - [#824](https://github.com/c9s/bbgo/pull/824): refactor: indicator: rewrite VWMA calculator + - [#823](https://github.com/c9s/bbgo/pull/823): refactor: refactor bollinger band indicator with the new series extend component + - [#821](https://github.com/c9s/bbgo/pull/821): refactor: refactor indicator api (canonicalize CalculateAndUpdate, PushK methods) + - [#820](https://github.com/c9s/bbgo/pull/820): feature: add risk functions + - [#818](https://github.com/c9s/bbgo/pull/818): backtest: correct final asset calculation + - [#817](https://github.com/c9s/bbgo/pull/817): optimizer: prepare database before executing backtests diff --git a/doc/release/v1.38.0.md b/doc/release/v1.38.0.md new file mode 100644 index 0000000000..e235a7f219 --- /dev/null +++ b/doc/release/v1.38.0.md @@ -0,0 +1,47 @@ + +## Improvements + +- [#849](https://github.com/c9s/bbgo/pull/849): optimizer: print equity diff in final report +- [#850](https://github.com/c9s/bbgo/pull/850): optimizer: calculate total number of grids before testing +- [#838](https://github.com/c9s/bbgo/pull/838): optimizer: improve: use marshal instead of marshal indent +- [#845](https://github.com/c9s/bbgo/pull/845): refactor: refactor standard indicator and add simple indicator interface +- [#825](https://github.com/c9s/bbgo/pull/825): refactor: new indicator api + +## Strategies + +- [#848](https://github.com/c9s/bbgo/pull/848): strategy/pivotshort: refactor trendEMA and add maxGradient config +- [#847](https://github.com/c9s/bbgo/pull/847): strategy/pivotshort: fine-tune and add more trade stats metrics +- [#846](https://github.com/c9s/bbgo/pull/846): strategy/pivotshort: refactor breaklow + add fake break stop +- [#834](https://github.com/c9s/bbgo/pull/834): strategy/pivotshort: add trade loss to the account value calculation +- [#833](https://github.com/c9s/bbgo/pull/833): strategy/pivotshort: fix margin quantity calculation +- [#840](https://github.com/c9s/bbgo/pull/840): strategy/supertrend: fix exit methods problem +- [#842](https://github.com/c9s/bbgo/pull/842): feature: bollmaker exit methods + +## Fixes + +- [#832](https://github.com/c9s/bbgo/pull/832): update: fix and update schedule strategy + + +[Full Changelog](https://github.com/c9s/bbgo/compare/v1.37.0...main) + + - [#849](https://github.com/c9s/bbgo/pull/849): optimizer: print equity diff in final report + - [#850](https://github.com/c9s/bbgo/pull/850): optimizer: calculate total number of grids before testing + - [#851](https://github.com/c9s/bbgo/pull/851): stabilize drift + - [#848](https://github.com/c9s/bbgo/pull/848): strategy/pivotshort: refactor trendEMA and add maxGradient config + - [#847](https://github.com/c9s/bbgo/pull/847): strategy/pivotshort: fine-tune and add more trade stats metrics + - [#846](https://github.com/c9s/bbgo/pull/846): strategy/pivotshort: refactor breaklow + add fake break stop + - [#813](https://github.com/c9s/bbgo/pull/813): feature: drift study + - [#844](https://github.com/c9s/bbgo/pull/844): strategy/pivotshort: refactor and update indicator api usage + - [#845](https://github.com/c9s/bbgo/pull/845): refactor: refactor standard indicator and add simple indicator interface + - [#843](https://github.com/c9s/bbgo/pull/843): fix splitted from feature/drift_study + - [#822](https://github.com/c9s/bbgo/pull/822): refactor: ewoDgtrd: upgrade order executor api + - [#842](https://github.com/c9s/bbgo/pull/842): feature: bollmaker exit methods + - [#840](https://github.com/c9s/bbgo/pull/840): strategy/supertrend: fix exit methods problem + - [#838](https://github.com/c9s/bbgo/pull/838): improve: use marshal instead of marshal indent + - [#837](https://github.com/c9s/bbgo/pull/837): risk: add account value calculator test case + - [#836](https://github.com/c9s/bbgo/pull/836): refactor: risk functions for leveraged quantity + - [#834](https://github.com/c9s/bbgo/pull/834): fix: strategy/pivotshort: add trade loss to the account value calculation + - [#833](https://github.com/c9s/bbgo/pull/833): fix: strategy/pivotshort: fix margin quantity calculation + - [#825](https://github.com/c9s/bbgo/pull/825): refactor: new indicator api + - [#831](https://github.com/c9s/bbgo/pull/831): feature: api: add strategy defaulter interface + - [#832](https://github.com/c9s/bbgo/pull/832): update: fix and update schedule strategy diff --git a/doc/release/v1.39.0.md b/doc/release/v1.39.0.md new file mode 100644 index 0000000000..e0bd92e583 --- /dev/null +++ b/doc/release/v1.39.0.md @@ -0,0 +1,55 @@ +## Fixes + +- fixed protective stop loss for long position. +- fixed trailing stop for long position. +- fixed trade collector concurrent write issue. +- fixed cpu profile starter. +- fixed rbtree and add panic check. +- fixed binance futures trade, position and order type conversion. + +## Features + +- added telegram photo upload support. +- added multi-symbol support to active order book and trade collector. +- added max/binance query order service support. +- added binance QueryOrderTrades API +- added ftx order amount fee conversion. +- added default fee rate to FTX exchange. +- added leverage/risk calculator. +- optimizer: calculate equity diff from whole assets instead of first symbol. +- added optimizeex command, a hyperparameter optimization tool + +## Strategies Updates + +- autoborrow: add debtRatio +- drift: added smart cancel, fixed position bugs, added multiple level trailing stop, removed takeProftFactor, use fisher transform, added 1m drift for takeprofit/stoploss, rebalance position according to the trendline. +- supertrend: output profit stats and calculate quantity by risk/leverage. +- pivotshort: added trendema and fixed initial ema value. +- pivotshort: added SideEffectTypeAutoRepay to pivotshort take-profit order +- factorzoo: integrated logistic regression, indicator refactoring and updates. + +[Full Changelog](https://github.com/c9s/bbgo/compare/v1.38.0...main) + + - [#882](https://github.com/c9s/bbgo/pull/882): strategy/autoborrow: add debt re-balancing + - [#877](https://github.com/c9s/bbgo/pull/877): strategy/supertrend: update example config + - [#878](https://github.com/c9s/bbgo/pull/878): Drift rebase + - [#875](https://github.com/c9s/bbgo/pull/875): pivotshort: trendema add initial date + - [#876](https://github.com/c9s/bbgo/pull/876): Fix: risk.AvailableQuote() should use Net() to get net value + - [#874](https://github.com/c9s/bbgo/pull/874): Fix binance futures + - [#872](https://github.com/c9s/bbgo/pull/872): fix: trailing stop properly works on both long and short positions + - [#873](https://github.com/c9s/bbgo/pull/873): improve: generalorderexecutor retries submit/cancel order once + - [#871](https://github.com/c9s/bbgo/pull/871): improve: improve maxapi, add v2 order api back + - [#869](https://github.com/c9s/bbgo/pull/869): Revert "feature: add smart cancel to drift" + - [#853](https://github.com/c9s/bbgo/pull/853): feature: add smart cancel to drift + - [#860](https://github.com/c9s/bbgo/pull/860): exchange: order fee-amount protection + - [#865](https://github.com/c9s/bbgo/pull/865): fix: protectivestoploss not working on long position + - [#868](https://github.com/c9s/bbgo/pull/868): fix: many minor fixes + - [#867](https://github.com/c9s/bbgo/pull/867): strategy: factorzoo: upgrade indicators and add comments + - [#862](https://github.com/c9s/bbgo/pull/862): Improve: supertrend strategy + - [#863](https://github.com/c9s/bbgo/pull/863): types: rbtree: resolve neel reusing problem + - [#852](https://github.com/c9s/bbgo/pull/852): feature: PositionModifier + - [#861](https://github.com/c9s/bbgo/pull/861): strategy/supertrend: re-organize exits part of config + - [#855](https://github.com/c9s/bbgo/pull/855): optimizeex: hyperparameter optimization tool + - [#856](https://github.com/c9s/bbgo/pull/856): exchange: FTX default fee + - [#857](https://github.com/c9s/bbgo/pull/857): optimizer: calculate equity diff from whole assets instead of first symbol + - [#854](https://github.com/c9s/bbgo/pull/854): fix: added SideEffectTypeAutoRepay to pivotshort take-profit order diff --git a/doc/release/v1.39.1.md b/doc/release/v1.39.1.md new file mode 100644 index 0000000000..a809a5e358 --- /dev/null +++ b/doc/release/v1.39.1.md @@ -0,0 +1,7 @@ +## Fixes + +- fixed backtest limit taker unlock issue. + +[Full Changelog](https://github.com/c9s/bbgo/compare/v1.39.0...main) + + - [#885](https://github.com/c9s/bbgo/pull/885): backtest: fix limit taker lock issue diff --git a/doc/release/v1.39.2.md b/doc/release/v1.39.2.md new file mode 100644 index 0000000000..91cc5574f2 --- /dev/null +++ b/doc/release/v1.39.2.md @@ -0,0 +1,16 @@ +## Fixes + +- fixed backtest non-closed kline filtering. +- fixed margin order sync issue. + +## Minor + +- added sortino ratio. +- added strategy config printing support. + +[Full Changelog](https://github.com/c9s/bbgo/compare/v1.39.1...main) + + - [#889](https://github.com/c9s/bbgo/pull/889): service/backtest: check and filter kline by its endTime + - [#888](https://github.com/c9s/bbgo/pull/888): binance: fix futures/margin order sync issue + - [#886](https://github.com/c9s/bbgo/pull/886): Add Sortino ratio + - [#881](https://github.com/c9s/bbgo/pull/881): print strategy config diff --git a/doc/release/v1.40.1.md b/doc/release/v1.40.1.md new file mode 100644 index 0000000000..04e4a3b16c --- /dev/null +++ b/doc/release/v1.40.1.md @@ -0,0 +1,7 @@ +## Fixes + +- Fixed backtest fee mode when fee currency is not base or quote. + +[Full Changelog](https://github.com/c9s/bbgo/compare/v1.40.0...main) + + - [#917](https://github.com/c9s/bbgo/pull/917): backtest: fix backtest fee mode when fee currency is not base or quote diff --git a/doc/release/v1.40.2.md b/doc/release/v1.40.2.md new file mode 100644 index 0000000000..efa21f01fd --- /dev/null +++ b/doc/release/v1.40.2.md @@ -0,0 +1,13 @@ +## Fixes + +- Fixed pivot high indicator (used low, sorry :p) + +## Minor Features + +- Added G-H filter and Kalman filter. +- Write strategy config into the backtest report directory. + +[Full Changelog](https://github.com/c9s/bbgo/compare/v1.40.1...main) + + - [#918](https://github.com/c9s/bbgo/pull/918): feature: write strategy config in the backtest report directory + - [#915](https://github.com/c9s/bbgo/pull/915): feature: add G-H filter and Kalman filter diff --git a/doc/release/v1.40.3.md b/doc/release/v1.40.3.md new file mode 100644 index 0000000000..273127fe48 --- /dev/null +++ b/doc/release/v1.40.3.md @@ -0,0 +1,34 @@ + +## Fixes + +- Fixed order executor close position (check base balance for long position). +- Fixed TrendEMA for pivotshort. +- Fixed tradeStats counter for live trading. +- Handle binance listenKeyExpired event to reconnect. +- Improved order submit remapping issue and retry. + +## Interaction + +- Added telegram image support. +- Added /resetposition command. + +## Strategies + +- supertrend: added pnl image support. +- supertrend: use MA by day instead of by trade. +- elliotwave: renewal. + +[Full Changelog](https://github.com/c9s/bbgo/compare/v1.40.2...main) + + - [#932](https://github.com/c9s/bbgo/pull/932): feature: strategy/bolllmaker trend ema + - [#930](https://github.com/c9s/bbgo/pull/930): Fix: Pivotshort + - [#929](https://github.com/c9s/bbgo/pull/929): feature: strategy/supertrend: draw pnl on + - [#931](https://github.com/c9s/bbgo/pull/931): fix: binance listenkey expired + - [#928](https://github.com/c9s/bbgo/pull/928): refactor: refactor interact strategy filter functions + - [#927](https://github.com/c9s/bbgo/pull/927): refactor: simplify submit order + - [#926](https://github.com/c9s/bbgo/pull/926): fix: handle created orders before we retry + - [#925](https://github.com/c9s/bbgo/pull/925): feature: order executor open position method + - [#922](https://github.com/c9s/bbgo/pull/922): fix: types/tradeStats: use last order id to identity consecutive win and loss + - [#921](https://github.com/c9s/bbgo/pull/921): strategy/supertrend: use ma by day instead of by trade + - [#910](https://github.com/c9s/bbgo/pull/910): SerialMarketDataStore, elliottwave renewal + - [#919](https://github.com/c9s/bbgo/pull/919): feature: add fixedpoint.Reducer, Counter and add update stats method on TradeStats diff --git a/doc/release/v1.40.4.md b/doc/release/v1.40.4.md new file mode 100644 index 0000000000..368e0a6742 --- /dev/null +++ b/doc/release/v1.40.4.md @@ -0,0 +1,7 @@ +[Full Changelog](https://github.com/c9s/bbgo/compare/v1.40.3...main) + + - [#935](https://github.com/c9s/bbgo/pull/935): bbgo: add price check and add max leverage for cross margin + - [#937](https://github.com/c9s/bbgo/pull/937): notifier: redirect error, panic, fatal error to telegram + - [#936](https://github.com/c9s/bbgo/pull/936): bollmaker: fix settings overriding + - [#934](https://github.com/c9s/bbgo/pull/934): pnl: fix nil position point issue + - [#933](https://github.com/c9s/bbgo/pull/933): bollmaker: fix backward compatibility of dynamic spread settings diff --git a/doc/release/v1.41.0.md b/doc/release/v1.41.0.md new file mode 100644 index 0000000000..5842adb0b2 --- /dev/null +++ b/doc/release/v1.41.0.md @@ -0,0 +1,46 @@ +## Feature + +- Added auto-repay to protective stop loss exit method +- Added --lightweight mode option + +## Improvements + +- Removed the old submitOrder notification and added the new notification switches. +- Improved accumulated volume stop method +- binance: add queryTrades rate limiter + +## Fixes + +- Fixed net asset value calculation +- Fixed telegram message error, there must be one message to send + +## Strategies + +- drift: fixed drift minus weight, preloaded kline not enough +- trendtrader: added new trendline trader strategy. +- pivotshort: fix fast high filtering. +- pivotshort: call open position and add more position options. + +[Full Changelog](https://github.com/c9s/bbgo/compare/v1.40.4...main) + + - [#960](https://github.com/c9s/bbgo/pull/960): improve: improve the existing notification switch settings + - [#961](https://github.com/c9s/bbgo/pull/961): Feature: Add auto-repay to exit_protective_stop_loss + - [#958](https://github.com/c9s/bbgo/pull/958): strategy/pivotshort: more improvements + - [#957](https://github.com/c9s/bbgo/pull/957): bbgo: remove submitOrder notification + - [#956](https://github.com/c9s/bbgo/pull/956): improve: bbgo: use margin asset borrowable amount to adjust the quantity + - [#953](https://github.com/c9s/bbgo/pull/953): fix: drift minus weight, preloaded kline not enough + - [#950](https://github.com/c9s/bbgo/pull/950): strategy/pivotshort + - [#954](https://github.com/c9s/bbgo/pull/954): DOC: update slack.md + - [#942](https://github.com/c9s/bbgo/pull/942): feature: add modify tg command. fix wdrift ma length + - [#952](https://github.com/c9s/bbgo/pull/952): fix: fix profit stats calculation and since time field initialisation + - [#951](https://github.com/c9s/bbgo/pull/951): DOC: add marketcap doc + - [#920](https://github.com/c9s/bbgo/pull/920): strategy: add trend trader + - [#947](https://github.com/c9s/bbgo/pull/947): improve: accumulated volume stop method + - [#945](https://github.com/c9s/bbgo/pull/945): FEATURE: marketcap: get marketcap values from coinmarketcap + - [#946](https://github.com/c9s/bbgo/pull/946): bbgo: fix telegram message error, there must be one message to send + - [#944](https://github.com/c9s/bbgo/pull/944): fix: fix net asset calculation + - [#943](https://github.com/c9s/bbgo/pull/943): FIX: fix market error + - [#941](https://github.com/c9s/bbgo/pull/941): pivotshort: fix fast high filtering + - [#940](https://github.com/c9s/bbgo/pull/940): pivotshort: call open position and add more position options + - [#939](https://github.com/c9s/bbgo/pull/939): binance: add queryTrades rate limiter + - [#938](https://github.com/c9s/bbgo/pull/938): bbgo: add lightweight mode diff --git a/doc/release/v1.42.0.md b/doc/release/v1.42.0.md new file mode 100644 index 0000000000..c16b586528 --- /dev/null +++ b/doc/release/v1.42.0.md @@ -0,0 +1,43 @@ +## Improvements + +- added isolation context + +## Fixes + +- fixed order executor ClosePosition for futures. +- fixed backtest order quantity check. +- fixed max kline api +- fixed optimizer limit + +## Strategies + +- strategy/supertrend: fixed linreg baseline slope. +- strategy/drift: added more fixes and updates +- strategy/irr: added new strategy. +- strategy/harmonic: added new harmonic shark pattern strategy. +- strategy/marketcap: reduce frequency of querying data from coinmarketcap + +[Full Changelog](https://github.com/c9s/bbgo/compare/v1.41.0...main) + + - [#987](https://github.com/c9s/bbgo/pull/987): fix: supertrend-strategy: LinReg baseline slope wrongly calculated + - [#986](https://github.com/c9s/bbgo/pull/986): fix: general order executor: ClosePosition() works on futures position + - [#983](https://github.com/c9s/bbgo/pull/983): fix: backtest: add order quantity check + - [#982](https://github.com/c9s/bbgo/pull/982): refactor isolation context for persistence facade configuration + - [#981](https://github.com/c9s/bbgo/pull/981): fix optimizer limit + - [#976](https://github.com/c9s/bbgo/pull/976): strategy: add harmonic shark pattern recognition + - [#977](https://github.com/c9s/bbgo/pull/977): strategy: fix irr + - [#978](https://github.com/c9s/bbgo/pull/978): refactor: refactor isolation and add more tests + - [#979](https://github.com/c9s/bbgo/pull/979): fix: fix max kline api + - [#974](https://github.com/c9s/bbgo/pull/974): refactor persistence for isolation + - [#973](https://github.com/c9s/bbgo/pull/973): refactor/feature: add isolation context support + - [#972](https://github.com/c9s/bbgo/pull/972): fix/drift_stoploss + - [#970](https://github.com/c9s/bbgo/pull/970): refactor: extract stoploss, fix highest/lowest in trailingExit + - [#969](https://github.com/c9s/bbgo/pull/969): fix: reduce Quantity precheck, drift condition, ewo refactor + - [#959](https://github.com/c9s/bbgo/pull/959): stratgy: add irr + - [#968](https://github.com/c9s/bbgo/pull/968): feature: add config dump / param dump / param modify for elliottwave + - [#965](https://github.com/c9s/bbgo/pull/965): binance: fix futures order conversion + - [#967](https://github.com/c9s/bbgo/pull/967): fix: wrong tag in drift + - [#955](https://github.com/c9s/bbgo/pull/955): feature: marketcap: reduce frequency of querying data from coinmarketcap + - [#963](https://github.com/c9s/bbgo/pull/963): feature: limit how many metrics is shown by optimizer + - [#964](https://github.com/c9s/bbgo/pull/964): series.Filter + drift.openPosition + modify openPosition behavior + fix consts + - [#962](https://github.com/c9s/bbgo/pull/962): fix: add rate limit on telegram api and split messages by unicode diff --git a/doc/strategy/marketcap.md b/doc/strategy/marketcap.md new file mode 100644 index 0000000000..aae4623c6e --- /dev/null +++ b/doc/strategy/marketcap.md @@ -0,0 +1,28 @@ +### Marketcap Strategy + +This strategy will rebalance your portfolio according to the market capitalization from coinmarketcap. + +### Prerequisite + +Setup your `COINMARKETCAP_API_KEY` in your environment variables. + +#### Parameters + +- `interval` + - The interval to rebalance your portfolio, e.g., `5m`, `1h` +- `quoteCurrency` + - The quote currency of your portfolio, e.g., `USDT`, `TWD`. +- `quoteCurrencyWeight` + - The weight of the quote currency in your portfolio. The rest of the weight will be distributed to other currencies by market capitalization. +- `baseCurrencies` + - A list of currencies you want to hold in your portfolio. +- `threshold` + - The threshold of the difference between the current weight and the target weight to trigger rebalancing. For example, if the threshold is `1%` and the current weight of `BTC` is `52%` and the target weight is `50%` then the strategy will sell `BTC` until it reaches `50%`. +- `dryRun` + - If `true`, then the strategy will not place orders. +- `maxAmount` + - The maximum amount of each order in quote currency. + +#### Examples + +See [marketcap.yaml](../../config/marketcap.yaml) diff --git a/doc/strategy/supertrend.md b/doc/strategy/supertrend.md new file mode 100644 index 0000000000..76fd08fd8b --- /dev/null +++ b/doc/strategy/supertrend.md @@ -0,0 +1,51 @@ +### Supertrend Strategy + +This strategy uses Supertrend indicator as trend, and DEMA indicator as noise filter. +Supertrend strategy needs margin enabled in order to submit short orders, and you can use `leverage` parameter to limit your risk. +**Please note, using leverage higher than 1 is highly risky.** + + +#### Parameters + +- `symbol` + - The trading pair symbol, e.g., `BTCUSDT`, `ETHUSDT` +- `interval` + - The K-line interval, e.g., `5m`, `1h` +- `leverage` + - The leverage of the orders. +- `fastDEMAWindow` + - The MA window of the fast DEMA. +- `slowDEMAWindow` + - The MA window of the slow DEMA. +- `superTrend` + - Supertrend indicator for deciding current trend. + - `averageTrueRangeWindow` + - The MA window of the ATR indicator used by Supertrend. + - `averageTrueRangeMultiplier` + - Multiplier for calculating upper and lower bond prices, the higher, the stronger the trends are, but also makes it less sensitive. +- `linearRegression` + - Use linear regression as trend confirmation + - `interval` + - Time interval of linear regression + - `window` + - Window of linear regression +- `takeProfitAtrMultiplier` + - TP according to ATR multiple, 0 to disable this. +- `stopLossByTriggeringK` + - Set SL price to the low/high of the triggering Kline. +- `stopByReversedSupertrend` + - TP/SL by reversed supertrend signal. +- `stopByReversedDema` + - TP/SL by reversed DEMA signal. +- `stopByReversedLinGre` + - TP/SL by reversed linear regression signal. +- `exits` + - Exit methods to TP/SL + - `roiStopLoss` + - The stop loss percentage of the position ROI (currently the price change) + - `percentage` + + +#### Examples + +See [supertrend.yaml](../../config/supertrend.yaml) \ No newline at end of file diff --git a/doc/topics/back-testing.md b/doc/topics/back-testing.md index 5cff0164d9..058a2a8d60 100644 --- a/doc/topics/back-testing.md +++ b/doc/topics/back-testing.md @@ -17,8 +17,18 @@ backtest: # the symbol data that you want to sync and back-test symbols: - BTCUSDT + + sessions: + - binance + + # feeMode is optional + # valid values are: quote, native, token + # quote: always deduct fee from the quote balance + # native: the crypto exchange fee deduction, base fee for buy order, quote fee for sell order. + # token: count fee as crypto exchange fee token + # feeMode: quote - account: + accounts: # the initial account balance you want to start with binance: # exchange name balances: @@ -33,6 +43,12 @@ Note on date formats, the following date formats are supported: And then, you can sync remote exchange k-lines (candle bars) data for back-testing: +```sh +bbgo backtest -v --sync --config config/grid.yaml +``` + +To customize the sync data range, add `--sync-from`: + ```sh bbgo backtest -v --sync --sync-only --sync-from 2020-11-01 --config config/grid.yaml ``` diff --git a/doc/topics/developing-strategy.md b/doc/topics/developing-strategy.md new file mode 100644 index 0000000000..aed7dd915c --- /dev/null +++ b/doc/topics/developing-strategy.md @@ -0,0 +1,565 @@ +# Developing Strategy + +There are two types of strategies in BBGO: + +1. built-in strategy: like grid, bollmaker, pricealert strategies, which are included in the pre-compiled binary. +2. external strategy: custom or private strategies that you don't want to expose to public. + +For built-in strategies, they are placed in `pkg/strategy` of the BBGO source repository. + +For external strategies, you can create a private repository as an isolated go package and place your strategy inside +it. + +In general, strategies are Go struct, defined in the Go package. + +## Quick Start + +To add your first strategy, the fastest way is to add it as a built-in strategy. + +Simply edit `pkg/cmd/strategy/builtin.go` and import your strategy package there. + +When BBGO starts, the strategy will be imported as a package, and register its struct to the engine. + +You can also create a new file called `pkg/cmd/strategy/short.go` and import your strategy package. + +``` +import ( + _ "github.com/c9s/bbgo/pkg/strategy/short" +) +``` + +Create a directory for your new strategy in the BBGO source code repository: + +```shell +mkdir -p pkg/strategy/short +``` + +Open a new file at `pkg/strategy/short/strategy.go` and paste the simplest strategy code: + +```go +package short + +import ( + "context" + "fmt" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/types" +) + +const ID = "short" + +func init() { + // Register our struct type to BBGO + // Note that you don't need to field the fields. + // BBGO uses reflect to parse your type information. + bbgo.RegisterStrategy(ID, &Strategy{}) +} + +type Strategy struct { + Symbol string `json:"symbol"` + Interval types.Interval `json:"interval"` +} + +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) +} + +func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + session.MarketDataStream.OnKLineClosed(func(k types.KLine) { + fmt.Println(k) + }) + return nil +} +``` + +This is the most simple strategy with only ~30 lines code, it subscribes to the kline channel with the given symbol from +the config, And when the kline is closed, it prints the kline to the console. + +Note that, when Run() is executed, the user data stream is not connected to the exchange yet, but the history market +data is already loaded, so if you need to submit an order on start, be sure to write your order submit code inside the +event closures like `OnKLineClosed` or `OnStart`. + +Now you can prepare your config file, create a file called `bbgo.yaml` with the following content: + +```yaml +exchangeStrategies: +- on: binance + short: + symbol: ETHUSDT + interval: 1m +``` + +And then, you should be able to run this strategy by running the following command: + +```shell +go run ./cmd/bbgo run +``` + +## The Strategy Struct + +BBGO loads the YAML config file and re-unmarshal the settings into your struct as JSON string, so you can define the +json tag to get the settings from the YAML config. + +For example, if you're writing a strategy in a package called `short`, to load the following config: + +```yaml +externalStrategies: +- on: binance + short: + symbol: BTCUSDT +``` + +You can write the following struct to load the symbol setting: + +```go +package short + +type Strategy struct { + Symbol string `json:"symbol"` +} + +``` + +To use the Symbol setting, you can get the value from the Run method of the strategy: + +```go +func (s *Strategy) Run(ctx context.Context, session *bbgo.ExchangeSession) error { + // you need to import the "log" package + log.Println("%s", s.Symbol) + return nil +} +``` + +Now you have the Go struct and the Go package, but BBGO does not know your strategy, so you need to register your +strategy. + +Define an ID const in your package: + +```go +const ID = "short" +``` + +Then call bbgo.RegisterStrategy with the ID you just defined and a struct reference: + +```go +func init() { + bbgo.RegisterStrategy(ID, &Strategy{}) +} +``` + +Note that you don't need to fill the fields in the struct, BBGO just need to know the type of struct. + +(BBGO use reflect to parse the fields from the given struct and allocate a new struct object from the given struct type +internally) + +## Exchange Session + +The `*bbgo.ExchangeSession` represents a connectivity to a crypto exchange, it's also a hub that connects to everything +you need, for example, standard indicators, account information, balance information, market data stream, user data +stream, exchange APIs, and so on. + +By default, BBGO checks the environment variables that you defined to detect which exchange session to be created. + +For example, environment variables like `BINANCE_API_KEY`, `BINANCE_API_SECRET` will be transformed into an exchange +session that connects to Binance. + +You can not only connect to multiple different crypt exchanges, but also create multiple sessions to the same crypto +exchange with few different options. + +To do that, add the following section to your `bbgo.yaml` config file: + +```yaml +--- +sessions: + binance: + exchange: binance + envVarPrefix: binance + binance_cross_margin: + exchange: binance + envVarPrefix: binance + margin: true + binance_margin_ethusdt: + exchange: binance + envVarPrefix: binance + margin: true + isolatedMargin: true + isolatedMarginSymbol: ETHUSDT + okex1: + exchange: okex + envVarPrefix: okex + okex2: + exchange: okex + envVarPrefix: okex +``` + +You can specify which exchange session you want to mount for each strategy in the config file, it's quiet simple: + +```yaml +exchangeStrategies: + +- on: binance_margin_ethusdt + short: + symbol: ETHUSDT + +- on: binance_margin + foo: + symbol: ETHUSDT + +- on: binance + bar: + symbol: ETHUSDT +``` + +## Market Data Stream and User Data Stream + +When BBGO connects to the exchange, it allocates two stream objects for different purposes. + +They are: + +- MarketDataStream receives market data from the exchange, for example, KLine data (candlestick, or bars), market public + trades. +- UserDataStream receives your personal trading data, for example, orders, executed trades, balance updates and other + private information. + +To add your market data subscription to the `MarketDataStream`, you can register your subscription in the `Subscribe` of +the strategy code, for example: + +```go +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: "1m"}) +} +``` + +Since the back-test engine is a kline-based engine, to subscribe market trades, you need to check if you're in the +back-test environment: + +```go +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { + if !bbgo.IsBackTesting { + session.Subscribe(types.MarketTradeChannel, s.Symbol, types.SubscribeOptions{}) + } +} +``` + +To receive the market data from the market data stream, you need to register the event callback: + +```go +func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { + // handle closed kline event here + }) + session.MarketDataStream.OnMarketTrade(func(trade types.Trade) { + // handle market trade event here + }) +} +``` + +In the above example, we register our event callback to the market data stream of the current exchange session, The +market data stream object here is a session-wide market data stream, so it's shared with other strategies that are also +using the same exchange session, so you might receive kline with different symbol or interval. + +It's better to add a condition to filter the kline events: + +```go +func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { + if kline.Symbol != s.Symbol || kline.Interval != s.Interval { + return + } + // handle your kline here + }) +} +``` + +You can also use the KLineWith method to wrap your kline closure with the filter condition: + +```go +func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + session.MarketDataStream.OnKLineClosed(types.KLineWith("BTCUSDT", types.Interval1m, func(kline types.KLine) { + // handle your kline here + }) +} +``` + +Note that, when the Run() method is executed, the user data stream and market data stream are not connected yet. + +## Submitting Orders + +To place an order, you can call `SubmitOrders` exchange API: + +```go +createdOrders, err := session.Exchange.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: "BTCUSDT", + Type: types.OrderTypeLimit, + Price: fixedpoint.NewFromFloat(18000.0), + Quantity: fixedpoint.NewFromFloat(1.0), +}) +if err != nil { + log.WithError(err).Errorf("can not submit orders") +} + +log.Infof("createdOrders: %+v", createdOrders) +``` + +There are some pre-defined order types you can use: + +- `types.OrderTypeLimit` +- `types.OrderTypeMarket` +- `types.OrderTypeStopMarket` +- `types.OrderTypeStopLimit` +- `types.OrderTypeLimitMaker` - forces the order to be a maker. + +Although it's crypto market, the above order types are actually derived from the stock market: + +A limit order is an order to buy or sell a stock with a restriction on the maximum price to be paid or the minimum price +to be received (the "limit price"). If the order is filled, it will only be at the specified limit price or better. +However, there is no assurance of execution. A limit order may be appropriate when you think you can buy at a price +lower than--or sell at a price higher than -- the current quote. + +A market order is an order to buy or sell a stock at the market's current best available price. A market order typically +ensures an execution, but it does not guarantee a specified price. Market orders are optimal when the primary goal is to +execute the trade immediately. A market order is generally appropriate when you think a stock is priced right, when you +are sure you want a fill on your order, or when you want an immediate execution. + +A stop order is an order to buy or sell a stock at the market price once the stock has traded at or through a specified +price (the "stop price"). If the stock reaches the stop price, the order becomes a market order and is filled at the +next available market price. + +## UserDataStream + +UserDataStream is an authenticated connection to the crypto exchange. You can receive the following data type from the +user data stream: + +- OrderUpdate +- TradeUpdate +- BalanceUpdate + +When you submit an order to the exchange, you might want to know when the order is filled or not, user data stream is +the real time notification let you receive the order update event. + +To get the order update from the user data stream: + +```go +session.UserDataStream.OnOrderUpdate(func(order types.Order) { + if order.Status == types.OrderStatusFilled { + log.Infof("your order is filled: %+v", order) + } +}) +``` + +However, order update only contains status, price, quantity of the order, if you're submitting market order, you won't know +the actual price of the order execution. + +One order can be filled by different size trades from the market, by collecting the trades, you can calculate the +average price of the order execution and the total trading fee that you used for the order. + +If you need to get the details of the trade execution. you need the trade update event: + +```go +session.UserDataStream.OnTrade(func(trade types.Trade) { + log.Infof("trade price %f, fee %f %s", trade.Price.Float64(), trade.Fee.Float64(), trade.FeeCurrency) +}) +``` + +To monitor your balance change, you can use the balance update event callback: + +```go +session.UserDataStream.OnBalanceUpdate(func(balances types.BalanceMap) { + log.Infof("balance update: %+v", balances) +}) +``` + +Note that, as we mentioned above, the user data stream is a session-wide stream, that means you might receive the order update event for other strategies. + +To prevent that, you need to manage your active order for your strategy: + +```go +activeBook := bbgo.NewActiveOrderBook("BTCUSDT") +activeBook.Bind(session.UserDataStream) +``` + +Then, when you create some orders, you can register your order to the active order book, so that it can manage the order +update: + +```go +createdOrders, err := session.Exchange.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: "BTCUSDT", + Type: types.OrderTypeLimit, + Price: fixedpoint.NewFromFloat(18000.0), + Quantity: fixedpoint.NewFromFloat(1.0), +}) +if err != nil { + log.WithError(err).Errorf("can not submit orders") +} + +activeBook.Add(createdOrders...) +``` + +## Notification + +You can use the notification API to send notification to Telegram or Slack: + +```go +bbgo.Notify(message) +bbgo.Notify(message, objs...) +bbgo.Notify(format, arg1, arg2, arg3, objs...) +bbgo.Notify(object, object2, object3) +``` + +Note that, if you're using the third format, simple arguments (float, bool, string... etc) will be used for calling the +fmt.Sprintf, and the extra arguments will be rendered as attachments. + +For example: + +```go +bbgo.Notify("%s found support price: %f", "BTCUSDT", 19000.0, kline) +``` + +The above call will render the first format string with the given float number 19000, and then attach the kline object as the attachment. + +## Handling Trades and Profit + +In order to manage the trades and orders for each strategy, BBGO designed an order executor API that helps you collect +the related trades and orders from the strategy, so trades from other strategies won't bother your logics. + +To do that, you can use the *bbgo.GeneralOrderExecutor: + +```go +var profitStats = types.NewProfitStats(s.Market) +var position = types.NewPositionFromMarket(s.Market) +var tradeStats = &types.TradeStats{} +orderExecutor := bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, position) + +// bind the trade events to update the profit stats +orderExecutor.BindProfitStats(profitStats) + +// bind the trade events to update the trade stats +orderExecutor.BindTradeStats(tradeStats) +orderExecutor.Bind() +``` + +## Graceful Shutdown + +When BBGO shuts down, you might want to clean up your open orders for your strategy, to do that, you can use the +OnShutdown API to register your handler. + +```go +bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + defer wg.Done() + + _, _ = fmt.Fprintln(os.Stderr, s.TradeStats.String()) + if err := s.orderExecutor.GracefulCancel(ctx) ; err != nil { + log.WithError(err).Error("graceful cancel order error") + } +}) +``` + +## Persistence + +When you need to adjust the parameters and restart BBGO process, everything in the memory will be reset after the +restart, how can we keep these data? + +Although BBGO is written in Golang, BBGO provides a useful dynamic system to help you persist your data. + +If you have some state needs to preserve before shutting down, you can simply add the `persistence` struct tag to the field, +and BBGO will automatically save and restore your state. For example, + +```go +type Strategy struct { + Position *types.Position `persistence:"position"` + ProfitStats *types.ProfitStats `persistence:"profit_stats"` + TradeStats *types.TradeStats `persistence:"trade_stats"` +} +``` + +And remember to add the `persistence` section in your bbgo.yaml config: + +```yaml +persistence: + redis: + host: 127.0.0.1 + port: 6379 + db: 0 +``` + +In the Run method of your strategy, you need to check if these fields are nil, and you need to initialize them: + +```go + if s.Position == nil { + s.Position = types.NewPositionFromMarket(s.Market) + } + + if s.ProfitStats == nil { + s.ProfitStats = types.NewProfitStats(s.Market) + } + + if s.TradeStats == nil { + s.TradeStats = types.NewTradeStats(s.Symbol) + } +``` + +That's it. Hit Ctrl-C and you should see BBGO saving your strategy states. + + +## Exit Method Set + +To integrate the built-in exit methods into your strategy, simply add a field with type bbgo.ExitMethodSet: + +```go +type Strategy struct { + ExitMethods bbgo.ExitMethodSet `json:"exits"` +} + +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { + s.ExitMethods.SetAndSubscribe(session, s) +} + +func (s *Strategy) Run() { + s.ExitMethods.Bind(session, s.orderExecutor) +} +``` + +And then you can use the following config structure to configure your exit settings like this: + +```yaml +- on: binance + pivotshort: + exits: + # (0) roiStopLoss is the stop loss percentage of the position ROI (currently the price change) + - roiStopLoss: + percentage: 0.8% + + # (1) roiTakeProfit is used to force taking profit by percentage of the position ROI (currently the price change) + # force to take the profit ROI exceeded the percentage. + - roiTakeProfit: + percentage: 35% + + # (2) protective stop loss -- short term + - protectiveStopLoss: + activationRatio: 0.6% + stopLossRatio: 0.1% + placeStopOrder: false + + # (3) protective stop loss -- long term + - protectiveStopLoss: + activationRatio: 5% + stopLossRatio: 1% + placeStopOrder: false + + # (4) lowerShadowTakeProfit is used to taking profit when the (lower shadow height / low price) > lowerShadowRatio + # you can grab a simple stats by the following SQL: + # SELECT ((close - low) / close) AS shadow_ratio FROM binance_klines WHERE symbol = 'ETHUSDT' AND `interval` = '5m' AND start_time > '2022-01-01' ORDER BY shadow_ratio DESC LIMIT 20; + - lowerShadowTakeProfit: + interval: 30m + window: 99 + ratio: 3% + + # (5) cumulatedVolumeTakeProfit is used to take profit when the cumulated quote volume from the klines exceeded a threshold + - cumulatedVolumeTakeProfit: + interval: 5m + window: 2 + minQuoteVolume: 200_000_000 + + +``` diff --git a/examples/create-self-trade/main.go b/examples/create-self-trade/main.go deleted file mode 100644 index 42c3d236c3..0000000000 --- a/examples/create-self-trade/main.go +++ /dev/null @@ -1,140 +0,0 @@ -package main - -import ( - "context" - "fmt" - "math" - "strings" - "syscall" - "time" - - "github.com/joho/godotenv" - log "github.com/sirupsen/logrus" - "github.com/spf13/cobra" - "github.com/spf13/viper" - - "github.com/c9s/bbgo/pkg/cmd/cmdutil" - "github.com/c9s/bbgo/pkg/types" -) - -func init() { - rootCmd.PersistentFlags().String("exchange", "binance", "exchange name") - rootCmd.PersistentFlags().String("symbol", "SANDUSDT", "symbol") -} - -var rootCmd = &cobra.Command{ - Use: "create-self-trade", - Short: "this program creates the self trade by getting the market ticker", - - // SilenceUsage is an option to silence usage when an error occurs. - SilenceUsage: true, - - RunE: func(cmd *cobra.Command, args []string) error { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - if err := godotenv.Load(".env.local"); err != nil { - log.Fatal(err) - } - - symbol, err := cmd.Flags().GetString("symbol") - if err != nil { - return err - } - - exchangeNameStr, err := cmd.Flags().GetString("exchange") - if err != nil { - return err - } - - exchangeName, err := types.ValidExchangeName(exchangeNameStr) - if err != nil { - return err - } - - exchange, err := cmdutil.NewExchange(exchangeName) - if err != nil { - return err - } - - markets, err := exchange.QueryMarkets(ctx) - if err != nil { - return err - } - - market, ok := markets[symbol] - if !ok { - return fmt.Errorf("market %s is not defined", symbol) - } - - stream := exchange.NewStream() - stream.OnTradeUpdate(func(trade types.Trade) { - log.Infof("trade: %+v", trade) - }) - - log.Info("connecting websocket...") - if err := stream.Connect(ctx); err != nil { - log.Fatal(err) - } - - time.Sleep(time.Second) - - ticker, err := exchange.QueryTicker(ctx, symbol) - if err != nil { - log.Fatal(err) - } - - price := ticker.Buy + market.TickSize - - if int64(ticker.Sell*1e8) == int64(price*1e8) { - log.Fatal("zero spread, can not continue") - } - - quantity := math.Max(market.MinNotional/price, market.MinQuantity) * 1.1 - - log.Infof("submiting order using quantity %f at price %f", quantity, price) - - createdOrders, err := exchange.SubmitOrders(ctx, []types.SubmitOrder{ - { - Symbol: symbol, - Market: market, - Side: types.SideTypeBuy, - Type: types.OrderTypeLimit, - Price: price, - Quantity: quantity, - TimeInForce: "GTC", - }, - { - Symbol: symbol, - Market: market, - Side: types.SideTypeSell, - Type: types.OrderTypeLimit, - Price: price, - Quantity: quantity, - TimeInForce: "GTC", - }, - }...) - - if err != nil { - return err - } - - log.Info(createdOrders) - - cmdutil.WaitForSignal(ctx, syscall.SIGINT, syscall.SIGTERM) - return nil - }, -} - -func main() { - viper.AutomaticEnv() - viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) - - if err := viper.BindPFlags(rootCmd.PersistentFlags()); err != nil { - log.WithError(err).Error("bind pflags error") - } - - if err := rootCmd.ExecuteContext(context.Background()); err != nil { - log.WithError(err).Error("cmd error") - } -} diff --git a/examples/binance-book/main.go b/examples/exchange-api/binance-book/main.go similarity index 100% rename from examples/binance-book/main.go rename to examples/exchange-api/binance-book/main.go diff --git a/examples/binance-margin/main.go b/examples/exchange-api/binance-margin/main.go similarity index 90% rename from examples/binance-margin/main.go rename to examples/exchange-api/binance-margin/main.go index dafaba3312..ed8604353a 100644 --- a/examples/binance-margin/main.go +++ b/examples/exchange-api/binance-margin/main.go @@ -14,6 +14,7 @@ import ( "github.com/c9s/bbgo/pkg/cmd/cmdutil" "github.com/c9s/bbgo/pkg/exchange/binance" + "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) @@ -70,7 +71,7 @@ var rootCmd = &cobra.Command{ return fmt.Errorf("market %s is not defined", symbol) } - marginAccount, err := exchange.QueryMarginAccount(ctx) + marginAccount, err := exchange.QueryCrossMarginAccount(ctx) if err != nil { return err } @@ -93,13 +94,13 @@ var rootCmd = &cobra.Command{ time.Sleep(time.Second) - createdOrders, err := exchange.SubmitOrders(ctx, types.SubmitOrder{ + createdOrder, err := exchange.SubmitOrder(ctx, types.SubmitOrder{ Symbol: symbol, Market: market, Side: types.SideTypeBuy, Type: types.OrderTypeLimit, - Price: price, - Quantity: quantity, + Price: fixedpoint.NewFromFloat(price), + Quantity: fixedpoint.NewFromFloat(quantity), MarginSideEffect: types.SideEffectTypeMarginBuy, TimeInForce: "GTC", }) @@ -107,7 +108,7 @@ var rootCmd = &cobra.Command{ return err } - log.Info(createdOrders) + log.Info(createdOrder) cmdutil.WaitForSignal(ctx, syscall.SIGINT, syscall.SIGTERM) return nil diff --git a/examples/interact/main.go b/examples/interact/main.go index 650f3299a2..98a2fc0fe3 100644 --- a/examples/interact/main.go +++ b/examples/interact/main.go @@ -36,11 +36,12 @@ type closePositionTask struct { confirmed bool } -type PositionInteraction struct { +type positionInteraction struct { closePositionTask closePositionTask } -func (m *PositionInteraction) Commands(i *interact.Interact) { +// Commands implements the custom interaction +func (m *positionInteraction) Commands(i *interact.Interact) { i.Command("/closePosition", "", func(reply interact.Reply) error { // send symbol options reply.Message("Choose your position") @@ -142,7 +143,7 @@ func main() { Token: "123", }) - interact.AddCustomInteraction(&PositionInteraction{}) + interact.AddCustomInteraction(&positionInteraction{}) if err := interact.Start(ctx); err != nil { log.Fatal(err) } diff --git a/examples/kucoin-accounts/main.go b/examples/kucoin-accounts/main.go index 669658ca2c..da4265d6cc 100644 --- a/examples/kucoin-accounts/main.go +++ b/examples/kucoin-accounts/main.go @@ -38,7 +38,7 @@ var rootCmd = &cobra.Command{ }, } -var client *kucoinapi.RestClient = nil +var client *kucoinapi.RestClient func main() { if _, err := os.Stat(".env.local"); err == nil { diff --git a/examples/kucoin/accounts.go b/examples/kucoin/accounts.go index 087a5a4cc0..7e09e13cc6 100644 --- a/examples/kucoin/accounts.go +++ b/examples/kucoin/accounts.go @@ -39,4 +39,3 @@ var accountsCmd = &cobra.Command{ return nil }, } - diff --git a/examples/kucoin/orders.go b/examples/kucoin/orders.go index 8e13b874de..4acb01cbee 100644 --- a/examples/kucoin/orders.go +++ b/examples/kucoin/orders.go @@ -32,7 +32,6 @@ func init() { ordersCmd.AddCommand(historyOrdersCmd) } - // go run ./examples/kucoin orders var ordersCmd = &cobra.Command{ Use: "orders", @@ -73,7 +72,6 @@ var ordersCmd = &cobra.Command{ }, } - // go run ./examples/kucoin orders history var historyOrdersCmd = &cobra.Command{ Use: "history [--symbol SYMBOL]", @@ -105,7 +103,6 @@ var historyOrdersCmd = &cobra.Command{ }, } - // usage: // go run ./examples/kucoin orders place --symbol LTC-USDT --price 50 --size 1 --order-type limit --side buy var placeOrderCmd = &cobra.Command{ @@ -124,14 +121,12 @@ var placeOrderCmd = &cobra.Command{ req.OrderType(kucoinapi.OrderType(orderType)) - side, err := cmd.Flags().GetString("side") if err != nil { return err } req.Side(kucoinapi.SideType(side)) - symbol, err := cmd.Flags().GetString("symbol") if err != nil { return err @@ -155,7 +150,6 @@ var placeOrderCmd = &cobra.Command{ } - size, err := cmd.Flags().GetString("size") if err != nil { return err @@ -172,8 +166,6 @@ var placeOrderCmd = &cobra.Command{ }, } - - // usage: var cancelOrderCmd = &cobra.Command{ Use: "cancel", diff --git a/examples/kucoin/symbols.go b/examples/kucoin/symbols.go index 52656fdef2..dd2420ce22 100644 --- a/examples/kucoin/symbols.go +++ b/examples/kucoin/symbols.go @@ -25,4 +25,3 @@ var symbolsCmd = &cobra.Command{ return nil }, } - diff --git a/examples/kucoin/tickers.go b/examples/kucoin/tickers.go index 6b7b060c65..0da748783e 100644 --- a/examples/kucoin/tickers.go +++ b/examples/kucoin/tickers.go @@ -36,7 +36,6 @@ var tickersCmd = &cobra.Command{ logrus.Infof("ticker: %+v", ticker) - tickerStats, err := client.MarketDataService.GetTicker24HStat(args[0]) if err != nil { return err @@ -46,4 +45,3 @@ var tickersCmd = &cobra.Command{ return nil }, } - diff --git a/examples/kucoin/websocket.go b/examples/kucoin/websocket.go index 287d0e2e7c..d522637d7c 100644 --- a/examples/kucoin/websocket.go +++ b/examples/kucoin/websocket.go @@ -77,16 +77,16 @@ var websocketCmd = &cobra.Command{ id := time.Now().UnixNano() / int64(time.Millisecond) wsCmds := []kucoin.WebSocketCommand{ /* - { - Id: id+1, - Type: "subscribe", - Topic: "/market/ticker:ETH-USDT", - PrivateChannel: false, - Response: true, - }, + { + Id: id+1, + Type: "subscribe", + Topic: "/market/ticker:ETH-USDT", + PrivateChannel: false, + Response: true, + }, */ { - Id: id+2, + Id: id + 2, Type: "subscribe", Topic: "/market/candles:ETH-USDT_1min", PrivateChannel: false, @@ -131,7 +131,6 @@ var websocketCmd = &cobra.Command{ logrus.WithError(err).Error("websocket ping error", err) } - case <-interrupt: logrus.Infof("interrupt") @@ -144,8 +143,8 @@ var websocketCmd = &cobra.Command{ } select { - case <-done: - case <-time.After(time.Second): + case <-done: + case <-time.After(time.Second): } return nil } diff --git a/examples/max-orders/main.go b/examples/max-orders/main.go deleted file mode 100644 index 7b97ede8c2..0000000000 --- a/examples/max-orders/main.go +++ /dev/null @@ -1,25 +0,0 @@ -package main - -import ( - "log" - "os" - - maxapi "github.com/c9s/bbgo/pkg/exchange/max/maxapi" -) - -func main() { - key := os.Getenv("MAX_API_KEY") - secret := os.Getenv("MAX_API_SECRET") - - maxRest := maxapi.NewRestClient(maxapi.ProductionAPIURL) - maxRest.Auth(key, secret) - - orders, err := maxRest.OrderService.All("maxusdt", 100, 1, maxapi.OrderStateDone) - if err != nil { - log.Fatal(err) - } - - for _, order := range orders { - log.Printf("%+v", order) - } -} diff --git a/examples/okex-book/main.go b/examples/okex-book/main.go index dc9715c443..77218d81c4 100644 --- a/examples/okex-book/main.go +++ b/examples/okex-book/main.go @@ -69,7 +69,6 @@ var rootCmd = &cobra.Command{ log.Infof("%+v", account) - log.Infof("ASSET BALANCES:") assetBalances, err := client.AssetBalances() if err != nil { diff --git a/frontend/api/bbgo.js b/frontend/api/bbgo.js deleted file mode 100644 index 79af07600a..0000000000 --- a/frontend/api/bbgo.js +++ /dev/null @@ -1,114 +0,0 @@ -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(baseURL + '/api/outbound-ip').then(response => { - cb(response.data.outboundIP) - }); -} - -export function querySyncStatus(cb) { - return axios.get(baseURL + '/api/environment/syncing').then(response => { - cb(response.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(baseURL + '/api/strategies/single').then(response => { - cb(response.data.strategies || []) - }); -} - - -export function querySessions(cb) { - return axios.get(baseURL + '/api/sessions', {}) - .then(response => { - cb(response.data.sessions || []) - }); -} - -export function querySessionSymbols(sessionName, cb) { - return axios.get(baseURL + `/api/sessions/${sessionName}/symbols`, {}) - .then(response => { - cb(response.data.symbols || []) - }); -} - -export function queryTrades(params, cb) { - axios.get(baseURL + '/api/trades', {params: params}) - .then(response => { - cb(response.data.trades || []) - }); -} - -export function queryClosedOrders(params, cb) { - axios.get(baseURL + '/api/orders/closed', {params: params}) - .then(response => { - cb(response.data.orders || []) - }); -} - -export function queryAssets(cb) { - axios.get(baseURL + '/api/assets', {}) - .then(response => { - cb(response.data.assets || []) - }); -} - -export function queryTradingVolume(params, cb) { - axios.get(baseURL + '/api/trading-volume', {params: params}) - .then(response => { - cb(response.data.tradingVolumes || []) - }); -} - - diff --git a/frontend/components/AddExchangeSessionForm.js b/frontend/components/AddExchangeSessionForm.js deleted file mode 100644 index 027fb15675..0000000000 --- a/frontend/components/AddExchangeSessionForm.js +++ /dev/null @@ -1,316 +0,0 @@ -import React from 'react'; -import Grid from '@material-ui/core/Grid'; -import Box from '@material-ui/core/Box'; -import Button from '@material-ui/core/Button'; -import Typography from '@material-ui/core/Typography'; -import TextField from '@material-ui/core/TextField'; -import FormControlLabel from '@material-ui/core/FormControlLabel'; -import FormHelperText from '@material-ui/core/FormHelperText'; -import InputLabel from '@material-ui/core/InputLabel'; -import FormControl from '@material-ui/core/FormControl'; -import InputAdornment from '@material-ui/core/InputAdornment'; -import IconButton from '@material-ui/core/IconButton'; - -import Checkbox from '@material-ui/core/Checkbox'; -import Select from '@material-ui/core/Select'; -import MenuItem from '@material-ui/core/MenuItem'; -import FilledInput from '@material-ui/core/FilledInput'; - -import Alert from '@material-ui/lab/Alert'; -import VisibilityOff from '@material-ui/icons/VisibilityOff'; -import Visibility from '@material-ui/icons/Visibility'; - -import {addSession, testSessionConnection} from '../api/bbgo'; - -import {makeStyles} from '@material-ui/core/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 ( - - - Add Exchange Session - - - - - - Exchange - - - - - - { - setSessionName(event.target.value) - }} - value={sessionName} - /> - - - - { - setCustomSessionName(event.target.checked); - }} value="1"/>} - label="Custom exchange session name" - /> - - By default, the session name will be the exchange type name, - e.g. binance or max.
- If you're using multiple exchange sessions, you might need to custom the session name.
- This is for advanced users. -
-
- - - - API Key - - { setShowApiKey(!showApiKey) }} - onMouseDown={(event) => { event.preventDefault() }} - edge="end" - > - {showApiKey ? : } - - - } - onChange={(event) => { - setApiKey(event.target.value) - resetTestResponse() - }} - /> - - - - - - - API Secret - - { setShowApiSecret(!showApiSecret) }} - onMouseDown={(event) => { event.preventDefault() }} - edge="end" - > - {showApiSecret ? : } - - - } - onChange={(event) => { - setApiSecret(event.target.value) - resetTestResponse() - }} - /> - - - - {exchangeType === "binance" ? ( - - { - setIsMargin(event.target.checked); - resetTestResponse(); - }} value="1"/>} - label="Use margin trading." - /> - This is only available for Binance. Please use the - leverage at your own risk. - - { - setIsIsolatedMargin(event.target.checked); - resetTestResponse() - }} value="1"/>} - label="Use isolated margin trading." - /> - This is only available for Binance. If this is - set, you can only trade one symbol with one session. - - {isIsolatedMargin ? - { - setIsolatedMarginSymbol(event.target.value); - resetTestResponse() - }} - fullWidth - required - /> - : null} - - ) : null} -
- -
- - - - - -
- - { - testResponse ? testResponse.error ? ( - - {testResponse.error} - - ) : testResponse.success ? ( - - Connection Test Succeeded - - ) : null : null - } - - { - response ? response.error ? ( - - {response.error} - - ) : response.success ? ( - - Exchange Session Added - - ) : null : null - } - - -
- ); -} diff --git a/frontend/components/ConfigureDatabaseForm.js b/frontend/components/ConfigureDatabaseForm.js deleted file mode 100644 index e9dd377029..0000000000 --- a/frontend/components/ConfigureDatabaseForm.js +++ /dev/null @@ -1,196 +0,0 @@ -import React from 'react'; -import Grid from '@material-ui/core/Grid'; -import Box from '@material-ui/core/Box'; -import Button from '@material-ui/core/Button'; -import Typography from '@material-ui/core/Typography'; -import TextField from '@material-ui/core/TextField'; -import FormHelperText from '@material-ui/core/FormHelperText'; -import Radio from '@material-ui/core/Radio'; -import RadioGroup from '@material-ui/core/RadioGroup'; -import FormControlLabel from '@material-ui/core/FormControlLabel'; -import FormControl from '@material-ui/core/FormControl'; -import FormLabel from '@material-ui/core/FormLabel'; - -import Alert from '@material-ui/lab/Alert'; - -import {configureDatabase, testDatabaseConnection} from '../api/bbgo'; - -import {makeStyles} from '@material-ui/core/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 ( - - - Configure Database - - - - 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. - - - - - - - - Database Driver - { - setDriver(event.target.value); - }}> - } label="Standard (Default)"/> - } label="MySQL"/> - - - - - - - - {driver === "mysql" ? ( - - { - setMysqlURL(event.target.value) - resetTestResponse() - }} - /> - MySQL DSN - - - If you have database installed on your machine, you can enter the DSN string like the - following - format: -
-
root:password@tcp(127.0.0.1:3306)/bbgo
- -
- Be sure to create your database before using it. You need to execute the following statement - to - create a database: -
-
CREATE DATABASE bbgo CHARSET utf8;
-
- -
- ) : ( - - - - If you don't know what to choose, just pick the standard driver (sqlite3). -
- For professionals, you can pick MySQL driver, BBGO works best with MySQL, especially for - larger data scale. -
-
-
- )} -
- - -
- - - -
- - { - testResponse ? testResponse.error ? ( - - {testResponse.error} - - ) : testResponse.success ? ( - - Connection Test Succeeded - - ) : null : null - } - - -
- ); - -} diff --git a/frontend/components/ConfigureGridStrategyForm.js b/frontend/components/ConfigureGridStrategyForm.js deleted file mode 100644 index 84bed3acaa..0000000000 --- a/frontend/components/ConfigureGridStrategyForm.js +++ /dev/null @@ -1,428 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import Grid from '@material-ui/core/Grid'; -import Button from '@material-ui/core/Button'; -import Typography from '@material-ui/core/Typography'; - -import {makeStyles} from '@material-ui/core/styles'; -import {attachStrategyOn, querySessions, querySessionSymbols} from "../api/bbgo"; - -import TextField from '@material-ui/core/TextField'; -import FormControlLabel from '@material-ui/core/FormControlLabel'; -import FormHelperText from '@material-ui/core/FormHelperText'; -import InputLabel from '@material-ui/core/InputLabel'; -import FormControl from '@material-ui/core/FormControl'; -import Radio from '@material-ui/core/Radio'; -import RadioGroup from '@material-ui/core/RadioGroup'; -import FormLabel from '@material-ui/core/FormLabel'; -import Select from '@material-ui/core/Select'; -import MenuItem from '@material-ui/core/MenuItem'; - -import Alert from '@material-ui/lab/Alert'; -import Box from "@material-ui/core/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 ( - { - 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 ( - { - 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 ( - - {session.name} - - ); - }) - - const symbolMenuItems = activeSessionSymbols.map((symbol, index) => { - return ( - - {symbol} - - ); - }) - - return ( - - - Add Grid Strategy - - - - 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. - - - - - - Session - - - - Select the exchange session you want to mount this strategy. - - - - - - Market - - - - Select the market you want to run this strategy - - - - - { - parseFloatCall(event.target.value, setUpperPrice) - }} - value={upperPrice} - InputProps={{ - inputComponent: PriceNumberFormat, - }} - /> - - - - { - parseFloatCall(event.target.value, setLowerPrice) - }} - value={lowerPrice} - InputProps={{ - inputComponent: PriceNumberFormat, - }} - /> - - - - { - parseFloatCall(event.target.value, setProfitSpread) - }} - value={profitSpread} - InputProps={{ - inputComponent: StandardNumberFormat, - }} - /> - - - - - - Order Quantity By - - } label="Fixed Amount"/> - } label="Fixed Quantity"/> - - - - - - {quantityBy === "fixedQuantity" ? ( - { - parseFloatCall(event.target.value, setFixedQuantity) - }} - value={fixedQuantity} - InputProps={{ - inputComponent: StandardNumberFormat, - }} - /> - ) : null} - - {quantityBy === "fixedAmount" ? ( - { - parseFloatCall(event.target.value, setFixedAmount) - }} - value={fixedAmount} - InputProps={{ - inputComponent: PriceNumberFormat, - }} - /> - ) : null} - - - - { - parseFloatCall(event.target.value, setGridNumber) - }} - value={gridNumber} - InputProps={{ - inputComponent: StandardNumberFormat, - }} - /> - - - -
- - - -
- - { - response ? response.error ? ( - - {response.error} - - ) : response.success ? ( - - Strategy Added - - ) : null : null - } - - -
- ); -} diff --git a/frontend/components/ConnectWallet.js b/frontend/components/ConnectWallet.js deleted file mode 100644 index f12dd14d2d..0000000000 --- a/frontend/components/ConnectWallet.js +++ /dev/null @@ -1,131 +0,0 @@ -import React from "react"; - -import {makeStyles} from '@material-ui/core/styles'; - -import Button from '@material-ui/core/Button'; -import ClickAwayListener from '@material-ui/core/ClickAwayListener'; -import Grow from '@material-ui/core/Grow'; -import Paper from '@material-ui/core/Paper'; -import Popper from '@material-ui/core/Popper'; -import MenuItem from '@material-ui/core/MenuItem'; -import MenuList from '@material-ui/core/MenuList'; -import ListItemText from "@material-ui/core/ListItemText"; -import PersonIcon from "@material-ui/icons/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? - (<> - - - {({ TransitionProps, placement }) => ( - - - - - {account &&

Account: {account}

}
- {etherBalance && ETH Balance: {formatEther(etherBalance)}} - {tokenBalance && BBG Balance: {formatEther(tokenBalance)}} -
-
-
-
- )} -
- ):(
- -
)} - - ) - -} diff --git a/frontend/components/ExchangeSessionTabPanel.js b/frontend/components/ExchangeSessionTabPanel.js deleted file mode 100644 index 24d410eaab..0000000000 --- a/frontend/components/ExchangeSessionTabPanel.js +++ /dev/null @@ -1,49 +0,0 @@ -import Paper from "@material-ui/core/Paper"; -import Tabs from "@material-ui/core/Tabs"; -import Tab from "@material-ui/core/Tab"; -import React, {useEffect, useState} from "react"; -import {querySessions} from '../api/bbgo' -import Typography from "@material-ui/core/Typography"; -import {makeStyles} from "@material-ui/core/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 - - Sessions - - - { - sessions.map((session) => { - return - }) - } - - -} diff --git a/frontend/components/ReviewSessions.js b/frontend/components/ReviewSessions.js deleted file mode 100644 index eec259251e..0000000000 --- a/frontend/components/ReviewSessions.js +++ /dev/null @@ -1,88 +0,0 @@ -import React from 'react'; -import Grid from '@material-ui/core/Grid'; -import Button from '@material-ui/core/Button'; -import Typography from '@material-ui/core/Typography'; -import List from '@material-ui/core/List'; -import ListItem from '@material-ui/core/ListItem'; -import ListItemText from '@material-ui/core/ListItemText'; -import ListItemIcon from '@material-ui/core/ListItemIcon'; -import PowerIcon from '@material-ui/icons/Power'; - -import {makeStyles} from '@material-ui/core/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 ( - - - - - - - ); - }) - - return ( - - - Review Sessions - - - - {items} - - -
- - - -
-
- ); -} diff --git a/frontend/components/ReviewStrategies.js b/frontend/components/ReviewStrategies.js deleted file mode 100644 index edfa02b0fd..0000000000 --- a/frontend/components/ReviewStrategies.js +++ /dev/null @@ -1,158 +0,0 @@ -import React from 'react'; -import Button from '@material-ui/core/Button'; -import Typography from '@material-ui/core/Typography'; -import List from '@material-ui/core/List'; -import Card from '@material-ui/core/Card'; -import CardHeader from '@material-ui/core/CardHeader'; -import CardContent from '@material-ui/core/CardContent'; -import Avatar from '@material-ui/core/Avatar'; -import IconButton from '@material-ui/core/IconButton'; -import MoreVertIcon from '@material-ui/icons/MoreVert'; -import Table from '@material-ui/core/Table'; -import TableBody from '@material-ui/core/TableBody'; -import TableCell from '@material-ui/core/TableCell'; -import TableContainer from '@material-ui/core/TableContainer'; -import TableHead from '@material-ui/core/TableHead'; -import TableRow from '@material-ui/core/TableRow'; - -import {makeStyles} from '@material-ui/core/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 ( - - - - - Field - Value - - - - {rows.map((row) => ( - - - {row.key} - - {row.val} - - ))} - -
-
- ); -} - -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 ( - - G - } - action={ - - - - } - title={title} - subheader={`Exchange ${mounts.map((m) => m.toUpperCase())}`} - /> - - - Strategy will be executed on session {mounts.join(',')} with the following configuration: - - - {configToTable(config)} - - - - ); - }) - - return ( - - - Review Strategies - - - - {items} - - -
- - - -
-
- ); -} diff --git a/frontend/components/SaveConfigAndRestart.js b/frontend/components/SaveConfigAndRestart.js deleted file mode 100644 index 72d6e9161d..0000000000 --- a/frontend/components/SaveConfigAndRestart.js +++ /dev/null @@ -1,107 +0,0 @@ -import React from 'react'; -import {useRouter} from 'next/router'; - - -import Button from '@material-ui/core/Button'; -import Typography from '@material-ui/core/Typography'; - -import {makeStyles} from '@material-ui/core/styles'; - -import {ping, saveConfig, setupRestart} from "../api/bbgo"; -import Box from "@material-ui/core/Box"; -import Alert from "@material-ui/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 ( - - - Save Config and Restart - - - - Click "Save and Restart" to save the configurations to the config file bbgo.yaml, - and save the exchange session credentials to the dotenv file .env.local. - - -
- - - -
- - { - response ? response.error ? ( - - {response.error} - - ) : response.success ? ( - - Config Saved - - ) : null : null - } - -
- ); -} diff --git a/frontend/components/SideBar.js b/frontend/components/SideBar.js deleted file mode 100644 index e345a451b3..0000000000 --- a/frontend/components/SideBar.js +++ /dev/null @@ -1,102 +0,0 @@ -import Drawer from "@material-ui/core/Drawer"; -import Divider from "@material-ui/core/Divider"; -import List from "@material-ui/core/List"; -import Link from "next/link"; -import ListItem from "@material-ui/core/ListItem"; -import ListItemIcon from "@material-ui/core/ListItemIcon"; -import DashboardIcon from "@material-ui/icons/Dashboard"; -import ListItemText from "@material-ui/core/ListItemText"; -import ListIcon from "@material-ui/icons/List"; -import TrendingUpIcon from "@material-ui/icons/TrendingUp"; -import React from "react"; -import {makeStyles} from "@material-ui/core/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 - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -} diff --git a/frontend/components/TotalAssetsDetails.js b/frontend/components/TotalAssetsDetails.js deleted file mode 100644 index a625fe2468..0000000000 --- a/frontend/components/TotalAssetsDetails.js +++ /dev/null @@ -1,86 +0,0 @@ -import React from 'react'; -import CardContent from "@material-ui/core/CardContent"; -import Card from "@material-ui/core/Card"; -import {makeStyles} from "@material-ui/core/styles"; -import List from '@material-ui/core/List'; -import ListItem from '@material-ui/core/ListItem'; -import ListItemText from '@material-ui/core/ListItemText'; -import ListItemAvatar from '@material-ui/core/ListItemAvatar'; -import Avatar from '@material-ui/core/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 ( - - { - (a.currency in logoCurrencies) ? ( - - - - ) : ( - - - - ) - } - - - ) - }) - - return ( - - - - {items} - - - - ); - -} diff --git a/frontend/components/TotalAssetsPie.js b/frontend/components/TotalAssetsPie.js deleted file mode 100644 index 48f58bbb5e..0000000000 --- a/frontend/components/TotalAssetsPie.js +++ /dev/null @@ -1,95 +0,0 @@ -import React, {useEffect, useState} from 'react'; - -import {ResponsivePie} from '@nivo/pie'; -import {queryAssets} from '../api/bbgo'; -import {currencyColor} from '../src/utils'; -import CardContent from "@material-ui/core/CardContent"; -import Card from "@material-ui/core/Card"; -import {makeStyles} from "@material-ui/core/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 ( - - - - - - ); - -} diff --git a/frontend/components/TotalAssetsSummary.js b/frontend/components/TotalAssetsSummary.js deleted file mode 100644 index 6ddf142307..0000000000 --- a/frontend/components/TotalAssetsSummary.js +++ /dev/null @@ -1,52 +0,0 @@ -import {useEffect, useState} from "react"; -import Card from '@material-ui/core/Card'; -import CardContent from '@material-ui/core/CardContent'; -import Typography from '@material-ui/core/Typography'; -import {makeStyles} from '@material-ui/core/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 - - - Total Account Balance - - - {Math.round(aggregateAssetsBy(assets, "inBTC") * 1e8) / 1e8} BTC - - - - Estimated Value - - - - {Math.round(aggregateAssetsBy(assets, "inUSD") * 100) / 100} USD - - - -} diff --git a/frontend/components/TradingVolumeBar.js b/frontend/components/TradingVolumeBar.js deleted file mode 100644 index d777583ede..0000000000 --- a/frontend/components/TradingVolumeBar.js +++ /dev/null @@ -1,152 +0,0 @@ -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 ; -} diff --git a/frontend/components/TradingVolumePanel.js b/frontend/components/TradingVolumePanel.js deleted file mode 100644 index 3ed9ea6257..0000000000 --- a/frontend/components/TradingVolumePanel.js +++ /dev/null @@ -1,66 +0,0 @@ -import Paper from "@material-ui/core/Paper"; -import Box from "@material-ui/core/Box"; -import Tabs from "@material-ui/core/Tabs"; -import Tab from "@material-ui/core/Tab"; -import React from "react"; -import TradingVolumeBar from "./TradingVolumeBar"; -import {makeStyles} from "@material-ui/core/styles"; -import Grid from "@material-ui/core/Grid"; -import Typography from "@material-ui/core/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 - - Trading Volume - - - - - - - - - - - - - - - - - - - - - - - - ; -} diff --git a/frontend/layouts/DashboardLayout.js b/frontend/layouts/DashboardLayout.js deleted file mode 100644 index e9cbfe87b0..0000000000 --- a/frontend/layouts/DashboardLayout.js +++ /dev/null @@ -1,58 +0,0 @@ -import React from "react"; - -import {makeStyles} from "@material-ui/core/styles"; -import AppBar from "@material-ui/core/AppBar"; -import Toolbar from "@material-ui/core/Toolbar"; -import Typography from "@material-ui/core/Typography"; -import Container from '@material-ui/core/Container'; - -import SideBar from "../components/SideBar"; - -import ConnectWallet from '../components/ConnectWallet'; - -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 ( -
- - - - BBGO - - {/* */} - - - - - - -
-
- - {children} - -
-
- ); -} diff --git a/frontend/layouts/PlainLayout.js b/frontend/layouts/PlainLayout.js deleted file mode 100644 index 26e9ac6ba1..0000000000 --- a/frontend/layouts/PlainLayout.js +++ /dev/null @@ -1,43 +0,0 @@ -import React from "react"; - -import {makeStyles} from "@material-ui/core/styles"; -import AppBar from "@material-ui/core/AppBar"; -import Toolbar from "@material-ui/core/Toolbar"; -import Typography from "@material-ui/core/Typography"; -import Container from '@material-ui/core/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
- - - - { props && props.title ? props.title : "BBGO Setup Wizard" } - - - - -
-
- - {props.children} - -
-
; -} diff --git a/frontend/next.config.js b/frontend/next.config.js deleted file mode 100644 index 1c9a5dc8ca..0000000000 --- a/frontend/next.config.js +++ /dev/null @@ -1,17 +0,0 @@ -const withTM = require('next-transpile-modules') - ([ - '@react-spring/three', - '@react-spring/web', - ]) - -module.exports = withTM({ - // disable webpack 5 to make it compatible with the following rules - webpack5: false, - webpack: (config, options) => { - config.module.rules.push({ - test: /react-spring/, - sideEffects: true, - }) - return config - }, -}) diff --git a/frontend/package.json b/frontend/package.json deleted file mode 100644 index fa8009c3b8..0000000000 --- a/frontend/package.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "name": "frontend", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "export": "next build && next export" - }, - "dependencies": { - "@material-ui/core": "^4.11.2", - "@material-ui/data-grid": "^4.0.0-alpha.18", - "@material-ui/icons": "^4.11.2", - "@material-ui/lab": "^4.0.0-alpha.57", - "@nivo/bar": "^0.73.1", - "@nivo/core": "^0.73.0", - "@nivo/pie": "^0.73.0", - "@usedapp/core": "0.5.4", - "axios": "^0.22.0", - "classnames": "^2.2.6", - "isomorphic-fetch": "^3.0.0", - "next": "^11.1.2", - "qrcode.react": "^3.0.1", - "react": "^17.0.1", - "react-dom": "^17.0.1", - "react-number-format": "^4.4.4", - "react-spring": "^9.3.0" - }, - "devDependencies": { - "@types/node": "^14.14.22", - "@types/react": "^17.0.0", - "next-transpile-modules": "^6.1.0", - "postcss-flexbugs-fixes": "^5.0.2", - "postcss-preset-env": "^6.7.0", - "typescript": "^4.1.3" - } -} diff --git a/frontend/pages/_app.tsx b/frontend/pages/_app.tsx deleted file mode 100644 index 745ffacb70..0000000000 --- a/frontend/pages/_app.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import Head from 'next/head'; - -import {ThemeProvider} from '@material-ui/core/styles'; - -import Dialog from '@material-ui/core/Dialog'; -import DialogContent from '@material-ui/core/DialogContent'; -import DialogContentText from '@material-ui/core/DialogContentText'; -import DialogTitle from '@material-ui/core/DialogTitle'; -import LinearProgress from '@material-ui/core/LinearProgress'; -import Box from '@material-ui/core/Box'; - -import CssBaseline from '@material-ui/core/CssBaseline'; -import theme from '../src/theme'; -import '../styles/globals.css' -import {querySessions, querySyncStatus} from "../api/bbgo"; -import {Sync} from "@material-ui/icons"; - -const SyncNotStarted = 0 -const Syncing = 1 -const SyncDone = 2 - -// session is configured, check if we're syncing data -let syncStatusPoller = null - -export default function MyApp(props) { - const {Component, pageProps} = props; - - const [loading, setLoading] = React.useState(true) - const [syncing, setSyncing] = React.useState(false) - - React.useEffect(() => { - // Remove the server-side injected CSS. - const jssStyles = document.querySelector('#jss-server-side'); - if (jssStyles) { - jssStyles.parentElement.removeChild(jssStyles); - } - - querySessions((sessions) => { - if (sessions.length > 0) { - setSyncing(true) - - const pollSyncStatus = () => { - querySyncStatus((status) => { - switch (status) { - case SyncNotStarted: - break - case Syncing: - setSyncing(true); - break; - case SyncDone: - clearInterval(syncStatusPoller); - setLoading(false); - setSyncing(false); - break; - } - }).catch((err) => { - console.error(err) - }) - } - - syncStatusPoller = setInterval(pollSyncStatus, 1000) - } else { - // no session found, so we can not sync any data - setLoading(false) - setSyncing(false) - } - }).catch((err) => { - console.error(err) - }) - - }, []); - - return ( - - - BBGO - - - - {/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */} - - { - loading ? (syncing ? ( - - - {"Syncing Trades"} - - - The environment is syncing trades from the exchange sessions. - Please wait a moment... - - - - - - - - ) : ( - - - {"Loading"} - - - Loading... - - - - - - - - )) : ( - - ) - } - - - ); -} - -MyApp.propTypes = { - Component: PropTypes.elementType.isRequired, - pageProps: PropTypes.object.isRequired, -}; diff --git a/frontend/pages/connect/index.js b/frontend/pages/connect/index.js deleted file mode 100644 index 2fb993869b..0000000000 --- a/frontend/pages/connect/index.js +++ /dev/null @@ -1,57 +0,0 @@ -import React, {useEffect, useState} from 'react'; - -import {makeStyles} from '@material-ui/core/styles'; -import Typography from '@material-ui/core/Typography'; -import Paper from '@material-ui/core/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 ( - - - - Sign In Using QR Codes - -
- -
-
-
- ); -} - diff --git a/frontend/pages/index.tsx b/frontend/pages/index.tsx deleted file mode 100644 index 00f9be499f..0000000000 --- a/frontend/pages/index.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import React, {useState} from 'react'; -import {useRouter} from 'next/router'; - -import {makeStyles} from '@material-ui/core/styles'; -import Typography from '@material-ui/core/Typography'; -import Box from '@material-ui/core/Box'; -import Grid from '@material-ui/core/Grid'; -import Paper from '@material-ui/core/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'; - - -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 ( - - - - Loading - - - - ); - } - - console.log("index: assets", assets) - - return ( - - - - - Total Assets - - -
- - - - - - - - - - -
-
- - - - -
-
- - ); -} - diff --git a/frontend/pages/orders.js b/frontend/pages/orders.js deleted file mode 100644 index 75da0045b9..0000000000 --- a/frontend/pages/orders.js +++ /dev/null @@ -1,72 +0,0 @@ -import React, {useEffect, useState} from 'react'; - -import {makeStyles} from '@material-ui/core/styles'; -import Typography from '@material-ui/core/Typography'; -import Paper from '@material-ui/core/Paper'; -import {queryClosedOrders} from '../api/bbgo'; -import {DataGrid} from '@material-ui/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 ( - - - - Orders - -
-
- -
-
-
-
- ); -} - diff --git a/frontend/pages/setup/index.js b/frontend/pages/setup/index.js deleted file mode 100644 index 857663a486..0000000000 --- a/frontend/pages/setup/index.js +++ /dev/null @@ -1,106 +0,0 @@ -import React from 'react'; - -import {makeStyles} from '@material-ui/core/styles'; -import Typography from '@material-ui/core/Typography'; -import Box from '@material-ui/core/Box'; -import Paper from '@material-ui/core/Paper'; -import Stepper from '@material-ui/core/Stepper'; -import Step from '@material-ui/core/Step'; -import StepLabel from '@material-ui/core/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 { - setActiveStep(1) - }}/>; - case 1: - return ( - { setActiveStep(0) }} - onAdded={() => { setActiveStep(2) }} - /> - ); - case 2: - return ( - { setActiveStep(1) }} - onNext={() => { setActiveStep(3) }} - /> - ); - case 3: - return ( - { setActiveStep(2) }} - onAdded={() => { setActiveStep(4) }} - /> - ); - case 4: - return ( - { setActiveStep(3) }} - onNext={() => { setActiveStep(5) }} - /> - ); - - case 5: - return ( - { setActiveStep(4) }} - onRestarted={() => { - - }} - /> - ) - - default: - throw new Error('Unknown step'); - } -} - -export default function Setup() { - const classes = useStyles(); - const [activeStep, setActiveStep] = React.useState(0); - - return ( - - - - - Setup Session - - - - {steps.map((label) => ( - - {label} - - ))} - - - - {getStepContent(activeStep, setActiveStep)} - - - - - ); -} - diff --git a/frontend/pages/trades.js b/frontend/pages/trades.js deleted file mode 100644 index 312d124058..0000000000 --- a/frontend/pages/trades.js +++ /dev/null @@ -1,67 +0,0 @@ -import React, {useEffect, useState} from 'react'; - -import {makeStyles} from '@material-ui/core/styles'; -import Typography from '@material-ui/core/Typography'; -import Paper from '@material-ui/core/Paper'; -import {queryTrades} from '../api/bbgo'; -import {DataGrid} from '@material-ui/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 ( - - - - Trades - -
-
- -
-
-
-
- ); -} - diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js deleted file mode 100644 index 6559c95d21..0000000000 --- a/frontend/postcss.config.js +++ /dev/null @@ -1,18 +0,0 @@ -module.exports = { - plugins: [ - // 'tailwindcss', - 'postcss-flexbugs-fixes', - [ - 'postcss-preset-env', - { - autoprefixer: { - flexbox: 'no-2009' - }, - stage: 3, - features: { - 'custom-properties': false - } - } - ] - ] -} diff --git a/frontend/src/utils.js b/frontend/src/utils.js deleted file mode 100644 index 07bad9cfb3..0000000000 --- a/frontend/src/utils.js +++ /dev/null @@ -1,28 +0,0 @@ - -export function currencyColor(currency) { - switch (currency) { - case "BTC": - return "#f69c3d" - case "ETH": - return "#497493" - case "MCO": - return "#032144" - case "OMG": - return "#2159ec" - case "LTC": - return "#949494" - case "USDT": - return "#2ea07b" - case "SAND": - return "#2E9AD0" - case "XRP": - return "#00AAE4" - case "BCH": - return "#8DC351" - case "MAX": - return "#2D4692" - case "TWD": - return "#4A7DED" - - } -} diff --git a/frontend/yarn.lock b/frontend/yarn.lock deleted file mode 100644 index 2988f4e844..0000000000 --- a/frontend/yarn.lock +++ /dev/null @@ -1,3889 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@babel/code-frame@7.12.11": - version "7.12.11" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" - integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw== - dependencies: - "@babel/highlight" "^7.10.4" - -"@babel/helper-plugin-utils@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz#5ac822ce97eec46741ab70a517971e443a70c5a9" - integrity sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ== - -"@babel/helper-validator-identifier@^7.14.5", "@babel/helper-validator-identifier@^7.14.9": - version "7.15.7" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz#220df993bfe904a4a6b02ab4f3385a5ebf6e2389" - integrity sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w== - -"@babel/highlight@^7.10.4": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.14.5.tgz#6861a52f03966405001f6aa534a01a24d99e8cd9" - integrity sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg== - dependencies: - "@babel/helper-validator-identifier" "^7.14.5" - chalk "^2.0.0" - js-tokens "^4.0.0" - -"@babel/plugin-syntax-jsx@7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.14.5.tgz#000e2e25d8673cce49300517a3eda44c263e4201" - integrity sha512-ohuFIsOMXJnbOMRfX7/w7LocdR6R7whhuRD4ax8IipLcLPlZGJKkBxgHp++U4N/vKyU16/YDQr2f5seajD3jIw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/runtime@7.15.3": - version "7.15.3" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.15.3.tgz#2e1c2880ca118e5b2f9988322bd8a7656a32502b" - integrity sha512-OvwMLqNXkCXSz1kSm58sEsNuhqOx/fKpnUnKnFB5v8uDda5bLNEHNgKPvhDN6IU0LDcnHQ90LlJ0Q6jnyBSIBA== - dependencies: - regenerator-runtime "^0.13.4" - -"@babel/runtime@^7.14.8", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.7": - version "7.15.4" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.15.4.tgz#fd17d16bfdf878e6dd02d19753a39fa8a8d9c84a" - integrity sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw== - dependencies: - regenerator-runtime "^0.13.4" - -"@babel/types@7.15.0": - version "7.15.0" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.15.0.tgz#61af11f2286c4e9c69ca8deb5f4375a73c72dcbd" - integrity sha512-OBvfqnllOIdX4ojTHpwZbpvz4j3EWyjkZEdmjH0/cgsd6QOdSgU8rLSk6ard/pcW7rlmjdVSX/AWOaORR1uNOQ== - dependencies: - "@babel/helper-validator-identifier" "^7.14.9" - to-fast-properties "^2.0.0" - -"@csstools/convert-colors@^1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@csstools/convert-colors/-/convert-colors-1.4.0.tgz#ad495dc41b12e75d588c6db8b9834f08fa131eb7" - integrity sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw== - -"@emotion/hash@^0.8.0": - version "0.8.0" - resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413" - integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow== - -"@ethersproject/abi@5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.4.0.tgz#a6d63bdb3672f738398846d4279fa6b6c9818242" - integrity sha512-9gU2H+/yK1j2eVMdzm6xvHSnMxk8waIHQGYCZg5uvAyH0rsAzxkModzBSpbAkAuhKFEovC2S9hM4nPuLym8IZw== - dependencies: - "@ethersproject/address" "^5.4.0" - "@ethersproject/bignumber" "^5.4.0" - "@ethersproject/bytes" "^5.4.0" - "@ethersproject/constants" "^5.4.0" - "@ethersproject/hash" "^5.4.0" - "@ethersproject/keccak256" "^5.4.0" - "@ethersproject/logger" "^5.4.0" - "@ethersproject/properties" "^5.4.0" - "@ethersproject/strings" "^5.4.0" - -"@ethersproject/abi@^5.4.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.5.0.tgz#fb52820e22e50b854ff15ce1647cc508d6660613" - integrity sha512-loW7I4AohP5KycATvc0MgujU6JyCHPqHdeoo9z3Nr9xEiNioxa65ccdm1+fsoJhkuhdRtfcL8cfyGamz2AxZ5w== - dependencies: - "@ethersproject/address" "^5.5.0" - "@ethersproject/bignumber" "^5.5.0" - "@ethersproject/bytes" "^5.5.0" - "@ethersproject/constants" "^5.5.0" - "@ethersproject/hash" "^5.5.0" - "@ethersproject/keccak256" "^5.5.0" - "@ethersproject/logger" "^5.5.0" - "@ethersproject/properties" "^5.5.0" - "@ethersproject/strings" "^5.5.0" - -"@ethersproject/abstract-provider@5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/abstract-provider/-/abstract-provider-5.4.0.tgz#415331031b0f678388971e1987305244edc04e1d" - integrity sha512-vPBR7HKUBY0lpdllIn7tLIzNN7DrVnhCLKSzY0l8WAwxz686m/aL7ASDzrVxV93GJtIub6N2t4dfZ29CkPOxgA== - dependencies: - "@ethersproject/bignumber" "^5.4.0" - "@ethersproject/bytes" "^5.4.0" - "@ethersproject/logger" "^5.4.0" - "@ethersproject/networks" "^5.4.0" - "@ethersproject/properties" "^5.4.0" - "@ethersproject/transactions" "^5.4.0" - "@ethersproject/web" "^5.4.0" - -"@ethersproject/abstract-provider@^5.4.0", "@ethersproject/abstract-provider@^5.5.0": - version "5.5.1" - resolved "https://registry.yarnpkg.com/@ethersproject/abstract-provider/-/abstract-provider-5.5.1.tgz#2f1f6e8a3ab7d378d8ad0b5718460f85649710c5" - integrity sha512-m+MA/ful6eKbxpr99xUYeRvLkfnlqzrF8SZ46d/xFB1A7ZVknYc/sXJG0RcufF52Qn2jeFj1hhcoQ7IXjNKUqg== - dependencies: - "@ethersproject/bignumber" "^5.5.0" - "@ethersproject/bytes" "^5.5.0" - "@ethersproject/logger" "^5.5.0" - "@ethersproject/networks" "^5.5.0" - "@ethersproject/properties" "^5.5.0" - "@ethersproject/transactions" "^5.5.0" - "@ethersproject/web" "^5.5.0" - -"@ethersproject/abstract-signer@5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/abstract-signer/-/abstract-signer-5.4.0.tgz#cd5f50b93141ee9f9f49feb4075a0b3eafb57d65" - integrity sha512-AieQAzt05HJZS2bMofpuxMEp81AHufA5D6M4ScKwtolj041nrfIbIi8ciNW7+F59VYxXq+V4c3d568Q6l2m8ew== - dependencies: - "@ethersproject/abstract-provider" "^5.4.0" - "@ethersproject/bignumber" "^5.4.0" - "@ethersproject/bytes" "^5.4.0" - "@ethersproject/logger" "^5.4.0" - "@ethersproject/properties" "^5.4.0" - -"@ethersproject/abstract-signer@^5.4.0", "@ethersproject/abstract-signer@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/abstract-signer/-/abstract-signer-5.5.0.tgz#590ff6693370c60ae376bf1c7ada59eb2a8dd08d" - integrity sha512-lj//7r250MXVLKI7sVarXAbZXbv9P50lgmJQGr2/is82EwEb8r7HrxsmMqAjTsztMYy7ohrIhGMIml+Gx4D3mA== - dependencies: - "@ethersproject/abstract-provider" "^5.5.0" - "@ethersproject/bignumber" "^5.5.0" - "@ethersproject/bytes" "^5.5.0" - "@ethersproject/logger" "^5.5.0" - "@ethersproject/properties" "^5.5.0" - -"@ethersproject/address@5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.4.0.tgz#ba2d00a0f8c4c0854933b963b9a3a9f6eb4a37a3" - integrity sha512-SD0VgOEkcACEG/C6xavlU1Hy3m5DGSXW3CUHkaaEHbAPPsgi0coP5oNPsxau8eTlZOk/bpa/hKeCNoK5IzVI2Q== - dependencies: - "@ethersproject/bignumber" "^5.4.0" - "@ethersproject/bytes" "^5.4.0" - "@ethersproject/keccak256" "^5.4.0" - "@ethersproject/logger" "^5.4.0" - "@ethersproject/rlp" "^5.4.0" - -"@ethersproject/address@^5.4.0", "@ethersproject/address@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.5.0.tgz#bcc6f576a553f21f3dd7ba17248f81b473c9c78f" - integrity sha512-l4Nj0eWlTUh6ro5IbPTgbpT4wRbdH5l8CQf7icF7sb/SI3Nhd9Y9HzhonTSTi6CefI0necIw7LJqQPopPLZyWw== - dependencies: - "@ethersproject/bignumber" "^5.5.0" - "@ethersproject/bytes" "^5.5.0" - "@ethersproject/keccak256" "^5.5.0" - "@ethersproject/logger" "^5.5.0" - "@ethersproject/rlp" "^5.5.0" - -"@ethersproject/base64@5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/base64/-/base64-5.4.0.tgz#7252bf65295954c9048c7ca5f43e5c86441b2a9a" - integrity sha512-CjQw6E17QDSSC5jiM9YpF7N1aSCHmYGMt9bWD8PWv6YPMxjsys2/Q8xLrROKI3IWJ7sFfZ8B3flKDTM5wlWuZQ== - dependencies: - "@ethersproject/bytes" "^5.4.0" - -"@ethersproject/base64@^5.4.0", "@ethersproject/base64@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/base64/-/base64-5.5.0.tgz#881e8544e47ed976930836986e5eb8fab259c090" - integrity sha512-tdayUKhU1ljrlHzEWbStXazDpsx4eg1dBXUSI6+mHlYklOXoXF6lZvw8tnD6oVaWfnMxAgRSKROg3cVKtCcppA== - dependencies: - "@ethersproject/bytes" "^5.5.0" - -"@ethersproject/basex@5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/basex/-/basex-5.4.0.tgz#0a2da0f4e76c504a94f2b21d3161ed9438c7f8a6" - integrity sha512-J07+QCVJ7np2bcpxydFVf/CuYo9mZ7T73Pe7KQY4c1lRlrixMeblauMxHXD0MPwFmUHZIILDNViVkykFBZylbg== - dependencies: - "@ethersproject/bytes" "^5.4.0" - "@ethersproject/properties" "^5.4.0" - -"@ethersproject/basex@^5.4.0", "@ethersproject/basex@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/basex/-/basex-5.5.0.tgz#e40a53ae6d6b09ab4d977bd037010d4bed21b4d3" - integrity sha512-ZIodwhHpVJ0Y3hUCfUucmxKsWQA5TMnavp5j/UOuDdzZWzJlRmuOjcTMIGgHCYuZmHt36BfiSyQPSRskPxbfaQ== - dependencies: - "@ethersproject/bytes" "^5.5.0" - "@ethersproject/properties" "^5.5.0" - -"@ethersproject/bignumber@5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.4.0.tgz#be8dea298c0ec71208ee60f0b245be0761217ad9" - integrity sha512-OXUu9f9hO3vGRIPxU40cignXZVaYyfx6j9NNMjebKdnaCL3anCLSSy8/b8d03vY6dh7duCC0kW72GEC4tZer2w== - dependencies: - "@ethersproject/bytes" "^5.4.0" - "@ethersproject/logger" "^5.4.0" - bn.js "^4.11.9" - -"@ethersproject/bignumber@^5.4.0", "@ethersproject/bignumber@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.5.0.tgz#875b143f04a216f4f8b96245bde942d42d279527" - integrity sha512-6Xytlwvy6Rn3U3gKEc1vP7nR92frHkv6wtVr95LFR3jREXiCPzdWxKQ1cx4JGQBXxcguAwjA8murlYN2TSiEbg== - dependencies: - "@ethersproject/bytes" "^5.5.0" - "@ethersproject/logger" "^5.5.0" - bn.js "^4.11.9" - -"@ethersproject/bytes@5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.4.0.tgz#56fa32ce3bf67153756dbaefda921d1d4774404e" - integrity sha512-H60ceqgTHbhzOj4uRc/83SCN9d+BSUnOkrr2intevqdtEMO1JFVZ1XL84OEZV+QjV36OaZYxtnt4lGmxcGsPfA== - dependencies: - "@ethersproject/logger" "^5.4.0" - -"@ethersproject/bytes@^5.4.0", "@ethersproject/bytes@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.5.0.tgz#cb11c526de657e7b45d2e0f0246fb3b9d29a601c" - integrity sha512-ABvc7BHWhZU9PNM/tANm/Qx4ostPGadAuQzWTr3doklZOhDlmcBqclrQe/ZXUIj3K8wC28oYeuRa+A37tX9kog== - dependencies: - "@ethersproject/logger" "^5.5.0" - -"@ethersproject/constants@5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/constants/-/constants-5.4.0.tgz#ee0bdcb30bf1b532d2353c977bf2ef1ee117958a" - integrity sha512-tzjn6S7sj9+DIIeKTJLjK9WGN2Tj0P++Z8ONEIlZjyoTkBuODN+0VfhAyYksKi43l1Sx9tX2VlFfzjfmr5Wl3Q== - dependencies: - "@ethersproject/bignumber" "^5.4.0" - -"@ethersproject/constants@^5.4.0", "@ethersproject/constants@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/constants/-/constants-5.5.0.tgz#d2a2cd7d94bd1d58377d1d66c4f53c9be4d0a45e" - integrity sha512-2MsRRVChkvMWR+GyMGY4N1sAX9Mt3J9KykCsgUFd/1mwS0UH1qw+Bv9k1UJb3X3YJYFco9H20pjSlOIfCG5HYQ== - dependencies: - "@ethersproject/bignumber" "^5.5.0" - -"@ethersproject/contracts@5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/contracts/-/contracts-5.4.0.tgz#e05fe6bd33acc98741e27d553889ec5920078abb" - integrity sha512-hkO3L3IhS1Z3ZtHtaAG/T87nQ7KiPV+/qnvutag35I0IkiQ8G3ZpCQ9NNOpSCzn4pWSW4CfzmtE02FcqnLI+hw== - dependencies: - "@ethersproject/abi" "^5.4.0" - "@ethersproject/abstract-provider" "^5.4.0" - "@ethersproject/abstract-signer" "^5.4.0" - "@ethersproject/address" "^5.4.0" - "@ethersproject/bignumber" "^5.4.0" - "@ethersproject/bytes" "^5.4.0" - "@ethersproject/constants" "^5.4.0" - "@ethersproject/logger" "^5.4.0" - "@ethersproject/properties" "^5.4.0" - "@ethersproject/transactions" "^5.4.0" - -"@ethersproject/hash@5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/hash/-/hash-5.4.0.tgz#d18a8e927e828e22860a011f39e429d388344ae0" - integrity sha512-xymAM9tmikKgbktOCjW60Z5sdouiIIurkZUr9oW5NOex5uwxrbsYG09kb5bMcNjlVeJD3yPivTNzViIs1GCbqA== - dependencies: - "@ethersproject/abstract-signer" "^5.4.0" - "@ethersproject/address" "^5.4.0" - "@ethersproject/bignumber" "^5.4.0" - "@ethersproject/bytes" "^5.4.0" - "@ethersproject/keccak256" "^5.4.0" - "@ethersproject/logger" "^5.4.0" - "@ethersproject/properties" "^5.4.0" - "@ethersproject/strings" "^5.4.0" - -"@ethersproject/hash@^5.4.0", "@ethersproject/hash@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/hash/-/hash-5.5.0.tgz#7cee76d08f88d1873574c849e0207dcb32380cc9" - integrity sha512-dnGVpK1WtBjmnp3mUT0PlU2MpapnwWI0PibldQEq1408tQBAbZpPidkWoVVuNMOl/lISO3+4hXZWCL3YV7qzfg== - dependencies: - "@ethersproject/abstract-signer" "^5.5.0" - "@ethersproject/address" "^5.5.0" - "@ethersproject/bignumber" "^5.5.0" - "@ethersproject/bytes" "^5.5.0" - "@ethersproject/keccak256" "^5.5.0" - "@ethersproject/logger" "^5.5.0" - "@ethersproject/properties" "^5.5.0" - "@ethersproject/strings" "^5.5.0" - -"@ethersproject/hdnode@5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/hdnode/-/hdnode-5.4.0.tgz#4bc9999b9a12eb5ce80c5faa83114a57e4107cac" - integrity sha512-pKxdS0KAaeVGfZPp1KOiDLB0jba11tG6OP1u11QnYfb7pXn6IZx0xceqWRr6ygke8+Kw74IpOoSi7/DwANhy8Q== - dependencies: - "@ethersproject/abstract-signer" "^5.4.0" - "@ethersproject/basex" "^5.4.0" - "@ethersproject/bignumber" "^5.4.0" - "@ethersproject/bytes" "^5.4.0" - "@ethersproject/logger" "^5.4.0" - "@ethersproject/pbkdf2" "^5.4.0" - "@ethersproject/properties" "^5.4.0" - "@ethersproject/sha2" "^5.4.0" - "@ethersproject/signing-key" "^5.4.0" - "@ethersproject/strings" "^5.4.0" - "@ethersproject/transactions" "^5.4.0" - "@ethersproject/wordlists" "^5.4.0" - -"@ethersproject/hdnode@^5.4.0", "@ethersproject/hdnode@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/hdnode/-/hdnode-5.5.0.tgz#4a04e28f41c546f7c978528ea1575206a200ddf6" - integrity sha512-mcSOo9zeUg1L0CoJH7zmxwUG5ggQHU1UrRf8jyTYy6HxdZV+r0PBoL1bxr+JHIPXRzS6u/UW4mEn43y0tmyF8Q== - dependencies: - "@ethersproject/abstract-signer" "^5.5.0" - "@ethersproject/basex" "^5.5.0" - "@ethersproject/bignumber" "^5.5.0" - "@ethersproject/bytes" "^5.5.0" - "@ethersproject/logger" "^5.5.0" - "@ethersproject/pbkdf2" "^5.5.0" - "@ethersproject/properties" "^5.5.0" - "@ethersproject/sha2" "^5.5.0" - "@ethersproject/signing-key" "^5.5.0" - "@ethersproject/strings" "^5.5.0" - "@ethersproject/transactions" "^5.5.0" - "@ethersproject/wordlists" "^5.5.0" - -"@ethersproject/json-wallets@5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/json-wallets/-/json-wallets-5.4.0.tgz#2583341cfe313fc9856642e8ace3080154145e95" - integrity sha512-igWcu3fx4aiczrzEHwG1xJZo9l1cFfQOWzTqwRw/xcvxTk58q4f9M7cjh51EKphMHvrJtcezJ1gf1q1AUOfEQQ== - dependencies: - "@ethersproject/abstract-signer" "^5.4.0" - "@ethersproject/address" "^5.4.0" - "@ethersproject/bytes" "^5.4.0" - "@ethersproject/hdnode" "^5.4.0" - "@ethersproject/keccak256" "^5.4.0" - "@ethersproject/logger" "^5.4.0" - "@ethersproject/pbkdf2" "^5.4.0" - "@ethersproject/properties" "^5.4.0" - "@ethersproject/random" "^5.4.0" - "@ethersproject/strings" "^5.4.0" - "@ethersproject/transactions" "^5.4.0" - aes-js "3.0.0" - scrypt-js "3.0.1" - -"@ethersproject/json-wallets@^5.4.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/json-wallets/-/json-wallets-5.5.0.tgz#dd522d4297e15bccc8e1427d247ec8376b60e325" - integrity sha512-9lA21XQnCdcS72xlBn1jfQdj2A1VUxZzOzi9UkNdnokNKke/9Ya2xA9aIK1SC3PQyBDLt4C+dfps7ULpkvKikQ== - dependencies: - "@ethersproject/abstract-signer" "^5.5.0" - "@ethersproject/address" "^5.5.0" - "@ethersproject/bytes" "^5.5.0" - "@ethersproject/hdnode" "^5.5.0" - "@ethersproject/keccak256" "^5.5.0" - "@ethersproject/logger" "^5.5.0" - "@ethersproject/pbkdf2" "^5.5.0" - "@ethersproject/properties" "^5.5.0" - "@ethersproject/random" "^5.5.0" - "@ethersproject/strings" "^5.5.0" - "@ethersproject/transactions" "^5.5.0" - aes-js "3.0.0" - scrypt-js "3.0.1" - -"@ethersproject/keccak256@5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/keccak256/-/keccak256-5.4.0.tgz#7143b8eea4976080241d2bd92e3b1f1bf7025318" - integrity sha512-FBI1plWet+dPUvAzPAeHzRKiPpETQzqSUWR1wXJGHVWi4i8bOSrpC3NwpkPjgeXG7MnugVc1B42VbfnQikyC/A== - dependencies: - "@ethersproject/bytes" "^5.4.0" - js-sha3 "0.5.7" - -"@ethersproject/keccak256@^5.0.0-beta.130", "@ethersproject/keccak256@^5.4.0", "@ethersproject/keccak256@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/keccak256/-/keccak256-5.5.0.tgz#e4b1f9d7701da87c564ffe336f86dcee82983492" - integrity sha512-5VoFCTjo2rYbBe1l2f4mccaRFN/4VQEYFwwn04aJV2h7qf4ZvI2wFxUE1XOX+snbwCLRzIeikOqtAoPwMza9kg== - dependencies: - "@ethersproject/bytes" "^5.5.0" - js-sha3 "0.8.0" - -"@ethersproject/logger@5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.4.0.tgz#f39adadf62ad610c420bcd156fd41270e91b3ca9" - integrity sha512-xYdWGGQ9P2cxBayt64d8LC8aPFJk6yWCawQi/4eJ4+oJdMMjEBMrIcIMZ9AxhwpPVmnBPrsB10PcXGmGAqgUEQ== - -"@ethersproject/logger@^5.4.0", "@ethersproject/logger@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.5.0.tgz#0c2caebeff98e10aefa5aef27d7441c7fd18cf5d" - integrity sha512-rIY/6WPm7T8n3qS2vuHTUBPdXHl+rGxWxW5okDfo9J4Z0+gRRZT0msvUdIJkE4/HS29GUMziwGaaKO2bWONBrg== - -"@ethersproject/networks@5.4.1": - version "5.4.1" - resolved "https://registry.yarnpkg.com/@ethersproject/networks/-/networks-5.4.1.tgz#2ce83b8e42aa85216e5d277a7952d97b6ce8d852" - integrity sha512-8SvowCKz9Uf4xC5DTKI8+il8lWqOr78kmiqAVLYT9lzB8aSmJHQMD1GSuJI0CW4hMAnzocpGpZLgiMdzsNSPig== - dependencies: - "@ethersproject/logger" "^5.4.0" - -"@ethersproject/networks@^5.4.0", "@ethersproject/networks@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/networks/-/networks-5.5.0.tgz#babec47cab892c51f8dd652ce7f2e3e14283981a" - integrity sha512-KWfP3xOnJeF89Uf/FCJdV1a2aDJe5XTN2N52p4fcQ34QhDqQFkgQKZ39VGtiqUgHcLI8DfT0l9azC3KFTunqtA== - dependencies: - "@ethersproject/logger" "^5.5.0" - -"@ethersproject/pbkdf2@5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/pbkdf2/-/pbkdf2-5.4.0.tgz#ed88782a67fda1594c22d60d0ca911a9d669641c" - integrity sha512-x94aIv6tiA04g6BnazZSLoRXqyusawRyZWlUhKip2jvoLpzJuLb//KtMM6PEovE47pMbW+Qe1uw+68ameJjB7g== - dependencies: - "@ethersproject/bytes" "^5.4.0" - "@ethersproject/sha2" "^5.4.0" - -"@ethersproject/pbkdf2@^5.4.0", "@ethersproject/pbkdf2@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/pbkdf2/-/pbkdf2-5.5.0.tgz#e25032cdf02f31505d47afbf9c3e000d95c4a050" - integrity sha512-SaDvQFvXPnz1QGpzr6/HToLifftSXGoXrbpZ6BvoZhmx4bNLHrxDe8MZisuecyOziP1aVEwzC2Hasj+86TgWVg== - dependencies: - "@ethersproject/bytes" "^5.5.0" - "@ethersproject/sha2" "^5.5.0" - -"@ethersproject/properties@5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/properties/-/properties-5.4.0.tgz#38ba20539b44dcc5d5f80c45ad902017dcdbefe7" - integrity sha512-7jczalGVRAJ+XSRvNA6D5sAwT4gavLq3OXPuV/74o3Rd2wuzSL035IMpIMgei4CYyBdialJMrTqkOnzccLHn4A== - dependencies: - "@ethersproject/logger" "^5.4.0" - -"@ethersproject/properties@^5.4.0", "@ethersproject/properties@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/properties/-/properties-5.5.0.tgz#61f00f2bb83376d2071baab02245f92070c59995" - integrity sha512-l3zRQg3JkD8EL3CPjNK5g7kMx4qSwiR60/uk5IVjd3oq1MZR5qUg40CNOoEJoX5wc3DyY5bt9EbMk86C7x0DNA== - dependencies: - "@ethersproject/logger" "^5.5.0" - -"@ethersproject/providers@5.4.1": - version "5.4.1" - resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.4.1.tgz#654267b563b833046b9c9647647cfc8267cb93b4" - integrity sha512-p06eiFKz8nu/5Ju0kIX024gzEQIgE5pvvGrBCngpyVjpuLtUIWT3097Agw4mTn9/dEA0FMcfByzFqacBMSgCVg== - dependencies: - "@ethersproject/abstract-provider" "^5.4.0" - "@ethersproject/abstract-signer" "^5.4.0" - "@ethersproject/address" "^5.4.0" - "@ethersproject/basex" "^5.4.0" - "@ethersproject/bignumber" "^5.4.0" - "@ethersproject/bytes" "^5.4.0" - "@ethersproject/constants" "^5.4.0" - "@ethersproject/hash" "^5.4.0" - "@ethersproject/logger" "^5.4.0" - "@ethersproject/networks" "^5.4.0" - "@ethersproject/properties" "^5.4.0" - "@ethersproject/random" "^5.4.0" - "@ethersproject/rlp" "^5.4.0" - "@ethersproject/sha2" "^5.4.0" - "@ethersproject/strings" "^5.4.0" - "@ethersproject/transactions" "^5.4.0" - "@ethersproject/web" "^5.4.0" - bech32 "1.1.4" - ws "7.4.6" - -"@ethersproject/random@5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/random/-/random-5.4.0.tgz#9cdde60e160d024be39cc16f8de3b9ce39191e16" - integrity sha512-pnpWNQlf0VAZDEOVp1rsYQosmv2o0ITS/PecNw+mS2/btF8eYdspkN0vIXrCMtkX09EAh9bdk8GoXmFXM1eAKw== - dependencies: - "@ethersproject/bytes" "^5.4.0" - "@ethersproject/logger" "^5.4.0" - -"@ethersproject/random@^5.4.0", "@ethersproject/random@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/random/-/random-5.5.0.tgz#305ed9e033ca537735365ac12eed88580b0f81f9" - integrity sha512-egGYZwZ/YIFKMHcoBUo8t3a8Hb/TKYX8BCBoLjudVCZh892welR3jOxgOmb48xznc9bTcMm7Tpwc1gHC1PFNFQ== - dependencies: - "@ethersproject/bytes" "^5.5.0" - "@ethersproject/logger" "^5.5.0" - -"@ethersproject/rlp@5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/rlp/-/rlp-5.4.0.tgz#de61afda5ff979454e76d3b3310a6c32ad060931" - integrity sha512-0I7MZKfi+T5+G8atId9QaQKHRvvasM/kqLyAH4XxBCBchAooH2EX5rL9kYZWwcm3awYV+XC7VF6nLhfeQFKVPg== - dependencies: - "@ethersproject/bytes" "^5.4.0" - "@ethersproject/logger" "^5.4.0" - -"@ethersproject/rlp@^5.4.0", "@ethersproject/rlp@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/rlp/-/rlp-5.5.0.tgz#530f4f608f9ca9d4f89c24ab95db58ab56ab99a0" - integrity sha512-hLv8XaQ8PTI9g2RHoQGf/WSxBfTB/NudRacbzdxmst5VHAqd1sMibWG7SENzT5Dj3yZ3kJYx+WiRYEcQTAkcYA== - dependencies: - "@ethersproject/bytes" "^5.5.0" - "@ethersproject/logger" "^5.5.0" - -"@ethersproject/sha2@5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/sha2/-/sha2-5.4.0.tgz#c9a8db1037014cbc4e9482bd662f86c090440371" - integrity sha512-siheo36r1WD7Cy+bDdE1BJ8y0bDtqXCOxRMzPa4bV1TGt/eTUUt03BHoJNB6reWJD8A30E/pdJ8WFkq+/uz4Gg== - dependencies: - "@ethersproject/bytes" "^5.4.0" - "@ethersproject/logger" "^5.4.0" - hash.js "1.1.7" - -"@ethersproject/sha2@^5.4.0", "@ethersproject/sha2@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/sha2/-/sha2-5.5.0.tgz#a40a054c61f98fd9eee99af2c3cc6ff57ec24db7" - integrity sha512-B5UBoglbCiHamRVPLA110J+2uqsifpZaTmid2/7W5rbtYVz6gus6/hSDieIU/6gaKIDcOj12WnOdiymEUHIAOA== - dependencies: - "@ethersproject/bytes" "^5.5.0" - "@ethersproject/logger" "^5.5.0" - hash.js "1.1.7" - -"@ethersproject/signing-key@5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/signing-key/-/signing-key-5.4.0.tgz#2f05120984e81cf89a3d5f6dec5c68ee0894fbec" - integrity sha512-q8POUeywx6AKg2/jX9qBYZIAmKSB4ubGXdQ88l40hmATj29JnG5pp331nAWwwxPn2Qao4JpWHNZsQN+bPiSW9A== - dependencies: - "@ethersproject/bytes" "^5.4.0" - "@ethersproject/logger" "^5.4.0" - "@ethersproject/properties" "^5.4.0" - bn.js "^4.11.9" - elliptic "6.5.4" - hash.js "1.1.7" - -"@ethersproject/signing-key@^5.4.0", "@ethersproject/signing-key@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/signing-key/-/signing-key-5.5.0.tgz#2aa37169ce7e01e3e80f2c14325f624c29cedbe0" - integrity sha512-5VmseH7qjtNmDdZBswavhotYbWB0bOwKIlOTSlX14rKn5c11QmJwGt4GHeo7NrL/Ycl7uo9AHvEqs5xZgFBTng== - dependencies: - "@ethersproject/bytes" "^5.5.0" - "@ethersproject/logger" "^5.5.0" - "@ethersproject/properties" "^5.5.0" - bn.js "^4.11.9" - elliptic "6.5.4" - hash.js "1.1.7" - -"@ethersproject/solidity@5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/solidity/-/solidity-5.4.0.tgz#1305e058ea02dc4891df18b33232b11a14ece9ec" - integrity sha512-XFQTZ7wFSHOhHcV1DpcWj7VXECEiSrBuv7JErJvB9Uo+KfCdc3QtUZV+Vjh/AAaYgezUEKbCtE6Khjm44seevQ== - dependencies: - "@ethersproject/bignumber" "^5.4.0" - "@ethersproject/bytes" "^5.4.0" - "@ethersproject/keccak256" "^5.4.0" - "@ethersproject/sha2" "^5.4.0" - "@ethersproject/strings" "^5.4.0" - -"@ethersproject/strings@5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/strings/-/strings-5.4.0.tgz#fb12270132dd84b02906a8d895ae7e7fa3d07d9a" - integrity sha512-k/9DkH5UGDhv7aReXLluFG5ExurwtIpUfnDNhQA29w896Dw3i4uDTz01Quaptbks1Uj9kI8wo9tmW73wcIEaWA== - dependencies: - "@ethersproject/bytes" "^5.4.0" - "@ethersproject/constants" "^5.4.0" - "@ethersproject/logger" "^5.4.0" - -"@ethersproject/strings@^5.4.0", "@ethersproject/strings@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/strings/-/strings-5.5.0.tgz#e6784d00ec6c57710755699003bc747e98c5d549" - integrity sha512-9fy3TtF5LrX/wTrBaT8FGE6TDJyVjOvXynXJz5MT5azq+E6D92zuKNx7i29sWW2FjVOaWjAsiZ1ZWznuduTIIQ== - dependencies: - "@ethersproject/bytes" "^5.5.0" - "@ethersproject/constants" "^5.5.0" - "@ethersproject/logger" "^5.5.0" - -"@ethersproject/transactions@5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.4.0.tgz#a159d035179334bd92f340ce0f77e83e9e1522e0" - integrity sha512-s3EjZZt7xa4BkLknJZ98QGoIza94rVjaEed0rzZ/jB9WrIuu/1+tjvYCWzVrystXtDswy7TPBeIepyXwSYa4WQ== - dependencies: - "@ethersproject/address" "^5.4.0" - "@ethersproject/bignumber" "^5.4.0" - "@ethersproject/bytes" "^5.4.0" - "@ethersproject/constants" "^5.4.0" - "@ethersproject/keccak256" "^5.4.0" - "@ethersproject/logger" "^5.4.0" - "@ethersproject/properties" "^5.4.0" - "@ethersproject/rlp" "^5.4.0" - "@ethersproject/signing-key" "^5.4.0" - -"@ethersproject/transactions@^5.4.0", "@ethersproject/transactions@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.5.0.tgz#7e9bf72e97bcdf69db34fe0d59e2f4203c7a2908" - integrity sha512-9RZYSKX26KfzEd/1eqvv8pLauCKzDTub0Ko4LfIgaERvRuwyaNV78mJs7cpIgZaDl6RJui4o49lHwwCM0526zA== - dependencies: - "@ethersproject/address" "^5.5.0" - "@ethersproject/bignumber" "^5.5.0" - "@ethersproject/bytes" "^5.5.0" - "@ethersproject/constants" "^5.5.0" - "@ethersproject/keccak256" "^5.5.0" - "@ethersproject/logger" "^5.5.0" - "@ethersproject/properties" "^5.5.0" - "@ethersproject/rlp" "^5.5.0" - "@ethersproject/signing-key" "^5.5.0" - -"@ethersproject/units@5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/units/-/units-5.4.0.tgz#d57477a4498b14b88b10396062c8cbbaf20c79fe" - integrity sha512-Z88krX40KCp+JqPCP5oPv5p750g+uU6gopDYRTBGcDvOASh6qhiEYCRatuM/suC4S2XW9Zz90QI35MfSrTIaFg== - dependencies: - "@ethersproject/bignumber" "^5.4.0" - "@ethersproject/constants" "^5.4.0" - "@ethersproject/logger" "^5.4.0" - -"@ethersproject/wallet@5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/wallet/-/wallet-5.4.0.tgz#fa5b59830b42e9be56eadd45a16a2e0933ad9353" - integrity sha512-wU29majLjM6AjCjpat21mPPviG+EpK7wY1+jzKD0fg3ui5fgedf2zEu1RDgpfIMsfn8fJHJuzM4zXZ2+hSHaSQ== - dependencies: - "@ethersproject/abstract-provider" "^5.4.0" - "@ethersproject/abstract-signer" "^5.4.0" - "@ethersproject/address" "^5.4.0" - "@ethersproject/bignumber" "^5.4.0" - "@ethersproject/bytes" "^5.4.0" - "@ethersproject/hash" "^5.4.0" - "@ethersproject/hdnode" "^5.4.0" - "@ethersproject/json-wallets" "^5.4.0" - "@ethersproject/keccak256" "^5.4.0" - "@ethersproject/logger" "^5.4.0" - "@ethersproject/properties" "^5.4.0" - "@ethersproject/random" "^5.4.0" - "@ethersproject/signing-key" "^5.4.0" - "@ethersproject/transactions" "^5.4.0" - "@ethersproject/wordlists" "^5.4.0" - -"@ethersproject/web@5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/web/-/web-5.4.0.tgz#49fac173b96992334ed36a175538ba07a7413d1f" - integrity sha512-1bUusGmcoRLYgMn6c1BLk1tOKUIFuTg8j+6N8lYlbMpDesnle+i3pGSagGNvwjaiLo4Y5gBibwctpPRmjrh4Og== - dependencies: - "@ethersproject/base64" "^5.4.0" - "@ethersproject/bytes" "^5.4.0" - "@ethersproject/logger" "^5.4.0" - "@ethersproject/properties" "^5.4.0" - "@ethersproject/strings" "^5.4.0" - -"@ethersproject/web@^5.4.0", "@ethersproject/web@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/web/-/web-5.5.0.tgz#0e5bb21a2b58fb4960a705bfc6522a6acf461e28" - integrity sha512-BEgY0eL5oH4mAo37TNYVrFeHsIXLRxggCRG/ksRIxI2X5uj5IsjGmcNiRN/VirQOlBxcUhCgHhaDLG4m6XAVoA== - dependencies: - "@ethersproject/base64" "^5.5.0" - "@ethersproject/bytes" "^5.5.0" - "@ethersproject/logger" "^5.5.0" - "@ethersproject/properties" "^5.5.0" - "@ethersproject/strings" "^5.5.0" - -"@ethersproject/wordlists@5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@ethersproject/wordlists/-/wordlists-5.4.0.tgz#f34205ec3bbc9e2c49cadaee774cf0b07e7573d7" - integrity sha512-FemEkf6a+EBKEPxlzeVgUaVSodU7G0Na89jqKjmWMlDB0tomoU8RlEMgUvXyqtrg8N4cwpLh8nyRnm1Nay1isA== - dependencies: - "@ethersproject/bytes" "^5.4.0" - "@ethersproject/hash" "^5.4.0" - "@ethersproject/logger" "^5.4.0" - "@ethersproject/properties" "^5.4.0" - "@ethersproject/strings" "^5.4.0" - -"@ethersproject/wordlists@^5.4.0", "@ethersproject/wordlists@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@ethersproject/wordlists/-/wordlists-5.5.0.tgz#aac74963aa43e643638e5172353d931b347d584f" - integrity sha512-bL0UTReWDiaQJJYOC9sh/XcRu/9i2jMrzf8VLRmPKx58ckSlOJiohODkECCO50dtLZHcGU6MLXQ4OOrgBwP77Q== - dependencies: - "@ethersproject/bytes" "^5.5.0" - "@ethersproject/hash" "^5.5.0" - "@ethersproject/logger" "^5.5.0" - "@ethersproject/properties" "^5.5.0" - "@ethersproject/strings" "^5.5.0" - -"@hapi/accept@5.0.2": - version "5.0.2" - resolved "https://registry.yarnpkg.com/@hapi/accept/-/accept-5.0.2.tgz#ab7043b037e68b722f93f376afb05e85c0699523" - integrity sha512-CmzBx/bXUR8451fnZRuZAJRlzgm0Jgu5dltTX/bszmR2lheb9BpyN47Q1RbaGTsvFzn0PXAEs+lXDKfshccYZw== - dependencies: - "@hapi/boom" "9.x.x" - "@hapi/hoek" "9.x.x" - -"@hapi/boom@9.x.x": - version "9.1.4" - resolved "https://registry.yarnpkg.com/@hapi/boom/-/boom-9.1.4.tgz#1f9dad367c6a7da9f8def24b4a986fc5a7bd9db6" - integrity sha512-Ls1oH8jaN1vNsqcaHVYJrKmgMcKsC1wcp8bujvXrHaAqD2iDYq3HoOwsxwo09Cuda5R5nC0o0IxlrlTuvPuzSw== - dependencies: - "@hapi/hoek" "9.x.x" - -"@hapi/hoek@9.x.x": - version "9.2.1" - resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.2.1.tgz#9551142a1980503752536b5050fd99f4a7f13b17" - integrity sha512-gfta+H8aziZsm8pZa0vj04KO6biEiisppNgA1kbJvFrrWu9Vm7eaUEy76DIxsuTaWvti5fkJVhllWc6ZTE+Mdw== - -"@material-ui/core@^4.11.2": - version "4.12.3" - resolved "https://registry.yarnpkg.com/@material-ui/core/-/core-4.12.3.tgz#80d665caf0f1f034e52355c5450c0e38b099d3ca" - integrity sha512-sdpgI/PL56QVsEJldwEe4FFaFTLUqN+rd7sSZiRCdx2E/C7z5yK0y/khAWVBH24tXwto7I1hCzNWfJGZIYJKnw== - dependencies: - "@babel/runtime" "^7.4.4" - "@material-ui/styles" "^4.11.4" - "@material-ui/system" "^4.12.1" - "@material-ui/types" "5.1.0" - "@material-ui/utils" "^4.11.2" - "@types/react-transition-group" "^4.2.0" - clsx "^1.0.4" - hoist-non-react-statics "^3.3.2" - popper.js "1.16.1-lts" - prop-types "^15.7.2" - react-is "^16.8.0 || ^17.0.0" - react-transition-group "^4.4.0" - -"@material-ui/data-grid@^4.0.0-alpha.18": - version "4.0.0-alpha.37" - resolved "https://registry.yarnpkg.com/@material-ui/data-grid/-/data-grid-4.0.0-alpha.37.tgz#89d907c4e94e6a0db4e89e4f59160f7811546ca2" - integrity sha512-3T2AG31aad/lWLMLwn1XUP4mUf3H9YZES17dGuYByzkRLCXbBZHBTPEnCctWukajzwm+v0KGg3QpwitGoiDAjA== - dependencies: - "@material-ui/utils" "^5.0.0-alpha.14" - clsx "^1.0.4" - prop-types "^15.7.2" - reselect "^4.0.0" - -"@material-ui/icons@^4.11.2": - version "4.11.2" - resolved "https://registry.yarnpkg.com/@material-ui/icons/-/icons-4.11.2.tgz#b3a7353266519cd743b6461ae9fdfcb1b25eb4c5" - integrity sha512-fQNsKX2TxBmqIGJCSi3tGTO/gZ+eJgWmMJkgDiOfyNaunNaxcklJQFaFogYcFl0qFuaEz1qaXYXboa/bUXVSOQ== - dependencies: - "@babel/runtime" "^7.4.4" - -"@material-ui/lab@^4.0.0-alpha.57": - version "4.0.0-alpha.60" - resolved "https://registry.yarnpkg.com/@material-ui/lab/-/lab-4.0.0-alpha.60.tgz#5ad203aed5a8569b0f1753945a21a05efa2234d2" - integrity sha512-fadlYsPJF+0fx2lRuyqAuJj7hAS1tLDdIEEdov5jlrpb5pp4b+mRDUqQTUxi4inRZHS1bEXpU8QWUhO6xX88aA== - dependencies: - "@babel/runtime" "^7.4.4" - "@material-ui/utils" "^4.11.2" - clsx "^1.0.4" - prop-types "^15.7.2" - react-is "^16.8.0 || ^17.0.0" - -"@material-ui/styles@^4.11.4": - version "4.11.4" - resolved "https://registry.yarnpkg.com/@material-ui/styles/-/styles-4.11.4.tgz#eb9dfccfcc2d208243d986457dff025497afa00d" - integrity sha512-KNTIZcnj/zprG5LW0Sao7zw+yG3O35pviHzejMdcSGCdWbiO8qzRgOYL8JAxAsWBKOKYwVZxXtHWaB5T2Kvxew== - dependencies: - "@babel/runtime" "^7.4.4" - "@emotion/hash" "^0.8.0" - "@material-ui/types" "5.1.0" - "@material-ui/utils" "^4.11.2" - clsx "^1.0.4" - csstype "^2.5.2" - hoist-non-react-statics "^3.3.2" - jss "^10.5.1" - jss-plugin-camel-case "^10.5.1" - jss-plugin-default-unit "^10.5.1" - jss-plugin-global "^10.5.1" - jss-plugin-nested "^10.5.1" - jss-plugin-props-sort "^10.5.1" - jss-plugin-rule-value-function "^10.5.1" - jss-plugin-vendor-prefixer "^10.5.1" - prop-types "^15.7.2" - -"@material-ui/system@^4.12.1": - version "4.12.1" - resolved "https://registry.yarnpkg.com/@material-ui/system/-/system-4.12.1.tgz#2dd96c243f8c0a331b2bb6d46efd7771a399707c" - integrity sha512-lUdzs4q9kEXZGhbN7BptyiS1rLNHe6kG9o8Y307HCvF4sQxbCgpL2qi+gUk+yI8a2DNk48gISEQxoxpgph0xIw== - dependencies: - "@babel/runtime" "^7.4.4" - "@material-ui/utils" "^4.11.2" - csstype "^2.5.2" - prop-types "^15.7.2" - -"@material-ui/types@5.1.0": - version "5.1.0" - resolved "https://registry.yarnpkg.com/@material-ui/types/-/types-5.1.0.tgz#efa1c7a0b0eaa4c7c87ac0390445f0f88b0d88f2" - integrity sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A== - -"@material-ui/utils@^4.11.2": - version "4.11.2" - resolved "https://registry.yarnpkg.com/@material-ui/utils/-/utils-4.11.2.tgz#f1aefa7e7dff2ebcb97d31de51aecab1bb57540a" - integrity sha512-Uul8w38u+PICe2Fg2pDKCaIG7kOyhowZ9vjiC1FsVwPABTW8vPPKfF6OvxRq3IiBaI1faOJmgdvMG7rMJARBhA== - dependencies: - "@babel/runtime" "^7.4.4" - prop-types "^15.7.2" - react-is "^16.8.0 || ^17.0.0" - -"@material-ui/utils@^5.0.0-alpha.14": - version "5.0.0-beta.5" - resolved "https://registry.yarnpkg.com/@material-ui/utils/-/utils-5.0.0-beta.5.tgz#de492037e1f1f0910fda32e6f11b66dfcde2a1c2" - integrity sha512-wtJ3ovXWZdTAz5eLBqvMpYH/IBJb3qMQbGCyL1i00+sf7AUlAuv4QLx+QtX/siA6L7IpxUQVfqpoCpQH1eYRpQ== - dependencies: - "@babel/runtime" "^7.14.8" - "@types/prop-types" "^15.7.4" - "@types/react-is" "^16.7.1 || ^17.0.0" - prop-types "^15.7.2" - react-is "^17.0.2" - -"@napi-rs/triples@^1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@napi-rs/triples/-/triples-1.0.3.tgz#76d6d0c3f4d16013c61e45dfca5ff1e6c31ae53c" - integrity sha512-jDJTpta+P4p1NZTFVLHJ/TLFVYVcOqv6l8xwOeBKNPMgY/zDYH/YH7SJbvrr/h1RcS9GzbPcLKGzpuK9cV56UA== - -"@next/env@11.1.2": - version "11.1.2" - resolved "https://registry.yarnpkg.com/@next/env/-/env-11.1.2.tgz#27996efbbc54c5f949f5e8c0a156e3aa48369b99" - integrity sha512-+fteyVdQ7C/OoulfcF6vd1Yk0FEli4453gr8kSFbU8sKseNSizYq6df5MKz/AjwLptsxrUeIkgBdAzbziyJ3mA== - -"@next/polyfill-module@11.1.2": - version "11.1.2" - resolved "https://registry.yarnpkg.com/@next/polyfill-module/-/polyfill-module-11.1.2.tgz#1fe92c364fdc81add775a16c678f5057c6aace98" - integrity sha512-xZmixqADM3xxtqBV0TpAwSFzWJP0MOQzRfzItHXf1LdQHWb0yofHHC+7eOrPFic8+ZGz5y7BdPkkgR1S25OymA== - -"@next/react-dev-overlay@11.1.2": - version "11.1.2" - resolved "https://registry.yarnpkg.com/@next/react-dev-overlay/-/react-dev-overlay-11.1.2.tgz#73795dc5454b7af168bac93df7099965ebb603be" - integrity sha512-rDF/mGY2NC69mMg2vDqzVpCOlWqnwPUXB2zkARhvknUHyS6QJphPYv9ozoPJuoT/QBs49JJd9KWaAzVBvq920A== - dependencies: - "@babel/code-frame" "7.12.11" - anser "1.4.9" - chalk "4.0.0" - classnames "2.2.6" - css.escape "1.5.1" - data-uri-to-buffer "3.0.1" - platform "1.3.6" - shell-quote "1.7.2" - source-map "0.8.0-beta.0" - stacktrace-parser "0.1.10" - strip-ansi "6.0.0" - -"@next/react-refresh-utils@11.1.2": - version "11.1.2" - resolved "https://registry.yarnpkg.com/@next/react-refresh-utils/-/react-refresh-utils-11.1.2.tgz#44ea40d8e773e4b77bad85e24f6ac041d5e4b4a5" - integrity sha512-hsoJmPfhVqjZ8w4IFzoo8SyECVnN+8WMnImTbTKrRUHOVJcYMmKLL7xf7T0ft00tWwAl/3f3Q3poWIN2Ueql/Q== - -"@next/swc-darwin-arm64@11.1.2": - version "11.1.2" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-11.1.2.tgz#93226c38db488c4b62b30a53b530e87c969b8251" - integrity sha512-hZuwOlGOwBZADA8EyDYyjx3+4JGIGjSHDHWrmpI7g5rFmQNltjlbaefAbiU5Kk7j3BUSDwt30quJRFv3nyJQ0w== - -"@next/swc-darwin-x64@11.1.2": - version "11.1.2" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-11.1.2.tgz#792003989f560c00677b5daeff360b35b510db83" - integrity sha512-PGOp0E1GisU+EJJlsmJVGE+aPYD0Uh7zqgsrpD3F/Y3766Ptfbe1lEPPWnRDl+OzSSrSrX1lkyM/Jlmh5OwNvA== - -"@next/swc-linux-x64-gnu@11.1.2": - version "11.1.2" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-11.1.2.tgz#8216b2ae1f21f0112958735c39dd861088108f37" - integrity sha512-YcDHTJjn/8RqvyJVB6pvEKXihDcdrOwga3GfMv/QtVeLphTouY4BIcEUfrG5+26Nf37MP1ywN3RRl1TxpurAsQ== - -"@next/swc-win32-x64-msvc@11.1.2": - version "11.1.2" - resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-11.1.2.tgz#e15824405df137129918205e43cb5e9339589745" - integrity sha512-e/pIKVdB+tGQYa1cW3sAeHm8gzEri/HYLZHT4WZojrUxgWXqx8pk7S7Xs47uBcFTqBDRvK3EcQpPLf3XdVsDdg== - -"@nivo/annotations@0.73.0": - version "0.73.0" - resolved "https://registry.yarnpkg.com/@nivo/annotations/-/annotations-0.73.0.tgz#6b88b3c1b58a3c8a3b5dcc114b1026c34c083a6f" - integrity sha512-1rLAX5tcQh1Zo8ToENIO2WI1va72UhKc9K3219rtGRhXsy3f2hbmRZiw/iOJCLqzGtmT8RIKA++hOExWSyh4dQ== - dependencies: - "@nivo/colors" "0.73.0" - "@react-spring/web" "9.2.4" - lodash "^4.17.21" - -"@nivo/arcs@0.73.0": - version "0.73.0" - resolved "https://registry.yarnpkg.com/@nivo/arcs/-/arcs-0.73.0.tgz#2211a0c41e8f6ed67374aeebdad607fbb3a1db2f" - integrity sha512-jIjqr3McQUrDWoP6X4CZh8Tg0HphLZdU6K1IDfna2nkG8Dkr2LcB7Ejt5SFbhff+0SC/hjvjNC//h8vcynl7yA== - dependencies: - "@nivo/colors" "0.73.0" - "@react-spring/web" "9.2.4" - d3-shape "^1.3.5" - -"@nivo/axes@0.73.0": - version "0.73.0" - resolved "https://registry.yarnpkg.com/@nivo/axes/-/axes-0.73.0.tgz#d4982bda3c21d318507e4c61b9cce31549f8c894" - integrity sha512-tyB+PqQTW117q9E5vz1jVTywDG6mjqD/RvtVGeqAwHziHAQxpSVb+r0UUtTFOgyaddEbLDaOXPqjD3l6Npo89g== - dependencies: - "@nivo/scales" "0.73.0" - "@react-spring/web" "9.2.4" - d3-format "^1.4.4" - d3-time "^1.0.11" - d3-time-format "^3.0.0" - -"@nivo/bar@^0.73.1": - version "0.73.1" - resolved "https://registry.yarnpkg.com/@nivo/bar/-/bar-0.73.1.tgz#86e59e25af151ead0c09298ec9caf133f38e957e" - integrity sha512-sBltCxCEgFV7kErW4K14OSUjsU1cmYAWWvKoelVI1NTJ0MWhH/Z4UVip6+Zk7/TOzdZntT0x3SE6cAc9Ov7rxg== - dependencies: - "@nivo/annotations" "0.73.0" - "@nivo/axes" "0.73.0" - "@nivo/colors" "0.73.0" - "@nivo/legends" "0.73.0" - "@nivo/recompose" "0.73.0" - "@nivo/scales" "0.73.0" - "@nivo/tooltip" "0.73.0" - "@react-spring/web" "9.2.4" - d3-scale "^3.2.3" - d3-shape "^1.2.2" - lodash "^4.17.21" - -"@nivo/colors@0.73.0": - version "0.73.0" - resolved "https://registry.yarnpkg.com/@nivo/colors/-/colors-0.73.0.tgz#fb22ec0d000b1b471cfa4a7156a787190be45bbe" - integrity sha512-6T1FBR+sv7cCfAZCkjeY71//W4RqDNGkXJqsnyJA/6EsjLEbOBc1l2v66cRmZs2bioOkYcS+f4hQxB9d1WAgwA== - dependencies: - d3-color "^2.0.0" - d3-scale "^3.2.3" - d3-scale-chromatic "^2.0.0" - lodash "^4.17.21" - react-motion "^0.5.2" - -"@nivo/core@^0.73.0": - version "0.73.0" - resolved "https://registry.yarnpkg.com/@nivo/core/-/core-0.73.0.tgz#58fac20c8cd7eac12bfdc96619554764ca225cdf" - integrity sha512-NFKSk5NQgC2NB3olG8hltN4b4Ri0rB0vt3q1yGmQj+RdGRS4f82Dtwt5Ratxu6QeZD8lt0DhqN9Q7TJ+j/kt0g== - dependencies: - "@nivo/recompose" "0.73.0" - "@react-spring/web" "9.2.4" - d3-color "^2.0.0" - d3-format "^1.4.4" - d3-hierarchy "^1.1.8" - d3-interpolate "^2.0.1" - d3-scale "^3.2.3" - d3-scale-chromatic "^2.0.0" - d3-shape "^1.3.5" - d3-time-format "^3.0.0" - lodash "^4.17.21" - resize-observer-polyfill "^1.5.1" - -"@nivo/legends@0.73.0": - version "0.73.0" - resolved "https://registry.yarnpkg.com/@nivo/legends/-/legends-0.73.0.tgz#ef344038c4ff03249ffffebaf14412d012004fda" - integrity sha512-OWyu3U6PJL2VGlAfoz6nTU4opXHlR0yp0h+0Q0rf/hMKQLiew6NmecKcR1Nx2Qw4dJHgOnZRXqQ6vQrhcNV3WQ== - -"@nivo/pie@^0.73.0": - version "0.73.0" - resolved "https://registry.yarnpkg.com/@nivo/pie/-/pie-0.73.0.tgz#4370bfdaaded5b0ba159cc544548b02373baf55a" - integrity sha512-CWwpK9NSs1hfqvhcVvYJMTk+qD/tuxKDloURjfgJglUOs5m2NT1osDJN64jr02aZ6wZyplkP6WvJuc4K6mej6Q== - dependencies: - "@nivo/arcs" "0.73.0" - "@nivo/colors" "0.73.0" - "@nivo/legends" "0.73.0" - "@nivo/tooltip" "0.73.0" - d3-shape "^1.3.5" - -"@nivo/recompose@0.73.0": - version "0.73.0" - resolved "https://registry.yarnpkg.com/@nivo/recompose/-/recompose-0.73.0.tgz#f228fdc633df5453c67640f59b74368071559e73" - integrity sha512-WzFJMkyfX1jGPxjcGjvMKxzodgARv9x+alWbr4o39wJ+0eqpZlq6K7oaJ0RnVazcKrKxIe7mKK2piZ0usRt+Zg== - dependencies: - react-lifecycles-compat "^3.0.4" - -"@nivo/scales@0.73.0": - version "0.73.0" - resolved "https://registry.yarnpkg.com/@nivo/scales/-/scales-0.73.0.tgz#ec1b660b3792fdc509326fd87e88e8f46a532f63" - integrity sha512-xnvCEXz6KkT7/SG4ubJ/hBtbMcTraZnJSKKSVkttBkvfIhkSpQChHkNX8g0Wd4hpwBv2QlltLbjoD6pJ0b/mRA== - dependencies: - d3-scale "^3.2.3" - d3-time "^1.0.11" - d3-time-format "^3.0.0" - lodash "^4.17.21" - -"@nivo/tooltip@0.73.0": - version "0.73.0" - resolved "https://registry.yarnpkg.com/@nivo/tooltip/-/tooltip-0.73.0.tgz#c76d69ad90c1fb90b65b221ea74ad07b9e69ec59" - integrity sha512-6tRRV5mzn1siArAlChXqBC/ISD0AV0tvGRbuyZokVXcM2xbvtfi6OAPaZ10UYbVejBo1rRI3gnhhyNkP0UG2ug== - dependencies: - "@react-spring/web" "9.2.4" - -"@node-rs/helper@1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@node-rs/helper/-/helper-1.2.1.tgz#e079b05f21ff4329d82c4e1f71c0290e4ecdc70c" - integrity sha512-R5wEmm8nbuQU0YGGmYVjEc0OHtYsuXdpRG+Ut/3wZ9XAvQWyThN08bTh2cBJgoZxHQUPtvRfeQuxcAgLuiBISg== - dependencies: - "@napi-rs/triples" "^1.0.3" - -"@react-spring/animated@~9.2.0", "@react-spring/animated@~9.2.6-beta.0": - version "9.2.6" - resolved "https://registry.yarnpkg.com/@react-spring/animated/-/animated-9.2.6.tgz#58f30fb75d8bfb7ccbc156cfd6b974a8f3dfd54e" - integrity sha512-xjL6nmixYNDvnpTs1FFMsMfSC0tURwPCU3b2jWNriYGLfwZ7c/TcyaEZA7yiNnmdFnuR3f3Z27AqIgaFC083Cw== - dependencies: - "@react-spring/shared" "~9.2.6-beta.0" - "@react-spring/types" "~9.2.6-beta.0" - -"@react-spring/animated@~9.3.0": - version "9.3.0" - resolved "https://registry.yarnpkg.com/@react-spring/animated/-/animated-9.3.0.tgz#294f7696e450c4ae3abd2b59a6dd08bf70b53d3f" - integrity sha512-QvuyW77eDvLhdJyO6FFldlWlvnuKK2cpOx4+Zr962RyT/0IO1tbNDRO6G1vM8va6mbv6tmfYmRGKmKYePN3kVg== - dependencies: - "@react-spring/shared" "~9.3.0" - "@react-spring/types" "~9.3.0" - -"@react-spring/core@~9.2.0": - version "9.2.6" - resolved "https://registry.yarnpkg.com/@react-spring/core/-/core-9.2.6.tgz#ae22338fe55d070caf03abb4293b5519ba620d93" - integrity sha512-uPHUxmu+w6mHJrfQTMtmGJ8iZEwiVxz9kH7dRyk69bkZJt9z+w0Oj3UF4J3VcECZsbm3HRhN2ogXSAaqGjwhQw== - dependencies: - "@react-spring/animated" "~9.2.6-beta.0" - "@react-spring/shared" "~9.2.6-beta.0" - "@react-spring/types" "~9.2.6-beta.0" - -"@react-spring/core@~9.3.0": - version "9.3.0" - resolved "https://registry.yarnpkg.com/@react-spring/core/-/core-9.3.0.tgz#2d0534c5b53c7e39b8e9ed3d996502828c90f4d4" - integrity sha512-SZQOIX7wkIagmucAi7zxqGGIb9A60o9n5922UrWo8Kl3FdG7FgrNwqr0kOI43/pMFeL70/PXwFhBatB03N5ctw== - dependencies: - "@react-spring/animated" "~9.3.0" - "@react-spring/shared" "~9.3.0" - "@react-spring/types" "~9.3.0" - -"@react-spring/konva@~9.3.0": - version "9.3.0" - resolved "https://registry.yarnpkg.com/@react-spring/konva/-/konva-9.3.0.tgz#97b23b2f235a9805d39279a0a1027c7d9646d6fb" - integrity sha512-lyUWxzEateE6Qxpc81oxJb5yiNDdj36Q9R9euJAgjl2dvUDaX85rVGqaB25+72yA1iQg5I4Kymj3UZVvPthRlA== - dependencies: - "@react-spring/animated" "~9.3.0" - "@react-spring/core" "~9.3.0" - "@react-spring/shared" "~9.3.0" - "@react-spring/types" "~9.3.0" - -"@react-spring/native@~9.3.0": - version "9.3.0" - resolved "https://registry.yarnpkg.com/@react-spring/native/-/native-9.3.0.tgz#6fee1ccaa8d70a19c239b27e95bcc050776f1725" - integrity sha512-lvKV5qxqnE5AMtTHv8xwAocGED4+VRxpljwBl1lbtileq3WnvOn7CpMLZNGc5TXjLWAE3zfoNJui69/jE/3uSw== - dependencies: - "@react-spring/animated" "~9.3.0" - "@react-spring/core" "~9.3.0" - "@react-spring/shared" "~9.3.0" - "@react-spring/types" "~9.3.0" - -"@react-spring/rafz@~9.2.6-beta.0": - version "9.2.6" - resolved "https://registry.yarnpkg.com/@react-spring/rafz/-/rafz-9.2.6.tgz#d97484003875bf5fb5e6ec22dee97cc208363e48" - integrity sha512-62SivLKEpo7EfHPkxO5J3g9Cr9LF6+1A1RVOMJhkcpEYtbdbmma/d63Xp8qpMPEpk7uuWxaTb6jjyxW33pW3sg== - -"@react-spring/rafz@~9.3.0": - version "9.3.0" - resolved "https://registry.yarnpkg.com/@react-spring/rafz/-/rafz-9.3.0.tgz#e791c0ae854f7c1a512ae87f34fff36934d82d29" - integrity sha512-FD04d2TNb3xOZ6+04qwDmC3d0H4X6gvhsxU71/nSm4PPYRqFzZEolcVPmrHlbGzco3bvXKI+Kp2pIrpXLPUJFA== - -"@react-spring/shared@~9.2.0", "@react-spring/shared@~9.2.6-beta.0": - version "9.2.6" - resolved "https://registry.yarnpkg.com/@react-spring/shared/-/shared-9.2.6.tgz#2c84e62cc0cfbbbbeb5546acd46c1f4b248bc562" - integrity sha512-Qrm9fopKG/RxZ3Rw+4euhrpnB3uXSyiON9skHbcBfmkkzagpkUR66MX1YLrhHw0UchcZuSDnXs0Lonzt1rpWag== - dependencies: - "@react-spring/rafz" "~9.2.6-beta.0" - "@react-spring/types" "~9.2.6-beta.0" - -"@react-spring/shared@~9.3.0": - version "9.3.0" - resolved "https://registry.yarnpkg.com/@react-spring/shared/-/shared-9.3.0.tgz#7b4393094a97a1384f74fd8088e0b896e8f0c411" - integrity sha512-7ZFY2Blu/wxbLGcYvQavyLUVi9bK/is1bsn11qZ9AaZb4iucRyIf2jgjBfKZFCq4qgi7S/7QmDQG7sucUyLELg== - dependencies: - "@react-spring/rafz" "~9.3.0" - "@react-spring/types" "~9.3.0" - -"@react-spring/three@~9.3.0": - version "9.3.0" - resolved "https://registry.yarnpkg.com/@react-spring/three/-/three-9.3.0.tgz#e3fc49de1411eb1a7aa937fec8db33252f11d294" - integrity sha512-RKMXXdcNK0nbwLbmle/0KT/idGGpOxvI5lT1KtN8R3cgJWQBKYWVtzg+B/RgmQVNxO/QNlsKGWTjURockTRSVQ== - dependencies: - "@react-spring/animated" "~9.3.0" - "@react-spring/core" "~9.3.0" - "@react-spring/shared" "~9.3.0" - "@react-spring/types" "~9.3.0" - -"@react-spring/types@~9.2.0", "@react-spring/types@~9.2.6-beta.0": - version "9.2.6" - resolved "https://registry.yarnpkg.com/@react-spring/types/-/types-9.2.6.tgz#f60722fcf9f8492ae16d0bdc47f0ea3c2a16d2cf" - integrity sha512-l7mCw182DtDMnCI8CB9orgTAEoFZRtdQ6aS6YeEAqYcy3nQZPmPggIHH9DxyLw7n7vBPRSzu9gCvUMgXKpTflg== - -"@react-spring/types@~9.3.0": - version "9.3.0" - resolved "https://registry.yarnpkg.com/@react-spring/types/-/types-9.3.0.tgz#54ec58ca40414984209c8baa75fddd394f9e2949" - integrity sha512-q4cDr2RSPblXMD3Rxvk6qcC7nmhhfV2izEBP06hb8ZCXznA6qJirG3RMpi29kBtEQiw1lWR59hAXKhauaPtbOA== - -"@react-spring/web@9.2.4": - version "9.2.4" - resolved "https://registry.yarnpkg.com/@react-spring/web/-/web-9.2.4.tgz#c6d5464a954bfd0d7bc90117050f796a95ebfa08" - integrity sha512-vtPvOalLFvuju/MDBtoSnCyt0xXSL6Amyv82fljOuWPl1yGd4M1WteijnYL9Zlriljl0a3oXcPunAVYTD9dbDQ== - dependencies: - "@react-spring/animated" "~9.2.0" - "@react-spring/core" "~9.2.0" - "@react-spring/shared" "~9.2.0" - "@react-spring/types" "~9.2.0" - -"@react-spring/web@~9.3.0": - version "9.3.0" - resolved "https://registry.yarnpkg.com/@react-spring/web/-/web-9.3.0.tgz#48d1ebdd1d484065e0a943dbbb343af259496427" - integrity sha512-OTAGKRdyz6fLRR1tABFyw9KMpytyATIndQrj0O6RG47GfjiInpf4+WZKxo763vpS7z1OlnkI81WLUm/sqOqAnA== - dependencies: - "@react-spring/animated" "~9.3.0" - "@react-spring/core" "~9.3.0" - "@react-spring/shared" "~9.3.0" - "@react-spring/types" "~9.3.0" - -"@react-spring/zdog@~9.3.0": - version "9.3.0" - resolved "https://registry.yarnpkg.com/@react-spring/zdog/-/zdog-9.3.0.tgz#d84b69375017d864514ebcf59511731c5cc0280f" - integrity sha512-JOQwtg/MQ6sWwmKNY4w/R1TVXohIUkrbSgDfgUEK45ERTDwZGZzIo9QbqHv4dwEBK4Wa2Hfrcdf8cnEaNNzdAQ== - dependencies: - "@react-spring/animated" "~9.3.0" - "@react-spring/core" "~9.3.0" - "@react-spring/shared" "~9.3.0" - "@react-spring/types" "~9.3.0" - -"@types/node@*": - version "16.11.6" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" - integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== - -"@types/node@^14.14.22": - version "14.17.32" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.32.tgz#2ca61c9ef8c77f6fa1733be9e623ceb0d372ad96" - integrity sha512-JcII3D5/OapPGx+eJ+Ik1SQGyt6WvuqdRfh9jUwL6/iHGjmyOriBDciBUu7lEIBTL2ijxwrR70WUnw5AEDmFvQ== - -"@types/prop-types@*", "@types/prop-types@^15.7.4": - version "15.7.4" - resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11" - integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ== - -"@types/react-is@^16.7.1 || ^17.0.0": - version "17.0.3" - resolved "https://registry.yarnpkg.com/@types/react-is/-/react-is-17.0.3.tgz#2d855ba575f2fc8d17ef9861f084acc4b90a137a" - integrity sha512-aBTIWg1emtu95bLTLx0cpkxwGW3ueZv71nE2YFBpL8k/z5czEW8yYpOo8Dp+UUAFAtKwNaOsh/ioSeQnWlZcfw== - dependencies: - "@types/react" "*" - -"@types/react-transition-group@^4.2.0": - version "4.4.4" - resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.4.tgz#acd4cceaa2be6b757db61ed7b432e103242d163e" - integrity sha512-7gAPz7anVK5xzbeQW9wFBDg7G++aPLAFY0QaSMOou9rJZpbuI58WAuJrgu+qR92l61grlnCUe7AFX8KGahAgug== - dependencies: - "@types/react" "*" - -"@types/react@*", "@types/react@^17.0.0": - version "17.0.33" - resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.33.tgz#e01ae3de7613dac1094569880bb3792732203ad5" - integrity sha512-pLWntxXpDPaU+RTAuSGWGSEL2FRTNyRQOjSWDke/rxRg14ncsZvx8AKWMWZqvc1UOaJIAoObdZhAWvRaHFi5rw== - dependencies: - "@types/prop-types" "*" - "@types/scheduler" "*" - csstype "^3.0.2" - -"@types/react@17.0.1": - version "17.0.1" - resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.1.tgz#eb1f1407dea8da3bc741879c1192aa703ab9975b" - integrity sha512-w8t9f53B2ei4jeOqf/gxtc2Sswnc3LBK5s0DyJcg5xd10tMHXts2N31cKjWfH9IC/JvEPa/YF1U4YeP1t4R6HQ== - dependencies: - "@types/prop-types" "*" - csstype "^3.0.2" - -"@types/scheduler@*": - version "0.16.2" - resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" - integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== - -"@usedapp/core@0.5.4": - version "0.5.4" - resolved "https://registry.yarnpkg.com/@usedapp/core/-/core-0.5.4.tgz#c03b1fa34e4ff658ee2d6a65ac74de46c7ab2b7f" - integrity sha512-KAGnH/PA8WBEOR14N9mE3woDXkraVHNVj64XvZTAyxFW9N6rSUX3e6LX85TQ3WRvX1fysppP9433c2ciUrx4Yw== - dependencies: - "@types/react" "17.0.1" - "@web3-react/core" "6.1.1" - "@web3-react/injected-connector" "6.0.7" - "@web3-react/network-connector" "6.1.5" - ethers "5.4.1" - lodash.merge "^4.6.2" - nanoid "3.1.22" - -"@web3-react/abstract-connector@^6.0.7": - version "6.0.7" - resolved "https://registry.yarnpkg.com/@web3-react/abstract-connector/-/abstract-connector-6.0.7.tgz#401b3c045f1e0fab04256311be49d5144e9badc6" - integrity sha512-RhQasA4Ox8CxUC0OENc1AJJm8UTybu/oOCM61Zjg6y0iF7Z0sqv1Ai1VdhC33hrQpA8qSBgoXN9PaP8jKmtdqg== - dependencies: - "@web3-react/types" "^6.0.7" - -"@web3-react/core@6.1.1": - version "6.1.1" - resolved "https://registry.yarnpkg.com/@web3-react/core/-/core-6.1.1.tgz#06c853890723f600b387b738a4b71ef41d5cccb7" - integrity sha512-HKXOgPNCmFvrVsed+aW/HlVhwzs8t3b+nzg3BoxgJQo/5yLiJXSumHRBdUrPxhBQiHkHRZiVPAvzf/8JMnm74Q== - dependencies: - "@ethersproject/keccak256" "^5.0.0-beta.130" - "@web3-react/abstract-connector" "^6.0.7" - "@web3-react/types" "^6.0.7" - tiny-invariant "^1.0.6" - tiny-warning "^1.0.3" - -"@web3-react/injected-connector@6.0.7": - version "6.0.7" - resolved "https://registry.yarnpkg.com/@web3-react/injected-connector/-/injected-connector-6.0.7.tgz#1e0be23f51fa07fe6547fe986768a46b74c3a426" - integrity sha512-Y7aJSz6pg+MWKtvdyuqyy6LWuH+4Tqtph1LWfiyVms9II9ar/9B/de4R8wh4wjg91wmHkU+D75yP09E/Soh2RA== - dependencies: - "@web3-react/abstract-connector" "^6.0.7" - "@web3-react/types" "^6.0.7" - tiny-warning "^1.0.3" - -"@web3-react/network-connector@6.1.5": - version "6.1.5" - resolved "https://registry.yarnpkg.com/@web3-react/network-connector/-/network-connector-6.1.5.tgz#1fcce1dc7b03dac23fcc01ad0b0c870cb0e39e0b" - integrity sha512-Uwk8iMG8YCnTeKmyXt3Q7QJN28qTs0YTTW8/aes2R26KmYWCk3GdL2eal0QcXUixJy/IjrhXzbwzHgpneJqrWg== - dependencies: - "@web3-react/abstract-connector" "^6.0.7" - "@web3-react/types" "^6.0.7" - tiny-invariant "^1.0.6" - -"@web3-react/types@^6.0.7": - version "6.0.7" - resolved "https://registry.yarnpkg.com/@web3-react/types/-/types-6.0.7.tgz#34a6204224467eedc6123abaf55fbb6baeb2809f" - integrity sha512-ofGmfDhxmNT1/P/MgVa8IKSkCStFiyvXe+U5tyZurKdrtTDFU+wJ/LxClPDtFerWpczNFPUSrKcuhfPX1sI6+A== - -aes-js@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-3.0.0.tgz#e21df10ad6c2053295bcbb8dab40b09dbea87e4d" - integrity sha1-4h3xCtbCBTKVvLuNq0Cwnb6ofk0= - -anser@1.4.9: - version "1.4.9" - resolved "https://registry.yarnpkg.com/anser/-/anser-1.4.9.tgz#1f85423a5dcf8da4631a341665ff675b96845760" - integrity sha512-AI+BjTeGt2+WFk4eWcqbQ7snZpDBt8SaLlj0RT2h5xfdWaiy51OjYvqwMrNzJLGy8iOAL6nKDITWO+rd4MkYEA== - -ansi-regex@^5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" - integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== - -ansi-styles@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" - integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== - dependencies: - color-convert "^1.9.0" - -ansi-styles@^4.1.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" - integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== - dependencies: - color-convert "^2.0.1" - -anymatch@~3.1.1: - version "3.1.2" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" - integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - -asn1.js@^5.2.0: - version "5.4.1" - resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" - integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA== - dependencies: - bn.js "^4.0.0" - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - safer-buffer "^2.1.0" - -assert@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/assert/-/assert-2.0.0.tgz#95fc1c616d48713510680f2eaf2d10dd22e02d32" - integrity sha512-se5Cd+js9dXJnu6Ag2JFc00t+HmHOen+8Q+L7O9zI0PqQXr20uk2J0XQqMxZEeo5U50o8Nvmmx7dZrl+Ufr35A== - dependencies: - es6-object-assign "^1.1.0" - is-nan "^1.2.1" - object-is "^1.0.1" - util "^0.12.0" - -assert@^1.1.1: - version "1.5.0" - resolved "https://registry.yarnpkg.com/assert/-/assert-1.5.0.tgz#55c109aaf6e0aefdb3dc4b71240c70bf574b18eb" - integrity sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA== - dependencies: - object-assign "^4.1.1" - util "0.10.3" - -ast-types@0.13.2: - version "0.13.2" - resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.13.2.tgz#df39b677a911a83f3a049644fb74fdded23cea48" - integrity sha512-uWMHxJxtfj/1oZClOxDEV1sQ1HCDkA4MG8Gr69KKeBjEVH0R84WlejZ0y2DcwyBlpAEMltmVYkVgqfLFb2oyiA== - -autoprefixer@^9.6.1: - version "9.8.8" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.8.tgz#fd4bd4595385fa6f06599de749a4d5f7a474957a" - integrity sha512-eM9d/swFopRt5gdJ7jrpCwgvEMIayITpojhkkSMRsFHYuH5bkSQ4p/9qTEHtmNudUZh22Tehu7I6CxAW0IXTKA== - dependencies: - browserslist "^4.12.0" - caniuse-lite "^1.0.30001109" - normalize-range "^0.1.2" - num2fraction "^1.2.2" - picocolors "^0.2.1" - postcss "^7.0.32" - postcss-value-parser "^4.1.0" - -available-typed-arrays@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" - integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== - -axios@^0.22.0: - version "0.22.0" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.22.0.tgz#bf702c41fb50fbca4539589d839a077117b79b25" - integrity sha512-Z0U3uhqQeg1oNcihswf4ZD57O3NrR1+ZXhxaROaWpDmsDTx7T2HNBV2ulBtie2hwJptu8UvgnJoK+BIqdzh/1w== - dependencies: - follow-redirects "^1.14.4" - -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - -base64-js@^1.0.2: - version "1.5.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" - integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== - -bech32@1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/bech32/-/bech32-1.1.4.tgz#e38c9f37bf179b8eb16ae3a772b40c356d4832e9" - integrity sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ== - -big.js@^5.2.2: - version "5.2.2" - resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" - integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== - -binary-extensions@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" - integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== - -bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.9: - version "4.12.0" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" - integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== - -bn.js@^5.0.0, bn.js@^5.1.1: - version "5.2.0" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.0.tgz#358860674396c6997771a9d051fcc1b57d4ae002" - integrity sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw== - -braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== - dependencies: - fill-range "^7.0.1" - -brorand@^1.0.1, brorand@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" - integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= - -browserify-aes@^1.0.0, browserify-aes@^1.0.4: - version "1.2.0" - resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" - integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA== - dependencies: - buffer-xor "^1.0.3" - cipher-base "^1.0.0" - create-hash "^1.1.0" - evp_bytestokey "^1.0.3" - inherits "^2.0.1" - safe-buffer "^5.0.1" - -browserify-cipher@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz#8d6474c1b870bfdabcd3bcfcc1934a10e94f15f0" - integrity sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w== - dependencies: - browserify-aes "^1.0.4" - browserify-des "^1.0.0" - evp_bytestokey "^1.0.0" - -browserify-des@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.2.tgz#3af4f1f59839403572f1c66204375f7a7f703e9c" - integrity sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A== - dependencies: - cipher-base "^1.0.1" - des.js "^1.0.0" - inherits "^2.0.1" - safe-buffer "^5.1.2" - -browserify-rsa@^4.0.0, browserify-rsa@^4.0.1: - version "4.1.0" - resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.1.0.tgz#b2fd06b5b75ae297f7ce2dc651f918f5be158c8d" - integrity sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog== - dependencies: - bn.js "^5.0.0" - randombytes "^2.0.1" - -browserify-sign@^4.0.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.1.tgz#eaf4add46dd54be3bb3b36c0cf15abbeba7956c3" - integrity sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg== - dependencies: - bn.js "^5.1.1" - browserify-rsa "^4.0.1" - create-hash "^1.2.0" - create-hmac "^1.1.7" - elliptic "^6.5.3" - inherits "^2.0.4" - parse-asn1 "^5.1.5" - readable-stream "^3.6.0" - safe-buffer "^5.2.0" - -browserify-zlib@0.2.0, browserify-zlib@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f" - integrity sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA== - dependencies: - pako "~1.0.5" - -browserslist@4.16.6: - version "4.16.6" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.6.tgz#d7901277a5a88e554ed305b183ec9b0c08f66fa2" - integrity sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ== - dependencies: - caniuse-lite "^1.0.30001219" - colorette "^1.2.2" - electron-to-chromium "^1.3.723" - escalade "^3.1.1" - node-releases "^1.1.71" - -browserslist@^4.12.0, browserslist@^4.6.4: - version "4.17.5" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.17.5.tgz#c827bbe172a4c22b123f5e337533ceebadfdd559" - integrity sha512-I3ekeB92mmpctWBoLXe0d5wPS2cBuRvvW0JyyJHMrk9/HmP2ZjrTboNAZ8iuGqaEIlKguljbQY32OkOJIRrgoA== - dependencies: - caniuse-lite "^1.0.30001271" - electron-to-chromium "^1.3.878" - escalade "^3.1.1" - node-releases "^2.0.1" - picocolors "^1.0.0" - -buffer-xor@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" - integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk= - -buffer@5.6.0: - version "5.6.0" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786" - integrity sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw== - dependencies: - base64-js "^1.0.2" - ieee754 "^1.1.4" - -buffer@^4.3.0: - version "4.9.2" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" - integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg== - dependencies: - base64-js "^1.0.2" - ieee754 "^1.1.4" - isarray "^1.0.0" - -builtin-status-codes@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" - integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug= - -bytes@3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" - integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== - -call-bind@^1.0.0, call-bind@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" - integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== - dependencies: - function-bind "^1.1.1" - get-intrinsic "^1.0.2" - -caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001202, caniuse-lite@^1.0.30001219, caniuse-lite@^1.0.30001228, caniuse-lite@^1.0.30001271: - version "1.0.30001271" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001271.tgz#0dda0c9bcae2cf5407cd34cac304186616cc83e8" - integrity sha512-BBruZFWmt3HFdVPS8kceTBIguKxu4f99n5JNp06OlPD/luoAMIaIK5ieV5YjnBLH3Nysai9sxj9rpJj4ZisXOA== - -chalk@2.4.2, chalk@^2.0.0: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - -chalk@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.0.0.tgz#6e98081ed2d17faab615eb52ac66ec1fe6209e72" - integrity sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - -chokidar@3.5.1: - version "3.5.1" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a" - integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw== - dependencies: - anymatch "~3.1.1" - braces "~3.0.2" - glob-parent "~5.1.0" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.5.0" - optionalDependencies: - fsevents "~2.3.1" - -cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" - integrity sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q== - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - -classnames@2.2.6: - version "2.2.6" - resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" - integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q== - -classnames@^2.2.6: - version "2.3.1" - resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e" - integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA== - -clsx@^1.0.4: - version "1.1.1" - resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188" - integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA== - -color-convert@^1.9.0: - version "1.9.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" - -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= - -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -colorette@^1.2.2: - version "1.4.0" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.4.0.tgz#5190fbb87276259a86ad700bff2c6d6faa3fca40" - integrity sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g== - -commondir@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" - integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= - -console-browserify@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" - integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA== - -constants-browserify@1.0.0, constants-browserify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" - integrity sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U= - -convert-source-map@1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" - integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA== - dependencies: - safe-buffer "~5.1.1" - -core-util-is@~1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" - integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== - -create-ecdh@^4.0.0: - version "4.0.4" - resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e" - integrity sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A== - dependencies: - bn.js "^4.1.0" - elliptic "^6.5.3" - -create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" - integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg== - dependencies: - cipher-base "^1.0.1" - inherits "^2.0.1" - md5.js "^1.3.4" - ripemd160 "^2.0.1" - sha.js "^2.4.0" - -create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: - version "1.1.7" - resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" - integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== - dependencies: - cipher-base "^1.0.3" - create-hash "^1.1.0" - inherits "^2.0.1" - ripemd160 "^2.0.0" - safe-buffer "^5.0.1" - sha.js "^2.4.8" - -crypto-browserify@3.12.0, crypto-browserify@^3.11.0: - version "3.12.0" - resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" - integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg== - dependencies: - browserify-cipher "^1.0.0" - browserify-sign "^4.0.0" - create-ecdh "^4.0.0" - create-hash "^1.1.0" - create-hmac "^1.1.0" - diffie-hellman "^5.0.0" - inherits "^2.0.1" - pbkdf2 "^3.0.3" - public-encrypt "^4.0.0" - randombytes "^2.0.0" - randomfill "^1.0.3" - -css-blank-pseudo@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz#dfdefd3254bf8a82027993674ccf35483bfcb3c5" - integrity sha512-LHz35Hr83dnFeipc7oqFDmsjHdljj3TQtxGGiNWSOsTLIAubSm4TEz8qCaKFpk7idaQ1GfWscF4E6mgpBysA1w== - dependencies: - postcss "^7.0.5" - -css-has-pseudo@^0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/css-has-pseudo/-/css-has-pseudo-0.10.0.tgz#3c642ab34ca242c59c41a125df9105841f6966ee" - integrity sha512-Z8hnfsZu4o/kt+AuFzeGpLVhFOGO9mluyHBaA2bA8aCGTwah5sT3WV/fTHH8UNZUytOIImuGPrl/prlb4oX4qQ== - dependencies: - postcss "^7.0.6" - postcss-selector-parser "^5.0.0-rc.4" - -css-prefers-color-scheme@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/css-prefers-color-scheme/-/css-prefers-color-scheme-3.1.1.tgz#6f830a2714199d4f0d0d0bb8a27916ed65cff1f4" - integrity sha512-MTu6+tMs9S3EUqzmqLXEcgNRbNkkD/TGFvowpeoWJn5Vfq7FMgsmRQs9X5NXAURiOBmOxm/lLjsDNXDE6k9bhg== - dependencies: - postcss "^7.0.5" - -css-vendor@^2.0.8: - version "2.0.8" - resolved "https://registry.yarnpkg.com/css-vendor/-/css-vendor-2.0.8.tgz#e47f91d3bd3117d49180a3c935e62e3d9f7f449d" - integrity sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ== - dependencies: - "@babel/runtime" "^7.8.3" - is-in-browser "^1.0.2" - -css.escape@1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb" - integrity sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s= - -cssdb@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-4.4.0.tgz#3bf2f2a68c10f5c6a08abd92378331ee803cddb0" - integrity sha512-LsTAR1JPEM9TpGhl/0p3nQecC2LJ0kD8X5YARu1hk/9I1gril5vDtMZyNxcEpxxDj34YNck/ucjuoUd66K03oQ== - -cssesc@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-2.0.0.tgz#3b13bd1bb1cb36e1bcb5a4dcd27f54c5dcb35703" - integrity sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg== - -cssesc@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" - integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== - -cssnano-preset-simple@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/cssnano-preset-simple/-/cssnano-preset-simple-3.0.0.tgz#e95d0012699ca2c741306e9a3b8eeb495a348dbe" - integrity sha512-vxQPeoMRqUT3c/9f0vWeVa2nKQIHFpogtoBvFdW4GQ3IvEJ6uauCP6p3Y5zQDLFcI7/+40FTgX12o7XUL0Ko+w== - dependencies: - caniuse-lite "^1.0.30001202" - -cssnano-simple@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/cssnano-simple/-/cssnano-simple-3.0.0.tgz#a4b8ccdef4c7084af97e19bc5b93b4ecf211e90f" - integrity sha512-oU3ueli5Dtwgh0DyeohcIEE00QVfbPR3HzyXdAl89SfnQG3y0/qcpfLVW+jPIh3/rgMZGwuW96rejZGaYE9eUg== - dependencies: - cssnano-preset-simple "^3.0.0" - -csstype@^2.5.2: - version "2.6.18" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.18.tgz#980a8b53085f34af313410af064f2bd241784218" - integrity sha512-RSU6Hyeg14am3Ah4VZEmeX8H7kLwEEirXe6aU2IPfKNvhXwTflK5HQRDNI0ypQXoqmm+QPyG2IaPuQE5zMwSIQ== - -csstype@^3.0.2: - version "3.0.9" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.9.tgz#6410af31b26bd0520933d02cbc64fce9ce3fbf0b" - integrity sha512-rpw6JPxK6Rfg1zLOYCSwle2GFOOsnjmDYDaBwEcwoOg4qlsIVCN789VkBZDJAGi4T07gI4YSutR43t9Zz4Lzuw== - -d3-array@2, d3-array@^2.3.0: - version "2.12.1" - resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-2.12.1.tgz#e20b41aafcdffdf5d50928004ececf815a465e81" - integrity sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ== - dependencies: - internmap "^1.0.0" - -"d3-color@1 - 2", d3-color@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-2.0.0.tgz#8d625cab42ed9b8f601a1760a389f7ea9189d62e" - integrity sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ== - -"d3-format@1 - 2": - version "2.0.0" - resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-2.0.0.tgz#a10bcc0f986c372b729ba447382413aabf5b0767" - integrity sha512-Ab3S6XuE/Q+flY96HXT0jOXcM4EAClYFnRGY5zsjRGNy6qCYrQsMffs7cV5Q9xejb35zxW5hf/guKw34kvIKsA== - -d3-format@^1.4.4: - version "1.4.5" - resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.4.5.tgz#374f2ba1320e3717eb74a9356c67daee17a7edb4" - integrity sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ== - -d3-hierarchy@^1.1.8: - version "1.1.9" - resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz#2f6bee24caaea43f8dc37545fa01628559647a83" - integrity sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ== - -"d3-interpolate@1 - 2", "d3-interpolate@1.2.0 - 2", d3-interpolate@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-2.0.1.tgz#98be499cfb8a3b94d4ff616900501a64abc91163" - integrity sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ== - dependencies: - d3-color "1 - 2" - -d3-path@1: - version "1.0.9" - resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.9.tgz#48c050bb1fe8c262493a8caf5524e3e9591701cf" - integrity sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg== - -d3-scale-chromatic@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-2.0.0.tgz#c13f3af86685ff91323dc2f0ebd2dabbd72d8bab" - integrity sha512-LLqy7dJSL8yDy7NRmf6xSlsFZ6zYvJ4BcWFE4zBrOPnQERv9zj24ohnXKRbyi9YHnYV+HN1oEO3iFK971/gkzA== - dependencies: - d3-color "1 - 2" - d3-interpolate "1 - 2" - -d3-scale@^3.2.3: - version "3.3.0" - resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-3.3.0.tgz#28c600b29f47e5b9cd2df9749c206727966203f3" - integrity sha512-1JGp44NQCt5d1g+Yy+GeOnZP7xHo0ii8zsQp6PGzd+C1/dl0KGsp9A7Mxwp+1D1o4unbTTxVdU/ZOIEBoeZPbQ== - dependencies: - d3-array "^2.3.0" - d3-format "1 - 2" - d3-interpolate "1.2.0 - 2" - d3-time "^2.1.1" - d3-time-format "2 - 3" - -d3-shape@^1.2.2, d3-shape@^1.3.5: - version "1.3.7" - resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.3.7.tgz#df63801be07bc986bc54f63789b4fe502992b5d7" - integrity sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw== - dependencies: - d3-path "1" - -"d3-time-format@2 - 3", d3-time-format@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-3.0.0.tgz#df8056c83659e01f20ac5da5fdeae7c08d5f1bb6" - integrity sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag== - dependencies: - d3-time "1 - 2" - -"d3-time@1 - 2", d3-time@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-2.1.1.tgz#e9d8a8a88691f4548e68ca085e5ff956724a6682" - integrity sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ== - dependencies: - d3-array "2" - -d3-time@^1.0.11: - version "1.1.0" - resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.1.0.tgz#b1e19d307dae9c900b7e5b25ffc5dcc249a8a0f1" - integrity sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA== - -data-uri-to-buffer@3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz#594b8973938c5bc2c33046535785341abc4f3636" - integrity sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og== - -debug@2: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -define-properties@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" - integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== - dependencies: - object-keys "^1.0.12" - -depd@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" - integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= - -des.js@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843" - integrity sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA== - dependencies: - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - -diffie-hellman@^5.0.0: - version "5.0.3" - resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" - integrity sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg== - dependencies: - bn.js "^4.1.0" - miller-rabin "^4.0.0" - randombytes "^2.0.0" - -dom-helpers@^5.0.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" - integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== - dependencies: - "@babel/runtime" "^7.8.7" - csstype "^3.0.2" - -domain-browser@4.19.0: - version "4.19.0" - resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-4.19.0.tgz#1093e17c0a17dbd521182fe90d49ac1370054af1" - integrity sha512-fRA+BaAWOR/yr/t7T9E9GJztHPeFjj8U35ajyAjCDtAAnTn1Rc1f6W6VGPJrO1tkQv9zWu+JRof7z6oQtiYVFQ== - -domain-browser@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" - integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== - -electron-to-chromium@^1.3.723, electron-to-chromium@^1.3.878: - version "1.3.879" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.879.tgz#4aba9700cfb241fb95c6ed69e31785e3d1605a43" - integrity sha512-zJo+D9GwbJvM31IdFmwcGvychhk4KKbKYo2GWlsn+C/dxz2NwmbhGJjWwTfFSF2+eFH7VvfA8MCZ8SOqTrlnpw== - -elliptic@6.5.4, elliptic@^6.5.3: - version "6.5.4" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" - integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ== - dependencies: - bn.js "^4.11.9" - brorand "^1.1.0" - hash.js "^1.0.0" - hmac-drbg "^1.0.1" - inherits "^2.0.4" - minimalistic-assert "^1.0.1" - minimalistic-crypto-utils "^1.0.1" - -emojis-list@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" - integrity sha1-TapNnbAPmBmIDHn6RXrlsJof04k= - -encoding@0.1.13: - version "0.1.13" - resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9" - integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== - dependencies: - iconv-lite "^0.6.2" - -enhanced-resolve@^5.7.0: - version "5.8.3" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.8.3.tgz#6d552d465cce0423f5b3d718511ea53826a7b2f0" - integrity sha512-EGAbGvH7j7Xt2nc0E7D99La1OiEs8LnyimkRgwExpUMScN6O+3x9tIWs7PLQZVNx4YD+00skHXPXi1yQHpAmZA== - dependencies: - graceful-fs "^4.2.4" - tapable "^2.2.0" - -es-abstract@^1.18.5: - version "1.19.1" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.19.1.tgz#d4885796876916959de78edaa0df456627115ec3" - integrity sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w== - dependencies: - call-bind "^1.0.2" - es-to-primitive "^1.2.1" - function-bind "^1.1.1" - get-intrinsic "^1.1.1" - get-symbol-description "^1.0.0" - has "^1.0.3" - has-symbols "^1.0.2" - internal-slot "^1.0.3" - is-callable "^1.2.4" - is-negative-zero "^2.0.1" - is-regex "^1.1.4" - is-shared-array-buffer "^1.0.1" - is-string "^1.0.7" - is-weakref "^1.0.1" - object-inspect "^1.11.0" - object-keys "^1.1.1" - object.assign "^4.1.2" - string.prototype.trimend "^1.0.4" - string.prototype.trimstart "^1.0.4" - unbox-primitive "^1.0.1" - -es-to-primitive@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" - integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== - dependencies: - is-callable "^1.1.4" - is-date-object "^1.0.1" - is-symbol "^1.0.2" - -es6-object-assign@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/es6-object-assign/-/es6-object-assign-1.1.0.tgz#c2c3582656247c39ea107cb1e6652b6f9f24523c" - integrity sha1-wsNYJlYkfDnqEHyx5mUrb58kUjw= - -escalade@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" - integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== - -escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= - -etag@1.8.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" - integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= - -ethers@5.4.1: - version "5.4.1" - resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.4.1.tgz#bcff1e9f45bf1a061bf313ec04e8d9881d2d53f9" - integrity sha512-SrcddMdCgP1hukDvCPd87Aipbf4NWjQvdfAbZ65XSZGbfyuYPtIrUJPDH5B1SBRsdlfiEgX3eoz28DdBDzMNFg== - dependencies: - "@ethersproject/abi" "5.4.0" - "@ethersproject/abstract-provider" "5.4.0" - "@ethersproject/abstract-signer" "5.4.0" - "@ethersproject/address" "5.4.0" - "@ethersproject/base64" "5.4.0" - "@ethersproject/basex" "5.4.0" - "@ethersproject/bignumber" "5.4.0" - "@ethersproject/bytes" "5.4.0" - "@ethersproject/constants" "5.4.0" - "@ethersproject/contracts" "5.4.0" - "@ethersproject/hash" "5.4.0" - "@ethersproject/hdnode" "5.4.0" - "@ethersproject/json-wallets" "5.4.0" - "@ethersproject/keccak256" "5.4.0" - "@ethersproject/logger" "5.4.0" - "@ethersproject/networks" "5.4.1" - "@ethersproject/pbkdf2" "5.4.0" - "@ethersproject/properties" "5.4.0" - "@ethersproject/providers" "5.4.1" - "@ethersproject/random" "5.4.0" - "@ethersproject/rlp" "5.4.0" - "@ethersproject/sha2" "5.4.0" - "@ethersproject/signing-key" "5.4.0" - "@ethersproject/solidity" "5.4.0" - "@ethersproject/strings" "5.4.0" - "@ethersproject/transactions" "5.4.0" - "@ethersproject/units" "5.4.0" - "@ethersproject/wallet" "5.4.0" - "@ethersproject/web" "5.4.0" - "@ethersproject/wordlists" "5.4.0" - -events@^3.0.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" - integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== - -evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" - integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA== - dependencies: - md5.js "^1.3.4" - safe-buffer "^5.1.1" - -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== - dependencies: - to-regex-range "^5.0.1" - -find-cache-dir@3.3.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.1.tgz#89b33fad4a4670daa94f855f7fbe31d6d84fe880" - integrity sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ== - dependencies: - commondir "^1.0.1" - make-dir "^3.0.2" - pkg-dir "^4.1.0" - -find-up@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" - integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== - dependencies: - locate-path "^5.0.0" - path-exists "^4.0.0" - -flatten@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.3.tgz#c1283ac9f27b368abc1e36d1ff7b04501a30356b" - integrity sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg== - -follow-redirects@^1.14.4: - version "1.14.4" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.4.tgz#838fdf48a8bbdd79e52ee51fb1c94e3ed98b9379" - integrity sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g== - -foreach@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" - integrity sha1-C+4AUBiusmDQo6865ljdATbsG5k= - -fsevents@~2.3.1: - version "2.3.2" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" - integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== - -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== - -get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6" - integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== - dependencies: - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.1" - -get-orientation@1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/get-orientation/-/get-orientation-1.1.2.tgz#20507928951814f8a91ded0a0e67b29dfab98947" - integrity sha512-/pViTfifW+gBbh/RnlFYHINvELT9Znt+SYyDKAUL6uV6By019AK/s+i9XP4jSwq7lwP38Fd8HVeTxym3+hkwmQ== - dependencies: - stream-parser "^0.3.1" - -get-symbol-description@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6" - integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw== - dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.1.1" - -glob-parent@~5.1.0: - version "5.1.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== - dependencies: - is-glob "^4.0.1" - -glob-to-regexp@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" - integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== - -graceful-fs@^4.1.2, graceful-fs@^4.2.4: - version "4.2.8" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a" - integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg== - -has-bigints@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113" - integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA== - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= - -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - -has-symbols@^1.0.1, has-symbols@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" - integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== - -has-tostringtag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" - integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== - dependencies: - has-symbols "^1.0.2" - -has@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== - dependencies: - function-bind "^1.1.1" - -hash-base@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33" - integrity sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA== - dependencies: - inherits "^2.0.4" - readable-stream "^3.6.0" - safe-buffer "^5.2.0" - -hash.js@1.1.7, hash.js@^1.0.0, hash.js@^1.0.3: - version "1.1.7" - resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" - integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== - dependencies: - inherits "^2.0.3" - minimalistic-assert "^1.0.1" - -he@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" - integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== - -hmac-drbg@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" - integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE= - dependencies: - hash.js "^1.0.3" - minimalistic-assert "^1.0.0" - minimalistic-crypto-utils "^1.0.1" - -hoist-non-react-statics@^3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" - integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== - dependencies: - react-is "^16.7.0" - -http-errors@1.7.3: - version "1.7.3" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" - integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== - dependencies: - depd "~1.1.2" - inherits "2.0.4" - setprototypeof "1.1.1" - statuses ">= 1.5.0 < 2" - toidentifier "1.0.0" - -https-browserify@1.0.0, https-browserify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" - integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= - -hyphenate-style-name@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz#691879af8e220aea5750e8827db4ef62a54e361d" - integrity sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ== - -iconv-lite@0.4.24: - version "0.4.24" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - -iconv-lite@^0.6.2: - version "0.6.3" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" - integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== - dependencies: - safer-buffer ">= 2.1.2 < 3.0.0" - -ieee754@^1.1.4: - version "1.2.1" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" - integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== - -image-size@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/image-size/-/image-size-1.0.0.tgz#58b31fe4743b1cec0a0ac26f5c914d3c5b2f0750" - integrity sha512-JLJ6OwBfO1KcA+TvJT+v8gbE6iWbj24LyDNFgFEN0lzegn6cC6a/p3NIDaepMsJjQjlUWqIC7wJv8lBFxPNjcw== - dependencies: - queue "6.0.2" - -indexes-of@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" - integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc= - -inherits@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" - integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE= - -inherits@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= - -inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3, inherits@~2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -internal-slot@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c" - integrity sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA== - dependencies: - get-intrinsic "^1.1.0" - has "^1.0.3" - side-channel "^1.0.4" - -internmap@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/internmap/-/internmap-1.0.1.tgz#0017cc8a3b99605f0302f2b198d272e015e5df95" - integrity sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw== - -is-arguments@^1.0.4: - version "1.1.1" - resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" - integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - -is-bigint@^1.0.1: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" - integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== - dependencies: - has-bigints "^1.0.1" - -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== - dependencies: - binary-extensions "^2.0.0" - -is-boolean-object@^1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" - integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - -is-callable@^1.1.4, is-callable@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945" - integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w== - -is-date-object@^1.0.1: - version "1.0.5" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" - integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== - dependencies: - has-tostringtag "^1.0.0" - -is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= - -is-generator-function@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" - integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== - dependencies: - has-tostringtag "^1.0.0" - -is-glob@^4.0.1, is-glob@~4.0.1: - version "4.0.3" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" - integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== - dependencies: - is-extglob "^2.1.1" - -is-in-browser@^1.0.2, is-in-browser@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/is-in-browser/-/is-in-browser-1.1.3.tgz#56ff4db683a078c6082eb95dad7dc62e1d04f835" - integrity sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU= - -is-nan@^1.2.1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d" - integrity sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w== - dependencies: - call-bind "^1.0.0" - define-properties "^1.1.3" - -is-negative-zero@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24" - integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w== - -is-number-object@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.6.tgz#6a7aaf838c7f0686a50b4553f7e54a96494e89f0" - integrity sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g== - dependencies: - has-tostringtag "^1.0.0" - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -is-regex@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" - integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - -is-shared-array-buffer@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz#97b0c85fbdacb59c9c446fe653b82cf2b5b7cfe6" - integrity sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA== - -is-string@^1.0.5, is-string@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" - integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== - dependencies: - has-tostringtag "^1.0.0" - -is-symbol@^1.0.2, is-symbol@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" - integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== - dependencies: - has-symbols "^1.0.2" - -is-typed-array@^1.1.3, is-typed-array@^1.1.7: - version "1.1.8" - resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.8.tgz#cbaa6585dc7db43318bc5b89523ea384a6f65e79" - integrity sha512-HqH41TNZq2fgtGT8WHVFVJhBVGuY3AnP3Q36K8JKXUxSxRgk/d+7NjmwG2vo2mYmXK8UYZKu0qH8bVP5gEisjA== - dependencies: - available-typed-arrays "^1.0.5" - call-bind "^1.0.2" - es-abstract "^1.18.5" - foreach "^2.0.5" - has-tostringtag "^1.0.0" - -is-weakref@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.1.tgz#842dba4ec17fa9ac9850df2d6efbc1737274f2a2" - integrity sha512-b2jKc2pQZjaeFYWEf7ScFj+Be1I+PXmlu572Q8coTXZ+LD/QQZ7ShPMst8h16riVgyXTQwUsFEl74mDvc/3MHQ== - dependencies: - call-bind "^1.0.0" - -isarray@^1.0.0, isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= - -isomorphic-fetch@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz#0267b005049046d2421207215d45d6a262b8b8b4" - integrity sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA== - dependencies: - node-fetch "^2.6.1" - whatwg-fetch "^3.4.1" - -jest-worker@27.0.0-next.5: - version "27.0.0-next.5" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.0.0-next.5.tgz#5985ee29b12a4e191f4aae4bb73b97971d86ec28" - integrity sha512-mk0umAQ5lT+CaOJ+Qp01N6kz48sJG2kr2n1rX0koqKf6FIygQV0qLOdN9SCYID4IVeSigDOcPeGLozdMLYfb5g== - dependencies: - "@types/node" "*" - merge-stream "^2.0.0" - supports-color "^8.0.0" - -js-sha3@0.5.7: - version "0.5.7" - resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.5.7.tgz#0d4ffd8002d5333aabaf4a23eed2f6374c9f28e7" - integrity sha1-DU/9gALVMzqrr0oj7tL2N0yfKOc= - -js-sha3@0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840" - integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q== - -"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" - integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== - -json5@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" - integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== - dependencies: - minimist "^1.2.0" - -jss-plugin-camel-case@^10.5.1: - version "10.8.2" - resolved "https://registry.yarnpkg.com/jss-plugin-camel-case/-/jss-plugin-camel-case-10.8.2.tgz#8d7f915c8115afaff8cbde08faf610ec9892fba6" - integrity sha512-2INyxR+1UdNuKf4v9It3tNfPvf7IPrtkiwzofeKuMd5D58/dxDJVUQYRVg/n460rTlHUfsEQx43hDrcxi9dSPA== - dependencies: - "@babel/runtime" "^7.3.1" - hyphenate-style-name "^1.0.3" - jss "10.8.2" - -jss-plugin-default-unit@^10.5.1: - version "10.8.2" - resolved "https://registry.yarnpkg.com/jss-plugin-default-unit/-/jss-plugin-default-unit-10.8.2.tgz#c66f12e02e0815d911b85c02c2a979ee7b4ce69a" - integrity sha512-UZ7cwT9NFYSG+SEy7noRU50s4zifulFdjkUNKE+u6mW7vFP960+RglWjTgMfh79G6OENZmaYnjHV/gcKV4nSxg== - dependencies: - "@babel/runtime" "^7.3.1" - jss "10.8.2" - -jss-plugin-global@^10.5.1: - version "10.8.2" - resolved "https://registry.yarnpkg.com/jss-plugin-global/-/jss-plugin-global-10.8.2.tgz#1a35632a693cf50113bcc5ffe6b51969df79c4ec" - integrity sha512-UaYMSPsYZ7s/ECGoj4KoHC2jwQd5iQ7K+FFGnCAILdQrv7hPmvM2Ydg45ThT/sH46DqktCRV2SqjRuxeBH8nRA== - dependencies: - "@babel/runtime" "^7.3.1" - jss "10.8.2" - -jss-plugin-nested@^10.5.1: - version "10.8.2" - resolved "https://registry.yarnpkg.com/jss-plugin-nested/-/jss-plugin-nested-10.8.2.tgz#79f3c7f75ea6a36ae72fe52e777035bb24d230c7" - integrity sha512-acRvuPJOb930fuYmhkJaa994EADpt8TxI63Iyg96C8FJ9T2xRyU5T6R1IYKRwUiqZo+2Sr7fdGzRTDD4uBZaMA== - dependencies: - "@babel/runtime" "^7.3.1" - jss "10.8.2" - tiny-warning "^1.0.2" - -jss-plugin-props-sort@^10.5.1: - version "10.8.2" - resolved "https://registry.yarnpkg.com/jss-plugin-props-sort/-/jss-plugin-props-sort-10.8.2.tgz#e25a7471868652c394562b6dc5433dcaea7dff6f" - integrity sha512-wqdcjayKRWBZnNpLUrXvsWqh+5J5YToAQ+8HNBNw0kZxVvCDwzhK2Nx6AKs7p+5/MbAh2PLgNW5Ym/ysbVAuqQ== - dependencies: - "@babel/runtime" "^7.3.1" - jss "10.8.2" - -jss-plugin-rule-value-function@^10.5.1: - version "10.8.2" - resolved "https://registry.yarnpkg.com/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.8.2.tgz#55354b55f1b2968a15976729968f767f02d64049" - integrity sha512-bW0EKAs+0HXpb6BKJhrn94IDdiWb0CnSluTkh0rGEgyzY/nmD1uV/Wf6KGlesGOZ9gmJzQy+9FFdxIUID1c9Ug== - dependencies: - "@babel/runtime" "^7.3.1" - jss "10.8.2" - tiny-warning "^1.0.2" - -jss-plugin-vendor-prefixer@^10.5.1: - version "10.8.2" - resolved "https://registry.yarnpkg.com/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.8.2.tgz#ebb4a482642f34091e454901e21176441dd5f475" - integrity sha512-DeGv18QsSiYLSVIEB2+l0af6OToUe0JB+trpzUxyqD2QRC/5AzzDrCrYffO5AHZ81QbffYvSN/pkfZaTWpRXlg== - dependencies: - "@babel/runtime" "^7.3.1" - css-vendor "^2.0.8" - jss "10.8.2" - -jss@10.8.2, jss@^10.5.1: - version "10.8.2" - resolved "https://registry.yarnpkg.com/jss/-/jss-10.8.2.tgz#4b2a30b094b924629a64928236017a52c7c97505" - integrity sha512-FkoUNxI329CKQ9OQC8L72MBF9KPf5q8mIupAJ5twU7G7XREW7ahb+7jFfrjZ4iy1qvhx1HwIWUIvkZBDnKkEdQ== - dependencies: - "@babel/runtime" "^7.3.1" - csstype "^3.0.2" - is-in-browser "^1.1.3" - tiny-warning "^1.0.2" - -loader-utils@1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7" - integrity sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA== - dependencies: - big.js "^5.2.2" - emojis-list "^2.0.0" - json5 "^1.0.1" - -locate-path@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" - integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== - dependencies: - p-locate "^4.1.0" - -lodash.merge@^4.6.2: - version "4.6.2" - resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" - integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== - -lodash.sortby@^4.7.0: - version "4.7.0" - resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" - integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= - -lodash@^4.17.21: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== - -loose-envify@^1.1.0, loose-envify@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" - integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== - dependencies: - js-tokens "^3.0.0 || ^4.0.0" - -make-dir@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" - integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== - dependencies: - semver "^6.0.0" - -md5.js@^1.3.4: - version "1.3.5" - resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" - integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg== - dependencies: - hash-base "^3.0.0" - inherits "^2.0.1" - safe-buffer "^5.1.2" - -merge-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" - integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== - -miller-rabin@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" - integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA== - dependencies: - bn.js "^4.0.0" - brorand "^1.0.1" - -minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" - integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== - -minimalistic-crypto-utils@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" - integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= - -minimist@^1.2.0: - version "1.2.5" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" - integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= - -nanoid@3.1.22: - version "3.1.22" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.22.tgz#b35f8fb7d151990a8aebd5aa5015c03cf726f844" - integrity sha512-/2ZUaJX2ANuLtTvqTlgqBQNJoQO398KyJgZloL0PZkC0dpysjncRUPsFe3DUPzz/y3h+u7C46np8RMuvF3jsSQ== - -nanoid@^3.1.23: - version "3.1.30" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.30.tgz#63f93cc548d2a113dc5dfbc63bfa09e2b9b64362" - integrity sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ== - -native-url@0.3.4: - version "0.3.4" - resolved "https://registry.yarnpkg.com/native-url/-/native-url-0.3.4.tgz#29c943172aed86c63cee62c8c04db7f5756661f8" - integrity sha512-6iM8R99ze45ivyH8vybJ7X0yekIcPf5GgLV5K0ENCbmRcaRIDoj37BC8iLEmaaBfqqb8enuZ5p0uhY+lVAbAcA== - dependencies: - querystring "^0.2.0" - -next-transpile-modules@^6.1.0: - version "6.4.1" - resolved "https://registry.yarnpkg.com/next-transpile-modules/-/next-transpile-modules-6.4.1.tgz#471ceff06439d1681447c43ef8856268f8596186" - integrity sha512-trUMkm+bkjMci7mXSOQkF3apEY18K6YuaeG4mkLwiQtDdsmdVXzopNyZGc2nJq8OwHYDgJr1QlsBQuQb/2vXlw== - dependencies: - enhanced-resolve "^5.7.0" - escalade "^3.1.1" - -next@^11.1.2: - version "11.1.2" - resolved "https://registry.yarnpkg.com/next/-/next-11.1.2.tgz#527475787a9a362f1bc916962b0c0655cc05bc91" - integrity sha512-azEYL0L+wFjv8lstLru3bgvrzPvK0P7/bz6B/4EJ9sYkXeW8r5Bjh78D/Ol7VOg0EIPz0CXoe72hzAlSAXo9hw== - dependencies: - "@babel/runtime" "7.15.3" - "@hapi/accept" "5.0.2" - "@next/env" "11.1.2" - "@next/polyfill-module" "11.1.2" - "@next/react-dev-overlay" "11.1.2" - "@next/react-refresh-utils" "11.1.2" - "@node-rs/helper" "1.2.1" - assert "2.0.0" - ast-types "0.13.2" - browserify-zlib "0.2.0" - browserslist "4.16.6" - buffer "5.6.0" - caniuse-lite "^1.0.30001228" - chalk "2.4.2" - chokidar "3.5.1" - constants-browserify "1.0.0" - crypto-browserify "3.12.0" - cssnano-simple "3.0.0" - domain-browser "4.19.0" - encoding "0.1.13" - etag "1.8.1" - find-cache-dir "3.3.1" - get-orientation "1.1.2" - https-browserify "1.0.0" - image-size "1.0.0" - jest-worker "27.0.0-next.5" - native-url "0.3.4" - node-fetch "2.6.1" - node-html-parser "1.4.9" - node-libs-browser "^2.2.1" - os-browserify "0.3.0" - p-limit "3.1.0" - path-browserify "1.0.1" - pnp-webpack-plugin "1.6.4" - postcss "8.2.15" - process "0.11.10" - querystring-es3 "0.2.1" - raw-body "2.4.1" - react-is "17.0.2" - react-refresh "0.8.3" - stream-browserify "3.0.0" - stream-http "3.1.1" - string_decoder "1.3.0" - styled-jsx "4.0.1" - timers-browserify "2.0.12" - tty-browserify "0.0.1" - use-subscription "1.5.1" - util "0.12.4" - vm-browserify "1.1.2" - watchpack "2.1.1" - optionalDependencies: - "@next/swc-darwin-arm64" "11.1.2" - "@next/swc-darwin-x64" "11.1.2" - "@next/swc-linux-x64-gnu" "11.1.2" - "@next/swc-win32-x64-msvc" "11.1.2" - -node-fetch@2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" - integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== - -node-fetch@^2.6.1: - version "2.6.5" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.5.tgz#42735537d7f080a7e5f78b6c549b7146be1742fd" - integrity sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ== - dependencies: - whatwg-url "^5.0.0" - -node-html-parser@1.4.9: - version "1.4.9" - resolved "https://registry.yarnpkg.com/node-html-parser/-/node-html-parser-1.4.9.tgz#3c8f6cac46479fae5800725edb532e9ae8fd816c" - integrity sha512-UVcirFD1Bn0O+TSmloHeHqZZCxHjvtIeGdVdGMhyZ8/PWlEiZaZ5iJzR189yKZr8p0FXN58BUeC7RHRkf/KYGw== - dependencies: - he "1.2.0" - -node-libs-browser@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425" - integrity sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q== - dependencies: - assert "^1.1.1" - browserify-zlib "^0.2.0" - buffer "^4.3.0" - console-browserify "^1.1.0" - constants-browserify "^1.0.0" - crypto-browserify "^3.11.0" - domain-browser "^1.1.1" - events "^3.0.0" - https-browserify "^1.0.0" - os-browserify "^0.3.0" - path-browserify "0.0.1" - process "^0.11.10" - punycode "^1.2.4" - querystring-es3 "^0.2.0" - readable-stream "^2.3.3" - stream-browserify "^2.0.1" - stream-http "^2.7.2" - string_decoder "^1.0.0" - timers-browserify "^2.0.4" - tty-browserify "0.0.0" - url "^0.11.0" - util "^0.11.0" - vm-browserify "^1.0.1" - -node-releases@^1.1.71: - version "1.1.77" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.77.tgz#50b0cfede855dd374e7585bf228ff34e57c1c32e" - integrity sha512-rB1DUFUNAN4Gn9keO2K1efO35IDK7yKHCdCaIMvFO7yUYmmZYeDjnGKle26G4rwj+LKRQpjyUUvMkPglwGCYNQ== - -node-releases@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.1.tgz#3d1d395f204f1f2f29a54358b9fb678765ad2fc5" - integrity sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA== - -normalize-path@^3.0.0, normalize-path@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - -normalize-range@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" - integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI= - -num2fraction@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" - integrity sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4= - -object-assign@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= - -object-inspect@^1.11.0, object-inspect@^1.9.0: - version "1.11.0" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.11.0.tgz#9dceb146cedd4148a0d9e51ab88d34cf509922b1" - integrity sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg== - -object-is@^1.0.1: - version "1.1.5" - resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac" - integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - -object-keys@^1.0.12, object-keys@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" - integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== - -object.assign@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" - integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== - dependencies: - call-bind "^1.0.0" - define-properties "^1.1.3" - has-symbols "^1.0.1" - object-keys "^1.1.1" - -os-browserify@0.3.0, os-browserify@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" - integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc= - -p-limit@3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" - integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== - dependencies: - yocto-queue "^0.1.0" - -p-limit@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" - integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== - dependencies: - p-try "^2.0.0" - -p-locate@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" - integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== - dependencies: - p-limit "^2.2.0" - -p-try@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" - integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== - -pako@~1.0.5: - version "1.0.11" - resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" - integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== - -parse-asn1@^5.0.0, parse-asn1@^5.1.5: - version "5.1.6" - resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.6.tgz#385080a3ec13cb62a62d39409cb3e88844cdaed4" - integrity sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw== - dependencies: - asn1.js "^5.2.0" - browserify-aes "^1.0.0" - evp_bytestokey "^1.0.0" - pbkdf2 "^3.0.3" - safe-buffer "^5.1.1" - -path-browserify@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a" - integrity sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ== - -path-browserify@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd" - integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g== - -path-exists@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" - integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== - -pbkdf2@^3.0.3: - version "3.1.2" - resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.2.tgz#dd822aa0887580e52f1a039dc3eda108efae3075" - integrity sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA== - dependencies: - create-hash "^1.1.2" - create-hmac "^1.1.4" - ripemd160 "^2.0.1" - safe-buffer "^5.0.1" - sha.js "^2.4.8" - -performance-now@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" - integrity sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU= - -performance-now@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" - integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= - -picocolors@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-0.2.1.tgz#570670f793646851d1ba135996962abad587859f" - integrity sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA== - -picocolors@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" - integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== - -picomatch@^2.0.4, picomatch@^2.2.1: - version "2.3.0" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" - integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== - -pkg-dir@^4.1.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" - integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== - dependencies: - find-up "^4.0.0" - -platform@1.3.6: - version "1.3.6" - resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.6.tgz#48b4ce983164b209c2d45a107adb31f473a6e7a7" - integrity sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg== - -pnp-webpack-plugin@1.6.4: - version "1.6.4" - resolved "https://registry.yarnpkg.com/pnp-webpack-plugin/-/pnp-webpack-plugin-1.6.4.tgz#c9711ac4dc48a685dabafc86f8b6dd9f8df84149" - integrity sha512-7Wjy+9E3WwLOEL30D+m8TSTF7qJJUJLONBnwQp0518siuMxUQUbgZwssaFX+QKlZkjHZcw/IpZCt/H0srrntSg== - dependencies: - ts-pnp "^1.1.6" - -popper.js@1.16.1-lts: - version "1.16.1-lts" - resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1-lts.tgz#cf6847b807da3799d80ee3d6d2f90df8a3f50b05" - integrity sha512-Kjw8nKRl1m+VrSFCoVGPph93W/qrSO7ZkqPpTf7F4bk/sqcfWK019dWBUpE/fBOsOQY1dks/Bmcbfn1heM/IsA== - -postcss-attribute-case-insensitive@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-4.0.2.tgz#d93e46b504589e94ac7277b0463226c68041a880" - integrity sha512-clkFxk/9pcdb4Vkn0hAHq3YnxBQ2p0CGD1dy24jN+reBck+EWxMbxSUqN4Yj7t0w8csl87K6p0gxBe1utkJsYA== - dependencies: - postcss "^7.0.2" - postcss-selector-parser "^6.0.2" - -postcss-color-functional-notation@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/postcss-color-functional-notation/-/postcss-color-functional-notation-2.0.1.tgz#5efd37a88fbabeb00a2966d1e53d98ced93f74e0" - integrity sha512-ZBARCypjEDofW4P6IdPVTLhDNXPRn8T2s1zHbZidW6rPaaZvcnCS2soYFIQJrMZSxiePJ2XIYTlcb2ztr/eT2g== - dependencies: - postcss "^7.0.2" - postcss-values-parser "^2.0.0" - -postcss-color-gray@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/postcss-color-gray/-/postcss-color-gray-5.0.0.tgz#532a31eb909f8da898ceffe296fdc1f864be8547" - integrity sha512-q6BuRnAGKM/ZRpfDascZlIZPjvwsRye7UDNalqVz3s7GDxMtqPY6+Q871liNxsonUw8oC61OG+PSaysYpl1bnw== - dependencies: - "@csstools/convert-colors" "^1.4.0" - postcss "^7.0.5" - postcss-values-parser "^2.0.0" - -postcss-color-hex-alpha@^5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/postcss-color-hex-alpha/-/postcss-color-hex-alpha-5.0.3.tgz#a8d9ca4c39d497c9661e374b9c51899ef0f87388" - integrity sha512-PF4GDel8q3kkreVXKLAGNpHKilXsZ6xuu+mOQMHWHLPNyjiUBOr75sp5ZKJfmv1MCus5/DWUGcK9hm6qHEnXYw== - dependencies: - postcss "^7.0.14" - postcss-values-parser "^2.0.1" - -postcss-color-mod-function@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/postcss-color-mod-function/-/postcss-color-mod-function-3.0.3.tgz#816ba145ac11cc3cb6baa905a75a49f903e4d31d" - integrity sha512-YP4VG+xufxaVtzV6ZmhEtc+/aTXH3d0JLpnYfxqTvwZPbJhWqp8bSY3nfNzNRFLgB4XSaBA82OE4VjOOKpCdVQ== - dependencies: - "@csstools/convert-colors" "^1.4.0" - postcss "^7.0.2" - postcss-values-parser "^2.0.0" - -postcss-color-rebeccapurple@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-4.0.1.tgz#c7a89be872bb74e45b1e3022bfe5748823e6de77" - integrity sha512-aAe3OhkS6qJXBbqzvZth2Au4V3KieR5sRQ4ptb2b2O8wgvB3SJBsdG+jsn2BZbbwekDG8nTfcCNKcSfe/lEy8g== - dependencies: - postcss "^7.0.2" - postcss-values-parser "^2.0.0" - -postcss-custom-media@^7.0.8: - version "7.0.8" - resolved "https://registry.yarnpkg.com/postcss-custom-media/-/postcss-custom-media-7.0.8.tgz#fffd13ffeffad73621be5f387076a28b00294e0c" - integrity sha512-c9s5iX0Ge15o00HKbuRuTqNndsJUbaXdiNsksnVH8H4gdc+zbLzr/UasOwNG6CTDpLFekVY4672eWdiiWu2GUg== - dependencies: - postcss "^7.0.14" - -postcss-custom-properties@^8.0.11: - version "8.0.11" - resolved "https://registry.yarnpkg.com/postcss-custom-properties/-/postcss-custom-properties-8.0.11.tgz#2d61772d6e92f22f5e0d52602df8fae46fa30d97" - integrity sha512-nm+o0eLdYqdnJ5abAJeXp4CEU1c1k+eB2yMCvhgzsds/e0umabFrN6HoTy/8Q4K5ilxERdl/JD1LO5ANoYBeMA== - dependencies: - postcss "^7.0.17" - postcss-values-parser "^2.0.1" - -postcss-custom-selectors@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/postcss-custom-selectors/-/postcss-custom-selectors-5.1.2.tgz#64858c6eb2ecff2fb41d0b28c9dd7b3db4de7fba" - integrity sha512-DSGDhqinCqXqlS4R7KGxL1OSycd1lydugJ1ky4iRXPHdBRiozyMHrdu0H3o7qNOCiZwySZTUI5MV0T8QhCLu+w== - dependencies: - postcss "^7.0.2" - postcss-selector-parser "^5.0.0-rc.3" - -postcss-dir-pseudo-class@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-5.0.0.tgz#6e3a4177d0edb3abcc85fdb6fbb1c26dabaeaba2" - integrity sha512-3pm4oq8HYWMZePJY+5ANriPs3P07q+LW6FAdTlkFH2XqDdP4HeeJYMOzn0HYLhRSjBO3fhiqSwwU9xEULSrPgw== - dependencies: - postcss "^7.0.2" - postcss-selector-parser "^5.0.0-rc.3" - -postcss-double-position-gradients@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/postcss-double-position-gradients/-/postcss-double-position-gradients-1.0.0.tgz#fc927d52fddc896cb3a2812ebc5df147e110522e" - integrity sha512-G+nV8EnQq25fOI8CH/B6krEohGWnF5+3A6H/+JEpOncu5dCnkS1QQ6+ct3Jkaepw1NGVqqOZH6lqrm244mCftA== - dependencies: - postcss "^7.0.5" - postcss-values-parser "^2.0.0" - -postcss-env-function@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/postcss-env-function/-/postcss-env-function-2.0.2.tgz#0f3e3d3c57f094a92c2baf4b6241f0b0da5365d7" - integrity sha512-rwac4BuZlITeUbiBq60h/xbLzXY43qOsIErngWa4l7Mt+RaSkT7QBjXVGTcBHupykkblHMDrBFh30zchYPaOUw== - dependencies: - postcss "^7.0.2" - postcss-values-parser "^2.0.0" - -postcss-flexbugs-fixes@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-5.0.2.tgz#2028e145313074fc9abe276cb7ca14e5401eb49d" - integrity sha512-18f9voByak7bTktR2QgDveglpn9DTbBWPUzSOe9g0N4WR/2eSt6Vrcbf0hmspvMI6YWGywz6B9f7jzpFNJJgnQ== - -postcss-focus-visible@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-focus-visible/-/postcss-focus-visible-4.0.0.tgz#477d107113ade6024b14128317ade2bd1e17046e" - integrity sha512-Z5CkWBw0+idJHSV6+Bgf2peDOFf/x4o+vX/pwcNYrWpXFrSfTkQ3JQ1ojrq9yS+upnAlNRHeg8uEwFTgorjI8g== - dependencies: - postcss "^7.0.2" - -postcss-focus-within@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-focus-within/-/postcss-focus-within-3.0.0.tgz#763b8788596cee9b874c999201cdde80659ef680" - integrity sha512-W0APui8jQeBKbCGZudW37EeMCjDeVxKgiYfIIEo8Bdh5SpB9sxds/Iq8SEuzS0Q4YFOlG7EPFulbbxujpkrV2w== - dependencies: - postcss "^7.0.2" - -postcss-font-variant@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-font-variant/-/postcss-font-variant-4.0.1.tgz#42d4c0ab30894f60f98b17561eb5c0321f502641" - integrity sha512-I3ADQSTNtLTTd8uxZhtSOrTCQ9G4qUVKPjHiDk0bV75QSxXjVWiJVJ2VLdspGUi9fbW9BcjKJoRvxAH1pckqmA== - dependencies: - postcss "^7.0.2" - -postcss-gap-properties@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postcss-gap-properties/-/postcss-gap-properties-2.0.0.tgz#431c192ab3ed96a3c3d09f2ff615960f902c1715" - integrity sha512-QZSqDaMgXCHuHTEzMsS2KfVDOq7ZFiknSpkrPJY6jmxbugUPTuSzs/vuE5I3zv0WAS+3vhrlqhijiprnuQfzmg== - dependencies: - postcss "^7.0.2" - -postcss-image-set-function@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/postcss-image-set-function/-/postcss-image-set-function-3.0.1.tgz#28920a2f29945bed4c3198d7df6496d410d3f288" - integrity sha512-oPTcFFip5LZy8Y/whto91L9xdRHCWEMs3e1MdJxhgt4jy2WYXfhkng59fH5qLXSCPN8k4n94p1Czrfe5IOkKUw== - dependencies: - postcss "^7.0.2" - postcss-values-parser "^2.0.0" - -postcss-initial@^3.0.0: - version "3.0.4" - resolved "https://registry.yarnpkg.com/postcss-initial/-/postcss-initial-3.0.4.tgz#9d32069a10531fe2ecafa0b6ac750ee0bc7efc53" - integrity sha512-3RLn6DIpMsK1l5UUy9jxQvoDeUN4gP939tDcKUHD/kM8SGSKbFAnvkpFpj3Bhtz3HGk1jWY5ZNWX6mPta5M9fg== - dependencies: - postcss "^7.0.2" - -postcss-lab-function@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/postcss-lab-function/-/postcss-lab-function-2.0.1.tgz#bb51a6856cd12289ab4ae20db1e3821ef13d7d2e" - integrity sha512-whLy1IeZKY+3fYdqQFuDBf8Auw+qFuVnChWjmxm/UhHWqNHZx+B99EwxTvGYmUBqe3Fjxs4L1BoZTJmPu6usVg== - dependencies: - "@csstools/convert-colors" "^1.4.0" - postcss "^7.0.2" - postcss-values-parser "^2.0.0" - -postcss-logical@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-logical/-/postcss-logical-3.0.0.tgz#2495d0f8b82e9f262725f75f9401b34e7b45d5b5" - integrity sha512-1SUKdJc2vuMOmeItqGuNaC+N8MzBWFWEkAnRnLpFYj1tGGa7NqyVBujfRtgNa2gXR+6RkGUiB2O5Vmh7E2RmiA== - dependencies: - postcss "^7.0.2" - -postcss-media-minmax@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-media-minmax/-/postcss-media-minmax-4.0.0.tgz#b75bb6cbc217c8ac49433e12f22048814a4f5ed5" - integrity sha512-fo9moya6qyxsjbFAYl97qKO9gyre3qvbMnkOZeZwlsW6XYFsvs2DMGDlchVLfAd8LHPZDxivu/+qW2SMQeTHBw== - dependencies: - postcss "^7.0.2" - -postcss-nesting@^7.0.0: - version "7.0.1" - resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-7.0.1.tgz#b50ad7b7f0173e5b5e3880c3501344703e04c052" - integrity sha512-FrorPb0H3nuVq0Sff7W2rnc3SmIcruVC6YwpcS+k687VxyxO33iE1amna7wHuRVzM8vfiYofXSBHNAZ3QhLvYg== - dependencies: - postcss "^7.0.2" - -postcss-overflow-shorthand@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postcss-overflow-shorthand/-/postcss-overflow-shorthand-2.0.0.tgz#31ecf350e9c6f6ddc250a78f0c3e111f32dd4c30" - integrity sha512-aK0fHc9CBNx8jbzMYhshZcEv8LtYnBIRYQD5i7w/K/wS9c2+0NSR6B3OVMu5y0hBHYLcMGjfU+dmWYNKH0I85g== - dependencies: - postcss "^7.0.2" - -postcss-page-break@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postcss-page-break/-/postcss-page-break-2.0.0.tgz#add52d0e0a528cabe6afee8b46e2abb277df46bf" - integrity sha512-tkpTSrLpfLfD9HvgOlJuigLuk39wVTbbd8RKcy8/ugV2bNBUW3xU+AIqyxhDrQr1VUj1RmyJrBn1YWrqUm9zAQ== - dependencies: - postcss "^7.0.2" - -postcss-place@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-place/-/postcss-place-4.0.1.tgz#e9f39d33d2dc584e46ee1db45adb77ca9d1dcc62" - integrity sha512-Zb6byCSLkgRKLODj/5mQugyuj9bvAAw9LqJJjgwz5cYryGeXfFZfSXoP1UfveccFmeq0b/2xxwcTEVScnqGxBg== - dependencies: - postcss "^7.0.2" - postcss-values-parser "^2.0.0" - -postcss-preset-env@^6.7.0: - version "6.7.0" - resolved "https://registry.yarnpkg.com/postcss-preset-env/-/postcss-preset-env-6.7.0.tgz#c34ddacf8f902383b35ad1e030f178f4cdf118a5" - integrity sha512-eU4/K5xzSFwUFJ8hTdTQzo2RBLbDVt83QZrAvI07TULOkmyQlnYlpwep+2yIK+K+0KlZO4BvFcleOCCcUtwchg== - dependencies: - autoprefixer "^9.6.1" - browserslist "^4.6.4" - caniuse-lite "^1.0.30000981" - css-blank-pseudo "^0.1.4" - css-has-pseudo "^0.10.0" - css-prefers-color-scheme "^3.1.1" - cssdb "^4.4.0" - postcss "^7.0.17" - postcss-attribute-case-insensitive "^4.0.1" - postcss-color-functional-notation "^2.0.1" - postcss-color-gray "^5.0.0" - postcss-color-hex-alpha "^5.0.3" - postcss-color-mod-function "^3.0.3" - postcss-color-rebeccapurple "^4.0.1" - postcss-custom-media "^7.0.8" - postcss-custom-properties "^8.0.11" - postcss-custom-selectors "^5.1.2" - postcss-dir-pseudo-class "^5.0.0" - postcss-double-position-gradients "^1.0.0" - postcss-env-function "^2.0.2" - postcss-focus-visible "^4.0.0" - postcss-focus-within "^3.0.0" - postcss-font-variant "^4.0.0" - postcss-gap-properties "^2.0.0" - postcss-image-set-function "^3.0.1" - postcss-initial "^3.0.0" - postcss-lab-function "^2.0.1" - postcss-logical "^3.0.0" - postcss-media-minmax "^4.0.0" - postcss-nesting "^7.0.0" - postcss-overflow-shorthand "^2.0.0" - postcss-page-break "^2.0.0" - postcss-place "^4.0.1" - postcss-pseudo-class-any-link "^6.0.0" - postcss-replace-overflow-wrap "^3.0.0" - postcss-selector-matches "^4.0.0" - postcss-selector-not "^4.0.0" - -postcss-pseudo-class-any-link@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-6.0.0.tgz#2ed3eed393b3702879dec4a87032b210daeb04d1" - integrity sha512-lgXW9sYJdLqtmw23otOzrtbDXofUdfYzNm4PIpNE322/swES3VU9XlXHeJS46zT2onFO7V1QFdD4Q9LiZj8mew== - dependencies: - postcss "^7.0.2" - postcss-selector-parser "^5.0.0-rc.3" - -postcss-replace-overflow-wrap@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-3.0.0.tgz#61b360ffdaedca84c7c918d2b0f0d0ea559ab01c" - integrity sha512-2T5hcEHArDT6X9+9dVSPQdo7QHzG4XKclFT8rU5TzJPDN7RIRTbO9c4drUISOVemLj03aezStHCR2AIcr8XLpw== - dependencies: - postcss "^7.0.2" - -postcss-selector-matches@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-selector-matches/-/postcss-selector-matches-4.0.0.tgz#71c8248f917ba2cc93037c9637ee09c64436fcff" - integrity sha512-LgsHwQR/EsRYSqlwdGzeaPKVT0Ml7LAT6E75T8W8xLJY62CE4S/l03BWIt3jT8Taq22kXP08s2SfTSzaraoPww== - dependencies: - balanced-match "^1.0.0" - postcss "^7.0.2" - -postcss-selector-not@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-selector-not/-/postcss-selector-not-4.0.1.tgz#263016eef1cf219e0ade9a913780fc1f48204cbf" - integrity sha512-YolvBgInEK5/79C+bdFMyzqTg6pkYqDbzZIST/PDMqa/o3qtXenD05apBG2jLgT0/BQ77d4U2UK12jWpilqMAQ== - dependencies: - balanced-match "^1.0.0" - postcss "^7.0.2" - -postcss-selector-parser@^5.0.0-rc.3, postcss-selector-parser@^5.0.0-rc.4: - version "5.0.0" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz#249044356697b33b64f1a8f7c80922dddee7195c" - integrity sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ== - dependencies: - cssesc "^2.0.0" - indexes-of "^1.0.1" - uniq "^1.0.1" - -postcss-selector-parser@^6.0.2: - version "6.0.6" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.6.tgz#2c5bba8174ac2f6981ab631a42ab0ee54af332ea" - integrity sha512-9LXrvaaX3+mcv5xkg5kFwqSzSH1JIObIx51PrndZwlmznwXRfxMddDvo9gve3gVR8ZTKgoFDdWkbRFmEhT4PMg== - dependencies: - cssesc "^3.0.0" - util-deprecate "^1.0.2" - -postcss-value-parser@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb" - integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ== - -postcss-values-parser@^2.0.0, postcss-values-parser@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/postcss-values-parser/-/postcss-values-parser-2.0.1.tgz#da8b472d901da1e205b47bdc98637b9e9e550e5f" - integrity sha512-2tLuBsA6P4rYTNKCXYG/71C7j1pU6pK503suYOmn4xYrQIzW+opD+7FAFNuGSdZC/3Qfy334QbeMu7MEb8gOxg== - dependencies: - flatten "^1.0.2" - indexes-of "^1.0.1" - uniq "^1.0.1" - -postcss@8.2.15: - version "8.2.15" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.15.tgz#9e66ccf07292817d226fc315cbbf9bc148fbca65" - integrity sha512-2zO3b26eJD/8rb106Qu2o7Qgg52ND5HPjcyQiK2B98O388h43A448LCslC0dI2P97wCAQRJsFvwTRcXxTKds+Q== - dependencies: - colorette "^1.2.2" - nanoid "^3.1.23" - source-map "^0.6.1" - -postcss@^7.0.14, postcss@^7.0.17, postcss@^7.0.2, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0.6: - version "7.0.39" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.39.tgz#9624375d965630e2e1f2c02a935c82a59cb48309" - integrity sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA== - dependencies: - picocolors "^0.2.1" - source-map "^0.6.1" - -process-nextick-args@~2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" - integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== - -process@0.11.10, process@^0.11.10: - version "0.11.10" - resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" - integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= - -prop-types@^15.5.8, prop-types@^15.6.2, prop-types@^15.7.2: - version "15.7.2" - resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" - integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== - dependencies: - loose-envify "^1.4.0" - object-assign "^4.1.1" - react-is "^16.8.1" - -public-encrypt@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" - integrity sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q== - dependencies: - bn.js "^4.1.0" - browserify-rsa "^4.0.0" - create-hash "^1.1.0" - parse-asn1 "^5.0.0" - randombytes "^2.0.1" - safe-buffer "^5.1.2" - -punycode@1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" - integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= - -punycode@^1.2.4: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= - -punycode@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" - integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== - -qrcode.react@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/qrcode.react/-/qrcode.react-3.0.1.tgz#0cb1d7cfdf3955737fbd3509c193985795ca0612" - integrity sha512-uCNm16ClMCrdM2R20c/zqmdwHcbMQf3K7ey39EiK/UgEKbqWeM0iH2QxW3iDVFzjQKFzH23ICgOyG4gNsJ0/gw== - -querystring-es3@0.2.1, querystring-es3@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" - integrity sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM= - -querystring@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" - integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= - -querystring@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.1.tgz#40d77615bb09d16902a85c3e38aa8b5ed761c2dd" - integrity sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg== - -queue@6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/queue/-/queue-6.0.2.tgz#b91525283e2315c7553d2efa18d83e76432fed65" - integrity sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA== - dependencies: - inherits "~2.0.3" - -raf@^3.1.0: - version "3.4.1" - resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" - integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== - dependencies: - performance-now "^2.1.0" - -randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: - version "2.1.0" - resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" - integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== - dependencies: - safe-buffer "^5.1.0" - -randomfill@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458" - integrity sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw== - dependencies: - randombytes "^2.0.5" - safe-buffer "^5.1.0" - -raw-body@2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.1.tgz#30ac82f98bb5ae8c152e67149dac8d55153b168c" - integrity sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA== - dependencies: - bytes "3.1.0" - http-errors "1.7.3" - iconv-lite "0.4.24" - unpipe "1.0.0" - -react-dom@^17.0.1: - version "17.0.2" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" - integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - scheduler "^0.20.2" - -react-is@17.0.2, "react-is@^16.8.0 || ^17.0.0", react-is@^17.0.2: - version "17.0.2" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" - integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== - -react-is@^16.7.0, react-is@^16.8.1: - version "16.13.1" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" - integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== - -react-lifecycles-compat@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" - integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== - -react-motion@^0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/react-motion/-/react-motion-0.5.2.tgz#0dd3a69e411316567927917c6626551ba0607316" - integrity sha512-9q3YAvHoUiWlP3cK0v+w1N5Z23HXMj4IF4YuvjvWegWqNPfLXsOBE/V7UvQGpXxHFKRQQcNcVQE31g9SB/6qgQ== - dependencies: - performance-now "^0.2.0" - prop-types "^15.5.8" - raf "^3.1.0" - -react-number-format@^4.4.4: - version "4.7.3" - resolved "https://registry.yarnpkg.com/react-number-format/-/react-number-format-4.7.3.tgz#e1706204a23c40fa17365a19f5059aabfca1886f" - integrity sha512-4EvcANjstypQ5anhanmdEioGc49qbnErfS+yqbhatC0vzQ1okplkWNb0DIY7ABu4RhaxzttEz6pypEy8KsqgBQ== - dependencies: - prop-types "^15.7.2" - -react-refresh@0.8.3: - version "0.8.3" - resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f" - integrity sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg== - -react-spring@^9.3.0: - version "9.3.0" - resolved "https://registry.yarnpkg.com/react-spring/-/react-spring-9.3.0.tgz#4d71eecbfd4f0823bf67e5943d2b0fb77f3e26ad" - integrity sha512-zxhMUCM4ha22724q1CshmbzKUfqdUp2HyA4P72+A0xVF/9bgaFuMukI8C8/Rjfdqw6sGg3hZNvmY9Z8n4cqWmg== - dependencies: - "@react-spring/core" "~9.3.0" - "@react-spring/konva" "~9.3.0" - "@react-spring/native" "~9.3.0" - "@react-spring/three" "~9.3.0" - "@react-spring/web" "~9.3.0" - "@react-spring/zdog" "~9.3.0" - -react-transition-group@^4.4.0: - version "4.4.2" - resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.2.tgz#8b59a56f09ced7b55cbd53c36768b922890d5470" - integrity sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg== - dependencies: - "@babel/runtime" "^7.5.5" - dom-helpers "^5.0.1" - loose-envify "^1.4.0" - prop-types "^15.6.2" - -react@^17.0.1: - version "17.0.2" - resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" - integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - -readable-stream@^2.0.2, readable-stream@^2.3.3, readable-stream@^2.3.6: - version "2.3.7" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" - integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -readable-stream@^3.5.0, readable-stream@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" - integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readdirp@~3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e" - integrity sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ== - dependencies: - picomatch "^2.2.1" - -regenerator-runtime@^0.13.4: - version "0.13.9" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" - integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== - -reselect@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.1.0.tgz#903711c6c8e04ad4a99e5419dc6b08536d8329e0" - integrity sha512-An39mlrk9bnL39pq9M6th54FaL7VaqZe2m8tBb746udYYO6MD7MOIdaqZ+tABkAOmHQSTqtL3NhUNE8HVvVcQQ== - -resize-observer-polyfill@^1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" - integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== - -ripemd160@^2.0.0, ripemd160@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" - integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA== - dependencies: - hash-base "^3.0.0" - inherits "^2.0.1" - -safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.1.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -scheduler@^0.20.2: - version "0.20.2" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91" - integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - -scrypt-js@3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/scrypt-js/-/scrypt-js-3.0.1.tgz#d314a57c2aef69d1ad98a138a21fe9eafa9ee312" - integrity sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA== - -semver@^6.0.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== - -setimmediate@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" - integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= - -setprototypeof@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" - integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== - -sha.js@^2.4.0, sha.js@^2.4.8: - version "2.4.11" - resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" - integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - -shell-quote@1.7.2: - version "1.7.2" - resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2" - integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg== - -side-channel@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" - integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== - dependencies: - call-bind "^1.0.0" - get-intrinsic "^1.0.2" - object-inspect "^1.9.0" - -source-map@0.7.3: - version "0.7.3" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" - integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== - -source-map@0.8.0-beta.0: - version "0.8.0-beta.0" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.8.0-beta.0.tgz#d4c1bb42c3f7ee925f005927ba10709e0d1d1f11" - integrity sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA== - dependencies: - whatwg-url "^7.0.0" - -source-map@^0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - -stacktrace-parser@0.1.10: - version "0.1.10" - resolved "https://registry.yarnpkg.com/stacktrace-parser/-/stacktrace-parser-0.1.10.tgz#29fb0cae4e0d0b85155879402857a1639eb6051a" - integrity sha512-KJP1OCML99+8fhOHxwwzyWrlUuVX5GQ0ZpJTd1DFXhdkrvg1szxfHhawXUZ3g9TkXORQd4/WG68jMlQZ2p8wlg== - dependencies: - type-fest "^0.7.1" - -"statuses@>= 1.5.0 < 2": - version "1.5.0" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" - integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= - -stream-browserify@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-3.0.0.tgz#22b0a2850cdf6503e73085da1fc7b7d0c2122f2f" - integrity sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA== - dependencies: - inherits "~2.0.4" - readable-stream "^3.5.0" - -stream-browserify@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b" - integrity sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg== - dependencies: - inherits "~2.0.1" - readable-stream "^2.0.2" - -stream-http@3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-3.1.1.tgz#0370a8017cf8d050b9a8554afe608f043eaff564" - integrity sha512-S7OqaYu0EkFpgeGFb/NPOoPLxFko7TPqtEeFg5DXPB4v/KETHG0Ln6fRFrNezoelpaDKmycEmmZ81cC9DAwgYg== - dependencies: - builtin-status-codes "^3.0.0" - inherits "^2.0.4" - readable-stream "^3.6.0" - xtend "^4.0.2" - -stream-http@^2.7.2: - version "2.8.3" - resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.3.tgz#b2d242469288a5a27ec4fe8933acf623de6514fc" - integrity sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw== - dependencies: - builtin-status-codes "^3.0.0" - inherits "^2.0.1" - readable-stream "^2.3.6" - to-arraybuffer "^1.0.0" - xtend "^4.0.0" - -stream-parser@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/stream-parser/-/stream-parser-0.3.1.tgz#1618548694420021a1182ff0af1911c129761773" - integrity sha1-FhhUhpRCACGhGC/wrxkRwSl2F3M= - dependencies: - debug "2" - -string-hash@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/string-hash/-/string-hash-1.1.3.tgz#e8aafc0ac1855b4666929ed7dd1275df5d6c811b" - integrity sha1-6Kr8CsGFW0Zmkp7X3RJ1311sgRs= - -string.prototype.trimend@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80" - integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - -string.prototype.trimstart@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed" - integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - -string_decoder@1.3.0, string_decoder@^1.0.0, string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - -strip-ansi@6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" - integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== - dependencies: - ansi-regex "^5.0.0" - -styled-jsx@4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-4.0.1.tgz#ae3f716eacc0792f7050389de88add6d5245b9e9" - integrity sha512-Gcb49/dRB1k8B4hdK8vhW27Rlb2zujCk1fISrizCcToIs+55B4vmUM0N9Gi4nnVfFZWe55jRdWpAqH1ldAKWvQ== - dependencies: - "@babel/plugin-syntax-jsx" "7.14.5" - "@babel/types" "7.15.0" - convert-source-map "1.7.0" - loader-utils "1.2.3" - source-map "0.7.3" - string-hash "1.1.3" - stylis "3.5.4" - stylis-rule-sheet "0.0.10" - -stylis-rule-sheet@0.0.10: - version "0.0.10" - resolved "https://registry.yarnpkg.com/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz#44e64a2b076643f4b52e5ff71efc04d8c3c4a430" - integrity sha512-nTbZoaqoBnmK+ptANthb10ZRZOGC+EmTLLUxeYIuHNkEKcmKgXX1XWKkUBT2Ac4es3NybooPe0SmvKdhKJZAuw== - -stylis@3.5.4: - version "3.5.4" - resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.5.4.tgz#f665f25f5e299cf3d64654ab949a57c768b73fbe" - integrity sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q== - -supports-color@^5.3.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - -supports-color@^7.1.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" - integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== - dependencies: - has-flag "^4.0.0" - -supports-color@^8.0.0: - version "8.1.1" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" - integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== - dependencies: - has-flag "^4.0.0" - -tapable@^2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" - integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== - -timers-browserify@2.0.12, timers-browserify@^2.0.4: - version "2.0.12" - resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.12.tgz#44a45c11fbf407f34f97bccd1577c652361b00ee" - integrity sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ== - dependencies: - setimmediate "^1.0.4" - -tiny-invariant@^1.0.6: - version "1.1.0" - resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875" - integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw== - -tiny-warning@^1.0.2, tiny-warning@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" - integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== - -to-arraybuffer@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" - integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M= - -to-fast-properties@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" - integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - -toidentifier@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" - integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== - -tr46@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" - integrity sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk= - dependencies: - punycode "^2.1.0" - -tr46@~0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" - integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= - -ts-pnp@^1.1.6: - version "1.2.0" - resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92" - integrity sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw== - -tty-browserify@0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" - integrity sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY= - -tty-browserify@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.1.tgz#3f05251ee17904dfd0677546670db9651682b811" - integrity sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw== - -type-fest@^0.7.1: - version "0.7.1" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.7.1.tgz#8dda65feaf03ed78f0a3f9678f1869147f7c5c48" - integrity sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg== - -typescript@^4.1.3: - version "4.4.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.4.tgz#2cd01a1a1f160704d3101fd5a58ff0f9fcb8030c" - integrity sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA== - -unbox-primitive@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471" - integrity sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw== - dependencies: - function-bind "^1.1.1" - has-bigints "^1.0.1" - has-symbols "^1.0.2" - which-boxed-primitive "^1.0.2" - -uniq@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" - integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8= - -unpipe@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" - integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= - -url@^0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" - integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE= - dependencies: - punycode "1.3.2" - querystring "0.2.0" - -use-subscription@1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/use-subscription/-/use-subscription-1.5.1.tgz#73501107f02fad84c6dd57965beb0b75c68c42d1" - integrity sha512-Xv2a1P/yReAjAbhylMfFplFKj9GssgTwN7RlcTxBujFQcloStWNDQdc4g4NRWH9xS4i/FDk04vQBptAXoF3VcA== - dependencies: - object-assign "^4.1.1" - -util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= - -util@0.10.3: - version "0.10.3" - resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" - integrity sha1-evsa/lCAUkZInj23/g7TeTNqwPk= - dependencies: - inherits "2.0.1" - -util@0.12.4, util@^0.12.0: - version "0.12.4" - resolved "https://registry.yarnpkg.com/util/-/util-0.12.4.tgz#66121a31420df8f01ca0c464be15dfa1d1850253" - integrity sha512-bxZ9qtSlGUWSOy9Qa9Xgk11kSslpuZwaxCg4sNIDj6FLucDab2JxnHwyNTCpHMtK1MjoQiWQ6DiUMZYbSrO+Sw== - dependencies: - inherits "^2.0.3" - is-arguments "^1.0.4" - is-generator-function "^1.0.7" - is-typed-array "^1.1.3" - safe-buffer "^5.1.2" - which-typed-array "^1.1.2" - -util@^0.11.0: - version "0.11.1" - resolved "https://registry.yarnpkg.com/util/-/util-0.11.1.tgz#3236733720ec64bb27f6e26f421aaa2e1b588d61" - integrity sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ== - dependencies: - inherits "2.0.3" - -vm-browserify@1.1.2, vm-browserify@^1.0.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" - integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== - -watchpack@2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.1.1.tgz#e99630550fca07df9f90a06056987baa40a689c7" - integrity sha512-Oo7LXCmc1eE1AjyuSBmtC3+Wy4HcV8PxWh2kP6fOl8yTlNS7r0K9l1ao2lrrUza7V39Y3D/BbJgY8VeSlc5JKw== - dependencies: - glob-to-regexp "^0.4.1" - graceful-fs "^4.1.2" - -webidl-conversions@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" - integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= - -webidl-conversions@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" - integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== - -whatwg-fetch@^3.4.1: - version "3.6.2" - resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz#dced24f37f2624ed0281725d51d0e2e3fe677f8c" - integrity sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA== - -whatwg-url@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" - integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0= - dependencies: - tr46 "~0.0.3" - webidl-conversions "^3.0.0" - -whatwg-url@^7.0.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06" - integrity sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg== - dependencies: - lodash.sortby "^4.7.0" - tr46 "^1.0.1" - webidl-conversions "^4.0.2" - -which-boxed-primitive@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" - integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== - dependencies: - is-bigint "^1.0.1" - is-boolean-object "^1.1.0" - is-number-object "^1.0.4" - is-string "^1.0.5" - is-symbol "^1.0.3" - -which-typed-array@^1.1.2: - version "1.1.7" - resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.7.tgz#2761799b9a22d4b8660b3c1b40abaa7739691793" - integrity sha512-vjxaB4nfDqwKI0ws7wZpxIlde1XrLX5uB0ZjpfshgmapJMD7jJWhZI+yToJTqaFByF0eNBcYxbjmCzoRP7CfEw== - dependencies: - available-typed-arrays "^1.0.5" - call-bind "^1.0.2" - es-abstract "^1.18.5" - foreach "^2.0.5" - has-tostringtag "^1.0.0" - is-typed-array "^1.1.7" - -ws@7.4.6: - version "7.4.6" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" - integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== - -xtend@^4.0.0, xtend@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" - integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== - -yocto-queue@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" - integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== diff --git a/go.mod b/go.mod index 8223e8961c..2531600cef 100644 --- a/go.mod +++ b/go.mod @@ -6,22 +6,34 @@ go 1.17 require ( github.com/DATA-DOG/go-sqlmock v1.5.0 - github.com/adshao/go-binance/v2 v2.3.5 - github.com/c9s/requestgen v1.1.1-0.20211230171502-c042072e23cd - github.com/c9s/rockhopper v1.2.1-0.20220426104534-f27cbb09846c + github.com/Masterminds/squirrel v1.5.3 + github.com/adshao/go-binance/v2 v2.3.8 + github.com/c-bata/goptuna v0.8.1 + github.com/c9s/requestgen v1.3.0 + github.com/c9s/rockhopper v1.2.2-0.20220617053729-ffdc87df194b + github.com/cheggaaa/pb/v3 v3.0.8 github.com/codingconcepts/env v0.0.0-20200821220118-a8fbf8d84482 + github.com/ethereum/go-ethereum v1.10.23 + github.com/evanphx/json-patch/v5 v5.6.0 + github.com/fatih/camelcase v1.0.0 github.com/fatih/color v1.13.0 + github.com/gertd/go-pluralize v0.2.1 github.com/gin-contrib/cors v1.3.1 github.com/gin-gonic/gin v1.7.0 github.com/go-redis/redis/v8 v8.8.0 github.com/go-sql-driver/mysql v1.6.0 + github.com/gofrs/flock v0.8.1 + github.com/golang/mock v1.6.0 github.com/google/uuid v1.3.0 github.com/gorilla/websocket v1.5.0 + github.com/jedib0t/go-pretty/v6 v6.3.6 github.com/jmoiron/sqlx v1.3.4 github.com/joho/godotenv v1.3.0 github.com/leekchan/accounting v0.0.0-20191218023648-17a4ce5f94d4 github.com/lestrrat-go/file-rotatelogs v2.2.0+incompatible github.com/mattn/go-shellwords v1.0.12 + github.com/muesli/clusters v0.0.0-20180605185049-a07a36e67d36 + github.com/muesli/kmeans v0.3.0 github.com/pkg/errors v0.9.1 github.com/pquerna/otp v1.3.0 github.com/prometheus/client_golang v1.11.0 @@ -33,82 +45,101 @@ require ( github.com/spf13/cobra v1.1.1 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.7.1 - github.com/stretchr/testify v1.7.0 + github.com/stretchr/testify v1.7.4 github.com/valyala/fastjson v1.5.1 + github.com/wcharczuk/go-chart/v2 v2.1.0 github.com/webview/webview v0.0.0-20210216142346-e0bfdf0e5d90 github.com/x-cray/logrus-prefixed-formatter v0.5.2 github.com/zserge/lorca v0.1.9 go.uber.org/multierr v1.7.0 - golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 - gonum.org/v1/gonum v0.8.1 + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c + golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba + gonum.org/v1/gonum v0.8.2 google.golang.org/grpc v1.45.0 google.golang.org/protobuf v1.28.0 gopkg.in/tucnak/telebot.v2 v2.5.0 - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b + gopkg.in/yaml.v3 v3.0.1 ) require ( + github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6 // indirect + github.com/VividCortex/ewma v1.1.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bitly/go-simplejson v0.5.0 // indirect github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect + github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/cockroachdb/apd v1.1.0 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/denisenkom/go-mssqldb v0.12.0 // indirect + github.com/deckarep/golang-set v1.8.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect + github.com/denisenkom/go-mssqldb v0.12.2 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239 // indirect github.com/fsnotify/fsnotify v1.4.9 // indirect github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-ole/go-ole v1.2.1 // indirect github.com/go-playground/locales v0.13.0 // indirect github.com/go-playground/universal-translator v0.17.0 // indirect github.com/go-playground/validator/v10 v10.4.1 // indirect + github.com/go-stack/stack v1.8.0 // indirect github.com/go-test/deep v1.0.6 // indirect github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect - github.com/golang-sql/sqlexp v0.0.0-20170517235910-f1bb20e5a188 // indirect + github.com/golang-sql/sqlexp v0.1.0 // indirect + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869 // indirect - github.com/json-iterator/go v1.1.11 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect + github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/leodido/go-urn v1.2.1 // indirect github.com/lestrrat-go/strftime v1.0.0 // indirect - github.com/lib/pq v1.10.5 // indirect + github.com/lib/pq v1.10.6 // indirect github.com/magiconair/properties v1.8.4 // indirect github.com/mattn/go-colorable v0.1.9 // indirect github.com/mattn/go-isatty v0.0.14 // indirect - github.com/mattn/go-sqlite3 v1.14.12 // indirect + github.com/mattn/go-runewidth v0.0.13 // indirect + github.com/mattn/go-sqlite3 v1.14.13 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/mitchellh/mapstructure v1.4.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.1 // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml v1.8.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/common v0.32.1 // indirect github.com/prometheus/procfs v0.7.3 // indirect - github.com/russross/blackfriday/v2 v2.0.1 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/rjeczalik/notify v0.9.1 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect github.com/shopspring/decimal v1.2.0 // indirect - github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect github.com/spf13/afero v1.5.1 // indirect github.com/spf13/cast v1.3.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/subosito/gotenv v1.2.0 // indirect github.com/tebeka/strftime v0.1.3 // indirect + github.com/tklauser/go-sysconf v0.3.5 // indirect + github.com/tklauser/numcpus v0.2.2 // indirect github.com/ugorji/go/codec v1.2.3 // indirect github.com/ziutek/mymysql v1.5.4 // indirect go.opentelemetry.io/otel v0.19.0 // indirect go.opentelemetry.io/otel/metric v0.19.0 // indirect go.opentelemetry.io/otel/trace v0.19.0 // indirect go.uber.org/atomic v1.9.0 // indirect - golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 // indirect - golang.org/x/net v0.0.0-20220403103023-749bd193bc2b // indirect - golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 // indirect + golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect + golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 // indirect + golang.org/x/net v0.0.0-20220607020251-c690dde0001d // indirect + golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c // indirect golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect golang.org/x/text v0.3.7 // indirect - golang.org/x/tools v0.1.9 // indirect + golang.org/x/tools v0.1.11 // indirect google.golang.org/genproto v0.0.0-20220405205423-9d709892a2bf // indirect gopkg.in/ini.v1 v1.62.0 // indirect + gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 7741a9028f..c7221e919e 100644 --- a/go.sum +++ b/go.sum @@ -37,21 +37,36 @@ github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJc github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/Masterminds/squirrel v1.5.3 h1:YPpoceAcxuzIljlr5iWpNKaql7hLeG1KLSrhvdHpkZc= +github.com/Masterminds/squirrel v1.5.3/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= +github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6 h1:fLjPD/aNc3UIOA6tDi6QXUemppXK3P9BI7mr2hd6gx8= +github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= +github.com/VictoriaMetrics/fastcache v1.6.0 h1:C/3Oi3EiBCqufydp1neRZkqcwmEiuRT9c3fqvvgKm5o= +github.com/VividCortex/ewma v1.1.1 h1:MnEK4VOv6n0RSY4vtRe3h11qjxL3+t0B8yOL8iMXdcM= +github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA= github.com/adshao/go-binance/v2 v2.3.5 h1:WVYZecm0w8l14YoWlnKZj6xxZT2AKMTHpMQSqIX1xxA= github.com/adshao/go-binance/v2 v2.3.5/go.mod h1:8Pg/FGTLyAhq8QXA0IkoReKyRpoxJcK3LVujKDAZV/c= +github.com/adshao/go-binance/v2 v2.3.8 h1:9VsAX4jUopnIOlzrvnKUFUf9SWB/nwPgJtUsM2dkj6A= +github.com/adshao/go-binance/v2 v2.3.8/go.mod h1:Z3MCnWI0gHC4Rea8TWiF3aN1t4nV9z3CaU/TeHcKsLM= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/apache/arrow/go/arrow v0.0.0-20201229220542-30ce2eb5d4dc/go.mod h1:c9sxoIT3YgLxH4UhLOCKaBlEojuMhVYpk4Ntv3opUTQ= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/awalterschulze/gographviz v0.0.0-20190221210632-1e9ccb565bca/go.mod h1:GEV5wmg4YquNw7v1kkyoX9etIk8yVmXj+AkDHuuETHs= +github.com/awalterschulze/gographviz v2.0.3+incompatible/go.mod h1:GEV5wmg4YquNw7v1kkyoX9etIk8yVmXj+AkDHuuETHs= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -64,20 +79,31 @@ github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4Yn github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= -github.com/c9s/requestgen v1.1.1-0.20211230171502-c042072e23cd h1:o87kZ8aHtxA1ZduOV2Z55T4PX2xM4OTGH/Z9W5AjKnU= -github.com/c9s/requestgen v1.1.1-0.20211230171502-c042072e23cd/go.mod h1:5n9FU3hr5307IiXAmbMiZbHYaPiys1u9jCWYexZr9qA= -github.com/c9s/rockhopper v1.2.1-0.20220426104534-f27cbb09846c h1:I3AHs+/fxnWX6eSRxzqQ/vp4jXW+ecVMGy1oy5d6fJ8= -github.com/c9s/rockhopper v1.2.1-0.20220426104534-f27cbb09846c/go.mod h1:EKObf66Cp7erWxym2de+07qNN5T1N9PXxHdh97N44EQ= +github.com/btcsuite/btcd/btcec/v2 v2.2.0 h1:fzn1qaOt32TuLjFlkzYSsBC35Q3KUjT1SwPxiMSCF5k= +github.com/btcsuite/btcd/btcec/v2 v2.2.0/go.mod h1:U7MHm051Al6XmscBQ0BoNydpOTsFAn707034b5nY8zU= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= +github.com/c-bata/goptuna v0.8.1 h1:25+n1MLv0yvCsD56xv4nqIus3oLHL9GuPAZDLIqmX1U= +github.com/c-bata/goptuna v0.8.1/go.mod h1:knmS8+Iyq5PPy1YUeIEq0pMFR4Y6x7z/CySc9HlZTCY= +github.com/c9s/requestgen v1.3.0 h1:3cTHvWIlrc37nGEdJLIO07XaVidDeOwcew06csBz++U= +github.com/c9s/requestgen v1.3.0/go.mod h1:5n9FU3hr5307IiXAmbMiZbHYaPiys1u9jCWYexZr9qA= +github.com/c9s/rockhopper v1.2.2-0.20220617053729-ffdc87df194b h1:wT8c03PHLv7+nZUIGqxAzRvIfYHNxMCNVWwvdGkOXTs= +github.com/c9s/rockhopper v1.2.2-0.20220617053729-ffdc87df194b/go.mod h1:EKObf66Cp7erWxym2de+07qNN5T1N9PXxHdh97N44EQ= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cheggaaa/pb/v3 v3.0.8 h1:bC8oemdChbke2FHIIGy9mn4DPJ2caZYQnfbRqwmdCoA= +github.com/cheggaaa/pb/v3 v3.0.8/go.mod h1:UICbiLec/XO6Hw6k+BHEtHeQFzzBH4i2/qk/ow1EJTA= +github.com/chewxy/hm v1.0.0/go.mod h1:qg9YI4q6Fkj/whwHR1D+bOGeF7SniIP40VweVepLjg0= +github.com/chewxy/math32 v1.0.0/go.mod h1:Miac6hA1ohdDUTagnvJy/q+aNnEk16qWUdb8ZVhvCN0= +github.com/chewxy/math32 v1.0.6/go.mod h1:dOB2rcuFrCn6UHrze36WSLVPKtzPMRAQvBvUwkSsLqs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/cfssl v0.0.0-20190808011637-b1ec8c586c2a/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= @@ -92,37 +118,63 @@ github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkE github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/cznic/cc v0.0.0-20181122101902-d673e9b70d4d/go.mod h1:m3fD/V+XTB35Kh9zw6dzjMY+We0Q7PMf6LLIC4vuG9k= +github.com/cznic/golex v0.0.0-20181122101858-9c343928389c/go.mod h1:+bmmJDNmKlhWNG+gwWCkaBoTy39Fs+bzRxVBzoTQbIc= +github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM= +github.com/cznic/strutil v0.0.0-20181122101858-275e90344537/go.mod h1:AHHPPPXTw0h6pVabbcbyGRK1DckRn7r/STdZEeIDzZc= +github.com/cznic/xc v0.0.0-20181122101856-45b06973881e/go.mod h1:3oFoiOvCDBYH+swwf5+k/woVmWy7h1Fcyu8Qig/jjX0= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deckarep/golang-set v1.8.0 h1:sk9/l/KqpunDwP7pSjUg0keiOOLEnOBHzykLrsPppp4= +github.com/deckarep/golang-set v1.8.0/go.mod h1:5nI87KwE7wgsBU1F4GKAw2Qod7p5kyS383rP6+o6qqo= +github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= -github.com/denisenkom/go-mssqldb v0.12.0 h1:VtrkII767ttSPNRfFekePK3sctr+joXgO58stqQbtUA= -github.com/denisenkom/go-mssqldb v0.12.0/go.mod h1:iiK0YP1ZeepvmBQk/QpLEhhTNJgfzrpArPY/aFvc9yU= +github.com/denisenkom/go-mssqldb v0.12.2 h1:1OcPn5GBIobjWNd+8yjfHNIaFX14B1pWI3F9HZy5KXw= +github.com/denisenkom/go-mssqldb v0.12.2/go.mod h1:lnIw1mZukFRZDJYQ0Pb833QS2IaC3l5HkEfra2LJ+sk= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= +github.com/edsrzf/mmap-go v1.0.0 h1:CEBF7HpRnUCSJgGUb5h1Gm7e3VkmVDrR8lvWVLtrOFw= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= +github.com/ethereum/go-ethereum v1.10.23 h1:Xk8XAT4/UuqcjMLIMF+7imjkg32kfVFKoeyQDaO2yWM= +github.com/ethereum/go-ethereum v1.10.23/go.mod h1:EYFyF19u3ezGLD4RqOkLq+ZCXzYbLoNDdZlMt7kyKFg= +github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= +github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239 h1:Ghm4eQYC0nEPnSJdVkTrXpu9KtoVCSo1hg7mtI7G9KU= github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239/go.mod h1:Gdwt2ce0yfBxPvZrHkprdPPTTS3N5rwmLE8T22KBXlw= +github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= +github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5 h1:FtmdgXiUlNeRsoNMFlKLDt+S+6hbjVMEW6RGQ7aUf7c= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqGNY4FhTFhk+o9oFHGINQ/+vhlm8HFzi6znCI= +github.com/gertd/go-pluralize v0.2.1 h1:M3uASbVjMnTsPb0PNqg+E/24Vwigyo/tvyMTtAlLgiA= +github.com/gertd/go-pluralize v0.2.1/go.mod h1:rbYaKDbsXxmRfr8uygAEKhOWsjyrrqrkHVpZvoOp8zk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/cors v1.3.1 h1:doAsuITavI4IOcd0Y19U4B+O0dNWihRyX//nn4sEmgA= github.com/gin-contrib/cors v1.3.1/go.mod h1:jjEJ4268OPZUcU7k9Pm653S7lXUGcqMADzFA61xsmDk= @@ -134,12 +186,15 @@ github.com/gin-gonic/gin v1.7.0/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjX github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gota/gota v0.10.1/go.mod h1:NZLQccXn0rABmkXjsaugRY6l+UH2dDZSgIgF8E2ipmA= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-ole/go-ole v1.2.1 h1:2lOsA72HgjxAuMlKpFiCbHTvu44PIVkZ5hqm3RSdI/E= +github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= @@ -155,18 +210,25 @@ github.com/go-redis/redis/v8 v8.8.0/go.mod h1:F7resOH5Kdug49Otu24RjHWwgK7u9AmtqW github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-test/deep v1.0.6 h1:UHSEyLZUwX9Qoi99vVwvewiMC8mM2bf7XEM2nqvzEn8= github.com/go-test/deep v1.0.6/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8= +github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= +github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/golang-jwt/jwt/v4 v4.3.0 h1:kHL1vqdqWNfATmA0FNMdmZNMyZI1U6O31X4rlIPoBog= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= -github.com/golang-sql/sqlexp v0.0.0-20170517235910-f1bb20e5a188 h1:+eHOFJl1BaXrQxKX+T06f78590z4qA2ZzBTqahsKSE4= -github.com/golang-sql/sqlexp v0.0.0-20170517235910-f1bb20e5a188/go.mod h1:vXjM/+wXQnTPR4KqTKDgJukSZ6amVRtWMPEjE6sQoK8= +github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= +github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -180,7 +242,10 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= @@ -197,8 +262,13 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/gonum/blas v0.0.0-20181208220705-f22b278b28ac/go.mod h1:P32wAyui1PQ58Oce/KYkOqQv8cVw1zAapXOl+dRFGbc= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/flatbuffers v1.10.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/flatbuffers v1.11.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/flatbuffers v1.12.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -222,12 +292,15 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.5/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorgonia/bindgen v0.0.0-20180812032444-09626750019e/go.mod h1:YzKk63P9jQHkwAo2rXHBv02yPxDzoQT2cBV0x5bGV/8= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= @@ -238,6 +311,7 @@ github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFb github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= @@ -250,19 +324,72 @@ github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d h1:dg1dEPuWpEqDnvIw251EVy4zlP8gWbsGj4BsUKCRpYs= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= +github.com/holiman/uint256 v1.2.0 h1:gpSYcPLWGv4sG43I2mVLiDZCNDh/EpGjSk8tmtxitHM= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huin/goupnp v1.0.3 h1:N8No57ls+MnjlB+JPiCVSOyy/ot7MJTqlo7rn+NYSqQ= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= +github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= +github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= +github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= +github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk= +github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= +github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= +github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= +github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= +github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= +github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= +github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= +github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= +github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0= +github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po= +github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ= +github.com/jackc/pgtype v1.6.2/go.mod h1:JCULISAZBFGrHaOXIIFiyfzW5VY0GRitRr8NeJsrdig= +github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= +github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= +github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= +github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA= +github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o= +github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg= +github.com/jackc/pgx/v4 v4.10.1/go.mod h1:QlrWebbs3kqEZPHCTGyxecvzG6tvIsYu+A5b1raylkA= +github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jedib0t/go-pretty/v6 v6.3.6 h1:A6w2BuyPMtf7M82BGRBys9bAba2C26ZX9lrlrZ7uH6U= +github.com/jedib0t/go-pretty/v6 v6.3.6/go.mod h1:MgmISkTWDSFu0xOqiZ0mKNntMQ2mDgOcwOkwBEkMDJI= github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869 h1:IPJ3dvxmJ4uczJe5YQdrYB16oTJlGSC/OyZDqUk9xX4= github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869/go.mod h1:cJ6Cj7dQo+O6GJNiMx+Pa94qKj+TG8ONdKHgMNIyyag= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jmoiron/sqlx v1.3.4 h1:wv+0IJZfL5z0uZoUjlpKgHkgaFSYD+r9CfrXjEXsO7w= github.com/jmoiron/sqlx v1.3.4/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= @@ -276,6 +403,8 @@ github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= @@ -284,8 +413,10 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= @@ -293,10 +424,17 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= github.com/leekchan/accounting v0.0.0-20191218023648-17a4ce5f94d4 h1:KZzDAtJ7ZLm0zSWVhN/zgyB8Ksx5H+P9irwbTcJ9FwI= github.com/leekchan/accounting v0.0.0-20191218023648-17a4ce5f94d4/go.mod h1:3timm6YPhY3YDaGxl0q3eaflX0eoSx3FXn7ckHe4tO0= +github.com/leesper/go_rng v0.0.0-20171009123644-5344a9259b21/go.mod h1:N0SVk0uhy+E1PZ3C9ctsPRlvOPAFPkCNlcPBDkt0N3U= +github.com/leesper/go_rng v0.0.0-20190531154944-a612b043e353/go.mod h1:N0SVk0uhy+E1PZ3C9ctsPRlvOPAFPkCNlcPBDkt0N3U= github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= @@ -308,26 +446,43 @@ github.com/lestrrat-go/file-rotatelogs v2.2.0+incompatible/go.mod h1:ZQnN8lSECae github.com/lestrrat-go/strftime v1.0.0 h1:wZIfTHGdu7TeGu318uLJwuQvTMt9UpRyS+XV2Rc4wo4= github.com/lestrrat-go/strftime v1.0.0/go.mod h1:E1nN3pCbtMSu1yjSVeyuRFVm/U0xoR76fd03sz+Qz4g= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lib/pq v1.10.5 h1:J+gdV2cUmX7ZqL2B0lFcW0m+egaHC2V3lpO8nWxyYiQ= -github.com/lib/pq v1.10.5/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs= +github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.4 h1:8KGKTcQQGm0Kv7vEbKFErAoAOFyyacLStRtQSeYtvkY= github.com/magiconair/properties v1.8.4/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= +github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= +github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= -github.com/mattn/go-sqlite3 v1.14.12 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ1Z0= -github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.13 h1:1tj15ngiFfcZzii7yd82foL+ks+ouQcj8j/TPq3fk1I= +github.com/mattn/go-sqlite3 v1.14.13/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= @@ -343,19 +498,27 @@ github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:F github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= +github.com/muesli/clusters v0.0.0-20180605185049-a07a36e67d36 h1:KMCH+/bbZsAbFgzCXD3aB0DRZXnwAO8NYDmfIfslo+M= +github.com/muesli/clusters v0.0.0-20180605185049-a07a36e67d36/go.mod h1:mw5KDqUj0eLj/6DUNINLVJNoPTFkEuGMHtJsXLviLkY= +github.com/muesli/kmeans v0.3.0 h1:cI2cpeS8m3pm+gTOdzl+7SlzZYSe+x0XoqXUyUvb1ro= +github.com/muesli/kmeans v0.3.0/go.mod h1:eNyybq0tX9/iBEP6EMU4Y7dpmGK0uEhODdZpnG1a/iQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg= @@ -378,6 +541,7 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -409,26 +573,43 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/tsdb v0.7.1 h1:YZcsG11NqnK4czYLrWd9mpEuAJIHVQLwdrleYfszMAA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc= +github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 h1:mZHayPoR0lNmnHyvtYjDeq0zlVHn9K/ZXoy17ylucdo= github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5/go.mod h1:GEXHk5HgEKCvEIIrSpFI3ozzG5xOKA2DVlEX/gGnewM= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rjeczalik/notify v0.9.1 h1:CLCKso/QK1snAlnhNR/CNvNiFU2saUtjV0bx3EwNeCE= +github.com/rjeczalik/notify v0.9.1/go.mod h1:rKwnCoCGeuQnwBtTSPL9Dad03Vh2n40ePRrjvIXnJho= github.com/robfig/cron/v3 v3.0.0 h1:kQ6Cb7aHOHTSzNVNEhmp8EcWKLb4CbiMW9h9VyIhO4E= github.com/robfig/cron/v3 v3.0.0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= +github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sajari/regression v1.0.1 h1:iTVc6ZACGCkoXC+8NdqH5tIreslDTT/bXxT6OmHR5PE= github.com/sajari/regression v1.0.1/go.mod h1:NeG/XTW1lYfGY7YV/Z0nYDV/RGh3wxwd1yW46835flM= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= @@ -459,39 +640,57 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/status-im/keycard-go v0.0.0-20190316090335-8537d3370df4 h1:Gb2Tyox57NRNuZ2d3rmvB3pcmbu7O1RS3m8WRx7ilrg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.1.4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.2.0/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.4 h1:wZRexSlwd7ZXfKINDLsO4r7WBt3gTKONc6K/VesHvHM= +github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/tebeka/strftime v0.1.3 h1:5HQXOqWKYRFfNyBMNVc9z5+QzuBtIXy03psIhtdJYto= github.com/tebeka/strftime v0.1.3/go.mod h1:7wJm3dZlpr4l/oVK0t1HYIc4rMzQ2XJlOMIUJUJH6XQ= +github.com/tklauser/go-sysconf v0.3.5 h1:uu3Xl4nkLzQfXNsWn15rPc/HQCJKObbt1dKJeWp3vU4= +github.com/tklauser/go-sysconf v0.3.5/go.mod h1:MkWzOF4RMCshBAMXuhXJs64Rte09mITnppBXY/rYEFI= +github.com/tklauser/numcpus v0.2.2 h1:oyhllyrScuYI6g+h/zUvNXNp1wy7x8qQy3t/piefldA= +github.com/tklauser/numcpus v0.2.2/go.mod h1:x3qojaO3uyYt0i56EW/VUYs7uBvdl2fkfZFu0T9wgjM= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tyler-smith/go-bip39 v1.0.1-0.20181017060643-dbb3b84ba2ef h1:wHSqTBrZW24CsNJDfeh9Ex6Pm0Rcpc7qrgKBiL44vF4= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= -github.com/ugorji/go v1.2.3 h1:WbFSXLxDFKVN69Sk8t+XHGzVCD7R8UoAATR8NqZgTbk= github.com/ugorji/go v1.2.3/go.mod h1:5l8GZ8hZvmL4uMdy+mhCO1LjswGRYco9Q3HfuisB21A= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ugorji/go/codec v1.2.3 h1:/mVYEV+Jo3IZKeA5gBngN0AvNnQltEDkR+eQikkWQu0= github.com/ugorji/go/codec v1.2.3/go.mod h1:5FxzDJIgeiWJZslYHPj+LS1dq1ZBQVelZFnjsFGI/Uc= +github.com/urfave/cli/v2 v2.10.2 h1:x3p8awjp/2arX+Nl/G2040AZpOCHS/eMJJ1/a+mye4Y= github.com/valyala/fastjson v1.5.1 h1:SXaQZVSwLjZOVhDEhjiCcDtnX0Feu7Z7A1+C5atpoHM= github.com/valyala/fastjson v1.5.1/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= +github.com/wcharczuk/go-chart/v2 v2.1.0 h1:tY2slqVQ6bN+yHSnDYwZebLQFkphK4WNrVwnt7CJZ2I= +github.com/wcharczuk/go-chart/v2 v2.1.0/go.mod h1:yx7MvAVNcP/kN9lKXM/NTce4au4DFN99j6i1OwDclNA= github.com/webview/webview v0.0.0-20210216142346-e0bfdf0e5d90 h1:G/O1RFjhc9hgVYjaPQ0Oceqxf3GwRQl/5XEAWYetjmg= github.com/webview/webview v0.0.0-20210216142346-e0bfdf0e5d90/go.mod h1:rpXAuuHgyEJb6kXcXldlkOjU6y4x+YcASKKXJNUhh0Y= github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg= github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xtgo/set v1.0.0/go.mod h1:d3NHzGzSa0NmB2NhFyECA+QdRp29oEn2xbT+TpeFoM8= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= github.com/zserge/lorca v0.1.9 h1:vbDdkqdp2/rmeg8GlyCewY2X8Z+b0s7BqWyIQL/gakc= @@ -511,31 +710,42 @@ go.opentelemetry.io/otel/oteltest v0.19.0/go.mod h1:tI4yxwh8U21v7JD6R3BcA/2+RBoT go.opentelemetry.io/otel/trace v0.19.0 h1:1ucYlenXIDA1OlHVLDZKX0ObXV5RLaq06DtUKz5e5zc= go.opentelemetry.io/otel/trace v0.19.0/go.mod h1:4IXiNextNOpPnRlI4ryK69mn5iC84bjBWZQA5DXz/qg= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec= go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA= -golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM= +golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= @@ -543,11 +753,13 @@ golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6 h1:QE6XYQK6naiK1EPAe1g/ILLxN5RBoH5xkJk3CqlMI/Y= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20220426173459-3bcf042a4bf5 h1:rxKZ2gOnYxjfmakvUUqh9Gyb6KXfrj7JWTxORTYqb0E= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 h1:QelT11PB4FXiDEXucrfNckHoFxwt8USGY1ajP1ZF5lM= +golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -566,7 +778,8 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -585,6 +798,7 @@ golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -600,17 +814,16 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220403103023-749bd193bc2b h1:vI32FkLJNAWtGD4BwkThwEy6XS7ZLLMHkSkYfF8M0W0= -golang.org/x/net v0.0.0-20220403103023-749bd193bc2b/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d h1:4SFsTMi4UahlKoloni7L4eYzhFRifURQLw+yv0QDCx8= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -627,6 +840,7 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -636,7 +850,10 @@ golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190226215855-775f8194d0f9/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -645,6 +862,7 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -669,20 +887,22 @@ golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 h1:xHms4gcpe1YE7A3yIllJXP16CMAGuqwO2lX1mTyyRRc= -golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c h1:aFV+BgZ4svzjfabn8ERpuB4JI4N6/rdy1iusx77G3oU= +golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -698,11 +918,12 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 h1:Hir2P/De0WpUhtrKGGjvSb2YxUgyZ7EFOSLIcSSpiwE= -golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba h1:O8mE0/t419eoIwhTFpKVkHiTs/Igowgfkj25AcZrtiE= +golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -711,14 +932,18 @@ golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -746,19 +971,26 @@ golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= -golang.org/x/tools v0.1.9 h1:j9KsMiaP1c3B0OTQGth0/k+miLGTgLsAFUCrF2vLcF8= -golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.11 h1:loJ25fNOEhSXfHrpoGj91eCUThwdNX6u24rO1xnNteY= +golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4= +golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df h1:5Pf6pFKu98ODmgnpvkJ3kFUOQGGLIzLIkbzUHp47618= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= -gonum.org/v1/gonum v0.8.1 h1:wGtP3yGpc5mCLOLeTeBdjeui9oZSz5De0eOjMLC/QuQ= -gonum.org/v1/gonum v0.8.1/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= -gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0 h1:OE9mWmgKkjJyEmDAAtGMPjXu+YNeGvK9VTSHY6+Qihc= +gonum.org/v1/gonum v0.0.0-20190226202314-149afe6ec0b6/go.mod h1:jevfED4GnIEnJrWW55YmY9DMhajHcnkqVnEXmEtMyNI= +gonum.org/v1/gonum v0.0.0-20190902003836-43865b531bee/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU= +gonum.org/v1/gonum v0.8.1-0.20200930085651-eea0b5cb5cc9/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= +gonum.org/v1/gonum v0.8.2 h1:CCXrcPKiGGotvnN6jfUsKk4rRqm7q09/YbKb5xCEvtM= +gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= +gonum.org/v1/netlib v0.0.0-20190221094214-0632e2ebbd2d/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/netlib v0.0.0-20201012070519-2390d26c3658 h1:/DNJ3wcvPHjTLVNG6rmSHK7uEwdBihyiJRJXB16wXoU= +gonum.org/v1/netlib v0.0.0-20201012070519-2390d26c3658/go.mod h1:zQa7n16lh3Z6FbSTYgjG+KNhz1bA/b9t3plFEaGMp+A= gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= @@ -783,6 +1015,7 @@ google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -812,6 +1045,7 @@ google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7Fc google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200911024640-645f7a48b24f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20220405205423-9d709892a2bf h1:JTjwKJX9erVpsw17w+OIPP7iAgEkN/r8urhWSunEDTs= google.golang.org/genproto v0.0.0-20220405205423-9d709892a2bf/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= @@ -826,10 +1060,12 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.45.0 h1:NEpgUqV3Z+ZjkqMsxMg11IaDrXY4RY6CQukSGK0uI1M= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v0.0.0-20200910201057-6591123024b3/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -849,13 +1085,17 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= +gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU= +gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= @@ -872,8 +1112,28 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorgonia.org/cu v0.9.0-beta/go.mod h1:RPEPIfaxxqUmeRe7T1T8a0NER+KxBI2McoLEXhP1Vd8= +gorgonia.org/cu v0.9.3/go.mod h1:LgyAYDkN7HWhh8orGnCY2R8pP9PYbO44ivEbLMatkVU= +gorgonia.org/dawson v1.1.0/go.mod h1:Px1mcziba8YUBIDsbzGwbKJ11uIblv/zkln4jNrZ9Ws= +gorgonia.org/dawson v1.2.0/go.mod h1:Px1mcziba8YUBIDsbzGwbKJ11uIblv/zkln4jNrZ9Ws= +gorgonia.org/gorgonia v0.9.2/go.mod h1:ZtOb9f/wM2OMta1ISGspQ4roGDgz9d9dKOaPNvGR+ec= +gorgonia.org/gorgonia v0.9.16/go.mod h1:EnZtUbxgbqMx8eCTGPq8C0RfBlr/WllVtMyAFUYG+b4= +gorgonia.org/tensor v0.9.0-beta/go.mod h1:05Y4laKuVlj4qFoZIZW1q/9n1jZkgDBOLmKXZdBLG1w= +gorgonia.org/tensor v0.9.16/go.mod h1:75SMdLLhZ+2oB0/EE8lFEIt1Caoykdd4bz1mAe59deg= +gorgonia.org/tensor v0.9.19/go.mod h1:75SMdLLhZ+2oB0/EE8lFEIt1Caoykdd4bz1mAe59deg= +gorgonia.org/vecf32 v0.7.0/go.mod h1:iHG+kvTMqGYA0SgahfO2k62WRnxmHsqAREGbayRDzy8= +gorgonia.org/vecf32 v0.9.0/go.mod h1:NCc+5D2oxddRL11hd+pCB1PEyXWOyiQxfZ/1wwhOXCA= +gorgonia.org/vecf64 v0.7.0/go.mod h1:1y4pmcSd+wh3phG+InwWQjYrqwyrtN9h27WLFVQfV1Q= +gorgonia.org/vecf64 v0.9.0/go.mod h1:hp7IOWCnRiVQKON73kkC/AUMtEXyf9kGlVrtPQ9ccVA= +gorm.io/driver/mysql v1.0.3/go.mod h1:twGxftLBlFgNVNakL7F+P/x9oYqoymG3YYT8cAfI9oI= +gorm.io/driver/postgres v1.0.8/go.mod h1:4eOzrI1MUfm6ObJU/UcmbXyiHSs8jSwH95G5P5dxcAg= +gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9DYw= +gorm.io/gorm v1.20.4/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= +gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= +gorm.io/gorm v1.20.12/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -881,6 +1141,11 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw= +modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= +modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= +modernc.org/strutil v1.1.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs= +modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= diff --git a/migrations/mysql/20211211034819_add_nav_history_details.sql b/migrations/mysql/20211211034819_add_nav_history_details.sql index 53c8534e6a..965fe800fc 100644 --- a/migrations/mysql/20211211034819_add_nav_history_details.sql +++ b/migrations/mysql/20211211034819_add_nav_history_details.sql @@ -2,21 +2,21 @@ -- +begin CREATE TABLE nav_history_details ( - gid bigint unsigned auto_increment PRIMARY KEY, - exchange VARCHAR(30) NOT NULL, - subaccount VARCHAR(30) NOT NULL, - time DATETIME(3) NOT NULL, - currency VARCHAR(12) NOT NULL, - balance_in_usd DECIMAL(32, 8) UNSIGNED DEFAULT 0.00000000 NOT NULL, - balance_in_btc DECIMAL(32, 8) UNSIGNED DEFAULT 0.00000000 NOT NULL, - balance DECIMAL(32, 8) UNSIGNED DEFAULT 0.00000000 NOT NULL, - available DECIMAL(32, 8) UNSIGNED DEFAULT 0.00000000 NOT NULL, - locked DECIMAL(32, 8) UNSIGNED DEFAULT 0.00000000 NOT NULL + gid bigint unsigned auto_increment PRIMARY KEY, + exchange VARCHAR(30) NOT NULL, + subaccount VARCHAR(30) NOT NULL, + time DATETIME(3) NOT NULL, + currency VARCHAR(12) NOT NULL, + balance_in_usd DECIMAL(32, 8) UNSIGNED DEFAULT 0.00000000 NOT NULL, + balance_in_btc DECIMAL(32, 8) UNSIGNED DEFAULT 0.00000000 NOT NULL, + balance DECIMAL(32, 8) UNSIGNED DEFAULT 0.00000000 NOT NULL, + available DECIMAL(32, 8) UNSIGNED DEFAULT 0.00000000 NOT NULL, + locked DECIMAL(32, 8) UNSIGNED DEFAULT 0.00000000 NOT NULL ); -- +end -- +begin CREATE INDEX idx_nav_history_details - on nav_history_details(time, currency, exchange); + on nav_history_details (time, currency, exchange); -- +end -- +down diff --git a/migrations/mysql/20220503144849_add_margin_info_to_nav.sql b/migrations/mysql/20220503144849_add_margin_info_to_nav.sql new file mode 100644 index 0000000000..618b3435fe --- /dev/null +++ b/migrations/mysql/20220503144849_add_margin_info_to_nav.sql @@ -0,0 +1,27 @@ +-- +up +-- +begin +ALTER TABLE `nav_history_details` + ADD COLUMN `session` VARCHAR(30) NOT NULL, + ADD COLUMN `is_margin` BOOLEAN NOT NULL DEFAULT FALSE, + ADD COLUMN `is_isolated` BOOLEAN NOT NULL DEFAULT FALSE, + ADD COLUMN `isolated_symbol` VARCHAR(30) NOT NULL DEFAULT '', + ADD COLUMN `net_asset` DECIMAL(32, 8) UNSIGNED DEFAULT 0.00000000 NOT NULL, + ADD COLUMN `borrowed` DECIMAL(32, 8) UNSIGNED DEFAULT 0.00000000 NOT NULL, + ADD COLUMN `price_in_usd` DECIMAL(32, 8) UNSIGNED DEFAULT 0.00000000 NOT NULL +; +-- +end + + +-- +down + +-- +begin +ALTER TABLE `nav_history_details` + DROP COLUMN `session`, + DROP COLUMN `net_asset`, + DROP COLUMN `borrowed`, + DROP COLUMN `price_in_usd`, + DROP COLUMN `is_margin`, + DROP COLUMN `is_isolated`, + DROP COLUMN `isolated_symbol` +; +-- +end diff --git a/migrations/mysql/20220504184155_fix_net_asset_column.sql b/migrations/mysql/20220504184155_fix_net_asset_column.sql new file mode 100644 index 0000000000..2d5a9d271b --- /dev/null +++ b/migrations/mysql/20220504184155_fix_net_asset_column.sql @@ -0,0 +1,19 @@ +-- +up +-- +begin +ALTER TABLE `nav_history_details` + MODIFY COLUMN `net_asset` DECIMAL(32, 8) DEFAULT 0.00000000 NOT NULL, + CHANGE COLUMN `balance_in_usd` `net_asset_in_usd` DECIMAL(32, 2) DEFAULT 0.00000000 NOT NULL, + CHANGE COLUMN `balance_in_btc` `net_asset_in_btc` DECIMAL(32, 20) DEFAULT 0.00000000 NOT NULL; +-- +end + +-- +begin +ALTER TABLE `nav_history_details` + ADD COLUMN `interest` DECIMAL(32, 20) UNSIGNED DEFAULT 0.00000000 NOT NULL; +-- +end + +-- +down + +-- +begin +ALTER TABLE `nav_history_details` + DROP COLUMN `interest`; +-- +end diff --git a/migrations/mysql/20220512170322_fix_profit_symbol_length.sql b/migrations/mysql/20220512170322_fix_profit_symbol_length.sql new file mode 100644 index 0000000000..60bc96be96 --- /dev/null +++ b/migrations/mysql/20220512170322_fix_profit_symbol_length.sql @@ -0,0 +1,11 @@ +-- +up +-- +begin +ALTER TABLE profits + CHANGE symbol symbol VARCHAR(20) NOT NULL; +-- +end + +-- +down + +-- +begin +SELECT 1; +-- +end diff --git a/migrations/mysql/20220520140707_kline_unique_idx.sql b/migrations/mysql/20220520140707_kline_unique_idx.sql new file mode 100644 index 0000000000..e45bde5f4b --- /dev/null +++ b/migrations/mysql/20220520140707_kline_unique_idx.sql @@ -0,0 +1,47 @@ +-- +up +-- +begin +CREATE UNIQUE INDEX idx_kline_binance_unique + ON binance_klines (`symbol`, `interval`, `start_time`); +-- +end + +-- +begin +CREATE UNIQUE INDEX idx_kline_max_unique + ON max_klines (`symbol`, `interval`, `start_time`); +-- +end + +-- +begin +CREATE UNIQUE INDEX `idx_kline_ftx_unique` + ON ftx_klines (`symbol`, `interval`, `start_time`); +-- +end + +-- +begin +CREATE UNIQUE INDEX `idx_kline_kucoin_unique` + ON kucoin_klines (`symbol`, `interval`, `start_time`); +-- +end + +-- +begin +CREATE UNIQUE INDEX `idx_kline_okex_unique` + ON okex_klines (`symbol`, `interval`, `start_time`); +-- +end + +-- +down + +-- +begin +DROP INDEX `idx_kline_ftx_unique` ON `ftx_klines`; +-- +end + +-- +begin +DROP INDEX `idx_kline_max_unique` ON `max_klines`; +-- +end + +-- +begin +DROP INDEX `idx_kline_binance_unique` ON `binance_klines`; +-- +end + +-- +begin +DROP INDEX `idx_kline_kucoin_unique` ON `kucoin_klines`; +-- +end + +-- +begin +DROP INDEX `idx_kline_okex_unique` ON `okex_klines`; +-- +end diff --git a/migrations/mysql/20220531012226_margin_loans.sql b/migrations/mysql/20220531012226_margin_loans.sql new file mode 100644 index 0000000000..dbbd1346cc --- /dev/null +++ b/migrations/mysql/20220531012226_margin_loans.sql @@ -0,0 +1,24 @@ +-- +up +CREATE TABLE `margin_loans` +( + `gid` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + + `transaction_id` BIGINT UNSIGNED NOT NULL, + + `exchange` VARCHAR(24) NOT NULL DEFAULT '', + + `asset` VARCHAR(24) NOT NULL DEFAULT '', + + `isolated_symbol` VARCHAR(24) NOT NULL DEFAULT '', + + -- quantity is the quantity of the trade that makes profit + `principle` DECIMAL(16, 8) UNSIGNED NOT NULL, + + `time` DATETIME(3) NOT NULL, + + PRIMARY KEY (`gid`), + UNIQUE KEY (`transaction_id`) +); + +-- +down +DROP TABLE IF EXISTS `margin_loans`; diff --git a/migrations/mysql/20220531013327_margin_repays.sql b/migrations/mysql/20220531013327_margin_repays.sql new file mode 100644 index 0000000000..873b1ed73a --- /dev/null +++ b/migrations/mysql/20220531013327_margin_repays.sql @@ -0,0 +1,24 @@ +-- +up +CREATE TABLE `margin_repays` +( + `gid` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + + `transaction_id` BIGINT UNSIGNED NOT NULL, + + `exchange` VARCHAR(24) NOT NULL DEFAULT '', + + `asset` VARCHAR(24) NOT NULL DEFAULT '', + + `isolated_symbol` VARCHAR(24) NOT NULL DEFAULT '', + + -- quantity is the quantity of the trade that makes profit + `principle` DECIMAL(16, 8) UNSIGNED NOT NULL, + + `time` DATETIME(3) NOT NULL, + + PRIMARY KEY (`gid`), + UNIQUE KEY (`transaction_id`) +); + +-- +down +DROP TABLE IF EXISTS `margin_repays`; diff --git a/migrations/mysql/20220531013542_margin_interests.sql b/migrations/mysql/20220531013542_margin_interests.sql new file mode 100644 index 0000000000..90169526b3 --- /dev/null +++ b/migrations/mysql/20220531013542_margin_interests.sql @@ -0,0 +1,24 @@ +-- +up +CREATE TABLE `margin_interests` +( + `gid` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + + `exchange` VARCHAR(24) NOT NULL DEFAULT '', + + `asset` VARCHAR(24) NOT NULL DEFAULT '', + + `isolated_symbol` VARCHAR(24) NOT NULL DEFAULT '', + + `principle` DECIMAL(16, 8) UNSIGNED NOT NULL, + + `interest` DECIMAL(20, 16) UNSIGNED NOT NULL, + + `interest_rate` DECIMAL(20, 16) UNSIGNED NOT NULL, + + `time` DATETIME(3) NOT NULL, + + PRIMARY KEY (`gid`) +); + +-- +down +DROP TABLE IF EXISTS `margin_interests`; diff --git a/migrations/mysql/20220531015005_margin_liquidations.sql b/migrations/mysql/20220531015005_margin_liquidations.sql new file mode 100644 index 0000000000..82ea81f1d6 --- /dev/null +++ b/migrations/mysql/20220531015005_margin_liquidations.sql @@ -0,0 +1,33 @@ +-- +up +CREATE TABLE `margin_liquidations` +( + `gid` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + + `exchange` VARCHAR(24) NOT NULL DEFAULT '', + + `symbol` VARCHAR(24) NOT NULL DEFAULT '', + + `order_id` BIGINT UNSIGNED NOT NULL, + + `is_isolated` BOOL NOT NULL DEFAULT false, + + `average_price` DECIMAL(16, 8) UNSIGNED NOT NULL, + + `price` DECIMAL(16, 8) UNSIGNED NOT NULL, + + `quantity` DECIMAL(16, 8) UNSIGNED NOT NULL, + + `executed_quantity` DECIMAL(16, 8) UNSIGNED NOT NULL, + + `side` VARCHAR(5) NOT NULL DEFAULT '', + + `time_in_force` VARCHAR(5) NOT NULL DEFAULT '', + + `time` DATETIME(3) NOT NULL, + + PRIMARY KEY (`gid`), + UNIQUE KEY (`order_id`, `exchange`) +); + +-- +down +DROP TABLE IF EXISTS `margin_liquidations`; diff --git a/migrations/sqlite3/20211211034818_add_nav_history_details.sql b/migrations/sqlite3/20211211034818_add_nav_history_details.sql index 163b9787ed..56860040ca 100644 --- a/migrations/sqlite3/20211211034818_add_nav_history_details.sql +++ b/migrations/sqlite3/20211211034818_add_nav_history_details.sql @@ -2,16 +2,16 @@ -- +begin CREATE TABLE `nav_history_details` ( - gid bigint unsigned auto_increment PRIMARY KEY, - `exchange` VARCHAR NOT NULL DEFAULT '', - `subaccount` VARCHAR NOT NULL DEFAULT '', - time DATETIME(3) NOT NULL DEFAULT (strftime('%s','now')), - currency VARCHAR NOT NULL, - balance_in_usd DECIMAL DEFAULT 0.00000000 NOT NULL, - balance_in_btc DECIMAL DEFAULT 0.00000000 NOT NULL, - balance DECIMAL DEFAULT 0.00000000 NOT NULL, - available DECIMAL DEFAULT 0.00000000 NOT NULL, - locked DECIMAL DEFAULT 0.00000000 NOT NULL + `gid` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + `exchange` VARCHAR(30) NOT NULL DEFAULT '', + `subaccount` VARCHAR(30) NOT NULL DEFAULT '', + `time` DATETIME(3) NOT NULL DEFAULT (strftime('%s', 'now')), + `currency` VARCHAR(30) NOT NULL, + `net_asset_in_usd` DECIMAL DEFAULT 0.00000000 NOT NULL, + `net_asset_in_btc` DECIMAL DEFAULT 0.00000000 NOT NULL, + `balance` DECIMAL DEFAULT 0.00000000 NOT NULL, + `available` DECIMAL DEFAULT 0.00000000 NOT NULL, + `locked` DECIMAL DEFAULT 0.00000000 NOT NULL ); -- +end -- +begin diff --git a/migrations/sqlite3/20220503144849_add_margin_info_to_nav.sql b/migrations/sqlite3/20220503144849_add_margin_info_to_nav.sql new file mode 100644 index 0000000000..7f2fc06e0e --- /dev/null +++ b/migrations/sqlite3/20220503144849_add_margin_info_to_nav.sql @@ -0,0 +1,12 @@ +-- +up +ALTER TABLE `nav_history_details` ADD COLUMN `session` VARCHAR(50) NOT NULL; +ALTER TABLE `nav_history_details` ADD COLUMN `borrowed` DECIMAL DEFAULT 0.00000000 NOT NULL; +ALTER TABLE `nav_history_details` ADD COLUMN `net_asset` DECIMAL DEFAULT 0.00000000 NOT NULL; +ALTER TABLE `nav_history_details` ADD COLUMN `price_in_usd` DECIMAL DEFAULT 0.00000000 NOT NULL; +ALTER TABLE `nav_history_details` ADD COLUMN `is_margin` BOOL DEFAULT FALSE NOT NULL; +ALTER TABLE `nav_history_details` ADD COLUMN `is_isolated` BOOL DEFAULT FALSE NOT NULL; +ALTER TABLE `nav_history_details` ADD COLUMN `isolated_symbol` VARCHAR(30) DEFAULT '' NOT NULL; + +-- +down +-- we can not rollback alter table change in sqlite +SELECT 1; diff --git a/migrations/sqlite3/20220504184155_fix_net_asset_column.sql b/migrations/sqlite3/20220504184155_fix_net_asset_column.sql new file mode 100644 index 0000000000..96993735ee --- /dev/null +++ b/migrations/sqlite3/20220504184155_fix_net_asset_column.sql @@ -0,0 +1,11 @@ +-- +up +-- +begin +ALTER TABLE `nav_history_details` ADD COLUMN `interest` DECIMAL DEFAULT 0.00000000 NOT NULL; +-- +end + + +-- +down + +-- +begin +SELECT 1; +-- +end diff --git a/migrations/sqlite3/20220512170330_fix_profit_symbol_length.sql b/migrations/sqlite3/20220512170330_fix_profit_symbol_length.sql new file mode 100644 index 0000000000..583c4051e3 --- /dev/null +++ b/migrations/sqlite3/20220512170330_fix_profit_symbol_length.sql @@ -0,0 +1,12 @@ +-- +up +-- +begin +-- We can not change column type in sqlite +-- However, SQLite does not enforce the length of a VARCHAR, i.e VARCHAR(8) == VARCHAR(20) == TEXT +SELECT 1; +-- +end + +-- +down + +-- +begin +SELECT 1; +-- +end diff --git a/migrations/sqlite3/20220520140707_kline_unique_idx.sql b/migrations/sqlite3/20220520140707_kline_unique_idx.sql new file mode 100644 index 0000000000..e45bde5f4b --- /dev/null +++ b/migrations/sqlite3/20220520140707_kline_unique_idx.sql @@ -0,0 +1,47 @@ +-- +up +-- +begin +CREATE UNIQUE INDEX idx_kline_binance_unique + ON binance_klines (`symbol`, `interval`, `start_time`); +-- +end + +-- +begin +CREATE UNIQUE INDEX idx_kline_max_unique + ON max_klines (`symbol`, `interval`, `start_time`); +-- +end + +-- +begin +CREATE UNIQUE INDEX `idx_kline_ftx_unique` + ON ftx_klines (`symbol`, `interval`, `start_time`); +-- +end + +-- +begin +CREATE UNIQUE INDEX `idx_kline_kucoin_unique` + ON kucoin_klines (`symbol`, `interval`, `start_time`); +-- +end + +-- +begin +CREATE UNIQUE INDEX `idx_kline_okex_unique` + ON okex_klines (`symbol`, `interval`, `start_time`); +-- +end + +-- +down + +-- +begin +DROP INDEX `idx_kline_ftx_unique` ON `ftx_klines`; +-- +end + +-- +begin +DROP INDEX `idx_kline_max_unique` ON `max_klines`; +-- +end + +-- +begin +DROP INDEX `idx_kline_binance_unique` ON `binance_klines`; +-- +end + +-- +begin +DROP INDEX `idx_kline_kucoin_unique` ON `kucoin_klines`; +-- +end + +-- +begin +DROP INDEX `idx_kline_okex_unique` ON `okex_klines`; +-- +end diff --git a/migrations/sqlite3/20220531012226_margin_loans.sql b/migrations/sqlite3/20220531012226_margin_loans.sql new file mode 100644 index 0000000000..2569e671a9 --- /dev/null +++ b/migrations/sqlite3/20220531012226_margin_loans.sql @@ -0,0 +1,21 @@ +-- +up +CREATE TABLE `margin_loans` +( + `gid` INTEGER PRIMARY KEY AUTOINCREMENT, + + `transaction_id` INTEGER NOT NULL, + + `exchange` VARCHAR(24) NOT NULL DEFAULT '', + + `asset` VARCHAR(24) NOT NULL DEFAULT '', + + `isolated_symbol` VARCHAR(24) NOT NULL DEFAULT '', + + -- quantity is the quantity of the trade that makes profit + `principle` DECIMAL(16, 8) NOT NULL, + + `time` DATETIME(3) NOT NULL +); + +-- +down +DROP TABLE IF EXISTS `margin_loans`; diff --git a/migrations/sqlite3/20220531013327_margin_repays.sql b/migrations/sqlite3/20220531013327_margin_repays.sql new file mode 100644 index 0000000000..c9f6123650 --- /dev/null +++ b/migrations/sqlite3/20220531013327_margin_repays.sql @@ -0,0 +1,21 @@ +-- +up +CREATE TABLE `margin_repays` +( + `gid` INTEGER PRIMARY KEY AUTOINCREMENT, + + `transaction_id` INTEGER NOT NULL, + + `exchange` VARCHAR(24) NOT NULL DEFAULT '', + + `asset` VARCHAR(24) NOT NULL DEFAULT '', + + `isolated_symbol` VARCHAR(24) NOT NULL DEFAULT '', + + -- quantity is the quantity of the trade that makes profit + `principle` DECIMAL(16, 8) NOT NULL, + + `time` DATETIME(3) NOT NULL +); + +-- +down +DROP TABLE IF EXISTS `margin_repays`; diff --git a/migrations/sqlite3/20220531013541_margin_interests.sql b/migrations/sqlite3/20220531013541_margin_interests.sql new file mode 100644 index 0000000000..f088f25814 --- /dev/null +++ b/migrations/sqlite3/20220531013541_margin_interests.sql @@ -0,0 +1,22 @@ +-- +up +CREATE TABLE `margin_interests` +( + `gid` INTEGER PRIMARY KEY AUTOINCREMENT, + + `exchange` VARCHAR(24) NOT NULL DEFAULT '', + + `asset` VARCHAR(24) NOT NULL DEFAULT '', + + `isolated_symbol` VARCHAR(24) NOT NULL DEFAULT '', + + `principle` DECIMAL(16, 8) NOT NULL, + + `interest` DECIMAL(20, 16) NOT NULL, + + `interest_rate` DECIMAL(20, 16) NOT NULL, + + `time` DATETIME(3) NOT NULL +); + +-- +down +DROP TABLE IF EXISTS `margin_interests`; diff --git a/migrations/sqlite3/20220531015005_margin_liquidations.sql b/migrations/sqlite3/20220531015005_margin_liquidations.sql new file mode 100644 index 0000000000..5a99afc362 --- /dev/null +++ b/migrations/sqlite3/20220531015005_margin_liquidations.sql @@ -0,0 +1,30 @@ +-- +up +CREATE TABLE `margin_liquidations` +( + `gid` INTEGER PRIMARY KEY AUTOINCREMENT, + + `exchange` VARCHAR(24) NOT NULL DEFAULT '', + + `symbol` VARCHAR(24) NOT NULL DEFAULT '', + + `order_id` INTEGER NOT NULL, + + `is_isolated` BOOL NOT NULL DEFAULT false, + + `average_price` DECIMAL(16, 8) NOT NULL, + + `price` DECIMAL(16, 8) NOT NULL, + + `quantity` DECIMAL(16, 8) NOT NULL, + + `executed_quantity` DECIMAL(16, 8) NOT NULL, + + `side` VARCHAR(5) NOT NULL DEFAULT '', + + `time_in_force` VARCHAR(5) NOT NULL DEFAULT '', + + `time` DATETIME(3) NOT NULL +); + +-- +down +DROP TABLE IF EXISTS `margin_liquidations`; diff --git a/pkg/accounting/cost_distribution.go b/pkg/accounting/cost_distribution.go index e66f065920..5c588b1821 100644 --- a/pkg/accounting/cost_distribution.go +++ b/pkg/accounting/cost_distribution.go @@ -11,10 +11,6 @@ import ( "github.com/c9s/bbgo/pkg/types" ) -func zero(a float64) bool { - return int(math.Round(a*1e8)) == 0 -} - type Stock types.Trade func (stock *Stock) String() string { diff --git a/pkg/accounting/pnl/avg_cost.go b/pkg/accounting/pnl/avg_cost.go index 7db141398c..f592719a78 100644 --- a/pkg/accounting/pnl/avg_cost.go +++ b/pkg/accounting/pnl/avg_cost.go @@ -12,20 +12,42 @@ import ( type AverageCostCalculator struct { TradingFeeCurrency string Market types.Market + ExchangeFee *types.ExchangeFee } -func (c *AverageCostCalculator) Calculate(symbol string, trades []types.Trade, currentPrice fixedpoint.Value) *AverageCostPnlReport { +func (c *AverageCostCalculator) Calculate(symbol string, trades []types.Trade, currentPrice fixedpoint.Value) *AverageCostPnLReport { // copy trades, so that we can truncate it. var bidVolume = fixedpoint.Zero var askVolume = fixedpoint.Zero var feeUSD = fixedpoint.Zero + var grossProfit = fixedpoint.Zero + var grossLoss = fixedpoint.Zero + + var position = types.NewPositionFromMarket(c.Market) + + if c.ExchangeFee != nil { + position.SetFeeRate(*c.ExchangeFee) + } else { + makerFeeRate := 0.075 * 0.01 + + if c.Market.QuoteCurrency == "BUSD" { + makerFeeRate = 0 + } + + position.SetFeeRate(types.ExchangeFee{ + // binance vip 0 uses 0.075% + MakerFeeRate: fixedpoint.NewFromFloat(makerFeeRate), + TakerFeeRate: fixedpoint.NewFromFloat(0.075 * 0.01), + }) + } if len(trades) == 0 { - return &AverageCostPnlReport{ + return &AverageCostPnLReport{ Symbol: symbol, Market: c.Market, LastPrice: currentPrice, NumTrades: 0, + Position: position, BuyVolume: bidVolume, SellVolume: askVolume, FeeInUSD: feeUSD, @@ -34,13 +56,6 @@ func (c *AverageCostCalculator) Calculate(symbol string, trades []types.Trade, c var currencyFees = map[string]fixedpoint.Value{} - var position = types.NewPositionFromMarket(c.Market) - position.SetFeeRate(types.ExchangeFee{ - // binance vip 0 uses 0.075% - MakerFeeRate: fixedpoint.NewFromFloat(0.075 * 0.01), - TakerFeeRate: fixedpoint.NewFromFloat(0.075 * 0.01), - }) - // TODO: configure the exchange fee rate here later // position.SetExchangeFeeRate() var totalProfit fixedpoint.Value @@ -64,6 +79,12 @@ func (c *AverageCostCalculator) Calculate(symbol string, trades []types.Trade, c totalNetProfit = totalNetProfit.Add(netProfit) } + if profit.Sign() > 0 { + grossProfit = grossProfit.Add(profit) + } else if profit.Sign() < 0 { + grossLoss = grossLoss.Add(profit) + } + if trade.IsBuyer { bidVolume = bidVolume.Add(trade.Quantity) } else { @@ -81,22 +102,28 @@ func (c *AverageCostCalculator) Calculate(symbol string, trades []types.Trade, c unrealizedProfit := currentPrice.Sub(position.AverageCost). Mul(position.GetBase()) - return &AverageCostPnlReport{ + + return &AverageCostPnLReport{ Symbol: symbol, Market: c.Market, LastPrice: currentPrice, NumTrades: len(trades), StartTime: time.Time(trades[0].Time), + Position: position, BuyVolume: bidVolume, SellVolume: askVolume, - Stock: position.GetBase(), - Profit: totalProfit, - NetProfit: totalNetProfit, - UnrealizedProfit: unrealizedProfit, - AverageCost: position.AverageCost, - FeeInUSD: totalProfit.Sub(totalNetProfit), - CurrencyFees: currencyFees, + BaseAssetPosition: position.GetBase(), + Profit: totalProfit, + NetProfit: totalNetProfit, + UnrealizedProfit: unrealizedProfit, + + GrossProfit: grossProfit, + GrossLoss: grossLoss, + + AverageCost: position.AverageCost, + FeeInUSD: totalProfit.Sub(totalNetProfit), + CurrencyFees: currencyFees, } } diff --git a/pkg/accounting/pnl/report.go b/pkg/accounting/pnl/report.go index 00dceb7754..1c81524230 100644 --- a/pkg/accounting/pnl/report.go +++ b/pkg/accounting/pnl/report.go @@ -5,56 +5,72 @@ import ( "strconv" "time" - "github.com/c9s/bbgo/pkg/fixedpoint" - log "github.com/sirupsen/logrus" + "github.com/fatih/color" "github.com/slack-go/slack" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/slack/slackstyle" "github.com/c9s/bbgo/pkg/types" ) -type AverageCostPnlReport struct { - LastPrice fixedpoint.Value `json:"lastPrice"` - StartTime time.Time `json:"startTime"` - Symbol string `json:"symbol"` - Market types.Market `json:"market"` - - NumTrades int `json:"numTrades"` - Profit fixedpoint.Value `json:"profit"` - NetProfit fixedpoint.Value `json:"netProfit"` - UnrealizedProfit fixedpoint.Value `json:"unrealizedProfit"` - AverageCost fixedpoint.Value `json:"averageCost"` - BuyVolume fixedpoint.Value `json:"buyVolume,omitempty"` - SellVolume fixedpoint.Value `json:"sellVolume,omitempty"` - FeeInUSD fixedpoint.Value `json:"feeInUSD"` - Stock fixedpoint.Value `json:"stock"` - CurrencyFees map[string]fixedpoint.Value `json:"currencyFees"` +type AverageCostPnLReport struct { + LastPrice fixedpoint.Value `json:"lastPrice"` + StartTime time.Time `json:"startTime"` + Symbol string `json:"symbol"` + Market types.Market `json:"market"` + + NumTrades int `json:"numTrades"` + Profit fixedpoint.Value `json:"profit"` + UnrealizedProfit fixedpoint.Value `json:"unrealizedProfit"` + + NetProfit fixedpoint.Value `json:"netProfit"` + GrossProfit fixedpoint.Value `json:"grossProfit"` + GrossLoss fixedpoint.Value `json:"grossLoss"` + Position *types.Position `json:"position,omitempty"` + + AverageCost fixedpoint.Value `json:"averageCost"` + BuyVolume fixedpoint.Value `json:"buyVolume,omitempty"` + SellVolume fixedpoint.Value `json:"sellVolume,omitempty"` + FeeInUSD fixedpoint.Value `json:"feeInUSD"` + BaseAssetPosition fixedpoint.Value `json:"baseAssetPosition"` + CurrencyFees map[string]fixedpoint.Value `json:"currencyFees"` } -func (report *AverageCostPnlReport) JSON() ([]byte, error) { +func (report *AverageCostPnLReport) JSON() ([]byte, error) { return json.MarshalIndent(report, "", " ") } -func (report AverageCostPnlReport) Print() { - log.Infof("TRADES SINCE: %v", report.StartTime) - log.Infof("NUMBER OF TRADES: %d", report.NumTrades) - log.Infof("AVERAGE COST: %s", types.USD.FormatMoney(report.AverageCost)) - log.Infof("TOTAL BUY VOLUME: %v", report.BuyVolume) - log.Infof("TOTAL SELL VOLUME: %v", report.SellVolume) - log.Infof("STOCK: %s", report.Stock.String()) - - // FIXME: - // log.Infof("FEE (USD): %f", report.FeeInUSD) - log.Infof("CURRENT PRICE: %s", types.USD.FormatMoney(report.LastPrice)) - log.Infof("CURRENCY FEES:") +func (report AverageCostPnLReport) Print() { + color.Green("TRADES SINCE: %v", report.StartTime) + color.Green("NUMBER OF TRADES: %d", report.NumTrades) + color.Green(report.Position.String()) + color.Green("AVERAGE COST: %s", types.USD.FormatMoney(report.AverageCost)) + color.Green("BASE ASSET POSITION: %s", report.BaseAssetPosition.String()) + + color.Green("TOTAL BUY VOLUME: %v", report.BuyVolume) + color.Green("TOTAL SELL VOLUME: %v", report.SellVolume) + + color.Green("CURRENT PRICE: %s", types.USD.FormatMoney(report.LastPrice)) + color.Green("CURRENCY FEES:") for currency, fee := range report.CurrencyFees { - log.Infof(" - %s: %s", currency, fee.String()) + color.Green(" - %s: %s", currency, fee.String()) + } + + if report.Profit.Sign() > 0 { + color.Green("PROFIT: %s", types.USD.FormatMoney(report.Profit)) + } else { + color.Red("PROFIT: %s", types.USD.FormatMoney(report.Profit)) + } + + if report.UnrealizedProfit.Sign() > 0 { + color.Green("UNREALIZED PROFIT: %s", types.USD.FormatMoney(report.UnrealizedProfit)) + } else { + color.Red("UNREALIZED PROFIT: %s", types.USD.FormatMoney(report.UnrealizedProfit)) } - log.Infof("PROFIT: %s", types.USD.FormatMoney(report.Profit)) - log.Infof("UNREALIZED PROFIT: %s", types.USD.FormatMoney(report.UnrealizedProfit)) } -func (report AverageCostPnlReport) SlackAttachment() slack.Attachment { +func (report AverageCostPnLReport) SlackAttachment() slack.Attachment { var color = slackstyle.Red if report.UnrealizedProfit.Sign() > 0 { @@ -75,7 +91,7 @@ func (report AverageCostPnlReport) SlackAttachment() slack.Attachment { // FIXME: // {Title: "Fee (USD)", Value: types.USD.FormatMoney(report.FeeInUSD), Short: true}, - {Title: "Stock", Value: report.Stock.String(), Short: true}, + {Title: "Base Asset Position", Value: report.BaseAssetPosition.String(), Short: true}, {Title: "Number of Trades", Value: strconv.Itoa(report.NumTrades), Short: true}, }, Footer: report.StartTime.Format(time.RFC822), diff --git a/pkg/backtest/assets_dummy.go b/pkg/backtest/assets_dummy.go new file mode 100644 index 0000000000..a6f0fcb376 --- /dev/null +++ b/pkg/backtest/assets_dummy.go @@ -0,0 +1,65 @@ +//go:build !web +// +build !web + +package backtest + +import ( + "bytes" + "errors" + "net/http" + "os" + "time" +) + +var assets = map[string][]byte{} + +var FS = &fs{} + +type fs struct{} + +func (fs *fs) Open(name string) (http.File, error) { + if name == "/" { + return fs, nil + } + b, ok := assets[name] + if !ok { + return nil, os.ErrNotExist + } + return &file{name: name, size: len(b), Reader: bytes.NewReader(b)}, nil +} + +func (fs *fs) Close() error { return nil } +func (fs *fs) Read(p []byte) (int, error) { return 0, nil } +func (fs *fs) Seek(offset int64, whence int) (int64, error) { return 0, nil } +func (fs *fs) Stat() (os.FileInfo, error) { return fs, nil } +func (fs *fs) Name() string { return "/" } +func (fs *fs) Size() int64 { return 0 } +func (fs *fs) Mode() os.FileMode { return 0755 } +func (fs *fs) ModTime() time.Time { return time.Time{} } +func (fs *fs) IsDir() bool { return true } +func (fs *fs) Sys() interface{} { return nil } +func (fs *fs) Readdir(count int) ([]os.FileInfo, error) { + files := []os.FileInfo{} + for name, data := range assets { + files = append(files, &file{name: name, size: len(data), Reader: bytes.NewReader(data)}) + } + return files, nil +} + +type file struct { + name string + size int + *bytes.Reader +} + +func (f *file) Close() error { return nil } +func (f *file) Readdir(count int) ([]os.FileInfo, error) { + return nil, errors.New("readdir is not supported") +} +func (f *file) Stat() (os.FileInfo, error) { return f, nil } +func (f *file) Name() string { return f.name } +func (f *file) Size() int64 { return int64(f.size) } +func (f *file) Mode() os.FileMode { return 0644 } +func (f *file) ModTime() time.Time { return time.Time{} } +func (f *file) IsDir() bool { return false } +func (f *file) Sys() interface{} { return nil } diff --git a/pkg/backtest/dumper.go b/pkg/backtest/dumper.go new file mode 100644 index 0000000000..ba2139384b --- /dev/null +++ b/pkg/backtest/dumper.go @@ -0,0 +1,97 @@ +package backtest + +import ( + "fmt" + "path/filepath" + "strconv" + "time" + + "go.uber.org/multierr" + + "github.com/c9s/bbgo/pkg/data/tsv" + "github.com/c9s/bbgo/pkg/types" +) + +const DateFormat = "2006-01-02T15:04" + +type symbolInterval struct { + Symbol string + Interval types.Interval +} + +// KLineDumper dumps the received kline data into a folder for the backtest report to load the charts. +type KLineDumper struct { + OutputDirectory string + writers map[symbolInterval]*tsv.Writer + filenames map[symbolInterval]string +} + +func NewKLineDumper(outputDirectory string) *KLineDumper { + return &KLineDumper{ + OutputDirectory: outputDirectory, + writers: make(map[symbolInterval]*tsv.Writer), + filenames: make(map[symbolInterval]string), + } +} + +func (d *KLineDumper) Filenames() map[symbolInterval]string { + return d.filenames +} + +func (d *KLineDumper) formatFileName(symbol string, interval types.Interval) string { + return filepath.Join(d.OutputDirectory, fmt.Sprintf("%s-%s.tsv", + symbol, + interval)) +} + +var csvHeader = []string{"date", "startTime", "endTime", "interval", "open", "high", "low", "close", "volume"} + +func (d *KLineDumper) encode(k types.KLine) []string { + return []string{ + time.Time(k.StartTime).Format(time.ANSIC), // ANSIC date - for javascript to parse (this works with Date.parse(date_str) + strconv.FormatInt(k.StartTime.Unix(), 10), + strconv.FormatInt(k.EndTime.Unix(), 10), + k.Interval.String(), + k.Open.String(), + k.High.String(), + k.Low.String(), + k.Close.String(), + k.Volume.String(), + } +} + +func (d *KLineDumper) Record(k types.KLine) error { + si := symbolInterval{Symbol: k.Symbol, Interval: k.Interval} + + w, ok := d.writers[si] + if !ok { + filename := d.formatFileName(k.Symbol, k.Interval) + w2, err := tsv.NewWriterFile(filename) + if err != nil { + return err + } + w = w2 + + d.writers[si] = w2 + d.filenames[si] = filename + + if err2 := w2.Write(csvHeader); err2 != nil { + return err2 + } + } + + return w.Write(d.encode(k)) +} + +func (d *KLineDumper) Close() error { + var err error = nil + for _, w := range d.writers { + w.Flush() + err2 := w.Close() + if err2 != nil { + err = multierr.Append(err, err2) + } + } + + return err +} diff --git a/pkg/backtest/dumper_test.go b/pkg/backtest/dumper_test.go new file mode 100644 index 0000000000..be13a22611 --- /dev/null +++ b/pkg/backtest/dumper_test.go @@ -0,0 +1,54 @@ +package backtest + +import ( + "encoding/csv" + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +func TestKLineDumper(t *testing.T) { + tempDir := os.TempDir() + _ = os.Mkdir(tempDir, 0755) + dumper := NewKLineDumper(tempDir) + + t1 := time.Now() + err := dumper.Record(types.KLine{ + Exchange: types.ExchangeBinance, + Symbol: "BTCUSDT", + StartTime: types.Time(t1), + EndTime: types.Time(t1.Add(time.Minute)), + Interval: types.Interval1m, + Open: fixedpoint.NewFromFloat(1000.0), + High: fixedpoint.NewFromFloat(2000.0), + Low: fixedpoint.NewFromFloat(3000.0), + Close: fixedpoint.NewFromFloat(4000.0), + Volume: fixedpoint.NewFromFloat(5000.0), + QuoteVolume: fixedpoint.NewFromFloat(6000.0), + NumberOfTrades: 10, + Closed: true, + }) + assert.NoError(t, err) + + err = dumper.Close() + assert.NoError(t, err) + + filenames := dumper.Filenames() + assert.NotEmpty(t, filenames) + for _, filename := range filenames { + f, err := os.Open(filename) + if assert.NoError(t, err) { + reader := csv.NewReader(f) + records, err2 := reader.Read() + if assert.NoError(t, err2) { + assert.NotEmptyf(t, records, "%v", records) + } + + } + } +} diff --git a/pkg/backtest/exchange.go b/pkg/backtest/exchange.go index e6708932df..0f4d1d44a9 100644 --- a/pkg/backtest/exchange.go +++ b/pkg/backtest/exchange.go @@ -30,30 +30,37 @@ package backtest import ( "context" "fmt" + "strconv" "sync" "time" - "github.com/c9s/bbgo/pkg/cache" + "github.com/sirupsen/logrus" "github.com/pkg/errors" + "github.com/c9s/bbgo/pkg/cache" + "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/service" "github.com/c9s/bbgo/pkg/types" ) +var log = logrus.WithField("cmd", "backtest") + var ErrUnimplemented = errors.New("unimplemented method") +var ErrNegativeQuantity = errors.New("order quantity can not be negative") +var ErrZeroQuantity = errors.New("order quantity can not be zero") type Exchange struct { - sourceName types.ExchangeName - publicExchange types.Exchange - srv *service.BacktestService - startTime, endTime time.Time + sourceName types.ExchangeName + publicExchange types.Exchange + srv *service.BacktestService + currentTime time.Time account *types.Account config *bbgo.Backtest - userDataStream, marketDataStream *Stream + MarketDataStream types.StandardStreamEmitter trades map[string][]types.Trade tradesMutex sync.Mutex @@ -65,6 +72,8 @@ type Exchange struct { matchingBooksMutex sync.Mutex markets types.MarketMap + + Src *ExchangeDataSource } func NewExchange(sourceName types.ExchangeName, sourceExchange types.Exchange, srv *service.BacktestService, config *bbgo.Backtest) (*Exchange, error) { @@ -75,15 +84,8 @@ func NewExchange(sourceName types.ExchangeName, sourceExchange types.Exchange, s return nil, err } - var startTime, endTime time.Time - startTime = config.StartTime.Time() - if config.EndTime != nil { - endTime = config.EndTime.Time() - } else { - endTime = time.Now() - } - - configAccount := config.Account[sourceName.String()] + startTime := config.StartTime.Time() + configAccount := config.GetAccount(sourceName.String()) account := &types.Account{ MakerFeeRate: configAccount.MakerFeeRate, @@ -101,8 +103,7 @@ func NewExchange(sourceName types.ExchangeName, sourceExchange types.Exchange, s srv: srv, config: config, account: account, - startTime: startTime, - endTime: endTime, + currentTime: startTime, closedOrders: make(map[string][]types.Order), trades: make(map[string][]types.Trade), } @@ -139,47 +140,62 @@ func (e *Exchange) addMatchingBook(symbol string, market types.Market) { } func (e *Exchange) _addMatchingBook(symbol string, market types.Market) { - e.matchingBooks[symbol] = &SimplePriceMatching{ - CurrentTime: e.startTime, - Account: e.account, - Market: market, + matching := &SimplePriceMatching{ + currentTime: e.currentTime, + account: e.account, + Market: market, + closedOrders: make(map[uint64]types.Order), + feeModeFunction: getFeeModeFunction(e.config.FeeMode), } + + e.matchingBooks[symbol] = matching } func (e *Exchange) NewStream() types.Stream { - return &Stream{exchange: e} + return &types.BacktestStream{ + StandardStreamEmitter: &types.StandardStream{}, + } } -func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) (createdOrders types.OrderSlice, err error) { - if e.userDataStream == nil { - return createdOrders, fmt.Errorf("SubmitOrders should be called after userDataStream been initialized") +func (e *Exchange) QueryOrder(ctx context.Context, q types.OrderQuery) (*types.Order, error) { + book := e.matchingBooks[q.Symbol] + oid, err := strconv.ParseUint(q.OrderID, 10, 64) + if err != nil { + return nil, err } - for _, order := range orders { - symbol := order.Symbol - matching, ok := e.matchingBook(symbol) - if !ok { - return nil, fmt.Errorf("matching engine is not initialized for symbol %s", symbol) - } - createdOrder, _, err := matching.PlaceOrder(order) - if err != nil { - return nil, err - } + order, ok := book.getOrder(oid) + if ok { + return &order, nil + } + return nil, nil +} + +func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (createdOrder *types.Order, err error) { + symbol := order.Symbol + matching, ok := e.matchingBook(symbol) + if !ok { + return nil, fmt.Errorf("matching engine is not initialized for symbol %s", symbol) + } - if createdOrder != nil { - createdOrders = append(createdOrders, *createdOrder) + if order.Quantity.Sign() < 0 { + return nil, ErrNegativeQuantity + } - // market order can be closed immediately. - switch createdOrder.Status { - case types.OrderStatusFilled, types.OrderStatusCanceled, types.OrderStatusRejected: - e.addClosedOrder(*createdOrder) - } + if order.Quantity.IsZero() { + return nil, ErrZeroQuantity + } - e.userDataStream.EmitOrderUpdate(*createdOrder) + createdOrder, _, err = matching.PlaceOrder(order) + if createdOrder != nil { + // market order can be closed immediately. + switch createdOrder.Status { + case types.OrderStatusFilled, types.OrderStatusCanceled, types.OrderStatusRejected: + e.addClosedOrder(*createdOrder) } } - return createdOrders, nil + return createdOrder, err } func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders []types.Order, err error) { @@ -201,20 +217,15 @@ func (e *Exchange) QueryClosedOrders(ctx context.Context, symbol string, since, } func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) error { - if e.userDataStream == nil { - return fmt.Errorf("CancelOrders should be called after userDataStream been initialized") - } for _, order := range orders { matching, ok := e.matchingBook(order.Symbol) if !ok { return fmt.Errorf("matching engine is not initialized for symbol %s", order.Symbol) } - canceledOrder, err := matching.CancelOrder(order) + _, err := matching.CancelOrder(order) if err != nil { return err } - - e.userDataStream.EmitOrderUpdate(canceledOrder) } return nil @@ -251,7 +262,7 @@ func (e *Exchange) QueryTicker(ctx context.Context, symbol string) (*types.Ticke return nil, fmt.Errorf("matching engine is not initialized for symbol %s", symbol) } - kline := matching.LastKLine + kline := matching.lastKLine return &types.Ticker{ Time: kline.EndTime.Time(), Volume: kline.Volume, @@ -281,11 +292,11 @@ func (e *Exchange) QueryMarkets(ctx context.Context) (types.MarketMap, error) { return e.markets, nil } -func (e Exchange) QueryDepositHistory(ctx context.Context, asset string, since, until time.Time) (allDeposits []types.Deposit, err error) { +func (e *Exchange) QueryDepositHistory(ctx context.Context, asset string, since, until time.Time) (allDeposits []types.Deposit, err error) { return nil, nil } -func (e Exchange) QueryWithdrawHistory(ctx context.Context, asset string, since, until time.Time) (allWithdraws []types.Withdraw, err error) { +func (e *Exchange) QueryWithdrawHistory(ctx context.Context, asset string, since, until time.Time) (allWithdraws []types.Withdraw, err error) { return nil, nil } @@ -296,39 +307,44 @@ func (e *Exchange) matchingBook(symbol string) (*SimplePriceMatching, bool) { return m, ok } -func (e *Exchange) InitMarketData() { - e.userDataStream.OnTradeUpdate(func(trade types.Trade) { +func (e *Exchange) BindUserData(userDataStream types.StandardStreamEmitter) { + userDataStream.OnTradeUpdate(func(trade types.Trade) { e.addTrade(trade) }) e.matchingBooksMutex.Lock() for _, matching := range e.matchingBooks { - matching.OnTradeUpdate(e.userDataStream.EmitTradeUpdate) - matching.OnOrderUpdate(e.userDataStream.EmitOrderUpdate) - matching.OnBalanceUpdate(e.userDataStream.EmitBalanceUpdate) + matching.OnTradeUpdate(userDataStream.EmitTradeUpdate) + matching.OnOrderUpdate(userDataStream.EmitOrderUpdate) + matching.OnBalanceUpdate(userDataStream.EmitBalanceUpdate) } e.matchingBooksMutex.Unlock() - } -func (e *Exchange) GetMarketData() (chan types.KLine, error) { +func (e *Exchange) SubscribeMarketData(startTime, endTime time.Time, requiredInterval types.Interval, extraIntervals ...types.Interval) (chan types.KLine, error) { log.Infof("collecting backtest configurations...") loadedSymbols := map[string]struct{}{} loadedIntervals := map[types.Interval]struct{}{ // 1m interval is required for the backtest matching engine - types.Interval1m: {}, - types.Interval1d: {}, + requiredInterval: {}, + } + + for _, it := range extraIntervals { + loadedIntervals[it] = struct{}{} } - for _, sub := range e.marketDataStream.Subscriptions { + + // collect subscriptions + for _, sub := range e.MarketDataStream.GetSubscriptions() { loadedSymbols[sub.Symbol] = struct{}{} switch sub.Channel { case types.KLineChannel: - loadedIntervals[types.Interval(sub.Options.Interval)] = struct{}{} + loadedIntervals[sub.Options.Interval] = struct{}{} default: - return nil, fmt.Errorf("stream channel %s is not supported in backtest", sub.Channel) + // Since Environment is not yet been injected at this point, no hard error + log.Errorf("stream channel %s is not supported in backtest", sub.Channel) } } @@ -344,7 +360,7 @@ func (e *Exchange) GetMarketData() (chan types.KLine, error) { log.Infof("using symbols: %v and intervals: %v for back-testing", symbols, intervals) log.Infof("querying klines from database...") - klineC, errC := e.srv.QueryKLinesCh(e.startTime, e.endTime, e, symbols, intervals) + klineC, errC := e.srv.QueryKLinesCh(startTime, endTime, e, symbols, intervals) go func() { if err := <-errC; err != nil { log.WithError(err).Error("backtest data feed error") @@ -353,23 +369,39 @@ func (e *Exchange) GetMarketData() (chan types.KLine, error) { return klineC, nil } -func (e *Exchange) ConsumeKLine(k types.KLine) { - if k.Interval == types.Interval1m { - matching, ok := e.matchingBook(k.Symbol) - if !ok { - log.Errorf("matching book of %s is not initialized", k.Symbol) - return - } +func (e *Exchange) ConsumeKLine(k types.KLine, requiredInterval types.Interval) { + matching, ok := e.matchingBook(k.Symbol) + if !ok { + log.Errorf("matching book of %s is not initialized", k.Symbol) + return + } + if matching.klineCache == nil { + matching.klineCache = make(map[types.Interval]types.KLine) + } + requiredKline, ok := matching.klineCache[k.Interval] + if ok { // pop out all the old + if requiredKline.Interval != requiredInterval { + panic(fmt.Sprintf("expect required kline interval %s, got interval %s", requiredInterval.String(), requiredKline.Interval.String())) + } + e.currentTime = requiredKline.EndTime.Time() // here we generate trades and order updates - matching.processKLine(k) + matching.processKLine(requiredKline) + matching.nextKLine = &k + for _, kline := range matching.klineCache { + e.MarketDataStream.EmitKLineClosed(kline) + for _, h := range e.Src.Callbacks { + h(kline, e.Src) + } + } + // reset the paramcache + matching.klineCache = make(map[types.Interval]types.KLine) } - - e.marketDataStream.EmitKLineClosed(k) + matching.klineCache[k.Interval] = k } func (e *Exchange) CloseMarketData() error { - if err := e.marketDataStream.Close(); err != nil { + if err := e.MarketDataStream.Close(); err != nil { log.WithError(err).Error("stream close error") return err } diff --git a/pkg/backtest/exchange_klinec.go b/pkg/backtest/exchange_klinec.go new file mode 100644 index 0000000000..4a3373a328 --- /dev/null +++ b/pkg/backtest/exchange_klinec.go @@ -0,0 +1,13 @@ +package backtest + +import ( + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/types" +) + +type ExchangeDataSource struct { + C chan types.KLine + Exchange *Exchange + Session *bbgo.ExchangeSession + Callbacks []func(types.KLine, *ExchangeDataSource) +} diff --git a/pkg/backtest/fee.go b/pkg/backtest/fee.go new file mode 100644 index 0000000000..34ed824850 --- /dev/null +++ b/pkg/backtest/fee.go @@ -0,0 +1,57 @@ +package backtest + +import ( + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +type FeeModeFunction func(order *types.Order, market *types.Market, feeRate fixedpoint.Value) (fee fixedpoint.Value, feeCurrency string) + +func feeModeFunctionToken(order *types.Order, _ *types.Market, feeRate fixedpoint.Value) (fee fixedpoint.Value, feeCurrency string) { + quoteQuantity := order.Quantity.Mul(order.Price) + feeCurrency = FeeToken + fee = quoteQuantity.Mul(feeRate) + return fee, feeCurrency +} + +func feeModeFunctionNative(order *types.Order, market *types.Market, feeRate fixedpoint.Value) (fee fixedpoint.Value, feeCurrency string) { + switch order.Side { + + case types.SideTypeBuy: + fee = order.Quantity.Mul(feeRate) + feeCurrency = market.BaseCurrency + + case types.SideTypeSell: + quoteQuantity := order.Quantity.Mul(order.Price) + fee = quoteQuantity.Mul(feeRate) + feeCurrency = market.QuoteCurrency + + } + + return fee, feeCurrency +} + +func feeModeFunctionQuote(order *types.Order, market *types.Market, feeRate fixedpoint.Value) (fee fixedpoint.Value, feeCurrency string) { + feeCurrency = market.QuoteCurrency + quoteQuantity := order.Quantity.Mul(order.Price) + fee = quoteQuantity.Mul(feeRate) + return fee, feeCurrency +} + +func getFeeModeFunction(feeMode bbgo.BacktestFeeMode) FeeModeFunction { + switch feeMode { + + case bbgo.BacktestFeeModeNative: + return feeModeFunctionNative + + case bbgo.BacktestFeeModeQuote: + return feeModeFunctionQuote + + case bbgo.BacktestFeeModeToken: + return feeModeFunctionToken + + default: + return feeModeFunctionQuote + } +} diff --git a/pkg/backtest/fee_test.go b/pkg/backtest/fee_test.go new file mode 100644 index 0000000000..375c4eb40f --- /dev/null +++ b/pkg/backtest/fee_test.go @@ -0,0 +1,124 @@ +package backtest + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +func Test_feeModeFunctionToken(t *testing.T) { + market := getTestMarket() + t.Run("sellOrder", func(t *testing.T) { + order := types.Order{ + SubmitOrder: types.SubmitOrder{ + Symbol: market.Symbol, + Side: types.SideTypeSell, + Type: types.OrderTypeLimit, + Quantity: fixedpoint.NewFromFloat(0.1), + Price: fixedpoint.NewFromFloat(20000.0), + TimeInForce: types.TimeInForceGTC, + }, + } + feeRate := fixedpoint.MustNewFromString("0.075%") + fee, feeCurrency := feeModeFunctionToken(&order, &market, feeRate) + assert.Equal(t, "1.5", fee.String()) + assert.Equal(t, "FEE", feeCurrency) + }) + + t.Run("buyOrder", func(t *testing.T) { + order := types.Order{ + SubmitOrder: types.SubmitOrder{ + Symbol: market.Symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeLimit, + Quantity: fixedpoint.NewFromFloat(0.1), + Price: fixedpoint.NewFromFloat(20000.0), + TimeInForce: types.TimeInForceGTC, + }, + } + + feeRate := fixedpoint.MustNewFromString("0.075%") + fee, feeCurrency := feeModeFunctionToken(&order, &market, feeRate) + assert.Equal(t, "1.5", fee.String()) + assert.Equal(t, "FEE", feeCurrency) + }) +} + +func Test_feeModeFunctionQuote(t *testing.T) { + market := getTestMarket() + t.Run("sellOrder", func(t *testing.T) { + order := types.Order{ + SubmitOrder: types.SubmitOrder{ + Symbol: market.Symbol, + Side: types.SideTypeSell, + Type: types.OrderTypeLimit, + Quantity: fixedpoint.NewFromFloat(0.1), + Price: fixedpoint.NewFromFloat(20000.0), + TimeInForce: types.TimeInForceGTC, + }, + } + feeRate := fixedpoint.MustNewFromString("0.075%") + fee, feeCurrency := feeModeFunctionQuote(&order, &market, feeRate) + assert.Equal(t, "1.5", fee.String()) + assert.Equal(t, "USDT", feeCurrency) + }) + + t.Run("buyOrder", func(t *testing.T) { + order := types.Order{ + SubmitOrder: types.SubmitOrder{ + Symbol: market.Symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeLimit, + Quantity: fixedpoint.NewFromFloat(0.1), + Price: fixedpoint.NewFromFloat(20000.0), + TimeInForce: types.TimeInForceGTC, + }, + } + + feeRate := fixedpoint.MustNewFromString("0.075%") + fee, feeCurrency := feeModeFunctionQuote(&order, &market, feeRate) + assert.Equal(t, "1.5", fee.String()) + assert.Equal(t, "USDT", feeCurrency) + }) +} + +func Test_feeModeFunctionNative(t *testing.T) { + market := getTestMarket() + t.Run("sellOrder", func(t *testing.T) { + order := types.Order{ + SubmitOrder: types.SubmitOrder{ + Symbol: market.Symbol, + Side: types.SideTypeSell, + Type: types.OrderTypeLimit, + Quantity: fixedpoint.NewFromFloat(0.1), + Price: fixedpoint.NewFromFloat(20000.0), + TimeInForce: types.TimeInForceGTC, + }, + } + feeRate := fixedpoint.MustNewFromString("0.075%") + fee, feeCurrency := feeModeFunctionNative(&order, &market, feeRate) + assert.Equal(t, "1.5", fee.String()) + assert.Equal(t, "USDT", feeCurrency) + }) + + t.Run("buyOrder", func(t *testing.T) { + order := types.Order{ + SubmitOrder: types.SubmitOrder{ + Symbol: market.Symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeLimit, + Quantity: fixedpoint.NewFromFloat(0.1), + Price: fixedpoint.NewFromFloat(20000.0), + TimeInForce: types.TimeInForceGTC, + }, + } + + feeRate := fixedpoint.MustNewFromString("0.075%") + fee, feeCurrency := feeModeFunctionNative(&order, &market, feeRate) + assert.Equal(t, "0.000075", fee.String()) + assert.Equal(t, "BTC", feeCurrency) + }) +} diff --git a/pkg/backtest/fixture_test.go b/pkg/backtest/fixture_test.go new file mode 100644 index 0000000000..d962b23403 --- /dev/null +++ b/pkg/backtest/fixture_test.go @@ -0,0 +1,93 @@ +package backtest + +import ( + "context" + "errors" + "math/rand" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +type KLineFixtureGenerator struct { + Symbol string + Interval types.Interval + StartTime, EndTime time.Time + StartPrice fixedpoint.Value +} + +func (g *KLineFixtureGenerator) Generate(ctx context.Context, c chan types.KLine) error { + defer close(c) + + startTime := g.StartTime + price := g.StartPrice + + if price.IsZero() { + return errors.New("startPrice can not be zero") + } + + for startTime.Before(g.EndTime) { + open := price + high := price.Mul(fixedpoint.NewFromFloat(1.01)) + low := price.Mul(fixedpoint.NewFromFloat(0.99)) + amp := high.Sub(low) + cls := low.Add(amp.Mul(fixedpoint.NewFromFloat(rand.Float64()))) + + vol := fixedpoint.NewFromFloat(rand.Float64() * 1000.0) + quoteVol := fixedpoint.NewFromFloat(rand.Float64() * 1000.0).Mul(price) + + nextStartTime := startTime.Add(g.Interval.Duration()) + k := types.KLine{ + Exchange: types.ExchangeBinance, + Symbol: g.Symbol, + StartTime: types.Time(startTime), + EndTime: types.Time(nextStartTime.Add(-time.Millisecond)), + Interval: g.Interval, + Open: open, + Close: cls, + High: high, + Low: low, + Volume: vol, + QuoteVolume: quoteVol, + Closed: true, + } + select { + case <-ctx.Done(): + return ctx.Err() + case c <- k: + } + price = cls + startTime = nextStartTime + } + + return nil +} + +func TestKLineFixtureGenerator(t *testing.T) { + startTime := time.Date(2022, time.January, 1, 0, 0, 0, 0, time.Local) + endTime := time.Date(2022, time.January, 31, 0, 0, 0, 0, time.Local) + ctx := context.Background() + g := &KLineFixtureGenerator{ + Symbol: "BTCUSDT", + Interval: types.Interval1m, + StartTime: startTime, + EndTime: endTime, + StartPrice: fixedpoint.NewFromFloat(18000.0), + } + + c := make(chan types.KLine, 20) + go func() { + err := g.Generate(ctx, c) + assert.NoError(t, err) + }() + for k := range c { + // high must higher than low + assert.True(t, k.High.Compare(k.Low) > 0) + assert.True(t, k.StartTime.After(startTime) || k.StartTime.Equal(startTime)) + assert.True(t, k.StartTime.Before(endTime)) + } +} diff --git a/pkg/backtest/manifests.go b/pkg/backtest/manifests.go new file mode 100644 index 0000000000..c457e91f5e --- /dev/null +++ b/pkg/backtest/manifests.go @@ -0,0 +1,47 @@ +package backtest + +import "encoding/json" + +type ManifestEntry struct { + Type string `json:"type"` + Filename string `json:"filename"` + StrategyID string `json:"strategyID"` + StrategyInstance string `json:"strategyInstance"` + StrategyProperty string `json:"strategyProperty"` +} + +type Manifests map[InstancePropertyIndex]string + +func (m *Manifests) UnmarshalJSON(j []byte) error { + var entries []ManifestEntry + if err := json.Unmarshal(j, &entries); err != nil { + return err + } + + mm := make(Manifests) + for _, entry := range entries { + index := InstancePropertyIndex{ + ID: entry.StrategyID, + InstanceID: entry.StrategyInstance, + Property: entry.StrategyProperty, + } + mm[index] = entry.Filename + } + *m = mm + return nil +} + +func (m Manifests) MarshalJSON() ([]byte, error) { + var arr []ManifestEntry + for k, v := range m { + arr = append(arr, ManifestEntry{ + Type: "strategyProperty", + Filename: v, + StrategyID: k.ID, + StrategyInstance: k.InstanceID, + StrategyProperty: k.Property, + }) + + } + return json.MarshalIndent(arr, "", " ") +} diff --git a/pkg/backtest/matching.go b/pkg/backtest/matching.go index f95a5fca48..57fe493a51 100644 --- a/pkg/backtest/matching.go +++ b/pkg/backtest/matching.go @@ -7,9 +7,11 @@ import ( "time" "github.com/pkg/errors" + "github.com/sirupsen/logrus" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/util" ) var orderID uint64 = 1 @@ -23,21 +25,48 @@ func incTradeID() uint64 { return atomic.AddUint64(&tradeID, 1) } +var klineMatchingLogger *logrus.Entry = nil + +// FeeToken is used to simulate the exchange platform fee token +// This is to ease the back-testing environment for closing positions. +const FeeToken = "FEE" + +var useFeeToken = true + +func init() { + logger := logrus.New() + if v, ok := util.GetEnvVarBool("DEBUG_MATCHING"); ok && v { + logger.SetLevel(logrus.DebugLevel) + } else { + logger.SetLevel(logrus.ErrorLevel) + } + klineMatchingLogger = logger.WithField("backtest", "klineEngine") + + if v, ok := util.GetEnvVarBool("BACKTEST_USE_FEE_TOKEN"); ok { + useFeeToken = v + } +} + // SimplePriceMatching implements a simple kline data driven matching engine for backtest //go:generate callbackgen -type SimplePriceMatching type SimplePriceMatching struct { Symbol string Market types.Market - mu sync.Mutex - bidOrders []types.Order - askOrders []types.Order + mu sync.Mutex + bidOrders []types.Order + askOrders []types.Order + closedOrders map[uint64]types.Order - LastPrice fixedpoint.Value - LastKLine types.KLine - CurrentTime time.Time + klineCache map[types.Interval]types.KLine + lastPrice fixedpoint.Value + lastKLine types.KLine + nextKLine *types.KLine + currentTime time.Time - Account *types.Account + feeModeFunction FeeModeFunction + + account *types.Account tradeUpdateCallbacks []func(trade types.Trade) orderUpdateCallbacks []func(order types.Order) @@ -74,7 +103,6 @@ func (m *SimplePriceMatching) CancelOrder(o types.Order) (types.Order, error) { } m.askOrders = orders m.mu.Unlock() - } if !found { @@ -83,37 +111,51 @@ func (m *SimplePriceMatching) CancelOrder(o types.Order) (types.Order, error) { switch o.Side { case types.SideTypeBuy: - if err := m.Account.UnlockBalance(m.Market.QuoteCurrency, o.Price.Mul(o.Quantity)); err != nil { + if err := m.account.UnlockBalance(m.Market.QuoteCurrency, o.Price.Mul(o.Quantity)); err != nil { return o, err } case types.SideTypeSell: - if err := m.Account.UnlockBalance(m.Market.BaseCurrency, o.Quantity); err != nil { + if err := m.account.UnlockBalance(m.Market.BaseCurrency, o.Quantity); err != nil { return o, err } } o.Status = types.OrderStatusCanceled m.EmitOrderUpdate(o) - m.EmitBalanceUpdate(m.Account.Balances()) + m.EmitBalanceUpdate(m.account.Balances()) return o, nil } -func (m *SimplePriceMatching) PlaceOrder(o types.SubmitOrder) (closedOrders *types.Order, trades *types.Trade, err error) { +// PlaceOrder returns the created order object, executed trade (if any) and error +func (m *SimplePriceMatching) PlaceOrder(o types.SubmitOrder) (*types.Order, *types.Trade, error) { + if o.Type == types.OrderTypeMarket { + if m.lastPrice.IsZero() { + panic("unexpected error: for market order, the last price can not be zero") + } + } + + isTaker := o.Type == types.OrderTypeMarket || isLimitTakerOrder(o, m.lastPrice) + // price for checking account balance, default price price := o.Price switch o.Type { case types.OrderTypeMarket: - if m.LastPrice.IsZero() { - panic("unexpected: last price can not be zero") - } + price = m.Market.TruncatePrice(m.lastPrice) + + case types.OrderTypeStopMarket: + // the actual price might be different. + o.StopPrice = m.Market.TruncatePrice(o.StopPrice) + price = o.StopPrice - price = m.LastPrice - case types.OrderTypeLimit, types.OrderTypeLimitMaker: + case types.OrderTypeLimit, types.OrderTypeStopLimit, types.OrderTypeLimitMaker: + o.Price = m.Market.TruncatePrice(o.Price) price = o.Price } + o.Quantity = m.Market.TruncateQuantity(o.Quantity) + if o.Quantity.Compare(m.Market.MinQuantity) < 0 { return nil, nil, fmt.Errorf("order quantity %s is less than minQuantity %s, order: %+v", o.Quantity.String(), m.Market.MinQuantity.String(), o) } @@ -125,40 +167,93 @@ func (m *SimplePriceMatching) PlaceOrder(o types.SubmitOrder) (closedOrders *typ switch o.Side { case types.SideTypeBuy: - if err := m.Account.LockBalance(m.Market.QuoteCurrency, quoteQuantity); err != nil { + if err := m.account.LockBalance(m.Market.QuoteCurrency, quoteQuantity); err != nil { return nil, nil, err } case types.SideTypeSell: - if err := m.Account.LockBalance(m.Market.BaseCurrency, o.Quantity); err != nil { + if err := m.account.LockBalance(m.Market.BaseCurrency, o.Quantity); err != nil { return nil, nil, err } } - m.EmitBalanceUpdate(m.Account.Balances()) + m.EmitBalanceUpdate(m.account.Balances()) // start from one orderID := incOrderID() order := m.newOrder(o, orderID) - if o.Type == types.OrderTypeMarket { + if isTaker { + var price fixedpoint.Value + if order.Type == types.OrderTypeMarket { + order.Price = m.Market.TruncatePrice(m.lastPrice) + price = order.Price + } else if order.Type == types.OrderTypeLimit { + // if limit order's price is with the range of next kline + // we assume it will be traded as a maker trade, and is traded at its original price + // TODO: if it is treated as a maker trade, fee should be specially handled + // otherwise, set NextKLine.Close(i.e., m.LastPrice) to be the taker traded price + if m.nextKLine != nil && m.nextKLine.High.Compare(order.Price) > 0 && order.Side == types.SideTypeBuy { + order.AveragePrice = order.Price + } else if m.nextKLine != nil && m.nextKLine.Low.Compare(order.Price) < 0 && order.Side == types.SideTypeSell { + order.AveragePrice = order.Price + } else { + order.AveragePrice = m.Market.TruncatePrice(m.lastPrice) + } + price = order.AveragePrice + } + + // emit the order update for Status:New m.EmitOrderUpdate(order) + // copy the order object to avoid side effect (for different callbacks) + var order2 = order + // emit trade before we publish order - trade := m.newTradeFromOrder(order, false) + trade := m.newTradeFromOrder(&order2, false, price) m.executeTrade(trade) + // unlock the rest balances for limit taker + if order.Type == types.OrderTypeLimit { + if order.AveragePrice.IsZero() { + return nil, nil, fmt.Errorf("the average price of the given limit taker order can not be zero") + } + + switch o.Side { + case types.SideTypeBuy: + // limit buy taker, the order price is higher than the current best ask price + // the executed price is lower than the given price, so we will use less quote currency to buy the base asset. + amount := order.Price.Sub(order.AveragePrice).Mul(order.Quantity) + if amount.Sign() > 0 { + if err := m.account.UnlockBalance(m.Market.QuoteCurrency, amount); err != nil { + return nil, nil, err + } + m.EmitBalanceUpdate(m.account.Balances()) + } + + case types.SideTypeSell: + // limit sell taker, the order price is lower than the current best bid price + // the executed price is higher than the given price, so we will get more quote currency back + amount := order.AveragePrice.Sub(order.Price).Mul(order.Quantity) + if amount.Sign() > 0 { + m.account.AddBalance(m.Market.QuoteCurrency, amount) + m.EmitBalanceUpdate(m.account.Balances()) + } + } + } + // update the order status - order.Status = types.OrderStatusFilled - order.ExecutedQuantity = order.Quantity - order.Price = price - order.IsWorking = false - m.EmitOrderUpdate(order) - return &order, &trade, nil + order2.Status = types.OrderStatusFilled + order2.ExecutedQuantity = order2.Quantity + order2.IsWorking = false + m.EmitOrderUpdate(order2) + + // let the exchange emit the "FILLED" order update (we need the closed order) + // m.EmitOrderUpdate(order2) + return &order2, &trade, nil } - // for limit maker orders - // TODO: handle limit taker order + // For limit maker orders (open status) switch o.Side { case types.SideTypeBuy: @@ -172,8 +267,7 @@ func (m *SimplePriceMatching) PlaceOrder(o types.SubmitOrder) (closedOrders *typ m.mu.Unlock() } - m.EmitOrderUpdate(order) - + m.EmitOrderUpdate(order) // emit order New status return &order, nil, nil } @@ -181,13 +275,31 @@ func (m *SimplePriceMatching) executeTrade(trade types.Trade) { var err error // execute trade, update account balances if trade.IsBuyer { - err = m.Account.UseLockedBalance(m.Market.QuoteCurrency, trade.Price.Mul(trade.Quantity)) + err = m.account.UseLockedBalance(m.Market.QuoteCurrency, trade.QuoteQuantity) + + // all-in buy trade, we can only deduct the fee from the quote quantity and re-calculate the base quantity + switch trade.FeeCurrency { + case m.Market.QuoteCurrency: + m.account.AddBalance(m.Market.QuoteCurrency, trade.Fee.Neg()) + m.account.AddBalance(m.Market.BaseCurrency, trade.Quantity) + case m.Market.BaseCurrency: + m.account.AddBalance(m.Market.BaseCurrency, trade.Quantity.Sub(trade.Fee)) + default: + m.account.AddBalance(m.Market.BaseCurrency, trade.Quantity) + } - m.Account.AddBalance(m.Market.BaseCurrency, trade.Quantity.Sub(trade.Fee.Div(trade.Price))) - } else { - err = m.Account.UseLockedBalance(m.Market.BaseCurrency, trade.Quantity) + } else { // sell trade + err = m.account.UseLockedBalance(m.Market.BaseCurrency, trade.Quantity) - m.Account.AddBalance(m.Market.QuoteCurrency, trade.Quantity.Mul(trade.Price).Sub(trade.Fee)) + switch trade.FeeCurrency { + case m.Market.QuoteCurrency: + m.account.AddBalance(m.Market.QuoteCurrency, trade.QuoteQuantity.Sub(trade.Fee)) + case m.Market.BaseCurrency: + m.account.AddBalance(m.Market.BaseCurrency, trade.Fee.Neg()) + m.account.AddBalance(m.Market.QuoteCurrency, trade.QuoteQuantity) + default: + m.account.AddBalance(m.Market.QuoteCurrency, trade.QuoteQuantity) + } } if err != nil { @@ -195,72 +307,114 @@ func (m *SimplePriceMatching) executeTrade(trade types.Trade) { } m.EmitTradeUpdate(trade) - m.EmitBalanceUpdate(m.Account.Balances()) - return + m.EmitBalanceUpdate(m.account.Balances()) } -func (m *SimplePriceMatching) newTradeFromOrder(order types.Order, isMaker bool) types.Trade { +func (m *SimplePriceMatching) getFeeRate(isMaker bool) (feeRate fixedpoint.Value) { // BINANCE uses 0.1% for both maker and taker // MAX uses 0.050% for maker and 0.15% for taker - var feeRate fixedpoint.Value if isMaker { - feeRate = m.Account.MakerFeeRate + feeRate = m.account.MakerFeeRate } else { - feeRate = m.Account.TakerFeeRate - } - - price := order.Price - switch order.Type { - case types.OrderTypeMarket, types.OrderTypeStopMarket: - if m.LastPrice.IsZero() { - panic("unexpected: last price can not be zero") - } - - price = m.LastPrice + feeRate = m.account.TakerFeeRate } + return feeRate +} +func (m *SimplePriceMatching) newTradeFromOrder(order *types.Order, isMaker bool, price fixedpoint.Value) types.Trade { + // BINANCE uses 0.1% for both maker and taker + // MAX uses 0.050% for maker and 0.15% for taker + var feeRate = m.getFeeRate(isMaker) + var quoteQuantity = order.Quantity.Mul(price) var fee fixedpoint.Value var feeCurrency string - switch order.Side { - - case types.SideTypeBuy: - fee = order.Quantity.Mul(feeRate) - feeCurrency = m.Market.BaseCurrency - - case types.SideTypeSell: - fee = order.Quantity.Mul(price).Mul(feeRate) - feeCurrency = m.Market.QuoteCurrency - + if m.feeModeFunction != nil { + fee, feeCurrency = m.feeModeFunction(order, &m.Market, feeRate) + } else { + fee, feeCurrency = feeModeFunctionQuote(order, &m.Market, feeRate) } + // update order time + order.UpdateTime = types.Time(m.currentTime) + var id = incTradeID() return types.Trade{ ID: id, OrderID: order.OrderID, - Exchange: "backtest", + Exchange: types.ExchangeBacktest, Price: price, Quantity: order.Quantity, - QuoteQuantity: order.Quantity.Mul(price), + QuoteQuantity: quoteQuantity, Symbol: order.Symbol, Side: order.Side, IsBuyer: order.Side == types.SideTypeBuy, IsMaker: isMaker, - Time: types.Time(m.CurrentTime), + Time: types.Time(m.currentTime), Fee: fee, FeeCurrency: feeCurrency, } } -func (m *SimplePriceMatching) BuyToPrice(price fixedpoint.Value) (closedOrders []types.Order, trades []types.Trade) { - var askOrders []types.Order +// buyToPrice means price go up and the limit sell should be triggered +func (m *SimplePriceMatching) buyToPrice(price fixedpoint.Value) (closedOrders []types.Order, trades []types.Trade) { + klineMatchingLogger.Debugf("kline buy to price %s", price.String()) + + var bidOrders []types.Order + for _, o := range m.bidOrders { + switch o.Type { + + case types.OrderTypeStopMarket: + // the price is still lower than the stop price, we will put the order back to the list + if price.Compare(o.StopPrice) < 0 { + // not triggering it, put it back + bidOrders = append(bidOrders, o) + break + } + + o.Type = types.OrderTypeMarket + o.ExecutedQuantity = o.Quantity + o.Price = price + o.Status = types.OrderStatusFilled + closedOrders = append(closedOrders, o) + case types.OrderTypeStopLimit: + // the price is still lower than the stop price, we will put the order back to the list + if price.Compare(o.StopPrice) < 0 { + bidOrders = append(bidOrders, o) + break + } + + // convert this order to limit order + // we use value object here, so it's a copy + o.Type = types.OrderTypeLimit + + // is it a taker order? + // higher than the current price, then it's a taker order + if o.Price.Compare(price) >= 0 { + // limit buy taker order, move it to the closed order + // we assume that we have no price slippage here, so the latest price will be the executed price + o.AveragePrice = price + o.ExecutedQuantity = o.Quantity + o.Status = types.OrderStatusFilled + closedOrders = append(closedOrders, o) + } else { + // keep it as a maker order + bidOrders = append(bidOrders, o) + } + default: + bidOrders = append(bidOrders, o) + } + } + m.bidOrders = bidOrders + + var askOrders []types.Order for _, o := range m.askOrders { switch o.Type { case types.OrderTypeStopMarket: // should we trigger the order - if price.Compare(o.StopPrice) <= 0 { + if price.Compare(o.StopPrice) < 0 { // not triggering it, put it back askOrders = append(askOrders, o) break @@ -274,7 +428,7 @@ func (m *SimplePriceMatching) BuyToPrice(price fixedpoint.Value) (closedOrders [ case types.OrderTypeStopLimit: // should we trigger the order? - if price.Compare(o.StopPrice) <= 0 { + if price.Compare(o.StopPrice) < 0 { askOrders = append(askOrders, o) break } @@ -282,10 +436,12 @@ func (m *SimplePriceMatching) BuyToPrice(price fixedpoint.Value) (closedOrders [ o.Type = types.OrderTypeLimit // is it a taker order? - if price.Compare(o.Price) >= 0 { - if o.Price.Compare(m.LastKLine.Low) < 0 { - o.Price = m.LastKLine.Low - } + // higher than the current price, then it's a taker order + if o.Price.Compare(price) <= 0 { + // limit sell order as taker, move it to the closed order + // we assume that we have no price slippage here, so the latest price will be the executed price + // TODO: simulate slippage here + o.AveragePrice = price o.ExecutedQuantity = o.Quantity o.Status = types.OrderStatusFilled closedOrders = append(closedOrders, o) @@ -296,9 +452,6 @@ func (m *SimplePriceMatching) BuyToPrice(price fixedpoint.Value) (closedOrders [ case types.OrderTypeLimit, types.OrderTypeLimitMaker: if price.Compare(o.Price) >= 0 { - if o.Price.Compare(m.LastKLine.Low) < 0 { - o.Price = m.LastKLine.Low - } o.ExecutedQuantity = o.Quantity o.Status = types.OrderStatusFilled closedOrders = append(closedOrders, o) @@ -313,61 +466,122 @@ func (m *SimplePriceMatching) BuyToPrice(price fixedpoint.Value) (closedOrders [ } m.askOrders = askOrders - m.LastPrice = price + m.lastPrice = price + + for i := range closedOrders { + o := closedOrders[i] + executedPrice := o.Price + if !o.AveragePrice.IsZero() { + executedPrice = o.AveragePrice + } - for _, o := range closedOrders { - trade := m.newTradeFromOrder(o, true) + trade := m.newTradeFromOrder(&o, !isTakerOrder(o), executedPrice) m.executeTrade(trade) + closedOrders[i] = o trades = append(trades, trade) m.EmitOrderUpdate(o) + + m.closedOrders[o.OrderID] = o } return closedOrders, trades } -func (m *SimplePriceMatching) SellToPrice(price fixedpoint.Value) (closedOrders []types.Order, trades []types.Trade) { - var sellPrice = price - var bidOrders []types.Order - for _, o := range m.bidOrders { +// sellToPrice simulates the price trend in down direction. +// When price goes down, buy orders should be executed, and the stop orders should be triggered. +func (m *SimplePriceMatching) sellToPrice(price fixedpoint.Value) (closedOrders []types.Order, trades []types.Trade) { + klineMatchingLogger.Debugf("kline sell to price %s", price.String()) + + // in this section we handle --- the price goes lower, and we trigger the stop sell + var askOrders []types.Order + for _, o := range m.askOrders { switch o.Type { case types.OrderTypeStopMarket: // should we trigger the order - if sellPrice.Compare(o.StopPrice) <= 0 { + if price.Compare(o.StopPrice) > 0 { + askOrders = append(askOrders, o) + break + } + + o.Type = types.OrderTypeMarket + o.ExecutedQuantity = o.Quantity + o.Price = price + o.Status = types.OrderStatusFilled + closedOrders = append(closedOrders, o) + + case types.OrderTypeStopLimit: + // if the price is lower than the stop price + // we should trigger the stop sell order + if price.Compare(o.StopPrice) > 0 { + askOrders = append(askOrders, o) + break + } + + o.Type = types.OrderTypeLimit + + // handle TAKER SELL + // if the order price is lower than the current price + // it's a taker order + if o.Price.Compare(price) <= 0 { + o.AveragePrice = price o.ExecutedQuantity = o.Quantity - o.Price = sellPrice o.Status = types.OrderStatusFilled closedOrders = append(closedOrders, o) } else { + askOrders = append(askOrders, o) + } + + default: + askOrders = append(askOrders, o) + } + } + m.askOrders = askOrders + + var bidOrders []types.Order + for _, o := range m.bidOrders { + switch o.Type { + + case types.OrderTypeStopMarket: + // price goes down and if the stop price is still lower than the current price + // or the stop price is not touched + // then we should skip this order + if price.Compare(o.StopPrice) > 0 { bidOrders = append(bidOrders, o) + break } + o.Type = types.OrderTypeMarket + o.ExecutedQuantity = o.Quantity + o.Price = price + o.Status = types.OrderStatusFilled + closedOrders = append(closedOrders, o) + case types.OrderTypeStopLimit: - // should we trigger the order - if sellPrice.Compare(o.StopPrice) <= 0 { - o.Type = types.OrderTypeLimit + // price goes down and if the stop price is still lower than the current price + // or the stop price is not touched + // then we should skip this order + if price.Compare(o.StopPrice) > 0 { + bidOrders = append(bidOrders, o) + break + } - if sellPrice.Compare(o.Price) <= 0 { - if o.Price.Compare(m.LastKLine.High) > 0 { - o.Price = m.LastKLine.High - } - o.ExecutedQuantity = o.Quantity - o.Status = types.OrderStatusFilled - closedOrders = append(closedOrders, o) - } else { - bidOrders = append(bidOrders, o) - } + o.Type = types.OrderTypeLimit + + // handle TAKER order + if o.Price.Compare(price) >= 0 { + o.AveragePrice = price + o.ExecutedQuantity = o.Quantity + o.Status = types.OrderStatusFilled + closedOrders = append(closedOrders, o) } else { bidOrders = append(bidOrders, o) } case types.OrderTypeLimit, types.OrderTypeLimitMaker: - if sellPrice.Compare(o.Price) <= 0 { - if o.Price.Compare(m.LastKLine.High) > 0 { - o.Price = m.LastKLine.High - } + if price.Compare(o.Price) <= 0 { o.ExecutedQuantity = o.Quantity o.Status = types.OrderStatusFilled closedOrders = append(closedOrders, o) @@ -381,54 +595,94 @@ func (m *SimplePriceMatching) SellToPrice(price fixedpoint.Value) (closedOrders } m.bidOrders = bidOrders - m.LastPrice = price + m.lastPrice = price + + for i := range closedOrders { + o := closedOrders[i] + executedPrice := o.Price + if !o.AveragePrice.IsZero() { + executedPrice = o.AveragePrice + } - for _, o := range closedOrders { - trade := m.newTradeFromOrder(o, true) + trade := m.newTradeFromOrder(&o, !isTakerOrder(o), executedPrice) m.executeTrade(trade) + closedOrders[i] = o trades = append(trades, trade) m.EmitOrderUpdate(o) + + m.closedOrders[o.OrderID] = o } return closedOrders, trades } +func (m *SimplePriceMatching) getOrder(orderID uint64) (types.Order, bool) { + if o, ok := m.closedOrders[orderID]; ok { + return o, true + } + + for _, o := range m.bidOrders { + if o.OrderID == orderID { + return o, true + } + } + + for _, o := range m.askOrders { + if o.OrderID == orderID { + return o, true + } + } + + return types.Order{}, false +} + func (m *SimplePriceMatching) processKLine(kline types.KLine) { - m.CurrentTime = kline.EndTime.Time() - m.LastKLine = kline + m.currentTime = kline.EndTime.Time() + + if m.lastPrice.IsZero() { + m.lastPrice = kline.Open + } else { + if m.lastPrice.Compare(kline.Open) > 0 { + m.sellToPrice(kline.Open) + } else { + m.buyToPrice(kline.Open) + } + } switch kline.Direction() { case types.DirectionDown: if kline.High.Compare(kline.Open) >= 0 { - m.BuyToPrice(kline.High) + m.buyToPrice(kline.High) } - if kline.Low.Compare(kline.Close) > 0 { - m.SellToPrice(kline.Low) - m.BuyToPrice(kline.Close) + // if low is lower than close, sell to low first, and then buy up to close + if kline.Low.Compare(kline.Close) < 0 { + m.sellToPrice(kline.Low) + m.buyToPrice(kline.Close) } else { - m.SellToPrice(kline.Close) + m.sellToPrice(kline.Close) } case types.DirectionUp: if kline.Low.Compare(kline.Open) <= 0 { - m.SellToPrice(kline.Low) + m.sellToPrice(kline.Low) } if kline.High.Compare(kline.Close) > 0 { - m.BuyToPrice(kline.High) - m.SellToPrice(kline.Close) + m.buyToPrice(kline.High) + m.sellToPrice(kline.Close) } else { - m.BuyToPrice(kline.Close) + m.buyToPrice(kline.Close) } default: // no trade up or down - if m.LastPrice.IsZero() { - m.BuyToPrice(kline.Close) + if m.lastPrice.IsZero() { + m.buyToPrice(kline.Close) } - } + + m.lastKLine = kline } func (m *SimplePriceMatching) newOrder(o types.SubmitOrder, orderID uint64) types.Order { @@ -439,7 +693,32 @@ func (m *SimplePriceMatching) newOrder(o types.SubmitOrder, orderID uint64) type Status: types.OrderStatusNew, ExecutedQuantity: fixedpoint.Zero, IsWorking: true, - CreationTime: types.Time(m.CurrentTime), - UpdateTime: types.Time(m.CurrentTime), + CreationTime: types.Time(m.currentTime), + UpdateTime: types.Time(m.currentTime), + } +} + +func isTakerOrder(o types.Order) bool { + if o.AveragePrice.IsZero() { + return false } + + switch o.Side { + case types.SideTypeBuy: + return o.AveragePrice.Compare(o.Price) < 0 + + case types.SideTypeSell: + return o.AveragePrice.Compare(o.Price) > 0 + + } + return false +} + +func isLimitTakerOrder(o types.SubmitOrder, currentPrice fixedpoint.Value) bool { + if currentPrice.IsZero() { + return false + } + + return o.Type == types.OrderTypeLimit && ((o.Side == types.SideTypeBuy && o.Price.Compare(currentPrice) >= 0) || + (o.Side == types.SideTypeSell && o.Price.Compare(currentPrice) <= 0)) } diff --git a/pkg/backtest/matching_test.go b/pkg/backtest/matching_test.go index afa1cbb71d..4b6432805b 100644 --- a/pkg/backtest/matching_test.go +++ b/pkg/backtest/matching_test.go @@ -21,17 +21,170 @@ func newLimitOrder(symbol string, side types.SideType, price, quantity float64) } } -func TestSimplePriceMatching_LimitOrder(t *testing.T) { +func TestSimplePriceMatching_orderUpdate(t *testing.T) { account := &types.Account{ MakerFeeRate: fixedpoint.NewFromFloat(0.075 * 0.01), TakerFeeRate: fixedpoint.NewFromFloat(0.075 * 0.01), } - account.UpdateBalances(types.BalanceMap{ - "USDT": {Currency: "USDT", Available: fixedpoint.NewFromFloat(1000000.0)}, - "BTC": {Currency: "BTC", Available: fixedpoint.NewFromFloat(100.0)}, + "USDT": {Currency: "USDT", Available: fixedpoint.NewFromFloat(10000.0)}, }) + market := types.Market{ + Symbol: "BTCUSDT", + PricePrecision: 8, + VolumePrecision: 8, + QuoteCurrency: "USDT", + BaseCurrency: "BTC", + MinNotional: fixedpoint.MustNewFromString("0.001"), + MinAmount: fixedpoint.MustNewFromString("10.0"), + MinQuantity: fixedpoint.MustNewFromString("0.001"), + StepSize: fixedpoint.MustNewFromString("0.00001"), + TickSize: fixedpoint.MustNewFromString("0.01"), + } + + t1 := time.Date(2021, 7, 1, 0, 0, 0, 0, time.UTC) + engine := &SimplePriceMatching{ + account: account, + Market: market, + currentTime: t1, + closedOrders: make(map[uint64]types.Order), + lastPrice: fixedpoint.NewFromFloat(25000), + } + + orderUpdateCnt := 0 + orderUpdateNewStatusCnt := 0 + orderUpdateFilledStatusCnt := 0 + var lastOrder types.Order + engine.OnOrderUpdate(func(order types.Order) { + lastOrder = order + orderUpdateCnt++ + switch order.Status { + case types.OrderStatusNew: + orderUpdateNewStatusCnt++ + + case types.OrderStatusFilled: + orderUpdateFilledStatusCnt++ + + } + }) + + // maker order + _, _, err := engine.PlaceOrder(newLimitOrder("BTCUSDT", types.SideTypeBuy, 24000.0, 0.1)) + assert.NoError(t, err) + assert.Equal(t, 1, orderUpdateCnt) // should get new status + assert.Equal(t, 1, orderUpdateNewStatusCnt) // should get new status + assert.Equal(t, 0, orderUpdateFilledStatusCnt) // should get new status + assert.Equal(t, types.OrderStatusNew, lastOrder.Status) + assert.Equal(t, fixedpoint.NewFromFloat(0.0), lastOrder.ExecutedQuantity) + + t2 := t1.Add(time.Minute) + + // should match 25000, 24000 + k := newKLine("BTCUSDT", types.Interval1m, t2, 26000, 27000, 23000, 25000) + engine.processKLine(k) + + assert.Equal(t, 2, orderUpdateCnt) // should got new and filled + assert.Equal(t, 1, orderUpdateNewStatusCnt) // should got new status + assert.Equal(t, 1, orderUpdateFilledStatusCnt) // should got new status + assert.Equal(t, types.OrderStatusFilled, lastOrder.Status) + assert.Equal(t, "0.1", lastOrder.ExecutedQuantity.String()) + assert.Equal(t, lastOrder.Quantity.String(), lastOrder.ExecutedQuantity.String()) +} + +func TestSimplePriceMatching_CancelOrder(t *testing.T) { + account := getTestAccount() + market := getTestMarket() + t1 := time.Date(2021, 7, 1, 0, 0, 0, 0, time.UTC) + engine := &SimplePriceMatching{ + account: account, + Market: market, + currentTime: t1, + closedOrders: make(map[uint64]types.Order), + lastPrice: fixedpoint.NewFromFloat(30000.0), + } + + createdOrder1, trade1, err := engine.PlaceOrder(newLimitOrder("BTCUSDT", types.SideTypeBuy, 20000.0, 0.1)) + assert.NoError(t, err) + assert.Nil(t, trade1) + assert.Len(t, engine.bidOrders, 1) + assert.Len(t, engine.askOrders, 0) + + createdOrder2, trade2, err := engine.PlaceOrder(newLimitOrder("BTCUSDT", types.SideTypeSell, 40000.0, 0.1)) + assert.NoError(t, err) + assert.Nil(t, trade2) + assert.Len(t, engine.bidOrders, 1) + assert.Len(t, engine.askOrders, 1) + + if assert.NotNil(t, createdOrder1) { + retOrder, err := engine.CancelOrder(*createdOrder1) + assert.NoError(t, err) + assert.NotNil(t, retOrder) + assert.Len(t, engine.bidOrders, 0) + assert.Len(t, engine.askOrders, 1) + } + + if assert.NotNil(t, createdOrder2) { + retOrder, err := engine.CancelOrder(*createdOrder2) + assert.NoError(t, err) + assert.NotNil(t, retOrder) + assert.Len(t, engine.bidOrders, 0) + assert.Len(t, engine.askOrders, 0) + } +} + +func TestSimplePriceMatching_processKLine(t *testing.T) { + account := getTestAccount() + market := getTestMarket() + + t1 := time.Date(2021, 7, 1, 0, 0, 0, 0, time.UTC) + engine := &SimplePriceMatching{ + account: account, + Market: market, + currentTime: t1, + closedOrders: make(map[uint64]types.Order), + lastPrice: fixedpoint.NewFromFloat(30000.0), + } + + for i := 0; i <= 5; i++ { + var p = 20000.0 + float64(i)*1000.0 + _, _, err := engine.PlaceOrder(newLimitOrder("BTCUSDT", types.SideTypeBuy, p, 0.001)) + assert.NoError(t, err) + } + + t2 := t1.Add(time.Minute) + + // should match 25000, 24000 + k := newKLine("BTCUSDT", types.Interval1m, t2, 30000, 27000, 23000, 25000) + assert.Equal(t, t2.Add(time.Minute-time.Millisecond), k.EndTime.Time()) + + engine.processKLine(k) + assert.Equal(t, 3, len(engine.bidOrders)) + assert.Len(t, engine.bidOrders, 3) + assert.Equal(t, 3, len(engine.closedOrders)) + + for _, o := range engine.closedOrders { + assert.Equal(t, k.EndTime.Time(), o.UpdateTime.Time()) + } +} + +func newKLine(symbol string, interval types.Interval, startTime time.Time, o, h, l, c float64) types.KLine { + return types.KLine{ + Symbol: symbol, + StartTime: types.Time(startTime), + EndTime: types.Time(startTime.Add(interval.Duration() - time.Millisecond)), + Interval: interval, + Open: fixedpoint.NewFromFloat(o), + High: fixedpoint.NewFromFloat(h), + Low: fixedpoint.NewFromFloat(l), + Close: fixedpoint.NewFromFloat(c), + Closed: true, + } +} + +// getTestMarket returns the BTCUSDT market information +// for tests, we always use BTCUSDT +func getTestMarket() types.Market { market := types.Market{ Symbol: "BTCUSDT", PricePrecision: 8, @@ -41,12 +194,256 @@ func TestSimplePriceMatching_LimitOrder(t *testing.T) { MinNotional: fixedpoint.MustNewFromString("0.001"), MinAmount: fixedpoint.MustNewFromString("10.0"), MinQuantity: fixedpoint.MustNewFromString("0.001"), + StepSize: fixedpoint.MustNewFromString("0.00001"), + TickSize: fixedpoint.MustNewFromString("0.01"), } + return market +} +func getTestAccount() *types.Account { + account := &types.Account{ + MakerFeeRate: fixedpoint.NewFromFloat(0.075 * 0.01), + TakerFeeRate: fixedpoint.NewFromFloat(0.075 * 0.01), + } + account.UpdateBalances(types.BalanceMap{ + "USDT": {Currency: "USDT", Available: fixedpoint.NewFromFloat(1000000.0)}, + "BTC": {Currency: "BTC", Available: fixedpoint.NewFromFloat(100.0)}, + }) + return account +} + +func TestSimplePriceMatching_LimitBuyTakerOrder(t *testing.T) { + account := getTestAccount() + market := getTestMarket() + engine := &SimplePriceMatching{ + account: account, + Market: market, + closedOrders: make(map[uint64]types.Order), + lastPrice: fixedpoint.NewFromFloat(19000.0), + } + + takerOrder := types.SubmitOrder{ + Symbol: market.Symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeLimit, + Quantity: fixedpoint.NewFromFloat(0.1), + Price: fixedpoint.NewFromFloat(20000.0), + TimeInForce: types.TimeInForceGTC, + } + createdOrder, trade, err := engine.PlaceOrder(takerOrder) + assert.NoError(t, err) + t.Logf("created order: %+v", createdOrder) + t.Logf("executed trade: %+v", trade) + + assert.Equal(t, "19000", trade.Price.String()) + assert.Equal(t, "19000", createdOrder.AveragePrice.String()) + assert.Equal(t, "20000", createdOrder.Price.String()) + + usdt, ok := account.Balance("USDT") + assert.True(t, ok) + assert.True(t, usdt.Locked.IsZero()) + + btc, ok := account.Balance("BTC") + assert.True(t, ok) + assert.True(t, btc.Locked.IsZero()) + assert.Equal(t, fixedpoint.NewFromFloat(100.0).Add(createdOrder.Quantity).String(), btc.Available.String()) + + usedQuoteAmount := createdOrder.AveragePrice.Mul(createdOrder.Quantity) + assert.Equal(t, "USDT", trade.FeeCurrency) + assert.Equal(t, usdt.Available.String(), fixedpoint.NewFromFloat(1000000.0).Sub(usedQuoteAmount).Sub(trade.Fee).String()) +} + +func TestSimplePriceMatching_StopLimitOrderBuy(t *testing.T) { + account := getTestAccount() + market := getTestMarket() + engine := &SimplePriceMatching{ + account: account, + Market: market, + closedOrders: make(map[uint64]types.Order), + lastPrice: fixedpoint.NewFromFloat(19000.0), + } + + stopBuyOrder := types.SubmitOrder{ + Symbol: market.Symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeStopLimit, + Quantity: fixedpoint.NewFromFloat(0.1), + Price: fixedpoint.NewFromFloat(22000.0), + StopPrice: fixedpoint.NewFromFloat(21000.0), + TimeInForce: types.TimeInForceGTC, + } + createdOrder, trade, err := engine.PlaceOrder(stopBuyOrder) + assert.NoError(t, err) + assert.Nil(t, trade, "place stop order should not trigger the stop buy") + assert.NotNil(t, createdOrder, "place stop order should not trigger the stop buy") + + // place some limit orders, so we ensure that the remaining orders are not removed. + _, _, err = engine.PlaceOrder(newLimitOrder(market.Symbol, types.SideTypeBuy, 18000, 0.01)) + assert.NoError(t, err) + _, _, err = engine.PlaceOrder(newLimitOrder(market.Symbol, types.SideTypeSell, 32000, 0.01)) + assert.NoError(t, err) + assert.Equal(t, 2, len(engine.bidOrders)) + assert.Equal(t, 1, len(engine.askOrders)) + + closedOrders, trades := engine.buyToPrice(fixedpoint.NewFromFloat(20000.0)) + assert.Len(t, closedOrders, 0, "price change far from the price should not trigger the stop buy") + assert.Len(t, trades, 0, "price change far from the price should not trigger the stop buy") + assert.Equal(t, 2, len(engine.bidOrders), "bid orders should be the same") + assert.Equal(t, 1, len(engine.askOrders), "ask orders should be the same") + + closedOrders, trades = engine.buyToPrice(fixedpoint.NewFromFloat(21001.0)) + assert.Len(t, closedOrders, 1, "should trigger the stop buy order") + assert.Len(t, trades, 1, "should have stop order trade executed") + + assert.Equal(t, types.OrderStatusFilled, closedOrders[0].Status) + assert.Equal(t, types.OrderTypeLimit, closedOrders[0].Type) + assert.Equal(t, "21001", trades[0].Price.String()) + assert.Equal(t, "22000", closedOrders[0].Price.String(), "order.Price should not be adjusted") + + assert.Equal(t, fixedpoint.NewFromFloat(21001.0).String(), engine.lastPrice.String()) + + stopOrder2 := types.SubmitOrder{ + Symbol: market.Symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeStopLimit, + Quantity: fixedpoint.NewFromFloat(0.1), + Price: fixedpoint.NewFromFloat(22000.0), + StopPrice: fixedpoint.NewFromFloat(21000.0), + TimeInForce: types.TimeInForceGTC, + } + createdOrder, trade, err = engine.PlaceOrder(stopOrder2) + assert.NoError(t, err) + assert.Nil(t, trade, "place stop order should not trigger the stop buy") + assert.NotNil(t, createdOrder, "place stop order should not trigger the stop buy") + assert.Len(t, engine.bidOrders, 2) + + closedOrders, trades = engine.sellToPrice(fixedpoint.NewFromFloat(20500.0)) + assert.Len(t, closedOrders, 1, "should trigger the stop buy order") + assert.Len(t, trades, 1, "should have stop order trade executed") + assert.Len(t, engine.bidOrders, 1, "should left one bid order") +} + +func TestSimplePriceMatching_StopLimitOrderSell(t *testing.T) { + account := getTestAccount() + market := getTestMarket() + engine := &SimplePriceMatching{ + account: account, + Market: market, + closedOrders: make(map[uint64]types.Order), + lastPrice: fixedpoint.NewFromFloat(22000.0), + } + + stopSellOrder := types.SubmitOrder{ + Symbol: market.Symbol, + Side: types.SideTypeSell, + Type: types.OrderTypeStopLimit, + Quantity: fixedpoint.NewFromFloat(0.1), + Price: fixedpoint.NewFromFloat(20000.0), + StopPrice: fixedpoint.NewFromFloat(21000.0), + TimeInForce: types.TimeInForceGTC, + } + createdOrder, trade, err := engine.PlaceOrder(stopSellOrder) + assert.NoError(t, err) + assert.Nil(t, trade, "place stop order should not trigger the stop sell") + assert.NotNil(t, createdOrder, "place stop order should not trigger the stop sell") + + // place some limit orders, so we ensure that the remaining orders are not removed. + _, _, err = engine.PlaceOrder(newLimitOrder(market.Symbol, types.SideTypeBuy, 18000, 0.01)) + assert.NoError(t, err) + _, _, err = engine.PlaceOrder(newLimitOrder(market.Symbol, types.SideTypeSell, 32000, 0.01)) + assert.NoError(t, err) + assert.Equal(t, 1, len(engine.bidOrders)) + assert.Equal(t, 2, len(engine.askOrders)) + + closedOrders, trades := engine.sellToPrice(fixedpoint.NewFromFloat(21500.0)) + assert.Len(t, closedOrders, 0, "price change far from the price should not trigger the stop buy") + assert.Len(t, trades, 0, "price change far from the price should not trigger the stop buy") + assert.Equal(t, 1, len(engine.bidOrders)) + assert.Equal(t, 2, len(engine.askOrders)) + + closedOrders, trades = engine.sellToPrice(fixedpoint.NewFromFloat(20990.0)) + assert.Len(t, closedOrders, 1, "should trigger the stop sell order") + assert.Len(t, trades, 1, "should have stop order trade executed") + assert.Equal(t, 1, len(engine.bidOrders)) + assert.Equal(t, 1, len(engine.askOrders)) + + assert.Equal(t, types.OrderStatusFilled, closedOrders[0].Status) + assert.Equal(t, types.OrderTypeLimit, closedOrders[0].Type) + assert.Equal(t, "20000", closedOrders[0].Price.String(), "limit order price should not be changed") + assert.Equal(t, "20990", trades[0].Price.String()) + assert.Equal(t, "20990", engine.lastPrice.String()) + + // place a stop limit sell order with a higher price than the current price + stopOrder2 := types.SubmitOrder{ + Symbol: market.Symbol, + Side: types.SideTypeSell, + Type: types.OrderTypeStopLimit, + Quantity: fixedpoint.NewFromFloat(0.1), + Price: fixedpoint.NewFromFloat(20000.0), + StopPrice: fixedpoint.NewFromFloat(21000.0), + TimeInForce: types.TimeInForceGTC, + } + + createdOrder, trade, err = engine.PlaceOrder(stopOrder2) + assert.NoError(t, err) + assert.Nil(t, trade, "place stop order should not trigger the stop sell") + assert.NotNil(t, createdOrder, "place stop order should not trigger the stop sell") + + closedOrders, trades = engine.buyToPrice(fixedpoint.NewFromFloat(21000.0)) + if assert.Len(t, closedOrders, 1, "should trigger the stop sell order") { + assert.Len(t, trades, 1, "should have stop order trade executed") + assert.Equal(t, types.SideTypeSell, closedOrders[0].Side) + assert.Equal(t, types.OrderStatusFilled, closedOrders[0].Status) + assert.Equal(t, types.OrderTypeLimit, closedOrders[0].Type) + assert.Equal(t, "21000", trades[0].Price.String(), "trade price should be the kline price not the order price") + assert.Equal(t, "21000", engine.lastPrice.String(), "engine last price should be updated correctly") + } +} + +func TestSimplePriceMatching_StopMarketOrderSell(t *testing.T) { + account := getTestAccount() + market := getTestMarket() + engine := &SimplePriceMatching{ + account: account, + Market: market, + closedOrders: make(map[uint64]types.Order), + lastPrice: fixedpoint.NewFromFloat(22000.0), + } + + stopOrder := types.SubmitOrder{ + Symbol: market.Symbol, + Side: types.SideTypeSell, + Type: types.OrderTypeStopMarket, + Quantity: fixedpoint.NewFromFloat(0.1), + Price: fixedpoint.NewFromFloat(20000.0), + StopPrice: fixedpoint.NewFromFloat(21000.0), + TimeInForce: types.TimeInForceGTC, + } + createdOrder, trade, err := engine.PlaceOrder(stopOrder) + assert.NoError(t, err) + assert.Nil(t, trade, "place stop order should not trigger the stop sell") + assert.NotNil(t, createdOrder, "place stop order should not trigger the stop sell") + + closedOrders, trades := engine.sellToPrice(fixedpoint.NewFromFloat(21500.0)) + assert.Len(t, closedOrders, 0, "price change far from the price should not trigger the stop buy") + assert.Len(t, trades, 0, "price change far from the price should not trigger the stop buy") + + closedOrders, trades = engine.sellToPrice(fixedpoint.NewFromFloat(20990.0)) + assert.Len(t, closedOrders, 1, "should trigger the stop sell order") + assert.Len(t, trades, 1, "should have stop order trade executed") + + assert.Equal(t, types.OrderStatusFilled, closedOrders[0].Status) + assert.Equal(t, types.OrderTypeMarket, closedOrders[0].Type) + assert.Equal(t, fixedpoint.NewFromFloat(20990.0), trades[0].Price, "trade price should be adjusted to the last price") +} + +func TestSimplePriceMatching_PlaceLimitOrder(t *testing.T) { + account := getTestAccount() + market := getTestMarket() engine := &SimplePriceMatching{ - CurrentTime: time.Now(), - Account: account, - Market: market, + account: account, + Market: market, + closedOrders: make(map[uint64]types.Order), } for i := 0; i < 5; i++ { @@ -63,11 +460,11 @@ func TestSimplePriceMatching_LimitOrder(t *testing.T) { assert.Len(t, engine.bidOrders, 5) assert.Len(t, engine.askOrders, 5) - closedOrders, trades := engine.SellToPrice(fixedpoint.NewFromFloat(8100.0)) + closedOrders, trades := engine.sellToPrice(fixedpoint.NewFromFloat(8100.0)) assert.Len(t, closedOrders, 0) assert.Len(t, trades, 0) - closedOrders, trades = engine.SellToPrice(fixedpoint.NewFromFloat(8000.0)) + closedOrders, trades = engine.sellToPrice(fixedpoint.NewFromFloat(8000.0)) assert.Len(t, closedOrders, 1) assert.Len(t, trades, 1) for _, trade := range trades { @@ -78,15 +475,15 @@ func TestSimplePriceMatching_LimitOrder(t *testing.T) { assert.Equal(t, types.SideTypeBuy, o.Side) } - closedOrders, trades = engine.SellToPrice(fixedpoint.NewFromFloat(7000.0)) + closedOrders, trades = engine.sellToPrice(fixedpoint.NewFromFloat(7000.0)) assert.Len(t, closedOrders, 4) assert.Len(t, trades, 4) - closedOrders, trades = engine.BuyToPrice(fixedpoint.NewFromFloat(8900.0)) + closedOrders, trades = engine.buyToPrice(fixedpoint.NewFromFloat(8900.0)) assert.Len(t, closedOrders, 0) assert.Len(t, trades, 0) - closedOrders, trades = engine.BuyToPrice(fixedpoint.NewFromFloat(9000.0)) + closedOrders, trades = engine.buyToPrice(fixedpoint.NewFromFloat(9000.0)) assert.Len(t, closedOrders, 1) assert.Len(t, trades, 1) for _, o := range closedOrders { @@ -96,7 +493,37 @@ func TestSimplePriceMatching_LimitOrder(t *testing.T) { assert.Equal(t, types.SideTypeSell, trade.Side) } - closedOrders, trades = engine.BuyToPrice(fixedpoint.NewFromFloat(9500.0)) + closedOrders, trades = engine.buyToPrice(fixedpoint.NewFromFloat(9500.0)) assert.Len(t, closedOrders, 4) assert.Len(t, trades, 4) } + +func TestSimplePriceMatching_LimitTakerOrder(t *testing.T) { + account := getTestAccount() + market := getTestMarket() + engine := &SimplePriceMatching{ + account: account, + Market: market, + closedOrders: make(map[uint64]types.Order), + lastPrice: fixedpoint.NewFromFloat(20000.0), + } + + closedOrder, trade, err := engine.PlaceOrder(newLimitOrder("BTCUSDT", types.SideTypeBuy, 21000.0, 1.0)) + assert.NoError(t, err) + if assert.NotNil(t, closedOrder) { + if assert.NotNil(t, trade) { + assert.Equal(t, "20000", trade.Price.String()) + assert.False(t, trade.IsMaker, "should be taker") + } + } + + closedOrder, trade, err = engine.PlaceOrder(newLimitOrder("BTCUSDT", types.SideTypeSell, 19000.0, 1.0)) + assert.NoError(t, err) + if assert.NotNil(t, closedOrder) { + assert.Equal(t, "19000", closedOrder.Price.String()) + if assert.NotNil(t, trade) { + assert.Equal(t, "20000", trade.Price.String()) + assert.False(t, trade.IsMaker, "should be taker") + } + } +} diff --git a/pkg/backtest/recorder.go b/pkg/backtest/recorder.go new file mode 100644 index 0000000000..f405840c84 --- /dev/null +++ b/pkg/backtest/recorder.go @@ -0,0 +1,156 @@ +package backtest + +import ( + "fmt" + "path/filepath" + "reflect" + "strings" + + "go.uber.org/multierr" + + "github.com/c9s/bbgo/pkg/data/tsv" + "github.com/c9s/bbgo/pkg/types" +) + +type Instance interface { + ID() string + InstanceID() string +} + +type InstancePropertyIndex struct { + ID string + InstanceID string + Property string +} + +type StateRecorder struct { + outputDirectory string + strategies []Instance + writers map[types.CsvFormatter]*tsv.Writer + lastLines map[types.CsvFormatter][]string + manifests Manifests +} + +func NewStateRecorder(outputDir string) *StateRecorder { + return &StateRecorder{ + outputDirectory: outputDir, + writers: make(map[types.CsvFormatter]*tsv.Writer), + lastLines: make(map[types.CsvFormatter][]string), + manifests: make(Manifests), + } +} + +func (r *StateRecorder) Snapshot() (int, error) { + var c int + for obj, writer := range r.writers { + records := obj.CsvRecords() + lastLine, hasLastLine := r.lastLines[obj] + + for _, record := range records { + if hasLastLine && equalStringSlice(lastLine, record) { + continue + } + + if err := writer.Write(record); err != nil { + return c, err + } + c++ + r.lastLines[obj] = record + } + + writer.Flush() + } + return c, nil +} + +func (r *StateRecorder) Scan(instance Instance) error { + r.strategies = append(r.strategies, instance) + + rt := reflect.TypeOf(instance) + rv := reflect.ValueOf(instance) + if rt.Kind() == reflect.Ptr { + rt = rt.Elem() + rv = rv.Elem() + } + + if rt.Kind() != reflect.Struct { + return fmt.Errorf("given object is not a struct: %+v", rt) + } + + for i := 0; i < rt.NumField(); i++ { + structField := rt.Field(i) + if !structField.IsExported() { + continue + } + + obj := rv.Field(i).Interface() + switch o := obj.(type) { + + case types.CsvFormatter: // interface type + typeName := strings.ToLower(structField.Type.Elem().Name()) + if typeName == "" { + return fmt.Errorf("%v is a non-defined type", structField.Type) + } + + if err := r.newCsvWriter(o, instance, typeName); err != nil { + return err + } + } + } + + return nil +} + +func (r *StateRecorder) formatCsvFilename(instance Instance, objType string) string { + return filepath.Join(r.outputDirectory, fmt.Sprintf("%s-%s.tsv", instance.InstanceID(), objType)) +} + +func (r *StateRecorder) Manifests() Manifests { + return r.manifests +} + +func (r *StateRecorder) newCsvWriter(o types.CsvFormatter, instance Instance, typeName string) error { + fn := r.formatCsvFilename(instance, typeName) + w, err := tsv.NewWriterFile(fn) + if err != nil { + return err + } + + r.manifests[InstancePropertyIndex{ + ID: instance.ID(), + InstanceID: instance.InstanceID(), + Property: typeName, + }] = fn + + r.writers[o] = w + return w.Write(o.CsvHeader()) +} + +func (r *StateRecorder) Close() error { + var err error + + for _, w := range r.writers { + err2 := w.Close() + if err2 != nil { + err = multierr.Append(err, err2) + } + } + + return err +} + +func equalStringSlice(a, b []string) bool { + if len(a) != len(b) { + return false + } + + for i := 0; i < len(a); i++ { + ad := a[i] + bd := b[i] + if ad != bd { + return false + } + } + + return true +} diff --git a/pkg/backtest/recorder_test.go b/pkg/backtest/recorder_test.go new file mode 100644 index 0000000000..3b6348d8ef --- /dev/null +++ b/pkg/backtest/recorder_test.go @@ -0,0 +1,61 @@ +package backtest + +import ( + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +type testStrategy struct { + Symbol string + + Position *types.Position +} + +func (s *testStrategy) ID() string { return "my-test" } +func (s *testStrategy) InstanceID() string { return "my-test:" + s.Symbol } + +func TestStateRecorder(t *testing.T) { + tmpDir, _ := os.MkdirTemp(os.TempDir(), "bbgo") + t.Logf("tmpDir: %s", tmpDir) + + st := &testStrategy{ + Symbol: "BTCUSDT", + Position: types.NewPosition("BTCUSDT", "BTC", "USDT"), + } + + recorder := NewStateRecorder(tmpDir) + err := recorder.Scan(st) + assert.NoError(t, err) + assert.Len(t, recorder.writers, 1) + + st.Position.AddTrade(types.Trade{ + OrderID: 1, + Exchange: types.ExchangeBinance, + Price: fixedpoint.NewFromFloat(18000.0), + Quantity: fixedpoint.NewFromFloat(1.0), + QuoteQuantity: fixedpoint.NewFromFloat(18000.0), + Symbol: "BTCUSDT", + Side: types.SideTypeBuy, + IsBuyer: true, + IsMaker: false, + Time: types.Time(time.Now()), + Fee: fixedpoint.NewFromFloat(0.00001), + FeeCurrency: "BNB", + IsMargin: false, + IsFutures: false, + IsIsolated: false, + }) + + n, err := recorder.Snapshot() + assert.NoError(t, err) + assert.Equal(t, 1, n) + + err = recorder.Close() + assert.NoError(t, err) +} diff --git a/pkg/backtest/report.go b/pkg/backtest/report.go new file mode 100644 index 0000000000..7af0340c3d --- /dev/null +++ b/pkg/backtest/report.go @@ -0,0 +1,259 @@ +package backtest + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + "time" + + "github.com/fatih/color" + "github.com/gofrs/flock" + + "github.com/c9s/bbgo/pkg/accounting/pnl" + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/util" +) + +type Run struct { + ID string `json:"id"` + Config *bbgo.Config `json:"config"` + Time time.Time `json:"time"` +} + +type ReportIndex struct { + Runs []Run `json:"runs,omitempty"` +} + +// SummaryReport is the summary of the back-test session +type SummaryReport struct { + StartTime time.Time `json:"startTime"` + EndTime time.Time `json:"endTime"` + Sessions []string `json:"sessions"` + Symbols []string `json:"symbols"` + Intervals []types.Interval `json:"intervals"` + InitialTotalBalances types.BalanceMap `json:"initialTotalBalances"` + FinalTotalBalances types.BalanceMap `json:"finalTotalBalances"` + + InitialEquityValue fixedpoint.Value `json:"initialEquityValue"` + FinalEquityValue fixedpoint.Value `json:"finalEquityValue"` + + // TotalProfit is the profit aggregated from the symbol reports + TotalProfit fixedpoint.Value `json:"totalProfit,omitempty"` + TotalUnrealizedProfit fixedpoint.Value `json:"totalUnrealizedProfit,omitempty"` + + TotalGrossProfit fixedpoint.Value `json:"totalGrossProfit,omitempty"` + TotalGrossLoss fixedpoint.Value `json:"totalGrossLoss,omitempty"` + + SymbolReports []SessionSymbolReport `json:"symbolReports,omitempty"` + + Manifests Manifests `json:"manifests,omitempty"` +} + +func ReadSummaryReport(filename string) (*SummaryReport, error) { + o, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + + var report SummaryReport + err = json.Unmarshal(o, &report) + return &report, err +} + +// SessionSymbolReport is the report per exchange session +// trades are merged, collected and re-calculated +type SessionSymbolReport struct { + Exchange types.ExchangeName `json:"exchange"` + Symbol string `json:"symbol,omitempty"` + Intervals []types.Interval `json:"intervals,omitempty"` + Subscriptions []types.Subscription `json:"subscriptions"` + Market types.Market `json:"market"` + LastPrice fixedpoint.Value `json:"lastPrice,omitempty"` + StartPrice fixedpoint.Value `json:"startPrice,omitempty"` + PnL *pnl.AverageCostPnLReport `json:"pnl,omitempty"` + InitialBalances types.BalanceMap `json:"initialBalances,omitempty"` + FinalBalances types.BalanceMap `json:"finalBalances,omitempty"` + Manifests Manifests `json:"manifests,omitempty"` + Sharpe fixedpoint.Value `json:"sharpeRatio"` + Sortino fixedpoint.Value `json:"sortinoRatio"` +} + +func (r *SessionSymbolReport) InitialEquityValue() fixedpoint.Value { + return InQuoteAsset(r.InitialBalances, r.Market, r.StartPrice) +} + +func (r *SessionSymbolReport) FinalEquityValue() fixedpoint.Value { + return InQuoteAsset(r.FinalBalances, r.Market, r.LastPrice) +} + +func (r *SessionSymbolReport) Print(wantBaseAssetBaseline bool) { + color.Green("%s %s PROFIT AND LOSS REPORT", r.Exchange, r.Symbol) + color.Green("===============================================") + r.PnL.Print() + + initQuoteAsset := r.InitialEquityValue() + finalQuoteAsset := r.FinalEquityValue() + color.Green("INITIAL ASSET IN %s ~= %s %s (1 %s = %v)", r.Market.QuoteCurrency, r.Market.FormatQuantity(initQuoteAsset), r.Market.QuoteCurrency, r.Market.BaseCurrency, r.StartPrice) + color.Green("FINAL ASSET IN %s ~= %s %s (1 %s = %v)", r.Market.QuoteCurrency, r.Market.FormatQuantity(finalQuoteAsset), r.Market.QuoteCurrency, r.Market.BaseCurrency, r.LastPrice) + + if r.PnL.Profit.Sign() > 0 { + color.Green("REALIZED PROFIT: +%v %s", r.PnL.Profit, r.Market.QuoteCurrency) + } else { + color.Red("REALIZED PROFIT: %v %s", r.PnL.Profit, r.Market.QuoteCurrency) + } + + if r.PnL.UnrealizedProfit.Sign() > 0 { + color.Green("UNREALIZED PROFIT: +%v %s", r.PnL.UnrealizedProfit, r.Market.QuoteCurrency) + } else { + color.Red("UNREALIZED PROFIT: %v %s", r.PnL.UnrealizedProfit, r.Market.QuoteCurrency) + } + + if finalQuoteAsset.Compare(initQuoteAsset) > 0 { + color.Green("ASSET INCREASED: +%v %s (+%s)", finalQuoteAsset.Sub(initQuoteAsset), r.Market.QuoteCurrency, finalQuoteAsset.Sub(initQuoteAsset).Div(initQuoteAsset).FormatPercentage(2)) + } else { + color.Red("ASSET DECREASED: %v %s (%s)", finalQuoteAsset.Sub(initQuoteAsset), r.Market.QuoteCurrency, finalQuoteAsset.Sub(initQuoteAsset).Div(initQuoteAsset).FormatPercentage(2)) + } + + if r.Sharpe.Sign() > 0 { + color.Green("REALIZED SHARPE RATIO: %s", r.Sharpe.FormatString(4)) + } else { + color.Red("REALIZED SHARPE RATIO: %s", r.Sharpe.FormatString(4)) + } + + if r.Sortino.Sign() > 0 { + color.Green("REALIZED SORTINO RATIO: %s", r.Sortino.FormatString(4)) + } else { + color.Red("REALIZED SORTINO RATIO: %s", r.Sortino.FormatString(4)) + } + + if wantBaseAssetBaseline { + if r.LastPrice.Compare(r.StartPrice) > 0 { + color.Green("%s BASE ASSET PERFORMANCE: +%s (= (%s - %s) / %s)", + r.Market.BaseCurrency, + r.LastPrice.Sub(r.StartPrice).Div(r.StartPrice).FormatPercentage(2), + r.LastPrice.FormatString(2), + r.StartPrice.FormatString(2), + r.StartPrice.FormatString(2)) + } else { + color.Red("%s BASE ASSET PERFORMANCE: %s (= (%s - %s) / %s)", + r.Market.BaseCurrency, + r.LastPrice.Sub(r.StartPrice).Div(r.StartPrice).FormatPercentage(2), + r.LastPrice.FormatString(2), + r.StartPrice.FormatString(2), + r.StartPrice.FormatString(2)) + } + } +} + +const SessionTimeFormat = "2006-01-02T15_04" + +// FormatSessionName returns the back-test session name +func FormatSessionName(sessions []string, symbols []string, startTime, endTime time.Time) string { + return fmt.Sprintf("%s_%s_%s-%s", + strings.Join(sessions, "-"), + strings.Join(symbols, "-"), + startTime.Format(SessionTimeFormat), + endTime.Format(SessionTimeFormat), + ) +} + +func WriteReportIndex(outputDirectory string, reportIndex *ReportIndex) error { + indexFile := getReportIndexPath(outputDirectory) + indexLock := flock.New(indexFile) + + if err := indexLock.Lock(); err != nil { + log.WithError(err).Errorf("report index file lock error while write report: %s", err) + return err + } + defer func() { + if err := indexLock.Unlock(); err != nil { + log.WithError(err).Errorf("report index file unlock error while write report: %s", err) + } + }() + + return writeReportIndexLocked(outputDirectory, reportIndex) +} + +func LoadReportIndex(outputDirectory string) (*ReportIndex, error) { + indexFile := getReportIndexPath(outputDirectory) + indexLock := flock.New(indexFile) + + if err := indexLock.Lock(); err != nil { + log.WithError(err).Errorf("report index file lock error while load report: %s", err) + return nil, err + } + defer func() { + if err := indexLock.Unlock(); err != nil { + log.WithError(err).Errorf("report index file unlock error while load report: %s", err) + } + }() + + return loadReportIndexLocked(indexFile) +} + +func AddReportIndexRun(outputDirectory string, run Run) error { + // append report index + indexFile := getReportIndexPath(outputDirectory) + indexLock := flock.New(indexFile) + + if err := indexLock.Lock(); err != nil { + log.WithError(err).Errorf("report index file lock error: %s", err) + return err + } + defer func() { + if err := indexLock.Unlock(); err != nil { + log.WithError(err).Errorf("report index file unlock error: %s", err) + } + }() + + reportIndex, err := loadReportIndexLocked(indexFile) + if err != nil { + return err + } + + reportIndex.Runs = append(reportIndex.Runs, run) + return writeReportIndexLocked(indexFile, reportIndex) +} + +// InQuoteAsset converts all balances in quote asset +func InQuoteAsset(balances types.BalanceMap, market types.Market, price fixedpoint.Value) fixedpoint.Value { + quote := balances[market.QuoteCurrency] + base := balances[market.BaseCurrency] + return base.Total().Mul(price).Add(quote.Total()) +} + +func getReportIndexPath(outputDirectory string) string { + return filepath.Join(outputDirectory, "index.json") +} + +// writeReportIndexLocked must be protected by file lock +func writeReportIndexLocked(indexFilePath string, reportIndex *ReportIndex) error { + if err := util.WriteJsonFile(indexFilePath, reportIndex); err != nil { + return err + } + return nil +} + +// loadReportIndexLocked must be protected by file lock +func loadReportIndexLocked(indexFilePath string) (*ReportIndex, error) { + var reportIndex ReportIndex + if fileInfo, err := os.Stat(indexFilePath); err != nil { + return nil, err + } else if fileInfo.Size() != 0 { + o, err := ioutil.ReadFile(indexFilePath) + if err != nil { + return nil, err + } + + if err := json.Unmarshal(o, &reportIndex); err != nil { + return nil, err + } + } + + return &reportIndex, nil +} diff --git a/pkg/backtest/stream.go b/pkg/backtest/stream.go deleted file mode 100644 index 91e93a7b92..0000000000 --- a/pkg/backtest/stream.go +++ /dev/null @@ -1,41 +0,0 @@ -package backtest - -import ( - "context" - - "github.com/sirupsen/logrus" - - "github.com/c9s/bbgo/pkg/types" -) - -var log = logrus.WithField("cmd", "backtest") - -type Stream struct { - types.StandardStream - - exchange *Exchange -} - -func (s *Stream) Connect(ctx context.Context) error { - if s.PublicOnly { - if s.exchange.marketDataStream != nil { - panic("you should not set up more than 1 market data stream in back-test") - } - s.exchange.marketDataStream = s - } else { - - // assign user data stream back - if s.exchange.userDataStream != nil { - panic("you should not set up more than 1 user data stream in back-test") - } - s.exchange.userDataStream = s - } - - s.EmitConnect() - s.EmitStart() - return nil -} - -func (s *Stream) Close() error { - return nil -} diff --git a/pkg/bbgo/active_book.go b/pkg/bbgo/active_book.go deleted file mode 100644 index c5eefca74e..0000000000 --- a/pkg/bbgo/active_book.go +++ /dev/null @@ -1,254 +0,0 @@ -package bbgo - -import ( - "context" - "encoding/json" - "time" - - log "github.com/sirupsen/logrus" - - "github.com/c9s/bbgo/pkg/types" -) - -const SentOrderWaitTime = 50 * time.Millisecond -const CancelOrderWaitTime = 20 * time.Millisecond - -// LocalActiveOrderBook manages the local active order books. -//go:generate callbackgen -type LocalActiveOrderBook -type LocalActiveOrderBook struct { - Symbol string - Asks, Bids *types.SyncOrderMap - filledCallbacks []func(o types.Order) -} - -func NewLocalActiveOrderBook(symbol string) *LocalActiveOrderBook { - return &LocalActiveOrderBook{ - Symbol: symbol, - Bids: types.NewSyncOrderMap(), - Asks: types.NewSyncOrderMap(), - } -} - -func (b *LocalActiveOrderBook) MarshalJSON() ([]byte, error) { - orders := b.Backup() - return json.Marshal(orders) -} - -func (b *LocalActiveOrderBook) Backup() []types.SubmitOrder { - return append(b.Bids.Backup(), b.Asks.Backup()...) -} - -func (b *LocalActiveOrderBook) BindStream(stream types.Stream) { - stream.OnOrderUpdate(b.orderUpdateHandler) -} - -func (b *LocalActiveOrderBook) waitAllClear(ctx context.Context, waitTime, timeout time.Duration) (bool, error) { - numOfOrders := b.NumOfOrders() - clear := numOfOrders == 0 - if clear { - return clear, nil - } - - timeoutC := time.After(timeout) - for { - time.Sleep(waitTime) - numOfOrders = b.NumOfOrders() - clear = numOfOrders == 0 - select { - case <-timeoutC: - return clear, nil - - case <-ctx.Done(): - return clear, ctx.Err() - - default: - if clear { - return clear, nil - } - } - } -} - -// GracefulCancel cancels the active orders gracefully -func (b *LocalActiveOrderBook) GracefulCancel(ctx context.Context, ex types.Exchange) error { - log.Debugf("[LocalActiveOrderBook] gracefully cancelling %s orders...", b.Symbol) - - startTime := time.Now() - // ensure every order is cancelled - for { - orders := b.Orders() - - // Some orders in the variable are not created on the server side yet, - // If we cancel these orders directly, we will get an unsent order error - // We wait here for a while for server to create these orders. - // time.Sleep(SentOrderWaitTime) - - // since ctx might be canceled, we should use background context here - if err := ex.CancelOrders(context.Background(), orders...); err != nil { - log.WithError(err).Errorf("[LocalActiveOrderBook] can not cancel %s orders", b.Symbol) - } - - log.Debugf("[LocalActiveOrderBook] waiting %s for %s orders to be cancelled...", CancelOrderWaitTime, b.Symbol) - - clear, err := b.waitAllClear(ctx, CancelOrderWaitTime, 5*time.Second) - if clear || err != nil { - break - } - - log.Warnf("[LocalActiveOrderBook] %d %s orders are not cancelled yet:", b.NumOfOrders(), b.Symbol) - b.Print() - - // verify the current open orders via the RESTful API - log.Warnf("[LocalActiveOrderBook] using REStful API to verify active orders...") - openOrders, err := ex.QueryOpenOrders(ctx, b.Symbol) - if err != nil { - log.WithError(err).Errorf("can not query %s open orders", b.Symbol) - continue - } - - openOrderStore := NewOrderStore(b.Symbol) - openOrderStore.Add(openOrders...) - for _, o := range orders { - // if it's not on the order book (open orders), we should remove it from our local side - if !openOrderStore.Exists(o.OrderID) { - b.Remove(o) - } - } - } - - log.Debugf("[LocalActiveOrderBook] all %s orders are cancelled successfully in %s", b.Symbol, time.Since(startTime)) - return nil -} - -func (b *LocalActiveOrderBook) orderUpdateHandler(order types.Order) { - log.Debugf("[LocalActiveOrderBook] received order update: %+v", order) - - switch order.Status { - case types.OrderStatusFilled: - // make sure we have the order and we remove it - if b.Remove(order) { - b.EmitFilled(order) - } - - case types.OrderStatusPartiallyFilled, types.OrderStatusNew: - b.Update(order) - - case types.OrderStatusCanceled, types.OrderStatusRejected: - log.Debugf("[LocalActiveOrderBook] order status %s, removing order %s", order.Status, order) - b.Remove(order) - - default: - log.Warnf("unhandled order status: %s", order.Status) - } -} - -func (b *LocalActiveOrderBook) Print() { - for _, o := range b.Bids.Orders() { - log.Infof("%s bid order: %d @ %v -> %s", o.Symbol, o.OrderID, o.Price, o.Status) - } - - for _, o := range b.Asks.Orders() { - log.Infof("%s ask order: %d @ %v -> %s", o.Symbol, o.OrderID, o.Price, o.Status) - } -} - -func (b *LocalActiveOrderBook) Update(orders ...types.Order) { - for _, order := range orders { - switch order.Side { - case types.SideTypeBuy: - b.Bids.Update(order) - - case types.SideTypeSell: - b.Asks.Update(order) - - } - } -} - -func (b *LocalActiveOrderBook) Add(orders ...types.Order) { - for _, order := range orders { - switch order.Side { - case types.SideTypeBuy: - b.Bids.Add(order) - - case types.SideTypeSell: - b.Asks.Add(order) - - default: - log.Errorf("unexpected order side %s, order: %#v", order.Side, order) - - } - } -} - -func (b *LocalActiveOrderBook) NumOfBids() int { - return b.Bids.Len() -} - -func (b *LocalActiveOrderBook) NumOfAsks() int { - return b.Asks.Len() -} - -func (b *LocalActiveOrderBook) Exists(order types.Order) bool { - - switch order.Side { - - case types.SideTypeBuy: - return b.Bids.Exists(order.OrderID) - - case types.SideTypeSell: - return b.Asks.Exists(order.OrderID) - - } - - return false -} - -func (b *LocalActiveOrderBook) Remove(order types.Order) bool { - switch order.Side { - case types.SideTypeBuy: - return b.Bids.Remove(order.OrderID) - - case types.SideTypeSell: - return b.Asks.Remove(order.OrderID) - - } - - return false -} - -// WriteOff writes off the filled order on the opposite side. -// This method does not write off order by order amount or order quantity. -func (b *LocalActiveOrderBook) WriteOff(order types.Order) bool { - if order.Status != types.OrderStatusFilled { - return false - } - - switch order.Side { - case types.SideTypeSell: - // find the filled bid to remove - if filledOrder, ok := b.Bids.AnyFilled(); ok { - b.Bids.Remove(filledOrder.OrderID) - b.Asks.Remove(order.OrderID) - return true - } - - case types.SideTypeBuy: - // find the filled ask order to remove - if filledOrder, ok := b.Asks.AnyFilled(); ok { - b.Asks.Remove(filledOrder.OrderID) - b.Bids.Remove(order.OrderID) - return true - } - } - - return false -} - -func (b *LocalActiveOrderBook) NumOfOrders() int { - return b.Asks.Len() + b.Bids.Len() -} - -func (b *LocalActiveOrderBook) Orders() types.OrderSlice { - return append(b.Asks.Orders(), b.Bids.Orders()...) -} diff --git a/pkg/bbgo/activeorderbook.go b/pkg/bbgo/activeorderbook.go new file mode 100644 index 0000000000..3196943b89 --- /dev/null +++ b/pkg/bbgo/activeorderbook.go @@ -0,0 +1,265 @@ +package bbgo + +import ( + "context" + "encoding/json" + "time" + + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/types" +) + +const CancelOrderWaitTime = 20 * time.Millisecond + +// ActiveOrderBook manages the local active order books. +//go:generate callbackgen -type ActiveOrderBook +type ActiveOrderBook struct { + Symbol string + orders *types.SyncOrderMap + filledCallbacks []func(o types.Order) +} + +func NewActiveOrderBook(symbol string) *ActiveOrderBook { + return &ActiveOrderBook{ + Symbol: symbol, + orders: types.NewSyncOrderMap(), + } +} + +func (b *ActiveOrderBook) MarshalJSON() ([]byte, error) { + orders := b.Backup() + return json.Marshal(orders) +} + +func (b *ActiveOrderBook) Backup() []types.SubmitOrder { + return b.orders.Backup() +} + +func (b *ActiveOrderBook) BindStream(stream types.Stream) { + stream.OnOrderUpdate(b.orderUpdateHandler) +} + +func (b *ActiveOrderBook) waitClear(ctx context.Context, order types.Order, waitTime, timeout time.Duration) (bool, error) { + if !b.Exists(order) { + return true, nil + } + + timeoutC := time.After(timeout) + for { + time.Sleep(waitTime) + clear := !b.Exists(order) + select { + case <-timeoutC: + return clear, nil + + case <-ctx.Done(): + return clear, ctx.Err() + + default: + if clear { + return clear, nil + } + } + } +} + +func (b *ActiveOrderBook) waitAllClear(ctx context.Context, waitTime, timeout time.Duration) (bool, error) { + numOfOrders := b.NumOfOrders() + clear := numOfOrders == 0 + if clear { + return clear, nil + } + + timeoutC := time.After(timeout) + for { + time.Sleep(waitTime) + numOfOrders = b.NumOfOrders() + clear = numOfOrders == 0 + select { + case <-timeoutC: + return clear, nil + + case <-ctx.Done(): + return clear, ctx.Err() + + default: + if clear { + return clear, nil + } + } + } +} + +// Cancel orders without confirmation +func (b *ActiveOrderBook) CancelNoWait(ctx context.Context, ex types.Exchange, orders ...types.Order) error { + // if no orders are given, set to cancelAll + if len(orders) == 0 { + orders = b.Orders() + } else { + // simple check on given input + for _, o := range orders { + if o.Symbol != b.Symbol { + return errors.New("[ActiveOrderBook] cancel " + b.Symbol + " orderbook with different symbol: " + o.Symbol) + } + } + } + // optimize order cancel for back-testing + if IsBackTesting { + return ex.CancelOrders(context.Background(), orders...) + } + log.Debugf("[ActiveOrderBook] no wait cancelling %s orders...", b.Symbol) + // since ctx might be canceled, we should use background context here + if err := ex.CancelOrders(context.Background(), orders...); err != nil { + log.WithError(err).Errorf("[ActiveOrderBook] no wait can not cancel %s orders", b.Symbol) + } + for _, o := range orders { + b.Remove(o) + } + return nil +} + +// GracefulCancel cancels the active orders gracefully +func (b *ActiveOrderBook) GracefulCancel(ctx context.Context, ex types.Exchange, orders ...types.Order) error { + // if no orders are given, set to cancelAll + if len(orders) == 0 { + orders = b.Orders() + } else { + // simple check on given input + for _, o := range orders { + if b.Symbol != "" && o.Symbol != b.Symbol { + return errors.New("[ActiveOrderBook] cancel " + b.Symbol + " orderbook with different symbol: " + o.Symbol) + } + } + } + // optimize order cancel for back-testing + if IsBackTesting { + return ex.CancelOrders(context.Background(), orders...) + } + + log.Debugf("[ActiveOrderBook] gracefully cancelling %s orders...", b.Symbol) + waitTime := CancelOrderWaitTime + + startTime := time.Now() + // ensure every order is cancelled + for { + // Some orders in the variable are not created on the server side yet, + // If we cancel these orders directly, we will get an unsent order error + // We wait here for a while for server to create these orders. + // time.Sleep(SentOrderWaitTime) + + // since ctx might be canceled, we should use background context here + if err := ex.CancelOrders(context.Background(), orders...); err != nil { + log.WithError(err).Errorf("[ActiveOrderBook] can not cancel %s orders", b.Symbol) + } + + log.Debugf("[ActiveOrderBook] waiting %s for %s orders to be cancelled...", waitTime, b.Symbol) + + clear, err := b.waitAllClear(ctx, waitTime, 5*time.Second) + if clear || err != nil { + break + } + + log.Warnf("[ActiveOrderBook] %d %s orders are not cancelled yet:", b.NumOfOrders(), b.Symbol) + b.Print() + + // verify the current open orders via the RESTful API + log.Warnf("[ActiveOrderBook] using REStful API to verify active orders...") + + var symbols = map[string]struct{}{} + for _, order := range orders { + symbols[order.Symbol] = struct{}{} + + } + var leftOrders []types.Order + + for symbol := range symbols { + openOrders, err := ex.QueryOpenOrders(ctx, symbol) + if err != nil { + log.WithError(err).Errorf("can not query %s open orders", symbol) + continue + } + + openOrderStore := NewOrderStore(symbol) + openOrderStore.Add(openOrders...) + for _, o := range orders { + // if it's not on the order book (open orders), we should remove it from our local side + if !openOrderStore.Exists(o.OrderID) { + b.Remove(o) + } else { + leftOrders = append(leftOrders, o) + } + } + } + orders = leftOrders + } + + log.Debugf("[ActiveOrderBook] all %s orders are cancelled successfully in %s", b.Symbol, time.Since(startTime)) + return nil +} + +func (b *ActiveOrderBook) orderUpdateHandler(order types.Order) { + hasSymbol := len(b.Symbol) > 0 + if hasSymbol && order.Symbol != b.Symbol { + return + } + + switch order.Status { + case types.OrderStatusFilled: + // make sure we have the order and we remove it + if b.Remove(order) { + b.EmitFilled(order) + } + + case types.OrderStatusPartiallyFilled, types.OrderStatusNew: + b.Update(order) + + case types.OrderStatusCanceled, types.OrderStatusRejected: + log.Debugf("[ActiveOrderBook] order status %s, removing order %s", order.Status, order) + b.Remove(order) + + default: + log.Warnf("unhandled order status: %s", order.Status) + } +} + +func (b *ActiveOrderBook) Print() { + for _, o := range b.orders.Orders() { + log.Infof("%s", o) + } +} + +func (b *ActiveOrderBook) Update(orders ...types.Order) { + hasSymbol := len(b.Symbol) > 0 + for _, order := range orders { + if hasSymbol && b.Symbol == order.Symbol { + b.orders.Update(order) + } + } +} + +func (b *ActiveOrderBook) Add(orders ...types.Order) { + hasSymbol := len(b.Symbol) > 0 + for _, order := range orders { + if hasSymbol && b.Symbol == order.Symbol { + b.orders.Add(order) + } + } +} + +func (b *ActiveOrderBook) Exists(order types.Order) bool { + return b.orders.Exists(order.OrderID) +} + +func (b *ActiveOrderBook) Remove(order types.Order) bool { + return b.orders.Remove(order.OrderID) +} + +func (b *ActiveOrderBook) NumOfOrders() int { + return b.orders.Len() +} + +func (b *ActiveOrderBook) Orders() types.OrderSlice { + return b.orders.Orders() +} diff --git a/pkg/bbgo/activeorderbook_callbacks.go b/pkg/bbgo/activeorderbook_callbacks.go new file mode 100644 index 0000000000..5110476043 --- /dev/null +++ b/pkg/bbgo/activeorderbook_callbacks.go @@ -0,0 +1,17 @@ +// Code generated by "callbackgen -type ActiveOrderBook"; DO NOT EDIT. + +package bbgo + +import ( + "github.com/c9s/bbgo/pkg/types" +) + +func (b *ActiveOrderBook) OnFilled(cb func(o types.Order)) { + b.filledCallbacks = append(b.filledCallbacks, cb) +} + +func (b *ActiveOrderBook) EmitFilled(o types.Order) { + for _, cb := range b.filledCallbacks { + cb(o) + } +} diff --git a/pkg/bbgo/backtestfeemode_enumer.go b/pkg/bbgo/backtestfeemode_enumer.go new file mode 100644 index 0000000000..5460436680 --- /dev/null +++ b/pkg/bbgo/backtestfeemode_enumer.go @@ -0,0 +1,117 @@ +// Code generated by "enumer -type=BacktestFeeMode -transform=snake -trimprefix BacktestFeeMode -yaml -json"; DO NOT EDIT. + +package bbgo + +import ( + "encoding/json" + "fmt" + "strings" +) + +const _BacktestFeeModeName = "quotenativetoken" + +var _BacktestFeeModeIndex = [...]uint8{0, 5, 11, 16} + +const _BacktestFeeModeLowerName = "quotenativetoken" + +func (i BacktestFeeMode) String() string { + if i < 0 || i >= BacktestFeeMode(len(_BacktestFeeModeIndex)-1) { + return fmt.Sprintf("BacktestFeeMode(%d)", i) + } + return _BacktestFeeModeName[_BacktestFeeModeIndex[i]:_BacktestFeeModeIndex[i+1]] +} + +// An "invalid array index" compiler error signifies that the constant values have changed. +// Re-run the stringer command to generate them again. +func _BacktestFeeModeNoOp() { + var x [1]struct{} + _ = x[BacktestFeeModeQuote-(0)] + _ = x[BacktestFeeModeNative-(1)] + _ = x[BacktestFeeModeToken-(2)] +} + +var _BacktestFeeModeValues = []BacktestFeeMode{BacktestFeeModeQuote, BacktestFeeModeNative, BacktestFeeModeToken} + +var _BacktestFeeModeNameToValueMap = map[string]BacktestFeeMode{ + _BacktestFeeModeName[0:5]: BacktestFeeModeQuote, + _BacktestFeeModeLowerName[0:5]: BacktestFeeModeQuote, + _BacktestFeeModeName[5:11]: BacktestFeeModeNative, + _BacktestFeeModeLowerName[5:11]: BacktestFeeModeNative, + _BacktestFeeModeName[11:16]: BacktestFeeModeToken, + _BacktestFeeModeLowerName[11:16]: BacktestFeeModeToken, +} + +var _BacktestFeeModeNames = []string{ + _BacktestFeeModeName[0:5], + _BacktestFeeModeName[5:11], + _BacktestFeeModeName[11:16], +} + +// BacktestFeeModeString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. +func BacktestFeeModeString(s string) (BacktestFeeMode, error) { + if val, ok := _BacktestFeeModeNameToValueMap[s]; ok { + return val, nil + } + + if val, ok := _BacktestFeeModeNameToValueMap[strings.ToLower(s)]; ok { + return val, nil + } + return 0, fmt.Errorf("%s does not belong to BacktestFeeMode values", s) +} + +// BacktestFeeModeValues returns all values of the enum +func BacktestFeeModeValues() []BacktestFeeMode { + return _BacktestFeeModeValues +} + +// BacktestFeeModeStrings returns a slice of all String values of the enum +func BacktestFeeModeStrings() []string { + strs := make([]string, len(_BacktestFeeModeNames)) + copy(strs, _BacktestFeeModeNames) + return strs +} + +// IsABacktestFeeMode returns "true" if the value is listed in the enum definition. "false" otherwise +func (i BacktestFeeMode) IsABacktestFeeMode() bool { + for _, v := range _BacktestFeeModeValues { + if i == v { + return true + } + } + return false +} + +// MarshalJSON implements the json.Marshaler interface for BacktestFeeMode +func (i BacktestFeeMode) MarshalJSON() ([]byte, error) { + return json.Marshal(i.String()) +} + +// UnmarshalJSON implements the json.Unmarshaler interface for BacktestFeeMode +func (i *BacktestFeeMode) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return fmt.Errorf("BacktestFeeMode should be a string, got %s", data) + } + + var err error + *i, err = BacktestFeeModeString(s) + return err +} + +// MarshalYAML implements a YAML Marshaler for BacktestFeeMode +func (i BacktestFeeMode) MarshalYAML() (interface{}, error) { + return i.String(), nil +} + +// UnmarshalYAML implements a YAML Unmarshaler for BacktestFeeMode +func (i *BacktestFeeMode) UnmarshalYAML(unmarshal func(interface{}) error) error { + var s string + if err := unmarshal(&s); err != nil { + return err + } + + var err error + *i, err = BacktestFeeModeString(s) + return err +} diff --git a/pkg/bbgo/bootstrap.go b/pkg/bbgo/bootstrap.go new file mode 100644 index 0000000000..59d5576926 --- /dev/null +++ b/pkg/bbgo/bootstrap.go @@ -0,0 +1,50 @@ +package bbgo + +import ( + "context" + + "github.com/pkg/errors" +) + +// BootstrapEnvironmentLightweight bootstrap the environment in lightweight mode +// - no database configuration +// - no notification +func BootstrapEnvironmentLightweight(ctx context.Context, environ *Environment, userConfig *Config) error { + if err := environ.ConfigureExchangeSessions(userConfig); err != nil { + return errors.Wrap(err, "exchange session configure error") + } + + if userConfig.Persistence != nil { + if err := ConfigurePersistence(ctx, userConfig.Persistence); err != nil { + return errors.Wrap(err, "persistence configure error") + } + } + + return nil +} + +func BootstrapEnvironment(ctx context.Context, environ *Environment, userConfig *Config) error { + if err := environ.ConfigureDatabase(ctx); err != nil { + return err + } + + if err := environ.ConfigureExchangeSessions(userConfig); err != nil { + return errors.Wrap(err, "exchange session configure error") + } + + if userConfig.Persistence != nil { + if err := ConfigurePersistence(ctx, userConfig.Persistence); err != nil { + return errors.Wrap(err, "persistence configure error") + } + } + + if err := environ.ConfigureNotificationSystem(userConfig); err != nil { + return errors.Wrap(err, "notification configure error") + } + + return nil +} + +func BootstrapBacktestEnvironment(ctx context.Context, environ *Environment) error { + return environ.ConfigureDatabase(ctx) +} diff --git a/pkg/bbgo/builder.go b/pkg/bbgo/builder.go index bf328c2c89..f23babf925 100644 --- a/pkg/bbgo/builder.go +++ b/pkg/bbgo/builder.go @@ -38,6 +38,7 @@ func main() { `)) +// generateRunFile renders the wrapper main.go template func generateRunFile(filepath string, config *Config, imports []string) error { var buf = bytes.NewBuffer(nil) if err := wrapperTemplate.Execute(buf, struct { @@ -53,6 +54,7 @@ func generateRunFile(filepath string, config *Config, imports []string) error { return ioutil.WriteFile(filepath, buf.Bytes(), 0644) } +// compilePackage generates the main.go file of the wrapper package func compilePackage(packageDir string, userConfig *Config, imports []string) error { if _, err := os.Stat(packageDir); os.IsNotExist(err) { if err := os.MkdirAll(packageDir, 0777); err != nil { @@ -68,6 +70,7 @@ func compilePackage(packageDir string, userConfig *Config, imports []string) err return nil } +// Build builds the bbgo wrapper binary with the given build target config func Build(ctx context.Context, userConfig *Config, targetConfig BuildTargetConfig) (string, error) { // combine global imports and target imports imports := append(userConfig.Build.Imports, targetConfig.Imports...) @@ -123,6 +126,7 @@ func Build(ctx context.Context, userConfig *Config, targetConfig BuildTargetConf return output, os.RemoveAll(packageDir) } +// BuildTarget builds the one of the targets. func BuildTarget(ctx context.Context, userConfig *Config, target BuildTargetConfig) (string, error) { buildDir := userConfig.Build.BuildDir if len(buildDir) == 0 { diff --git a/pkg/bbgo/config.go b/pkg/bbgo/config.go index ceea5a9ad4..9da4275581 100644 --- a/pkg/bbgo/config.go +++ b/pkg/bbgo/config.go @@ -7,11 +7,13 @@ import ( "io/ioutil" "reflect" "runtime" + "strings" "github.com/pkg/errors" "gopkg.in/yaml.v3" "github.com/c9s/bbgo/pkg/datatype" + "github.com/c9s/bbgo/pkg/dynamic" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/service" "github.com/c9s/bbgo/pkg/types" @@ -74,15 +76,17 @@ type TelegramNotification struct { Broadcast bool `json:"broadcast" yaml:"broadcast"` } -type NotificationConfig struct { - Slack *SlackNotification `json:"slack,omitempty" yaml:"slack,omitempty"` +type NotificationSwitches struct { + Trade bool `json:"trade" yaml:"trade"` + Position bool `json:"position" yaml:"position"` + OrderUpdate bool `json:"orderUpdate" yaml:"orderUpdate"` + SubmitOrder bool `json:"submitOrder" yaml:"submitOrder"` +} +type NotificationConfig struct { + Slack *SlackNotification `json:"slack,omitempty" yaml:"slack,omitempty"` Telegram *TelegramNotification `json:"telegram,omitempty" yaml:"telegram,omitempty"` - - SymbolChannels map[string]string `json:"symbolChannels,omitempty" yaml:"symbolChannels,omitempty"` - SessionChannels map[string]string `json:"sessionChannels,omitempty" yaml:"sessionChannels,omitempty"` - - Routing *SlackNotificationRouting `json:"routing,omitempty" yaml:"routing,omitempty"` + Switches *NotificationSwitches `json:"switches" yaml:"switches"` } type Session struct { @@ -99,25 +103,75 @@ type Session struct { IsolatedMarginSymbol string `json:"isolatedMarginSymbol,omitempty" yaml:"isolatedMarginSymbol,omitempty"` } +//go:generate go run github.com/dmarkham/enumer -type=BacktestFeeMode -transform=snake -trimprefix BacktestFeeMode -yaml -json +type BacktestFeeMode int + +const ( + // BackTestFeeModeQuoteFee is designed for clean position but which also counts the fee in the quote balance. + // buy order = quote currency fee + // sell order = quote currency fee + BacktestFeeModeQuote BacktestFeeMode = iota // quote + + // BackTestFeeModeNativeFee is the default crypto exchange fee mode. + // buy order = base currency fee + // sell order = quote currency fee + BacktestFeeModeNative // BackTestFeeMode = "native" + + // BackTestFeeModeFeeToken is the mode which calculates fee from the outside of the balances. + // the fee will not be included in the balances nor the profit. + BacktestFeeModeToken // BackTestFeeMode = "token" +) + type Backtest struct { StartTime types.LooseFormatTime `json:"startTime,omitempty" yaml:"startTime,omitempty"` EndTime *types.LooseFormatTime `json:"endTime,omitempty" yaml:"endTime,omitempty"` // RecordTrades is an option, if set to true, back-testing should record the trades into database - RecordTrades bool `json:"recordTrades,omitempty" yaml:"recordTrades,omitempty"` - Account map[string]BacktestAccount `json:"account" yaml:"account"` - Symbols []string `json:"symbols" yaml:"symbols"` - Sessions []string `json:"sessions" yaml:"sessions"` + RecordTrades bool `json:"recordTrades,omitempty" yaml:"recordTrades,omitempty"` + + // Deprecated: + // Account is deprecated, use Accounts instead + Account map[string]BacktestAccount `json:"account" yaml:"account"` + + FeeMode BacktestFeeMode `json:"feeMode" yaml:"feeMode"` + + Accounts map[string]BacktestAccount `json:"accounts" yaml:"accounts"` + Symbols []string `json:"symbols" yaml:"symbols"` + Sessions []string `json:"sessions" yaml:"sessions"` + + // sync 1 second interval KLines + SyncSecKLines bool `json:"syncSecKLines,omitempty" yaml:"syncSecKLines,omitempty"` +} + +func (b *Backtest) GetAccount(n string) BacktestAccount { + accountConfig, ok := b.Accounts[n] + if ok { + return accountConfig + } + + accountConfig, ok = b.Account[n] + if ok { + return accountConfig + } + + return DefaultBacktestAccount } type BacktestAccount struct { - // TODO: MakerFeeRate should replace the commission fields MakerFeeRate fixedpoint.Value `json:"makerFeeRate,omitempty" yaml:"makerFeeRate,omitempty"` TakerFeeRate fixedpoint.Value `json:"takerFeeRate,omitempty" yaml:"takerFeeRate,omitempty"` Balances BacktestAccountBalanceMap `json:"balances" yaml:"balances"` } +var DefaultBacktestAccount = BacktestAccount{ + MakerFeeRate: fixedpoint.MustNewFromString("0.050%"), + TakerFeeRate: fixedpoint.MustNewFromString("0.075%"), + Balances: BacktestAccountBalanceMap{ + "USDT": fixedpoint.NewFromFloat(10000), + }, +} + type BA BacktestAccount func (b *BacktestAccount) UnmarshalYAML(value *yaml.Node) error { @@ -180,22 +234,69 @@ func GetNativeBuildTargetConfig() BuildTargetConfig { } } +type SyncSymbol struct { + Symbol string `json:"symbol" yaml:"symbol"` + Session string `json:"session" yaml:"session"` +} + +func (ss *SyncSymbol) UnmarshalYAML(unmarshal func(a interface{}) error) (err error) { + var s string + if err = unmarshal(&s); err == nil { + aa := strings.SplitN(s, ":", 2) + if len(aa) > 1 { + ss.Session = aa[0] + ss.Symbol = aa[1] + } else { + ss.Symbol = aa[0] + } + return nil + } + + type localSyncSymbol SyncSymbol + var ssNew localSyncSymbol + if err = unmarshal(&ssNew); err == nil { + *ss = SyncSymbol(ssNew) + return nil + } + + return err +} + +func categorizeSyncSymbol(slice []SyncSymbol) (map[string][]string, []string) { + var rest []string + var m = make(map[string][]string) + for _, ss := range slice { + if len(ss.Session) > 0 { + m[ss.Session] = append(m[ss.Session], ss.Symbol) + } else { + rest = append(rest, ss.Symbol) + } + } + return m, rest +} + type SyncConfig struct { // Sessions to sync, if ignored, all defined sessions will sync Sessions []string `json:"sessions,omitempty" yaml:"sessions,omitempty"` - // Symbols is the list of symbol to sync, if ignored, symbols wlll be discovered by your existing crypto balances - Symbols []string `json:"symbols,omitempty" yaml:"symbols,omitempty"` + // Symbols is the list of session:symbol pair to sync, if ignored, symbols wlll be discovered by your existing crypto balances + // Valid formats are: {session}:{symbol}, {symbol} or in YAML object form {symbol: "BTCUSDT", session:"max" } + Symbols []SyncSymbol `json:"symbols,omitempty" yaml:"symbols,omitempty"` - // DepositHistory for syncing deposit history + // DepositHistory is for syncing deposit history DepositHistory bool `json:"depositHistory" yaml:"depositHistory"` - // WithdrawHistory for syncing withdraw history + // WithdrawHistory is for syncing withdraw history WithdrawHistory bool `json:"withdrawHistory" yaml:"withdrawHistory"` - // RewardHistory for syncing reward history + // RewardHistory is for syncing reward history RewardHistory bool `json:"rewardHistory" yaml:"rewardHistory"` + // MarginHistory is for syncing margin related history: loans, repays, interests and liquidations + MarginHistory bool `json:"marginHistory" yaml:"marginHistory"` + + MarginAssets []string `json:"marginAssets" yaml:"marginAssets"` + // Since is the date where you want to start syncing data Since *types.LooseFormatTime `json:"since,omitempty"` @@ -298,6 +399,38 @@ func (c *Config) YAML() ([]byte, error) { return buf.Bytes(), err } +func (c *Config) GetSignature() string { + var s string + + var ps []string + + // for single exchange strategy + if len(c.ExchangeStrategies) == 1 && len(c.CrossExchangeStrategies) == 0 { + mount := c.ExchangeStrategies[0].Mounts[0] + ps = append(ps, mount) + + strategy := c.ExchangeStrategies[0].Strategy + + id := strategy.ID() + ps = append(ps, id) + + if symbol, ok := dynamic.LookupSymbolField(reflect.ValueOf(strategy)); ok { + ps = append(ps, symbol) + } + } + + startTime := c.Backtest.StartTime.Time() + ps = append(ps, startTime.Format("2006-01-02")) + + if c.Backtest.EndTime != nil { + endTime := c.Backtest.EndTime.Time() + ps = append(ps, endTime.Format("2006-01-02")) + } + + s = strings.Join(ps, "_") + return s +} + type Stash map[string]interface{} func loadStash(config []byte) (Stash, error) { diff --git a/pkg/bbgo/config_test.go b/pkg/bbgo/config_test.go index f8603808ff..2598a52fc9 100644 --- a/pkg/bbgo/config_test.go +++ b/pkg/bbgo/config_test.go @@ -48,13 +48,6 @@ func TestLoadConfig(t *testing.T) { wantErr: false, f: func(t *testing.T, config *Config) { assert.NotNil(t, config.Notifications) - assert.NotNil(t, config.Notifications.SessionChannels) - assert.NotNil(t, config.Notifications.SymbolChannels) - assert.Equal(t, map[string]string{ - "^BTC": "#btc", - "^ETH": "#eth", - }, config.Notifications.SymbolChannels) - assert.NotNil(t, config.Notifications.Routing) assert.Equal(t, "#dev-bbgo", config.Notifications.Slack.DefaultChannel) assert.Equal(t, "#error", config.Notifications.Slack.ErrorChannel) }, @@ -82,16 +75,25 @@ func TestLoadConfig(t *testing.T) { assert.Equal(t, map[string]interface{}{ "sessions": map[string]interface{}{ "max": map[string]interface{}{ - "exchange": "max", - "envVarPrefix": "MAX", - "takerFeeRate": 0., - "makerFeeRate": 0., + "exchange": "max", + "envVarPrefix": "MAX", + "takerFeeRate": 0., + "makerFeeRate": 0., + "modifyOrderAmountForFee": false, }, "binance": map[string]interface{}{ - "exchange": "binance", - "envVarPrefix": "BINANCE", - "takerFeeRate": 0., - "makerFeeRate": 0., + "exchange": "binance", + "envVarPrefix": "BINANCE", + "takerFeeRate": 0., + "makerFeeRate": 0., + "modifyOrderAmountForFee": false, + }, + "ftx": map[string]interface{}{ + "exchange": "ftx", + "envVarPrefix": "FTX", + "takerFeeRate": 0., + "makerFeeRate": 0., + "modifyOrderAmountForFee": true, }, }, "build": map[string]interface{}{ @@ -214,5 +216,59 @@ func TestLoadConfig(t *testing.T) { } }) } +} + +func TestSyncSymbol(t *testing.T) { + t.Run("symbol", func(t *testing.T) { + var ss []SyncSymbol + var err = yaml.Unmarshal([]byte(`- BTCUSDT`), &ss) + assert.NoError(t, err) + assert.Equal(t, []SyncSymbol{ + {Symbol: "BTCUSDT"}, + }, ss) + }) + + t.Run("session:symbol", func(t *testing.T) { + var ss []SyncSymbol + var err = yaml.Unmarshal([]byte(`- max:BTCUSDT`), &ss) + assert.NoError(t, err) + assert.Equal(t, []SyncSymbol{ + {Session: "max", Symbol: "BTCUSDT"}, + }, ss) + }) + + t.Run("object", func(t *testing.T) { + var ss []SyncSymbol + var err = yaml.Unmarshal([]byte(`- { session: "max", symbol: "BTCUSDT" }`), &ss) + assert.NoError(t, err) + assert.Equal(t, []SyncSymbol{ + {Session: "max", Symbol: "BTCUSDT"}, + }, ss) + }) +} + +func TestBackTestFeeMode(t *testing.T) { + var mode BacktestFeeMode + var err = yaml.Unmarshal([]byte(`quote`), &mode) + assert.NoError(t, err) + assert.Equal(t, BacktestFeeModeQuote, mode) +} + +func Test_categorizeSyncSymbol(t *testing.T) { + var ss []SyncSymbol + var err = yaml.Unmarshal([]byte(` +- BTCUSDT +- ETHUSDT +- max:MAXUSDT +- max:USDTTWD +- binance:BNBUSDT +`), &ss) + assert.NoError(t, err) + assert.NotEmpty(t, ss) + sm, rest := categorizeSyncSymbol(ss) + assert.NotEmpty(t, rest) + assert.NotEmpty(t, sm) + assert.Equal(t, []string{"MAXUSDT", "USDTTWD"}, sm["max"]) + assert.Equal(t, []string{"BNBUSDT"}, sm["binance"]) } diff --git a/pkg/bbgo/consts.go b/pkg/bbgo/consts.go new file mode 100644 index 0000000000..5a627b94b1 --- /dev/null +++ b/pkg/bbgo/consts.go @@ -0,0 +1,5 @@ +package bbgo + +import "github.com/c9s/bbgo/pkg/fixedpoint" + +var one = fixedpoint.One diff --git a/pkg/bbgo/context.go b/pkg/bbgo/context.go deleted file mode 100644 index 5c5a19364a..0000000000 --- a/pkg/bbgo/context.go +++ /dev/null @@ -1,10 +0,0 @@ -package bbgo - -import ( - "sync" -) - -// deprecated: legacy context struct -type Context struct { - sync.Mutex -} diff --git a/pkg/bbgo/doc.go b/pkg/bbgo/doc.go new file mode 100644 index 0000000000..ceb21a00ac --- /dev/null +++ b/pkg/bbgo/doc.go @@ -0,0 +1,3 @@ +// Package bbgo provides the core BBGO API for strategies + +package bbgo diff --git a/pkg/bbgo/environment.go b/pkg/bbgo/environment.go index 5c2353d70c..9f57420dbc 100644 --- a/pkg/bbgo/environment.go +++ b/pkg/bbgo/environment.go @@ -13,7 +13,6 @@ import ( "sync" "time" - "github.com/codingconcepts/env" "github.com/pkg/errors" "github.com/pquerna/otp" log "github.com/sirupsen/logrus" @@ -21,7 +20,7 @@ import ( "github.com/spf13/viper" "gopkg.in/tucnak/telebot.v2" - "github.com/c9s/bbgo/pkg/cmd/cmdutil" + "github.com/c9s/bbgo/pkg/exchange" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/interact" "github.com/c9s/bbgo/pkg/notifier/slacknotifier" @@ -37,6 +36,16 @@ func init() { rand.Seed(time.Now().UnixNano()) } +// IsBackTesting is a global variable that indicates the current environment is back-test or not. +var IsBackTesting = false + +var BackTestService *service.BacktestService + +func SetBackTesting(s *service.BacktestService) { + BackTestService = s + IsBackTesting = true +} + var LoadedExchangeStrategies = make(map[string]SingleExchangeStrategy) var LoadedCrossExchangeStrategies = make(map[string]CrossExchangeStrategy) @@ -69,20 +78,18 @@ const ( // Environment presents the real exchange data layer type Environment struct { - // Notifiability here for environment is for the streaming data notification - // note that, for back tests, we don't need notification. - Notifiability - - PersistenceServiceFacade *service.PersistenceServiceFacade - DatabaseService *service.DatabaseService - OrderService *service.OrderService - TradeService *service.TradeService - ProfitService *service.ProfitService - PositionService *service.PositionService - BacktestService *service.BacktestService - RewardService *service.RewardService - SyncService *service.SyncService - AccountService *service.AccountService + DatabaseService *service.DatabaseService + OrderService *service.OrderService + TradeService *service.TradeService + ProfitService *service.ProfitService + PositionService *service.PositionService + BacktestService *service.BacktestService + RewardService *service.RewardService + MarginService *service.MarginService + SyncService *service.SyncService + AccountService *service.AccountService + WithdrawService *service.WithdrawService + DepositService *service.DepositService // startTime is the time of start point (which is used in the backtest) startTime time.Time @@ -99,16 +106,15 @@ type Environment struct { } func NewEnvironment() *Environment { + + now := time.Now() return &Environment{ // default trade scan time - syncStartTime: time.Now().AddDate(-1, 0, 0), // defaults to sync from 1 year ago + syncStartTime: now.AddDate(-1, 0, 0), // defaults to sync from 1 year ago sessions: make(map[string]*ExchangeSession), - startTime: time.Now(), + startTime: now, syncStatus: SyncNotStarted, - PersistenceServiceFacade: &service.PersistenceServiceFacade{ - Memory: service.NewMemoryService(), - }, } } @@ -176,11 +182,14 @@ func (environ *Environment) ConfigureDatabaseDriver(ctx context.Context, driver environ.AccountService = &service.AccountService{DB: db} environ.ProfitService = &service.ProfitService{DB: db} environ.PositionService = &service.PositionService{DB: db} - + environ.MarginService = &service.MarginService{DB: db} + environ.WithdrawService = &service.WithdrawService{DB: db} + environ.DepositService = &service.DepositService{DB: db} environ.SyncService = &service.SyncService{ TradeService: environ.TradeService, OrderService: environ.OrderService, RewardService: environ.RewardService, + MarginService: environ.MarginService, WithdrawService: &service.WithdrawService{DB: db}, DepositService: &service.DepositService{DB: db}, } @@ -190,9 +199,6 @@ func (environ *Environment) ConfigureDatabaseDriver(ctx context.Context, driver // AddExchangeSession adds the existing exchange session or pre-created exchange session func (environ *Environment) AddExchangeSession(name string, session *ExchangeSession) *ExchangeSession { - // update Notifiability from the environment - session.Notifiability = environ.Notifiability - environ.sessions[name] = session return session } @@ -215,12 +221,12 @@ func (environ *Environment) ConfigureExchangeSessions(userConfig *Config) error func (environ *Environment) AddExchangesByViperKeys() error { for _, n := range types.SupportedExchanges { if viper.IsSet(string(n) + "-api-key") { - exchange, err := cmdutil.NewExchangeWithEnvVarPrefix(n, "") + ex, err := exchange.NewWithEnvVarPrefix(n, "") if err != nil { return err } - environ.AddExchange(n.String(), exchange) + environ.AddExchange(n.String(), ex) } } @@ -239,6 +245,10 @@ func (environ *Environment) AddExchangesFromSessionConfig(sessions map[string]*E return nil } +func (environ *Environment) IsBackTesting() bool { + return environ.BacktestService != nil +} + // Init prepares the data that will be used by the strategies func (environ *Environment) Init(ctx context.Context) (err error) { for n := range environ.sessions { @@ -265,182 +275,15 @@ func (environ *Environment) Start(ctx context.Context) (err error) { return } -func (environ *Environment) ConfigurePersistence(conf *PersistenceConfig) error { - if conf.Redis != nil { - if err := env.Set(conf.Redis); err != nil { - return err - } - - environ.PersistenceServiceFacade.Redis = service.NewRedisPersistenceService(conf.Redis) - } - - if conf.Json != nil { - if _, err := os.Stat(conf.Json.Directory); os.IsNotExist(err) { - if err2 := os.MkdirAll(conf.Json.Directory, 0777); err2 != nil { - log.WithError(err2).Errorf("can not create directory: %s", conf.Json.Directory) - return err2 - } - } - - environ.PersistenceServiceFacade.Json = &service.JsonPersistenceService{Directory: conf.Json.Directory} - } - - return nil -} - -// ConfigureNotificationRouting configures the notification rules -// for symbol-based routes, we should register the same symbol rules for each session. -// for session-based routes, we should set the fixed callbacks for each session -func (environ *Environment) ConfigureNotificationRouting(conf *NotificationConfig) error { - // configure routing here - if conf.SymbolChannels != nil { - environ.SymbolChannelRouter.AddRoute(conf.SymbolChannels) - } - if conf.SessionChannels != nil { - environ.SessionChannelRouter.AddRoute(conf.SessionChannels) - } - - if conf.Routing != nil { - // configure passive object notification routing - switch conf.Routing.Trade { - case "$silent": // silent, do not setup notification - - case "$session": - defaultTradeUpdateHandler := func(trade types.Trade) { - environ.Notify(&trade) - } - for name := range environ.sessions { - session := environ.sessions[name] - - // if we can route session name to channel successfully... - channel, ok := environ.SessionChannelRouter.Route(name) - if ok { - session.UserDataStream.OnTradeUpdate(func(trade types.Trade) { - environ.NotifyTo(channel, &trade) - }) - } else { - session.UserDataStream.OnTradeUpdate(defaultTradeUpdateHandler) - } - } - - case "$symbol": - // configure object routes for Trade - environ.ObjectChannelRouter.Route(func(obj interface{}) (channel string, ok bool) { - trade, matched := obj.(*types.Trade) - if !matched { - return - } - channel, ok = environ.SymbolChannelRouter.Route(trade.Symbol) - return - }) - - // use same handler for each session - handler := func(trade types.Trade) { - channel, ok := environ.RouteObject(&trade) - if ok { - environ.NotifyTo(channel, &trade) - } else { - environ.Notify(&trade) - } - } - for _, session := range environ.sessions { - session.UserDataStream.OnTradeUpdate(handler) - } - } - - switch conf.Routing.Order { - - case "$silent": // silent, do not setup notification - - case "$session": - defaultOrderUpdateHandler := func(order types.Order) { - text := util.Render(TemplateOrderReport, order) - environ.Notify(text, &order) - } - for name := range environ.sessions { - session := environ.sessions[name] - - // if we can route session name to channel successfully... - channel, ok := environ.SessionChannelRouter.Route(name) - if ok { - session.UserDataStream.OnOrderUpdate(func(order types.Order) { - text := util.Render(TemplateOrderReport, order) - environ.NotifyTo(channel, text, &order) - }) - } else { - session.UserDataStream.OnOrderUpdate(defaultOrderUpdateHandler) - } - } - - case "$symbol": - // add object route - environ.ObjectChannelRouter.Route(func(obj interface{}) (channel string, ok bool) { - order, matched := obj.(*types.Order) - if !matched { - return - } - channel, ok = environ.SymbolChannelRouter.Route(order.Symbol) - return - }) - - // use same handler for each session - handler := func(order types.Order) { - text := util.Render(TemplateOrderReport, order) - channel, ok := environ.RouteObject(&order) - if ok { - environ.NotifyTo(channel, text, &order) - } else { - environ.Notify(text, &order) - } - } - for _, session := range environ.sessions { - session.UserDataStream.OnOrderUpdate(handler) - } - } - - switch conf.Routing.SubmitOrder { - - case "$silent": // silent, do not setup notification - - case "$symbol": - // add object route - environ.ObjectChannelRouter.Route(func(obj interface{}) (channel string, ok bool) { - order, matched := obj.(*types.SubmitOrder) - if !matched { - return - } - - channel, ok = environ.SymbolChannelRouter.Route(order.Symbol) - return - }) - - } - - // currently, not used - // FIXME: this is causing cyclic import - /* - switch conf.Routing.PnL { - case "$symbol": - environ.ObjectChannelRouter.Route(func(obj interface{}) (channel string, ok bool) { - report, matched := obj.(*pnl.AverageCostPnlReport) - if !matched { - return - } - channel, ok = environ.SymbolChannelRouter.Route(report.Symbol) - return - }) - } - */ - - } - return nil -} - func (environ *Environment) SetStartTime(t time.Time) *Environment { environ.startTime = t return environ } +func (environ *Environment) StartTime() time.Time { + return environ.startTime +} + // SetSyncStartTime overrides the default trade scan time (-7 days) func (environ *Environment) SetSyncStartTime(t time.Time) *Environment { environ.syncStartTime = t @@ -569,84 +412,112 @@ func (environ *Environment) setSyncing(status SyncStatus) { environ.syncStatusMutex.Unlock() } -// Sync syncs all registered exchange sessions -func (environ *Environment) Sync(ctx context.Context, userConfig ...*Config) error { - if environ.SyncService == nil { - return nil +func (environ *Environment) syncWithUserConfig(ctx context.Context, userConfig *Config) error { + sessions := environ.sessions + selectedSessions := userConfig.Sync.Sessions + if len(selectedSessions) > 0 { + sessions = environ.SelectSessions(selectedSessions...) } - environ.syncMutex.Lock() - defer environ.syncMutex.Unlock() + since := time.Now().AddDate(0, -6, 0) + if userConfig.Sync.Since != nil { + since = userConfig.Sync.Since.Time() + } - environ.setSyncing(Syncing) - defer environ.setSyncing(SyncDone) + syncSymbolMap, restSymbols := categorizeSyncSymbol(userConfig.Sync.Symbols) + for _, session := range sessions { + syncSymbols := restSymbols + if ss, ok := syncSymbolMap[session.Name]; ok { + syncSymbols = append(syncSymbols, ss...) + } - // sync by the defined user config - if len(userConfig) > 0 && userConfig[0] != nil && userConfig[0].Sync != nil { - syncSymbols := userConfig[0].Sync.Symbols - sessions := environ.sessions - selectedSessions := userConfig[0].Sync.Sessions - if len(selectedSessions) > 0 { - sessions = environ.SelectSessions(selectedSessions...) + if err := environ.syncSession(ctx, session, syncSymbols...); err != nil { + return err } - for _, session := range sessions { - if err := environ.syncSession(ctx, session, syncSymbols...); err != nil { + if userConfig.Sync.DepositHistory { + if err := environ.SyncService.SyncDepositHistory(ctx, session.Exchange, since); err != nil { return err } + } - if userConfig[0].Sync.DepositHistory { - if err := environ.SyncService.SyncDepositHistory(ctx, session.Exchange); err != nil { - return err - } + if userConfig.Sync.WithdrawHistory { + if err := environ.SyncService.SyncWithdrawHistory(ctx, session.Exchange, since); err != nil { + return err } + } - if userConfig[0].Sync.WithdrawHistory { - if err := environ.SyncService.SyncWithdrawHistory(ctx, session.Exchange); err != nil { - return err - } + if userConfig.Sync.RewardHistory { + if err := environ.SyncService.SyncRewardHistory(ctx, session.Exchange, since); err != nil { + return err } + } - if userConfig[0].Sync.RewardHistory { - if err := environ.SyncService.SyncRewardHistory(ctx, session.Exchange); err != nil { - return err - } + if userConfig.Sync.MarginHistory { + if err := environ.SyncService.SyncMarginHistory(ctx, session.Exchange, + since, + userConfig.Sync.MarginAssets...); err != nil { + return err } } + } + + return nil +} + +// Sync syncs all registered exchange sessions +func (environ *Environment) Sync(ctx context.Context, userConfig ...*Config) error { + if environ.SyncService == nil { + return nil + } + // for paper trade mode, skip sync + if util.IsPaperTrade() { return nil } + environ.syncMutex.Lock() + defer environ.syncMutex.Unlock() + + environ.setSyncing(Syncing) + defer environ.setSyncing(SyncDone) + + // sync by the defined user config + if len(userConfig) > 0 && userConfig[0] != nil && userConfig[0].Sync != nil { + return environ.syncWithUserConfig(ctx, userConfig[0]) + } + // the default sync logics for _, session := range environ.sessions { if err := environ.syncSession(ctx, session); err != nil { return err } + } - if len(userConfig) == 0 || userConfig[0].Sync == nil { - continue - } - - if userConfig[0].Sync.DepositHistory { - if err := environ.SyncService.SyncDepositHistory(ctx, session.Exchange); err != nil { - return err - } - } + return nil +} - if userConfig[0].Sync.WithdrawHistory { - if err := environ.SyncService.SyncWithdrawHistory(ctx, session.Exchange); err != nil { - return err - } - } +func (environ *Environment) RecordAsset(t time.Time, session *ExchangeSession, assets types.AssetMap) { + // skip for back-test + if environ.BacktestService != nil { + return + } - if userConfig[0].Sync.RewardHistory { - if err := environ.SyncService.SyncRewardHistory(ctx, session.Exchange); err != nil { - return err - } - } + if environ.DatabaseService == nil || environ.AccountService == nil { + return } - return nil + if err := environ.AccountService.InsertAsset( + t, + session.Name, + session.ExchangeName, + session.SubAccount, + session.Margin, + session.IsolatedMargin, + session.IsolatedMarginSymbol, + assets); err != nil { + log.WithError(err).Errorf("can not insert asset record") + } } func (environ *Environment) RecordPosition(position *types.Position, trade types.Trade, profit *types.Profit) { @@ -659,12 +530,15 @@ func (environ *Environment) RecordPosition(position *types.Position, trade types return } - if position.Strategy == "" && profit.Strategy != "" { - position.Strategy = profit.Strategy - } + // set profit info to position + if profit != nil { + if position.Strategy == "" && profit.Strategy != "" { + position.Strategy = profit.Strategy + } - if position.StrategyInstanceID == "" && profit.StrategyInstanceID != "" { - position.StrategyInstanceID = profit.StrategyInstanceID + if position.StrategyInstanceID == "" && profit.StrategyInstanceID != "" { + position.StrategyInstanceID = profit.StrategyInstanceID + } } if profit != nil { @@ -679,17 +553,6 @@ func (environ *Environment) RecordPosition(position *types.Position, trade types log.WithError(err).Errorf("can not insert position record") } } - - // if: - // 1) we are not using sync - // 2) and not sync-ing trades from the user data stream - if environ.TradeService != nil && (environ.syncConfig == nil || - (environ.syncConfig.UserDataStream == nil) || - (environ.syncConfig.UserDataStream != nil && !environ.syncConfig.UserDataStream.Trades)) { - if err := environ.TradeService.Insert(trade); err != nil { - log.WithError(err).Errorf("can not insert trade record: %+v", trade) - } - } } func (environ *Environment) RecordProfit(profit types.Profit) { @@ -736,24 +599,12 @@ func (environ *Environment) syncSession(ctx context.Context, session *ExchangeSe } func (environ *Environment) ConfigureNotificationSystem(userConfig *Config) error { - environ.Notifiability = Notifiability{ - SymbolChannelRouter: NewPatternChannelRouter(nil), - SessionChannelRouter: NewPatternChannelRouter(nil), - ObjectChannelRouter: NewObjectChannelRouter(), - } - // setup default notification config if userConfig.Notifications == nil { - userConfig.Notifications = &NotificationConfig{ - Routing: &SlackNotificationRouting{ - Trade: "$session", - Order: "$silent", - SubmitOrder: "$silent", - }, - } + userConfig.Notifications = &NotificationConfig{} } - var persistence = environ.PersistenceServiceFacade.Get() + var persistence = persistenceServiceFacade.Get() err := environ.setupInteraction(persistence) if err != nil { @@ -775,7 +626,7 @@ func (environ *Environment) ConfigureNotificationSystem(userConfig *Config) erro } if userConfig.Notifications != nil { - if err := environ.ConfigureNotificationRouting(userConfig.Notifications); err != nil { + if err := environ.ConfigureNotification(userConfig.Notifications); err != nil { return err } } @@ -783,6 +634,32 @@ func (environ *Environment) ConfigureNotificationSystem(userConfig *Config) erro return nil } +func (environ *Environment) ConfigureNotification(config *NotificationConfig) error { + if config.Switches != nil { + if config.Switches.Trade { + tradeHandler := func(trade types.Trade) { + Notify(trade) + } + + for _, session := range environ.sessions { + session.UserDataStream.OnTradeUpdate(tradeHandler) + } + } + + if config.Switches.OrderUpdate { + orderUpdateHandler := func(order types.Order) { + Notify(order) + } + + for _, session := range environ.sessions { + session.UserDataStream.OnOrderUpdate(orderUpdateHandler) + } + } + } + + return nil +} + // getAuthStoreID returns the authentication store id // if telegram bot token is defined, the bot id will be used. // if not, env var $USER will be used. @@ -803,7 +680,7 @@ func getAuthStoreID() string { } func (environ *Environment) setupInteraction(persistence service.PersistenceService) error { - var otpQRCodeImagePath = fmt.Sprintf("otp.png") + var otpQRCodeImagePath = "otp.png" var key *otp.Key var keyURL string var authStore = environ.getAuthStore(persistence) @@ -872,10 +749,11 @@ func (environ *Environment) setupInteraction(persistence service.PersistenceServ } interact.AddCustomInteraction(&interact.AuthInteract{ - Strict: authStrict, - Mode: authMode, - Token: authToken, // can be empty string here - OneTimePasswordKey: key, // can be nil here + Strict: authStrict, + Mode: authMode, + Token: authToken, // can be empty string here + // pragma: allowlist nextline secret + OneTimePasswordKey: key, // can be nil here }) return nil } @@ -898,7 +776,7 @@ func (environ *Environment) setupSlack(userConfig *Config, slackToken string, pe // app-level token (for specific api) slackAppToken := viper.GetString("slack-app-token") - if !strings.HasPrefix(slackAppToken, "xapp-") { + if len(slackAppToken) > 0 && !strings.HasPrefix(slackAppToken, "xapp-") { log.Errorf("SLACK_APP_TOKEN must have the prefix \"xapp-\".") return } @@ -912,7 +790,10 @@ func (environ *Environment) setupSlack(userConfig *Config, slackToken string, pe var slackOpts = []slack.Option{ slack.OptionLog(stdlog.New(os.Stdout, "api: ", stdlog.Lshortfile|stdlog.LstdFlags)), - slack.OptionAppLevelToken(slackAppToken), + } + + if len(slackAppToken) > 0 { + slackOpts = append(slackOpts, slack.OptionAppLevelToken(slackAppToken)) } if b, ok := util.GetEnvVarBool("DEBUG_SLACK"); ok { @@ -922,7 +803,7 @@ func (environ *Environment) setupSlack(userConfig *Config, slackToken string, pe var client = slack.New(slackToken, slackOpts...) var notifier = slacknotifier.New(client, conf.DefaultChannel) - environ.AddNotifier(notifier) + Notification.AddNotifier(notifier) // allocate a store, so that we can save the chatID for the owner var messenger = interact.NewSlack(client) @@ -974,7 +855,9 @@ func (environ *Environment) setupTelegram(userConfig *Config, telegramBotToken s } var notifier = telegramnotifier.New(bot, opts...) - environ.Notifiability.AddNotifier(notifier) + Notification.AddNotifier(notifier) + + log.AddHook(telegramnotifier.NewLogHook(notifier)) // allocate a store, so that we can save the chatID for the owner var messenger = interact.NewTelegram(bot) @@ -1065,7 +948,7 @@ To scan your OTP QR code, please run the following command: open %s -For telegram, send the auth command with the generated one-time password to the bbo bot you created to enable the notification: +For telegram, send the auth command with the generated one-time password to the bbgo bot you created to enable the notification: /auth diff --git a/pkg/bbgo/exit.go b/pkg/bbgo/exit.go new file mode 100644 index 0000000000..e4a59e6e08 --- /dev/null +++ b/pkg/bbgo/exit.go @@ -0,0 +1,138 @@ +package bbgo + +import ( + "bytes" + "encoding/json" + "reflect" + + "github.com/pkg/errors" + + "github.com/c9s/bbgo/pkg/dynamic" +) + +type ExitMethodSet []ExitMethod + +func (s *ExitMethodSet) SetAndSubscribe(session *ExchangeSession, parent interface{}) { + for i := range *s { + m := (*s)[i] + + // manually inherit configuration from strategy + m.Inherit(parent) + m.Subscribe(session) + } +} + +func (s *ExitMethodSet) Bind(session *ExchangeSession, orderExecutor *GeneralOrderExecutor) { + for _, method := range *s { + method.Bind(session, orderExecutor) + } +} + +type ExitMethod struct { + RoiStopLoss *RoiStopLoss `json:"roiStopLoss"` + ProtectiveStopLoss *ProtectiveStopLoss `json:"protectiveStopLoss"` + RoiTakeProfit *RoiTakeProfit `json:"roiTakeProfit"` + TrailingStop *TrailingStop2 `json:"trailingStop"` + + // Exit methods for short positions + // ================================================= + LowerShadowTakeProfit *LowerShadowTakeProfit `json:"lowerShadowTakeProfit"` + CumulatedVolumeTakeProfit *CumulatedVolumeTakeProfit `json:"cumulatedVolumeTakeProfit"` + SupportTakeProfit *SupportTakeProfit `json:"supportTakeProfit"` +} + +func (e ExitMethod) String() string { + var buf bytes.Buffer + if e.RoiStopLoss != nil { + b, _ := json.Marshal(e.RoiStopLoss) + buf.WriteString("roiStopLoss: " + string(b) + ", ") + } + + if e.ProtectiveStopLoss != nil { + b, _ := json.Marshal(e.ProtectiveStopLoss) + buf.WriteString("protectiveStopLoss: " + string(b) + ", ") + } + + if e.RoiTakeProfit != nil { + b, _ := json.Marshal(e.RoiTakeProfit) + buf.WriteString("rioTakeProft: " + string(b) + ", ") + } + + if e.LowerShadowTakeProfit != nil { + b, _ := json.Marshal(e.LowerShadowTakeProfit) + buf.WriteString("lowerShadowTakeProft: " + string(b) + ", ") + } + + if e.CumulatedVolumeTakeProfit != nil { + b, _ := json.Marshal(e.CumulatedVolumeTakeProfit) + buf.WriteString("cumulatedVolumeTakeProfit: " + string(b) + ", ") + } + + if e.TrailingStop != nil { + b, _ := json.Marshal(e.TrailingStop) + buf.WriteString("trailingStop: " + string(b) + ", ") + } + + if e.SupportTakeProfit != nil { + b, _ := json.Marshal(e.SupportTakeProfit) + buf.WriteString("supportTakeProfit: " + string(b) + ", ") + } + + return buf.String() +} + +// Inherit is used for inheriting properties from the given strategy struct +// for example, some exit method requires the default interval and symbol name from the strategy param object +func (m *ExitMethod) Inherit(parent interface{}) { + // we need to pass some information from the strategy configuration to the exit methods, like symbol, interval and window + rt := reflect.TypeOf(m).Elem() + rv := reflect.ValueOf(m).Elem() + for j := 0; j < rv.NumField(); j++ { + if !rt.Field(j).IsExported() { + continue + } + + fieldValue := rv.Field(j) + if fieldValue.Kind() == reflect.Ptr && fieldValue.IsNil() { + continue + } + + dynamic.InheritStructValues(fieldValue.Interface(), parent) + } +} + +func (m *ExitMethod) Subscribe(session *ExchangeSession) { + if err := dynamic.CallStructFieldsMethod(m, "Subscribe", session); err != nil { + panic(errors.Wrap(err, "dynamic Subscribe call failed")) + } +} + +func (m *ExitMethod) Bind(session *ExchangeSession, orderExecutor *GeneralOrderExecutor) { + if m.ProtectiveStopLoss != nil { + m.ProtectiveStopLoss.Bind(session, orderExecutor) + } + + if m.RoiStopLoss != nil { + m.RoiStopLoss.Bind(session, orderExecutor) + } + + if m.RoiTakeProfit != nil { + m.RoiTakeProfit.Bind(session, orderExecutor) + } + + if m.LowerShadowTakeProfit != nil { + m.LowerShadowTakeProfit.Bind(session, orderExecutor) + } + + if m.CumulatedVolumeTakeProfit != nil { + m.CumulatedVolumeTakeProfit.Bind(session, orderExecutor) + } + + if m.SupportTakeProfit != nil { + m.SupportTakeProfit.Bind(session, orderExecutor) + } + + if m.TrailingStop != nil { + m.TrailingStop.Bind(session, orderExecutor) + } +} diff --git a/pkg/bbgo/exit_cumulated_volume_take_profit.go b/pkg/bbgo/exit_cumulated_volume_take_profit.go new file mode 100644 index 0000000000..440dedf492 --- /dev/null +++ b/pkg/bbgo/exit_cumulated_volume_take_profit.go @@ -0,0 +1,96 @@ +package bbgo + +import ( + "context" + + log "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +// CumulatedVolumeTakeProfit +// This exit method cumulate the volume by N bars, if the cumulated volume exceeded a threshold, then we take profit. +// +// To query the historical quote volume, use the following query: +// +// > SELECT start_time, `interval`, quote_volume, open, close FROM binance_klines WHERE symbol = 'ETHUSDT' AND `interval` = '5m' ORDER BY quote_volume DESC LIMIT 20; +// +type CumulatedVolumeTakeProfit struct { + Symbol string `json:"symbol"` + + types.IntervalWindow + + Ratio fixedpoint.Value `json:"ratio"` + MinQuoteVolume fixedpoint.Value `json:"minQuoteVolume"` + + session *ExchangeSession + orderExecutor *GeneralOrderExecutor +} + +func (s *CumulatedVolumeTakeProfit) Bind(session *ExchangeSession, orderExecutor *GeneralOrderExecutor) { + s.session = session + s.orderExecutor = orderExecutor + + position := orderExecutor.Position() + + store, _ := session.MarketDataStore(position.Symbol) + + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { + closePrice := kline.Close + openPrice := kline.Open + if position.IsClosed() || position.IsDust(closePrice) { + return + } + + roi := position.ROI(closePrice) + if roi.Sign() < 0 { + return + } + + klines, ok := store.KLinesOfInterval(s.Interval) + if !ok { + log.Warnf("history kline not found") + return + } + + if len(*klines) < s.Window { + return + } + + var cbv = fixedpoint.Zero + var cqv = fixedpoint.Zero + for i := 0; i < s.Window; i++ { + last := (*klines)[len(*klines)-1-i] + cqv = cqv.Add(last.QuoteVolume) + cbv = cbv.Add(last.Volume) + } + + if cqv.Compare(s.MinQuoteVolume) < 0 { + return + } + + // If the closed price is below the open price, it means the sell taker is still strong. + if closePrice.Compare(openPrice) < 0 { + log.Infof("[CumulatedVolumeTakeProfit] closePrice %f is below openPrice %f, skip taking profit", closePrice.Float64(), openPrice.Float64()) + return + } + + upperShadow := kline.GetUpperShadowHeight() + lowerShadow := kline.GetLowerShadowHeight() + if upperShadow.Compare(lowerShadow) > 0 { + log.Infof("[CumulatedVolumeTakeProfit] upper shadow is longer than the lower shadow, skip taking profit") + return + } + + Notify("[CumulatedVolumeTakeProfit] %s TakeProfit triggered by cumulated volume (window: %d) %f > %f, price = %f", + position.Symbol, + s.Window, + cqv.Float64(), + s.MinQuoteVolume.Float64(), kline.Close.Float64()) + + if err := orderExecutor.ClosePosition(context.Background(), fixedpoint.One, "cumulatedVolumeTakeProfit"); err != nil { + log.WithError(err).Errorf("close position error") + } + })) +} diff --git a/pkg/bbgo/exit_lower_shadow_take_profit.go b/pkg/bbgo/exit_lower_shadow_take_profit.go new file mode 100644 index 0000000000..1ffae6c368 --- /dev/null +++ b/pkg/bbgo/exit_lower_shadow_take_profit.go @@ -0,0 +1,66 @@ +package bbgo + +import ( + "context" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +type LowerShadowTakeProfit struct { + // inherit from the strategy + types.IntervalWindow + + // inherit from the strategy + Symbol string `json:"symbol"` + + Ratio fixedpoint.Value `json:"ratio"` + session *ExchangeSession + orderExecutor *GeneralOrderExecutor +} + +func (s *LowerShadowTakeProfit) Subscribe(session *ExchangeSession) { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) +} + +func (s *LowerShadowTakeProfit) Bind(session *ExchangeSession, orderExecutor *GeneralOrderExecutor) { + s.session = session + s.orderExecutor = orderExecutor + + stdIndicatorSet := session.StandardIndicatorSet(s.Symbol) + ewma := stdIndicatorSet.EWMA(s.IntervalWindow) + + + position := orderExecutor.Position() + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { + closePrice := kline.Close + if position.IsClosed() || position.IsDust(closePrice) { + return + } + + roi := position.ROI(closePrice) + if roi.Sign() < 0 { + return + } + + if s.Ratio.IsZero() { + return + } + + // skip close price higher than the ewma + if closePrice.Float64() > ewma.Last() { + return + } + + if kline.GetLowerShadowHeight().Div(kline.Close).Compare(s.Ratio) > 0 { + Notify("%s TakeProfit triggered by shadow ratio %f, price = %f", + position.Symbol, + kline.GetLowerShadowRatio().Float64(), + kline.Close.Float64(), + kline) + + _ = orderExecutor.ClosePosition(context.Background(), fixedpoint.One) + return + } + })) +} diff --git a/pkg/bbgo/exit_protective_stop_loss.go b/pkg/bbgo/exit_protective_stop_loss.go new file mode 100644 index 0000000000..73a743cd38 --- /dev/null +++ b/pkg/bbgo/exit_protective_stop_loss.go @@ -0,0 +1,203 @@ +package bbgo + +import ( + "context" + + log "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +const enableMarketTradeStop = false + +// ProtectiveStopLoss provides a way to protect your profit but also keep a room for the price volatility +// Set ActivationRatio to 1% means if the price is away from your average cost by 1%, we will activate the protective stop loss +// and the StopLossRatio is the minimal profit ratio you want to keep for your position. +// If you set StopLossRatio to 0.1% and ActivationRatio to 1%, +// when the price goes away from your average cost by 1% and then goes back to below your (average_cost * (1 - 0.1%)) +// The stop will trigger. +type ProtectiveStopLoss struct { + Symbol string `json:"symbol"` + + // ActivationRatio is the trigger condition of this ROI protection stop loss + // When the price goes lower (for short position) with the ratio, the protection stop will be activated. + // This number should be positive to protect the profit + ActivationRatio fixedpoint.Value `json:"activationRatio"` + + // StopLossRatio is the ratio for stop loss. This number should be positive to protect the profit. + // negative ratio will cause loss. + StopLossRatio fixedpoint.Value `json:"stopLossRatio"` + + // PlaceStopOrder places the stop order on exchange and lock the balance + PlaceStopOrder bool `json:"placeStopOrder"` + + session *ExchangeSession + orderExecutor *GeneralOrderExecutor + stopLossPrice fixedpoint.Value + stopLossOrder *types.Order +} + +func (s *ProtectiveStopLoss) Subscribe(session *ExchangeSession) { + // use 1m kline to handle roi stop + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: types.Interval1m}) +} + +func (s *ProtectiveStopLoss) shouldActivate(position *types.Position, closePrice fixedpoint.Value) bool { + if position.IsLong() { + r := one.Add(s.ActivationRatio) + activationPrice := position.AverageCost.Mul(r) + return closePrice.Compare(activationPrice) > 0 + } else if position.IsShort() { + r := one.Sub(s.ActivationRatio) + activationPrice := position.AverageCost.Mul(r) + // for short position, if the close price is less than the activation price then this is a profit position. + return closePrice.Compare(activationPrice) < 0 + } + + return false +} + +func (s *ProtectiveStopLoss) placeStopOrder(ctx context.Context, position *types.Position, orderExecutor OrderExecutor) error { + if s.stopLossOrder != nil { + if err := orderExecutor.CancelOrders(ctx, *s.stopLossOrder); err != nil { + log.WithError(err).Errorf("failed to cancel stop limit order: %+v", s.stopLossOrder) + } + s.stopLossOrder = nil + } + + createdOrders, err := orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: position.Symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeStopLimit, + Quantity: position.GetQuantity(), + Price: s.stopLossPrice.Mul(one.Add(fixedpoint.NewFromFloat(0.005))), // +0.5% from the trigger price, slippage protection + StopPrice: s.stopLossPrice, + Market: position.Market, + Tag: "protectiveStopLoss", + MarginSideEffect: types.SideEffectTypeAutoRepay, + }) + + if len(createdOrders) > 0 { + s.stopLossOrder = &createdOrders[0] + } + return err +} + +func (s *ProtectiveStopLoss) shouldStop(closePrice fixedpoint.Value, position *types.Position) bool { + if s.stopLossPrice.IsZero() { + return false + } + + if position.IsShort() { + return closePrice.Compare(s.stopLossPrice) >= 0 + } else if position.IsLong() { + return closePrice.Compare(s.stopLossPrice) <= 0 + } + + return false +} + +func (s *ProtectiveStopLoss) Bind(session *ExchangeSession, orderExecutor *GeneralOrderExecutor) { + s.session = session + s.orderExecutor = orderExecutor + + orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { + if position.IsClosed() { + s.stopLossOrder = nil + s.stopLossPrice = fixedpoint.Zero + } + }) + + session.UserDataStream.OnOrderUpdate(func(order types.Order) { + if s.stopLossOrder == nil { + return + } + + if order.OrderID == s.stopLossOrder.OrderID { + switch order.Status { + case types.OrderStatusFilled, types.OrderStatusCanceled: + s.stopLossOrder = nil + s.stopLossPrice = fixedpoint.Zero + } + } + }) + + position := orderExecutor.Position() + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, func(kline types.KLine) { + isPositionOpened := !position.IsClosed() && !position.IsDust(kline.Close) + if isPositionOpened { + s.handleChange(context.Background(), position, kline.Close, s.orderExecutor) + } else { + s.stopLossPrice = fixedpoint.Zero + } + })) + + if !IsBackTesting && enableMarketTradeStop { + session.MarketDataStream.OnMarketTrade(func(trade types.Trade) { + if trade.Symbol != position.Symbol { + return + } + + if s.stopLossPrice.IsZero() || s.PlaceStopOrder { + return + } + + s.checkStopPrice(trade.Price, position) + }) + } +} + +func (s *ProtectiveStopLoss) handleChange(ctx context.Context, position *types.Position, closePrice fixedpoint.Value, orderExecutor *GeneralOrderExecutor) { + if s.stopLossOrder != nil { + // use RESTful to query the order status + // orderQuery := orderExecutor.Session().Exchange.(types.ExchangeOrderQueryService) + // order, err := orderQuery.QueryOrder(ctx, types.OrderQuery{ + // Symbol: s.stopLossOrder.Symbol, + // OrderID: strconv.FormatUint(s.stopLossOrder.OrderID, 10), + // }) + // if err != nil { + // log.WithError(err).Errorf("query order failed") + // } + } + + if s.stopLossPrice.IsZero() { + if s.shouldActivate(position, closePrice) { + // calculate stop loss price + if position.IsShort() { + s.stopLossPrice = position.AverageCost.Mul(one.Sub(s.StopLossRatio)) + } else if position.IsLong() { + s.stopLossPrice = position.AverageCost.Mul(one.Add(s.StopLossRatio)) + } + + Notify("[ProtectiveStopLoss] %s protection stop loss activated, current price = %f, average cost = %f, stop loss price = %f", + position.Symbol, closePrice.Float64(), position.AverageCost.Float64(), s.stopLossPrice.Float64()) + + if s.PlaceStopOrder { + if err := s.placeStopOrder(ctx, position, orderExecutor); err != nil { + log.WithError(err).Errorf("failed to place stop limit order") + } + return + } + } else { + // not activated, skip setup stop order + return + } + } + + // check stop price + s.checkStopPrice(closePrice, position) +} + +func (s *ProtectiveStopLoss) checkStopPrice(closePrice fixedpoint.Value, position *types.Position) { + if s.stopLossPrice.IsZero() { + return + } + + if s.shouldStop(closePrice, position) { + Notify("[ProtectiveStopLoss] protection stop order is triggered at price %f", closePrice.Float64(), position) + if err := s.orderExecutor.ClosePosition(context.Background(), one, "protectiveStopLoss"); err != nil { + log.WithError(err).Errorf("failed to close position") + } + } +} diff --git a/pkg/bbgo/exit_roi_stop_loss.go b/pkg/bbgo/exit_roi_stop_loss.go new file mode 100644 index 0000000000..6060022182 --- /dev/null +++ b/pkg/bbgo/exit_roi_stop_loss.go @@ -0,0 +1,56 @@ +package bbgo + +import ( + "context" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +type RoiStopLoss struct { + Symbol string + Percentage fixedpoint.Value `json:"percentage"` + + session *ExchangeSession + orderExecutor *GeneralOrderExecutor +} + +func (s *RoiStopLoss) Subscribe(session *ExchangeSession) { + // use 1m kline to handle roi stop + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: types.Interval1m}) +} + +func (s *RoiStopLoss) Bind(session *ExchangeSession, orderExecutor *GeneralOrderExecutor) { + s.session = session + s.orderExecutor = orderExecutor + + position := orderExecutor.Position() + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, func(kline types.KLine) { + s.checkStopPrice(kline.Close, position) + })) + + if !IsBackTesting && enableMarketTradeStop { + session.MarketDataStream.OnMarketTrade(func(trade types.Trade) { + if trade.Symbol != position.Symbol { + return + } + + s.checkStopPrice(trade.Price, position) + }) + } +} + +func (s *RoiStopLoss) checkStopPrice(closePrice fixedpoint.Value, position *types.Position) { + if position.IsClosed() || position.IsDust(closePrice) { + return + } + + roi := position.ROI(closePrice) + // logrus.Debugf("ROIStopLoss: price=%f roi=%s stop=%s", closePrice.Float64(), roi.Percentage(), s.Percentage.Neg().Percentage()) + if roi.Compare(s.Percentage.Neg()) < 0 { + // stop loss + Notify("[RoiStopLoss] %s stop loss triggered by ROI %s/%s, price: %f", position.Symbol, roi.Percentage(), s.Percentage.Neg().Percentage(), closePrice.Float64()) + _ = s.orderExecutor.ClosePosition(context.Background(), fixedpoint.One, "roiStopLoss") + return + } +} diff --git a/pkg/bbgo/exit_roi_take_profit.go b/pkg/bbgo/exit_roi_take_profit.go new file mode 100644 index 0000000000..d6af7d7713 --- /dev/null +++ b/pkg/bbgo/exit_roi_take_profit.go @@ -0,0 +1,43 @@ +package bbgo + +import ( + "context" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +// RoiTakeProfit force takes the profit by the given ROI percentage. +type RoiTakeProfit struct { + Symbol string `json:"symbol"` + Percentage fixedpoint.Value `json:"percentage"` + + session *ExchangeSession + orderExecutor *GeneralOrderExecutor +} + +func (s *RoiTakeProfit) Subscribe(session *ExchangeSession) { + // use 1m kline to handle roi stop + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: types.Interval1m}) +} + +func (s *RoiTakeProfit) Bind(session *ExchangeSession, orderExecutor *GeneralOrderExecutor) { + s.session = session + s.orderExecutor = orderExecutor + + position := orderExecutor.Position() + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, func(kline types.KLine) { + closePrice := kline.Close + if position.IsClosed() || position.IsDust(closePrice) { + return + } + + roi := position.ROI(closePrice) + if roi.Compare(s.Percentage) >= 0 { + // stop loss + Notify("[RoiTakeProfit] %s take profit is triggered by ROI %s/%s, price: %f", position.Symbol, roi.Percentage(), s.Percentage.Percentage(), kline.Close.Float64()) + _ = orderExecutor.ClosePosition(context.Background(), fixedpoint.One, "roiTakeProfit") + return + } + })) +} diff --git a/pkg/bbgo/exit_support_take_profit.go b/pkg/bbgo/exit_support_take_profit.go new file mode 100644 index 0000000000..7998ce39db --- /dev/null +++ b/pkg/bbgo/exit_support_take_profit.go @@ -0,0 +1,132 @@ +package bbgo + +import ( + "context" + + "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +// SupportTakeProfit finds the previous support price and take profit at the previous low. +type SupportTakeProfit struct { + Symbol string + types.IntervalWindow + + Ratio fixedpoint.Value `json:"ratio"` + + pivot *indicator.PivotLow + orderExecutor *GeneralOrderExecutor + session *ExchangeSession + activeOrders *ActiveOrderBook + currentSupportPrice fixedpoint.Value + + triggeredPrices []fixedpoint.Value +} + +func (s *SupportTakeProfit) Subscribe(session *ExchangeSession) { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) +} + +func (s *SupportTakeProfit) Bind(session *ExchangeSession, orderExecutor *GeneralOrderExecutor) { + s.session = session + s.orderExecutor = orderExecutor + s.activeOrders = NewActiveOrderBook(s.Symbol) + session.UserDataStream.OnOrderUpdate(func(order types.Order) { + if s.activeOrders.Exists(order) { + if !s.currentSupportPrice.IsZero() { + s.triggeredPrices = append(s.triggeredPrices, s.currentSupportPrice) + } + } + }) + s.activeOrders.BindStream(session.UserDataStream) + + position := orderExecutor.Position() + + s.pivot = session.StandardIndicatorSet(s.Symbol).PivotLow(s.IntervalWindow) + + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { + if !s.updateSupportPrice(kline.Close) { + return + } + + if !position.IsOpened(kline.Close) { + logrus.Infof("position is not opened, skip updating support take profit order") + return + } + + buyPrice := s.currentSupportPrice.Mul(one.Add(s.Ratio)) + quantity := position.GetQuantity() + ctx := context.Background() + + if err := orderExecutor.GracefulCancelActiveOrderBook(ctx, s.activeOrders); err != nil { + logrus.WithError(err).Errorf("cancel order failed") + } + + Notify("placing %s take profit order at price %f", s.Symbol, buyPrice.Float64()) + createdOrders, err := orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: s.Symbol, + Type: types.OrderTypeLimitMaker, + Side: types.SideTypeBuy, + Price: buyPrice, + Quantity: quantity, + Tag: "supportTakeProfit", + MarginSideEffect: types.SideEffectTypeAutoRepay, + }) + + if err != nil { + logrus.WithError(err).Errorf("can not submit orders: %+v", createdOrders) + } + + s.activeOrders.Add(createdOrders...) + })) +} + +func (s *SupportTakeProfit) updateSupportPrice(closePrice fixedpoint.Value) bool { + logrus.Infof("[supportTakeProfit] lows: %v", s.pivot.Values) + + groupDistance := 0.01 + minDistance := 0.05 + supportPrices := findPossibleSupportPrices(closePrice.Float64()*(1.0-minDistance), groupDistance, s.pivot.Values) + if len(supportPrices) == 0 { + return false + } + + logrus.Infof("[supportTakeProfit] found possible support prices: %v", supportPrices) + + // nextSupportPrice are sorted in increasing order + nextSupportPrice := fixedpoint.NewFromFloat(supportPrices[len(supportPrices)-1]) + + // it's price that we have been used to take profit + for _, p := range s.triggeredPrices { + var l = p.Mul(one.Sub(fixedpoint.NewFromFloat(0.01))) + var h = p.Mul(one.Add(fixedpoint.NewFromFloat(0.01))) + if p.Compare(l) > 0 && p.Compare(h) < 0 { + return false + } + } + + currentBuyPrice := s.currentSupportPrice.Mul(one.Add(s.Ratio)) + + if s.currentSupportPrice.IsZero() { + logrus.Infof("setup next support take profit price at %f", nextSupportPrice.Float64()) + s.currentSupportPrice = nextSupportPrice + return true + } + + // the close price is already lower than the support price, than we should update + if closePrice.Compare(currentBuyPrice) < 0 || nextSupportPrice.Compare(s.currentSupportPrice) > 0 { + logrus.Infof("setup next support take profit price at %f", nextSupportPrice.Float64()) + s.currentSupportPrice = nextSupportPrice + return true + } + + return false +} + +func findPossibleSupportPrices(closePrice float64, groupDistance float64, lows []float64) []float64 { + return floats.Group(floats.Lower(lows, closePrice), groupDistance) +} diff --git a/pkg/bbgo/exit_test.go b/pkg/bbgo/exit_test.go new file mode 100644 index 0000000000..12fda5bec5 --- /dev/null +++ b/pkg/bbgo/exit_test.go @@ -0,0 +1,8 @@ +package bbgo + +import "testing" + +func TestExitMethod(t *testing.T) { + em := &ExitMethod{} + em.Subscribe(&ExchangeSession{}) +} diff --git a/pkg/bbgo/exit_trailing_stop.go b/pkg/bbgo/exit_trailing_stop.go new file mode 100644 index 0000000000..6ba12ca34f --- /dev/null +++ b/pkg/bbgo/exit_trailing_stop.go @@ -0,0 +1,185 @@ +package bbgo + +import ( + "context" + "fmt" + + log "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +type TrailingStop2 struct { + Symbol string + + // CallbackRate is the callback rate from the previous high price + CallbackRate fixedpoint.Value `json:"callbackRate,omitempty"` + + ActivationRatio fixedpoint.Value `json:"activationRatio,omitempty"` + + // ClosePosition is a percentage of the position to be closed + ClosePosition fixedpoint.Value `json:"closePosition,omitempty"` + + // MinProfit is the percentage of the minimum profit ratio. + // Stop order will be activated only when the price reaches above this threshold. + MinProfit fixedpoint.Value `json:"minProfit,omitempty"` + + // Interval is the time resolution to update the stop order + // KLine per Interval will be used for updating the stop order + Interval types.Interval `json:"interval,omitempty"` + + Side types.SideType `json:"side,omitempty"` + + latestHigh fixedpoint.Value + + // activated: when the price reaches the min profit price, we set the activated to true to enable trailing stop + activated bool + + // private fields + session *ExchangeSession + orderExecutor *GeneralOrderExecutor +} + +func (s *TrailingStop2) Subscribe(session *ExchangeSession) { + // use 1m kline to handle roi stop + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) +} + +func (s *TrailingStop2) Bind(session *ExchangeSession, orderExecutor *GeneralOrderExecutor) { + s.session = session + s.orderExecutor = orderExecutor + s.latestHigh = fixedpoint.Zero + + position := orderExecutor.Position() + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { + if err := s.checkStopPrice(kline.Close, position); err != nil { + log.WithError(err).Errorf("error") + } + })) + + if !IsBackTesting && enableMarketTradeStop { + session.MarketDataStream.OnMarketTrade(func(trade types.Trade) { + if trade.Symbol != position.Symbol { + return + } + + if err := s.checkStopPrice(trade.Price, position); err != nil { + log.WithError(err).Errorf("error") + } + }) + } +} + +// getRatio returns the ratio between the price and the average cost of the position +func (s *TrailingStop2) getRatio(price fixedpoint.Value, position *types.Position) (fixedpoint.Value, error) { + switch s.Side { + case types.SideTypeBuy: + // for short position, it's: + // (avg_cost - price) / avg_cost + return position.AverageCost.Sub(price).Div(position.AverageCost), nil + case types.SideTypeSell: + return price.Sub(position.AverageCost).Div(position.AverageCost), nil + default: + if position.IsLong() { + return price.Sub(position.AverageCost).Div(position.AverageCost), nil + } else if position.IsShort() { + return position.AverageCost.Sub(price).Div(position.AverageCost), nil + } + } + + return fixedpoint.Zero, fmt.Errorf("unexpected side type: %v", s.Side) +} + +func (s *TrailingStop2) checkStopPrice(price fixedpoint.Value, position *types.Position) error { + if position.IsClosed() || position.IsDust(price) { + return nil + } + + if !s.MinProfit.IsZero() { + // check if we have the minimal profit + roi := position.ROI(price) + if roi.Compare(s.MinProfit) >= 0 { + Notify("[trailingStop] activated: ROI %f > minimal profit ratio %f", roi.Float64(), s.MinProfit.Float64()) + s.activated = true + } + } else if !s.ActivationRatio.IsZero() { + ratio, err := s.getRatio(price, position) + if err != nil { + return err + } + + if ratio.Compare(s.ActivationRatio) >= 0 { + s.activated = true + } + } + + // update the latest high for the sell order, or the latest low for the buy order + if s.latestHigh.IsZero() { + s.latestHigh = price + } else { + switch s.Side { + case types.SideTypeBuy: + s.latestHigh = fixedpoint.Min(price, s.latestHigh) + case types.SideTypeSell: + s.latestHigh = fixedpoint.Max(price, s.latestHigh) + default: + if position.IsLong() { + s.latestHigh = fixedpoint.Max(price, s.latestHigh) + } else if position.IsShort() { + s.latestHigh = fixedpoint.Min(price, s.latestHigh) + } + } + } + + if !s.activated { + return nil + } + + switch s.Side { + case types.SideTypeBuy: + change := price.Sub(s.latestHigh).Div(s.latestHigh) + if change.Compare(s.CallbackRate) >= 0 { + // submit order + return s.triggerStop(price) + } + case types.SideTypeSell: + change := s.latestHigh.Sub(price).Div(s.latestHigh) + if change.Compare(s.CallbackRate) >= 0 { + // submit order + return s.triggerStop(price) + } + default: + if position.IsLong() { + change := s.latestHigh.Sub(price).Div(s.latestHigh) + if change.Compare(s.CallbackRate) >= 0 { + // submit order + return s.triggerStop(price) + } + } else if position.IsShort() { + change := price.Sub(s.latestHigh).Div(s.latestHigh) + if change.Compare(s.CallbackRate) >= 0 { + // submit order + return s.triggerStop(price) + } + } + } + + return nil +} + +func (s *TrailingStop2) triggerStop(price fixedpoint.Value) error { + // reset activated flag + defer func() { + s.activated = false + s.latestHigh = fixedpoint.Zero + }() + Notify("[TrailingStop] %s stop loss triggered. price: %f callback rate: %f", s.Symbol, price.Float64(), s.CallbackRate.Float64()) + ctx := context.Background() + p := fixedpoint.One + if !s.ClosePosition.IsZero() { + p = s.ClosePosition + } + + return s.orderExecutor.ClosePosition(ctx, p, "trailingStop") +} diff --git a/pkg/bbgo/exit_trailing_stop_test.go b/pkg/bbgo/exit_trailing_stop_test.go new file mode 100644 index 0000000000..42bd37e88d --- /dev/null +++ b/pkg/bbgo/exit_trailing_stop_test.go @@ -0,0 +1,184 @@ +package bbgo + +import ( + "testing" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/types/mocks" +) + +// getTestMarket returns the BTCUSDT market information +// for tests, we always use BTCUSDT +func getTestMarket() types.Market { + market := types.Market{ + Symbol: "BTCUSDT", + PricePrecision: 8, + VolumePrecision: 8, + QuoteCurrency: "USDT", + BaseCurrency: "BTC", + MinNotional: fixedpoint.MustNewFromString("0.001"), + MinAmount: fixedpoint.MustNewFromString("10.0"), + MinQuantity: fixedpoint.MustNewFromString("0.001"), + } + return market +} + +func TestTrailingStop_ShortPosition(t *testing.T) { + market := getTestMarket() + + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockEx := mocks.NewMockExchange(mockCtrl) + mockEx.EXPECT().NewStream().Return(&types.StandardStream{}).Times(2) + mockEx.EXPECT().SubmitOrder(gomock.Any(), types.SubmitOrder{ + Symbol: "BTCUSDT", + Side: types.SideTypeBuy, + Type: types.OrderTypeMarket, + Market: market, + Quantity: fixedpoint.NewFromFloat(1.0), + Tag: "trailingStop", + MarginSideEffect: types.SideEffectTypeAutoRepay, + }) + + session := NewExchangeSession("test", mockEx) + assert.NotNil(t, session) + + session.markets[market.Symbol] = market + + position := types.NewPositionFromMarket(market) + position.AverageCost = fixedpoint.NewFromFloat(20000.0) + position.Base = fixedpoint.NewFromFloat(-1.0) + + orderExecutor := NewGeneralOrderExecutor(session, "BTCUSDT", "test", "test-01", position) + + activationRatio := fixedpoint.NewFromFloat(0.01) + callbackRatio := fixedpoint.NewFromFloat(0.01) + stop := &TrailingStop2{ + Symbol: "BTCUSDT", + Interval: types.Interval1m, + Side: types.SideTypeBuy, + CallbackRate: callbackRatio, + ActivationRatio: activationRatio, + } + stop.Bind(session, orderExecutor) + + // the same price + currentPrice := fixedpoint.NewFromFloat(20000.0) + err := stop.checkStopPrice(currentPrice, position) + if assert.NoError(t, err) { + assert.False(t, stop.activated) + } + + // 20000 - 1% = 19800 + currentPrice = currentPrice.Mul(one.Sub(activationRatio)) + assert.Equal(t, fixedpoint.NewFromFloat(19800.0), currentPrice) + + err = stop.checkStopPrice(currentPrice, position) + if assert.NoError(t, err) { + assert.True(t, stop.activated) + assert.Equal(t, fixedpoint.NewFromFloat(19800.0), stop.latestHigh) + } + + // 19800 - 1% = 19602 + currentPrice = currentPrice.Mul(one.Sub(callbackRatio)) + assert.Equal(t, fixedpoint.NewFromFloat(19602.0), currentPrice) + + err = stop.checkStopPrice(currentPrice, position) + if assert.NoError(t, err) { + assert.Equal(t, fixedpoint.NewFromFloat(19602.0), stop.latestHigh) + assert.True(t, stop.activated) + } + + // 19602 + 1% = 19798.02 + currentPrice = currentPrice.Mul(one.Add(callbackRatio)) + assert.Equal(t, fixedpoint.NewFromFloat(19798.02), currentPrice) + + err = stop.checkStopPrice(currentPrice, position) + if assert.NoError(t, err) { + assert.Equal(t, fixedpoint.Zero, stop.latestHigh) + assert.False(t, stop.activated) + } +} + +func TestTrailingStop_LongPosition(t *testing.T) { + market := getTestMarket() + + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockEx := mocks.NewMockExchange(mockCtrl) + mockEx.EXPECT().NewStream().Return(&types.StandardStream{}).Times(2) + mockEx.EXPECT().SubmitOrder(gomock.Any(), types.SubmitOrder{ + Symbol: "BTCUSDT", + Side: types.SideTypeSell, + Type: types.OrderTypeMarket, + Market: market, + Quantity: fixedpoint.NewFromFloat(1.0), + Tag: "trailingStop", + MarginSideEffect: types.SideEffectTypeAutoRepay, + }) + + session := NewExchangeSession("test", mockEx) + assert.NotNil(t, session) + + session.markets[market.Symbol] = market + + position := types.NewPositionFromMarket(market) + position.AverageCost = fixedpoint.NewFromFloat(20000.0) + position.Base = fixedpoint.NewFromFloat(1.0) + + orderExecutor := NewGeneralOrderExecutor(session, "BTCUSDT", "test", "test-01", position) + + activationRatio := fixedpoint.NewFromFloat(0.01) + callbackRatio := fixedpoint.NewFromFloat(0.01) + stop := &TrailingStop2{ + Symbol: "BTCUSDT", + Interval: types.Interval1m, + Side: types.SideTypeSell, + CallbackRate: callbackRatio, + ActivationRatio: activationRatio, + } + stop.Bind(session, orderExecutor) + + // the same price + currentPrice := fixedpoint.NewFromFloat(20000.0) + err := stop.checkStopPrice(currentPrice, position) + if assert.NoError(t, err) { + assert.False(t, stop.activated) + } + + // 20000 + 1% = 20200 + currentPrice = currentPrice.Mul(one.Add(activationRatio)) + assert.Equal(t, fixedpoint.NewFromFloat(20200.0), currentPrice) + + err = stop.checkStopPrice(currentPrice, position) + if assert.NoError(t, err) { + assert.True(t, stop.activated) + assert.Equal(t, fixedpoint.NewFromFloat(20200.0), stop.latestHigh) + } + + // 20200 + 1% = 20402 + currentPrice = currentPrice.Mul(one.Add(callbackRatio)) + assert.Equal(t, fixedpoint.NewFromFloat(20402.0), currentPrice) + + err = stop.checkStopPrice(currentPrice, position) + if assert.NoError(t, err) { + assert.Equal(t, fixedpoint.NewFromFloat(20402.0), stop.latestHigh) + assert.True(t, stop.activated) + } + + // 20402 - 1% + currentPrice = currentPrice.Mul(one.Sub(callbackRatio)) + assert.Equal(t, fixedpoint.NewFromFloat(20197.98), currentPrice) + + err = stop.checkStopPrice(currentPrice, position) + if assert.NoError(t, err) { + assert.Equal(t, fixedpoint.Zero, stop.latestHigh) + assert.False(t, stop.activated) + } +} diff --git a/pkg/bbgo/graceful_callbacks.go b/pkg/bbgo/graceful_callbacks.go deleted file mode 100644 index 848b6fdcd3..0000000000 --- a/pkg/bbgo/graceful_callbacks.go +++ /dev/null @@ -1,18 +0,0 @@ -// Code generated by "callbackgen -type Graceful"; DO NOT EDIT. - -package bbgo - -import ( - "context" - "sync" -) - -func (g *Graceful) OnShutdown(cb func(ctx context.Context, wg *sync.WaitGroup)) { - g.shutdownCallbacks = append(g.shutdownCallbacks, cb) -} - -func (g *Graceful) EmitShutdown(ctx context.Context, wg *sync.WaitGroup) { - for _, cb := range g.shutdownCallbacks { - cb(ctx, wg) - } -} diff --git a/pkg/bbgo/graceful_shutdown.go b/pkg/bbgo/graceful_shutdown.go new file mode 100644 index 0000000000..5f6be55a01 --- /dev/null +++ b/pkg/bbgo/graceful_shutdown.go @@ -0,0 +1,34 @@ +package bbgo + +import ( + "context" + "sync" + + "github.com/sirupsen/logrus" +) + +type ShutdownHandler func(ctx context.Context, wg *sync.WaitGroup) + +//go:generate callbackgen -type GracefulShutdown +type GracefulShutdown struct { + shutdownCallbacks []ShutdownHandler +} + +// Shutdown is a blocking call to emit all shutdown callbacks at the same time. +// The context object here should not be canceled context, you need to create a todo context. +func (g *GracefulShutdown) Shutdown(shutdownCtx context.Context) { + var wg sync.WaitGroup + wg.Add(len(g.shutdownCallbacks)) + go g.EmitShutdown(shutdownCtx, &wg) + wg.Wait() +} + +func OnShutdown(ctx context.Context, f ShutdownHandler) { + isolatedContext := GetIsolationFromContext(ctx) + isolatedContext.gracefulShutdown.OnShutdown(f) +} + +func Shutdown(shutdownCtx context.Context) { + logrus.Infof("shutting down...") + defaultIsolation.gracefulShutdown.Shutdown(shutdownCtx) +} diff --git a/pkg/bbgo/gracefulshutdown_callbacks.go b/pkg/bbgo/gracefulshutdown_callbacks.go new file mode 100644 index 0000000000..560b956b0b --- /dev/null +++ b/pkg/bbgo/gracefulshutdown_callbacks.go @@ -0,0 +1,18 @@ +// Code generated by "callbackgen -type GracefulShutdown"; DO NOT EDIT. + +package bbgo + +import ( + "context" + "sync" +) + +func (g *GracefulShutdown) OnShutdown(cb ShutdownHandler) { + g.shutdownCallbacks = append(g.shutdownCallbacks, cb) +} + +func (g *GracefulShutdown) EmitShutdown(ctx context.Context, wg *sync.WaitGroup) { + for _, cb := range g.shutdownCallbacks { + cb(ctx, wg) + } +} diff --git a/pkg/bbgo/injection_test.go b/pkg/bbgo/injection_test.go deleted file mode 100644 index dd63703207..0000000000 --- a/pkg/bbgo/injection_test.go +++ /dev/null @@ -1,105 +0,0 @@ -package bbgo - -import ( - "reflect" - "testing" - "time" - - "github.com/stretchr/testify/assert" - - "github.com/c9s/bbgo/pkg/service" - "github.com/c9s/bbgo/pkg/types" -) - -func Test_injectField(t *testing.T) { - type TT struct { - TradeService *service.TradeService - } - - // only pointer object can be set. - var tt = &TT{} - - // get the value of the pointer, or it can not be set. - var rv = reflect.ValueOf(tt).Elem() - - _, ret := hasField(rv, "TradeService") - assert.True(t, ret) - - ts := &service.TradeService{} - - err := injectField(rv, "TradeService", ts, true) - assert.NoError(t, err) -} - -func Test_parseStructAndInject(t *testing.T) { - t.Run("skip nil", func(t *testing.T) { - ss := struct { - a int - Env *Environment - }{ - a: 1, - Env: nil, - } - err := parseStructAndInject(&ss, nil) - assert.NoError(t, err) - assert.Nil(t, ss.Env) - }) - t.Run("pointer", func(t *testing.T) { - ss := struct { - a int - Env *Environment - }{ - a: 1, - Env: nil, - } - err := parseStructAndInject(&ss, &Environment{}) - assert.NoError(t, err) - assert.NotNil(t, ss.Env) - }) - - t.Run("composition", func(t *testing.T) { - type TT struct { - *service.TradeService - } - ss := TT{} - err := parseStructAndInject(&ss, &service.TradeService{}) - assert.NoError(t, err) - assert.NotNil(t, ss.TradeService) - }) - - t.Run("struct", func(t *testing.T) { - ss := struct { - a int - Env Environment - }{ - a: 1, - } - err := parseStructAndInject(&ss, Environment{ - startTime: time.Now(), - }) - assert.NoError(t, err) - assert.NotEqual(t, time.Time{}, ss.Env.startTime) - }) - t.Run("interface/any", func(t *testing.T) { - ss := struct { - Any interface{} // anything - }{ - Any: nil, - } - err := parseStructAndInject(&ss, &Environment{ - startTime: time.Now(), - }) - assert.NoError(t, err) - assert.NotNil(t, ss.Any) - }) - t.Run("interface/stringer", func(t *testing.T) { - ss := struct { - Stringer types.Stringer // stringer interface - }{ - Stringer: nil, - } - err := parseStructAndInject(&ss, &types.Trade{}) - assert.NoError(t, err) - assert.NotNil(t, ss.Stringer) - }) -} diff --git a/pkg/bbgo/interact.go b/pkg/bbgo/interact.go index e65d36131c..d39cff261f 100644 --- a/pkg/bbgo/interact.go +++ b/pkg/bbgo/interact.go @@ -8,6 +8,7 @@ import ( "strconv" "strings" + "github.com/c9s/bbgo/pkg/dynamic" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/interact" "github.com/c9s/bbgo/pkg/types" @@ -17,6 +18,10 @@ type PositionCloser interface { ClosePosition(ctx context.Context, percentage fixedpoint.Value) error } +type PositionResetter interface { + ResetPosition() error +} + type PositionReader interface { CurrentPosition() *types.Position } @@ -27,12 +32,20 @@ type closePositionContext struct { percentage fixedpoint.Value } +type modifyPositionContext struct { + signature string + modifier *types.Position + target string + value fixedpoint.Value +} + type CoreInteraction struct { environment *Environment trader *Trader - exchangeStrategies map[string]SingleExchangeStrategy - closePositionContext closePositionContext + exchangeStrategies map[string]SingleExchangeStrategy + closePositionContext closePositionContext + modifyPositionContext modifyPositionContext } func NewCoreInteraction(environment *Environment, trader *Trader) *CoreInteraction { @@ -43,36 +56,25 @@ func NewCoreInteraction(environment *Environment, trader *Trader) *CoreInteracti } } -func getStrategySignatures(exchangeStrategies map[string]SingleExchangeStrategy) []string { - var strategies []string - for signature := range exchangeStrategies { - strategies = append(strategies, signature) - } - - return strategies +type SimpleInteraction struct { + Command string + Description string + F interface{} + Cmd *interact.Command } -func filterStrategyByInterface(checkInterface interface{}, exchangeStrategies map[string]SingleExchangeStrategy) (strategies map[string]SingleExchangeStrategy, found bool) { - found = false - rt := reflect.TypeOf(checkInterface).Elem() - for signature, strategy := range exchangeStrategies { - if ok := reflect.TypeOf(strategy).Implements(rt); ok { - strategies[signature] = strategy - found = true - } - } - - return strategies, found +func (it *SimpleInteraction) Commands(i *interact.Interact) { + it.Cmd = i.PrivateCommand(it.Command, it.Description, it.F) } -func generateStrategyButtonsForm(strategies map[string]SingleExchangeStrategy) [][3]string { - var buttonsForm [][3]string - signatures := getStrategySignatures(strategies) - for _, signature := range signatures { - buttonsForm = append(buttonsForm, [3]string{signature, "strategy", signature}) +func RegisterCommand(command, desc string, f interface{}) *interact.Command { + it := &SimpleInteraction{ + Command: command, + Description: desc, + F: f, } - - return buttonsForm + interact.AddCustomInteraction(it) + return it.Cmd } func (it *CoreInteraction) Commands(i *interact.Interact) { @@ -122,11 +124,11 @@ func (it *CoreInteraction) Commands(i *interact.Interact) { i.PrivateCommand("/position", "Show Position", func(reply interact.Reply) error { // it.trader.exchangeStrategies // send symbol options - if strategies, found := filterStrategyByInterface((*PositionReader)(nil), it.exchangeStrategies); found { + if strategies, err := filterStrategiesByInterface(it.exchangeStrategies, (*PositionReader)(nil)); err == nil && len(strategies) > 0 { reply.AddMultipleButtons(generateStrategyButtonsForm(strategies)) reply.Message("Please choose one strategy") } else { - reply.Message("No strategy supports PositionReader") + reply.Message("No any strategy supports PositionReader") } return nil }).Cycle(func(signature string, reply interact.Reply) error { @@ -143,16 +145,14 @@ func (it *CoreInteraction) Commands(i *interact.Interact) { } position := reader.CurrentPosition() - if position != nil { - reply.Send("Your current position:") - reply.Send(position.PlainText()) - - if position.Base.IsZero() { - reply.Message(fmt.Sprintf("Strategy %q has no opened position", signature)) - return fmt.Errorf("strategy %T has no opened position", strategy) - } + if position == nil || position.Base.IsZero() { + reply.Message(fmt.Sprintf("Strategy %q has no opened position", signature)) + return fmt.Errorf("strategy %T has no opened position", strategy) } + reply.Send("Your current position:") + reply.Message(position.PlainText()) + if kc, ok := reply.(interact.KeyboardController); ok { kc.RemoveKeyboard() } @@ -160,14 +160,57 @@ func (it *CoreInteraction) Commands(i *interact.Interact) { return nil }) + i.PrivateCommand("/resetposition", "Reset position", func(reply interact.Reply) error { + strategies, err := filterStrategies(it.exchangeStrategies, func(s SingleExchangeStrategy) bool { + return testInterface(s, (*PositionResetter)(nil)) || hasTypeField(s, &types.Position{}) + }) + + if err == nil && len(strategies) > 0 { + reply.AddMultipleButtons(generateStrategyButtonsForm(strategies)) + reply.Message("Please choose one strategy") + } else { + reply.Message("No strategy supports PositionResetter interface") + } + return nil + }).Next(func(signature string, reply interact.Reply) error { + strategy, ok := it.exchangeStrategies[signature] + if !ok { + reply.Message("Strategy not found") + return fmt.Errorf("strategy %s not found", signature) + } + + resetter, implemented := strategy.(PositionResetter) + if implemented { + return resetter.ResetPosition() + } + + reset := false + err := dynamic.IterateFields(strategy, func(ft reflect.StructField, fv reflect.Value) error { + posType := reflect.TypeOf(&types.Position{}) + if ft.Type == posType { + if pos, typeOk := fv.Interface().(*types.Position); typeOk { + pos.Reset() + reset = true + } + } + return nil + }) + + if reset { + reply.Message("Position is reset") + } + + return err + }) + i.PrivateCommand("/closeposition", "Close position", func(reply interact.Reply) error { // it.trader.exchangeStrategies // send symbol options - if strategies, found := filterStrategyByInterface((*PositionCloser)(nil), it.exchangeStrategies); found { + if strategies, err := filterStrategiesByInterface(it.exchangeStrategies, (*PositionCloser)(nil)); err == nil && len(strategies) > 0 { reply.AddMultipleButtons(generateStrategyButtonsForm(strategies)) reply.Message("Please choose one strategy") } else { - reply.Message("No strategy supports PositionCloser") + reply.Message("No strategy supports PositionCloser interface") } return nil }).Next(func(signature string, reply interact.Reply) error { @@ -232,7 +275,7 @@ func (it *CoreInteraction) Commands(i *interact.Interact) { i.PrivateCommand("/status", "Strategy Status", func(reply interact.Reply) error { // it.trader.exchangeStrategies // send symbol options - if strategies, found := filterStrategyByInterface((*StrategyStatusReader)(nil), it.exchangeStrategies); found { + if strategies, err := filterStrategiesByInterface(it.exchangeStrategies, (*StrategyStatusReader)(nil)); err == nil && len(strategies) > 0 { reply.AddMultipleButtons(generateStrategyButtonsForm(strategies)) reply.Message("Please choose a strategy") } else { @@ -240,6 +283,12 @@ func (it *CoreInteraction) Commands(i *interact.Interact) { } return nil }).Next(func(signature string, reply interact.Reply) error { + defer func() { + if kc, ok := reply.(interact.KeyboardController); ok { + kc.RemoveKeyboard() + } + }() + strategy, ok := it.exchangeStrategies[signature] if !ok { reply.Message("Strategy not found") @@ -254,10 +303,6 @@ func (it *CoreInteraction) Commands(i *interact.Interact) { status := controller.GetStatus() - if kc, ok := reply.(interact.KeyboardController); ok { - kc.RemoveKeyboard() - } - if status == types.StrategyStatusRunning { reply.Message(fmt.Sprintf("Strategy %s is running.", signature)) } else if status == types.StrategyStatusStopped { @@ -270,7 +315,7 @@ func (it *CoreInteraction) Commands(i *interact.Interact) { i.PrivateCommand("/suspend", "Suspend Strategy", func(reply interact.Reply) error { // it.trader.exchangeStrategies // send symbol options - if strategies, found := filterStrategyByInterface((*StrategyToggler)(nil), it.exchangeStrategies); found { + if strategies, err := filterStrategiesByInterface(it.exchangeStrategies, (*StrategyToggler)(nil)); err == nil && len(strategies) > 0 { reply.AddMultipleButtons(generateStrategyButtonsForm(strategies)) reply.Message("Please choose one strategy") } else { @@ -278,6 +323,12 @@ func (it *CoreInteraction) Commands(i *interact.Interact) { } return nil }).Next(func(signature string, reply interact.Reply) error { + defer func() { + if kc, ok := reply.(interact.KeyboardController); ok { + kc.RemoveKeyboard() + } + }() + strategy, ok := it.exchangeStrategies[signature] if !ok { reply.Message("Strategy not found") @@ -296,23 +347,19 @@ func (it *CoreInteraction) Commands(i *interact.Interact) { return nil } - if kc, ok := reply.(interact.KeyboardController); ok { - kc.RemoveKeyboard() - } - if err := controller.Suspend(); err != nil { reply.Message(fmt.Sprintf("Failed to suspend the strategy, %s", err.Error())) return err } - reply.Message(fmt.Sprintf("Strategy %s suspended.", signature)) + reply.Message(fmt.Sprintf("Strategy %s is now suspended.", signature)) return nil }) i.PrivateCommand("/resume", "Resume Strategy", func(reply interact.Reply) error { // it.trader.exchangeStrategies // send symbol options - if strategies, found := filterStrategyByInterface((*StrategyToggler)(nil), it.exchangeStrategies); found { + if strategies, err := filterStrategiesByInterface(it.exchangeStrategies, (*StrategyToggler)(nil)); err == nil && len(strategies) > 0 { reply.AddMultipleButtons(generateStrategyButtonsForm(strategies)) reply.Message("Please choose one strategy") } else { @@ -320,6 +367,12 @@ func (it *CoreInteraction) Commands(i *interact.Interact) { } return nil }).Next(func(signature string, reply interact.Reply) error { + defer func() { + if kc, ok := reply.(interact.KeyboardController); ok { + kc.RemoveKeyboard() + } + }() + strategy, ok := it.exchangeStrategies[signature] if !ok { reply.Message("Strategy not found") @@ -338,23 +391,19 @@ func (it *CoreInteraction) Commands(i *interact.Interact) { return nil } - if kc, ok := reply.(interact.KeyboardController); ok { - kc.RemoveKeyboard() - } - if err := controller.Resume(); err != nil { reply.Message(fmt.Sprintf("Failed to resume the strategy, %s", err.Error())) return err } - reply.Message(fmt.Sprintf("Strategy %s resumed.", signature)) + reply.Message(fmt.Sprintf("Strategy %s is now resumed.", signature)) return nil }) i.PrivateCommand("/emergencystop", "Emergency Stop", func(reply interact.Reply) error { // it.trader.exchangeStrategies // send symbol options - if strategies, found := filterStrategyByInterface((*EmergencyStopper)(nil), it.exchangeStrategies); found { + if strategies, err := filterStrategiesByInterface(it.exchangeStrategies, (*EmergencyStopper)(nil)); err == nil && len(strategies) > 0 { reply.AddMultipleButtons(generateStrategyButtonsForm(strategies)) reply.Message("Please choose one strategy") } else { @@ -386,6 +435,80 @@ func (it *CoreInteraction) Commands(i *interact.Interact) { reply.Message(fmt.Sprintf("Strategy %s stopped and the position closed.", signature)) return nil }) + + // Position updater + i.PrivateCommand("/modifyposition", "Modify Strategy Position", func(reply interact.Reply) error { + // it.trader.exchangeStrategies + // send symbol options + if strategies, err := filterStrategiesByField(it.exchangeStrategies, "Position", reflect.TypeOf(&types.Position{})); err == nil && len(strategies) > 0 { + reply.AddMultipleButtons(generateStrategyButtonsForm(strategies)) + reply.Message("Please choose one strategy") + } else { + reply.Message("No strategy supports Position Modify") + } + return nil + }).Next(func(signature string, reply interact.Reply) error { + strategy, ok := it.exchangeStrategies[signature] + if !ok { + reply.Message("Strategy not found") + return fmt.Errorf("strategy %s not found", signature) + } + + r := reflect.ValueOf(strategy).Elem() + f := r.FieldByName("Position") + positionModifier, implemented := f.Interface().(*types.Position) + if !implemented { + reply.Message(fmt.Sprintf("Strategy %s does not support Position Modify", signature)) + return fmt.Errorf("strategy %s does not implement Position Modify", signature) + } + + it.modifyPositionContext.modifier = positionModifier + it.modifyPositionContext.signature = signature + + reply.Message("Please choose what you want to change") + reply.AddButton("base", "Base", "base") + reply.AddButton("quote", "Quote", "quote") + reply.AddButton("cost", "Average Cost", "cost") + + return nil + }).Next(func(target string, reply interact.Reply) error { + if target != "base" && target != "quote" && target != "cost" { + reply.Message(fmt.Sprintf("%q is not a valid target string", target)) + return fmt.Errorf("%q is not a valid target string", target) + } + + it.modifyPositionContext.target = target + + reply.Message("Enter the amount to change") + + return nil + }).Next(func(valueStr string, reply interact.Reply) error { + value, err := fixedpoint.NewFromString(valueStr) + if err != nil { + reply.Message(fmt.Sprintf("%q is not a valid value string", valueStr)) + return err + } + + if kc, ok := reply.(interact.KeyboardController); ok { + kc.RemoveKeyboard() + } + + if it.modifyPositionContext.target == "base" { + err = it.modifyPositionContext.modifier.ModifyBase(value) + } else if it.modifyPositionContext.target == "quote" { + err = it.modifyPositionContext.modifier.ModifyQuote(value) + } else if it.modifyPositionContext.target == "cost" { + err = it.modifyPositionContext.modifier.ModifyAverageCost(value) + } + + if err != nil { + reply.Message(fmt.Sprintf("Failed to modify position of the strategy, %s", err.Error())) + return err + } + + reply.Message(fmt.Sprintf("Position of strategy %s modified.", it.modifyPositionContext.signature)) + return nil + }) } func (it *CoreInteraction) Initialize() error { @@ -404,20 +527,21 @@ func (it *CoreInteraction) Initialize() error { return nil } +// getStrategySignature returns strategy instance unique signature func getStrategySignature(strategy SingleExchangeStrategy) (string, error) { + // Returns instance ID + var signature = dynamic.CallID(strategy) + if signature != "" { + return signature, nil + } + + // Use reflect to build instance signature rv := reflect.ValueOf(strategy).Elem() if rv.Kind() != reflect.Struct { return "", fmt.Errorf("strategy %T instance is not a struct", strategy) } - var signature = path.Base(rv.Type().PkgPath()) - - var id = strategy.ID() - - if !strings.EqualFold(id, signature) { - signature += "." + strings.ToLower(id) - } - + signature = path.Base(rv.Type().PkgPath()) for i := 0; i < rv.NumField(); i++ { field := rv.Field(i) fieldName := rv.Type().Field(i).Name @@ -444,3 +568,68 @@ func parseFloatPercent(s string, bitSize int) (f float64, err error) { } return f / 100.0, nil } + +func getStrategySignatures(exchangeStrategies map[string]SingleExchangeStrategy) []string { + var strategies []string + for signature := range exchangeStrategies { + strategies = append(strategies, signature) + } + + return strategies +} + +// filterStrategies filters the exchange strategies by a filter tester function +// if filter() returns true, the strategy will be added to the returned map. +func filterStrategies(exchangeStrategies map[string]SingleExchangeStrategy, filter func(s SingleExchangeStrategy) bool) (map[string]SingleExchangeStrategy, error) { + retStrategies := make(map[string]SingleExchangeStrategy) + for signature, strategy := range exchangeStrategies { + if ok := filter(strategy); ok { + retStrategies[signature] = strategy + } + } + + return retStrategies, nil +} + +func hasTypeField(obj interface{}, typ interface{}) bool { + targetType := reflect.TypeOf(typ) + found := false + _ = dynamic.IterateFields(obj, func(ft reflect.StructField, fv reflect.Value) error { + if fv.Type() == targetType { + found = true + } + + return nil + }) + return found +} + +func testInterface(obj interface{}, checkType interface{}) bool { + rt := reflect.TypeOf(checkType).Elem() + return reflect.TypeOf(obj).Implements(rt) +} + +func filterStrategiesByInterface(exchangeStrategies map[string]SingleExchangeStrategy, checkInterface interface{}) (map[string]SingleExchangeStrategy, error) { + rt := reflect.TypeOf(checkInterface).Elem() + return filterStrategies(exchangeStrategies, func(s SingleExchangeStrategy) bool { + return reflect.TypeOf(s).Implements(rt) + }) +} + +func filterStrategiesByField(exchangeStrategies map[string]SingleExchangeStrategy, fieldName string, fieldType reflect.Type) (map[string]SingleExchangeStrategy, error) { + return filterStrategies(exchangeStrategies, func(s SingleExchangeStrategy) bool { + r := reflect.ValueOf(s).Elem() + f := r.FieldByName(fieldName) + return !f.IsZero() && f.Type() == fieldType + }) +} + +func generateStrategyButtonsForm(strategies map[string]SingleExchangeStrategy) [][3]string { + var buttonsForm [][3]string + signatures := getStrategySignatures(strategies) + for _, signature := range signatures { + buttonsForm = append(buttonsForm, [3]string{signature, "strategy", signature}) + } + + return buttonsForm +} diff --git a/pkg/bbgo/interact_modify.go b/pkg/bbgo/interact_modify.go new file mode 100644 index 0000000000..9ebc970f93 --- /dev/null +++ b/pkg/bbgo/interact_modify.go @@ -0,0 +1,66 @@ +package bbgo + +import ( + "encoding/json" + "fmt" + "reflect" + + "github.com/c9s/bbgo/pkg/dynamic" + "github.com/c9s/bbgo/pkg/interact" + "github.com/c9s/bbgo/pkg/util" + log "github.com/sirupsen/logrus" +) + +func RegisterModifier(s interface{}) { + val := reflect.ValueOf(s) + if val.Type().Kind() == util.Pointer { + val = val.Elem() + } + var targetName string + var currVal interface{} + var mapping map[string]string + // currently we only allow users to modify the first layer of fields + RegisterCommand("/modify", "Modify config", func(reply interact.Reply) { + reply.Message("Please choose the field name in config to modify:") + mapping = make(map[string]string) + dynamic.GetModifiableFields(val, func(tagName, name string) { + mapping[tagName] = name + reply.AddButton(tagName, tagName, tagName) + }) + }).Next(func(target string, reply interact.Reply) error { + targetName = mapping[target] + field, ok := dynamic.GetModifiableField(val, targetName) + if !ok { + reply.Message(fmt.Sprintf("target %s is not modifiable", targetName)) + return fmt.Errorf("target %s is not modifiable", targetName) + } + currVal = field.Interface() + if e, err := json.Marshal(currVal); err == nil { + currVal = string(e) + } + reply.Message(fmt.Sprintf("Please enter the new value, current value: %v", currVal)) + return nil + }).Next(func(value string, reply interact.Reply) { + log.Infof("try to modify from %s to %s", currVal, value) + if kc, ok := reply.(interact.KeyboardController); ok { + kc.RemoveKeyboard() + } + field, ok := dynamic.GetModifiableField(val, targetName) + if !ok { + reply.Message(fmt.Sprintf("target %s is not modifiable", targetName)) + return + } + x := reflect.New(field.Type()) + xi := x.Interface() + if err := json.Unmarshal([]byte(value), &xi); err != nil { + reply.Message(fmt.Sprintf("fail to unmarshal the value: %s, err: %v", value, err)) + return + } + field.Set(x.Elem()) + newVal := field.Interface() + if e, err := json.Marshal(value); err == nil { + newVal = string(e) + } + reply.Message(fmt.Sprintf("update to %v successfully", newVal)) + }) +} diff --git a/pkg/bbgo/interact_test.go b/pkg/bbgo/interact_test.go index 2f03b7ae8f..7423b7a45a 100644 --- a/pkg/bbgo/interact_test.go +++ b/pkg/bbgo/interact_test.go @@ -2,19 +2,32 @@ package bbgo import ( "context" + "fmt" "testing" "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" ) type myStrategy struct { - Symbol string `json:"symbol"` + Symbol string `json:"symbol"` + Position *types.Position } -func (m myStrategy) ID() string { +func (m *myStrategy) ID() string { return "mystrategy" } +func (m *myStrategy) InstanceID() string { + return fmt.Sprintf("%s:%s", m.ID(), m.Symbol) +} + +func (m *myStrategy) ClosePosition(ctx context.Context, percentage fixedpoint.Value) error { + return nil +} + func (m *myStrategy) Run(ctx context.Context, orderExecutor OrderExecutor, session *ExchangeSession) error { return nil } @@ -24,5 +37,22 @@ func Test_getStrategySignature(t *testing.T) { Symbol: "BTCUSDT", }) assert.NoError(t, err) - assert.Equal(t, "bbgo.mystrategy.BTCUSDT", signature) + assert.Equal(t, "mystrategy:BTCUSDT", signature) +} + +func Test_hasTypeField(t *testing.T) { + s := &myStrategy{ + Symbol: "BTCUSDT", + } + ok := hasTypeField(s, &types.Position{}) + assert.True(t, ok) +} + +func Test_testInterface(t *testing.T) { + s := &myStrategy{ + Symbol: "BTCUSDT", + } + + ok := testInterface(s, (*PositionCloser)(nil)) + assert.True(t, ok) } diff --git a/pkg/bbgo/isolation.go b/pkg/bbgo/isolation.go new file mode 100644 index 0000000000..df3d43ae07 --- /dev/null +++ b/pkg/bbgo/isolation.go @@ -0,0 +1,56 @@ +package bbgo + +import ( + "context" + + "github.com/c9s/bbgo/pkg/service" +) + +const IsolationContextKey = "bbgo" + +var defaultIsolation = NewDefaultIsolation() + +type Isolation struct { + gracefulShutdown GracefulShutdown + persistenceServiceFacade *service.PersistenceServiceFacade +} + +func NewDefaultIsolation() *Isolation { + return &Isolation{ + gracefulShutdown: GracefulShutdown{}, + persistenceServiceFacade: persistenceServiceFacade, + } +} + +func NewIsolation(persistenceFacade *service.PersistenceServiceFacade) *Isolation { + return &Isolation{ + gracefulShutdown: GracefulShutdown{}, + persistenceServiceFacade: persistenceFacade, + } +} + +func GetIsolationFromContext(ctx context.Context) *Isolation { + isolatedContext, ok := ctx.Value(IsolationContextKey).(*Isolation) + if ok { + return isolatedContext + } + + return defaultIsolation +} + +// NewTodoContextWithExistingIsolation creates a new context object with the existing isolation of the parent context. +func NewTodoContextWithExistingIsolation(parent context.Context) context.Context { + isolatedContext := GetIsolationFromContext(parent) + todo := context.WithValue(context.TODO(), IsolationContextKey, isolatedContext) + return todo +} + +// NewContextWithIsolation creates a new context from the parent context with a custom isolation +func NewContextWithIsolation(parent context.Context, isolation *Isolation) context.Context { + return context.WithValue(parent, IsolationContextKey, isolation) +} + +// NewContextWithDefaultIsolation creates a new context from the parent context with a default isolation +func NewContextWithDefaultIsolation(parent context.Context) context.Context { + return context.WithValue(parent, IsolationContextKey, defaultIsolation) +} diff --git a/pkg/bbgo/isolation_test.go b/pkg/bbgo/isolation_test.go new file mode 100644 index 0000000000..bd0688ef09 --- /dev/null +++ b/pkg/bbgo/isolation_test.go @@ -0,0 +1,24 @@ +package bbgo + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetIsolationFromContext(t *testing.T) { + ctx := context.Background() + isolation := GetIsolationFromContext(ctx) + assert.NotNil(t, isolation) + assert.NotNil(t, isolation.persistenceServiceFacade) + assert.NotNil(t, isolation.gracefulShutdown) +} + +func TestNewDefaultIsolation(t *testing.T) { + isolation := NewDefaultIsolation() + assert.NotNil(t, isolation) + assert.NotNil(t, isolation.persistenceServiceFacade) + assert.NotNil(t, isolation.gracefulShutdown) + assert.Equal(t, persistenceServiceFacade, isolation.persistenceServiceFacade) +} diff --git a/pkg/bbgo/localactiveorderbook_callbacks.go b/pkg/bbgo/localactiveorderbook_callbacks.go deleted file mode 100644 index cd58cb5d29..0000000000 --- a/pkg/bbgo/localactiveorderbook_callbacks.go +++ /dev/null @@ -1,17 +0,0 @@ -// Code generated by "callbackgen -type LocalActiveOrderBook"; DO NOT EDIT. - -package bbgo - -import ( - "github.com/c9s/bbgo/pkg/types" -) - -func (b *LocalActiveOrderBook) OnFilled(cb func(o types.Order)) { - b.filledCallbacks = append(b.filledCallbacks, cb) -} - -func (b *LocalActiveOrderBook) EmitFilled(o types.Order) { - for _, cb := range b.filledCallbacks { - cb(o) - } -} diff --git a/pkg/bbgo/log.go b/pkg/bbgo/log.go new file mode 100644 index 0000000000..f30d11b655 --- /dev/null +++ b/pkg/bbgo/log.go @@ -0,0 +1 @@ +package bbgo diff --git a/pkg/bbgo/marketdatastore.go b/pkg/bbgo/marketdatastore.go index 03f17d1f24..11f06902ee 100644 --- a/pkg/bbgo/marketdatastore.go +++ b/pkg/bbgo/marketdatastore.go @@ -5,7 +5,7 @@ import "github.com/c9s/bbgo/pkg/types" const MaxNumOfKLines = 5_000 const MaxNumOfKLinesTruncate = 100 -// MarketDataStore receives and maintain the public market data +// MarketDataStore receives and maintain the public market data of a single symbol //go:generate callbackgen -type MarketDataStore type MarketDataStore struct { Symbol string @@ -14,6 +14,7 @@ type MarketDataStore struct { KLineWindows map[types.Interval]*types.KLineWindow `json:"-"` kLineWindowUpdateCallbacks []func(interval types.Interval, klines types.KLineWindow) + kLineClosedCallbacks []func(k types.KLine) } func NewMarketDataStore(symbol string) *MarketDataStore { @@ -47,18 +48,19 @@ func (store *MarketDataStore) handleKLineClosed(kline types.KLine) { store.AddKLine(kline) } -func (store *MarketDataStore) AddKLine(kline types.KLine) { - window, ok := store.KLineWindows[kline.Interval] +func (store *MarketDataStore) AddKLine(k types.KLine) { + window, ok := store.KLineWindows[k.Interval] if !ok { var tmp = make(types.KLineWindow, 0, 1000) - store.KLineWindows[kline.Interval] = &tmp + store.KLineWindows[k.Interval] = &tmp window = &tmp } - window.Add(kline) + window.Add(k) if len(*window) > MaxNumOfKLines { *window = (*window)[MaxNumOfKLinesTruncate-1:] } - store.EmitKLineWindowUpdate(kline.Interval, *window) + store.EmitKLineClosed(k) + store.EmitKLineWindowUpdate(k.Interval, *window) } diff --git a/pkg/bbgo/marketdatastore_callbacks.go b/pkg/bbgo/marketdatastore_callbacks.go index 4acaccb103..0cc2fd8a79 100644 --- a/pkg/bbgo/marketdatastore_callbacks.go +++ b/pkg/bbgo/marketdatastore_callbacks.go @@ -15,3 +15,13 @@ func (store *MarketDataStore) EmitKLineWindowUpdate(interval types.Interval, kli cb(interval, klines) } } + +func (store *MarketDataStore) OnKLineClosed(cb func(k types.KLine)) { + store.kLineClosedCallbacks = append(store.kLineClosedCallbacks, cb) +} + +func (store *MarketDataStore) EmitKLineClosed(k types.KLine) { + for _, cb := range store.kLineClosedCallbacks { + cb(k) + } +} diff --git a/pkg/bbgo/notifier.go b/pkg/bbgo/notification.go similarity index 57% rename from pkg/bbgo/notifier.go rename to pkg/bbgo/notification.go index ea18572b87..2d8420297e 100644 --- a/pkg/bbgo/notifier.go +++ b/pkg/bbgo/notification.go @@ -1,8 +1,40 @@ package bbgo +import ( + "bytes" + + "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/util" +) + +var Notification = &Notifiability{ + SymbolChannelRouter: NewPatternChannelRouter(nil), + SessionChannelRouter: NewPatternChannelRouter(nil), + ObjectChannelRouter: NewObjectChannelRouter(), +} + +func Notify(obj interface{}, args ...interface{}) { + Notification.Notify(obj, args...) +} + +func NotifyTo(channel string, obj interface{}, args ...interface{}) { + Notification.NotifyTo(channel, obj, args...) +} + +func SendPhoto(buffer *bytes.Buffer) { + Notification.SendPhoto(buffer) +} + +func SendPhotoTo(channel string, buffer *bytes.Buffer) { + Notification.SendPhotoTo(channel, buffer) +} + type Notifier interface { NotifyTo(channel string, obj interface{}, args ...interface{}) Notify(obj interface{}, args ...interface{}) + SendPhotoTo(channel string, buffer *bytes.Buffer) + SendPhoto(buffer *bytes.Buffer) } type NullNotifier struct{} @@ -11,6 +43,10 @@ func (n *NullNotifier) NotifyTo(channel string, obj interface{}, args ...interfa func (n *NullNotifier) Notify(obj interface{}, args ...interface{}) {} +func (n *NullNotifier) SendPhoto(buffer *bytes.Buffer) {} + +func (n *NullNotifier) SendPhotoTo(channel string, buffer *bytes.Buffer) {} + type Notifiability struct { notifiers []Notifier SessionChannelRouter *PatternChannelRouter `json:"-"` @@ -48,6 +84,11 @@ func (m *Notifiability) AddNotifier(notifier Notifier) { } func (m *Notifiability) Notify(obj interface{}, args ...interface{}) { + if str, ok := obj.(string); ok { + simpleArgs := util.FilterSimpleArgs(args) + logrus.Infof(str, simpleArgs...) + } + for _, n := range m.notifiers { n.Notify(obj, args...) } @@ -58,3 +99,15 @@ func (m *Notifiability) NotifyTo(channel string, obj interface{}, args ...interf n.NotifyTo(channel, obj, args...) } } + +func (m *Notifiability) SendPhoto(buffer *bytes.Buffer) { + for _, n := range m.notifiers { + n.SendPhoto(buffer) + } +} + +func (m *Notifiability) SendPhotoTo(channel string, buffer *bytes.Buffer) { + for _, n := range m.notifiers { + n.SendPhotoTo(channel, buffer) + } +} diff --git a/pkg/bbgo/order_execution.go b/pkg/bbgo/order_execution.go index 6092efd35c..1caab6b5be 100644 --- a/pkg/bbgo/order_execution.go +++ b/pkg/bbgo/order_execution.go @@ -6,6 +6,7 @@ import ( "github.com/pkg/errors" log "github.com/sirupsen/logrus" + "go.uber.org/multierr" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" @@ -14,11 +15,6 @@ import ( type OrderExecutor interface { SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) (createdOrders types.OrderSlice, err error) CancelOrders(ctx context.Context, orders ...types.Order) error - - OnTradeUpdate(cb func(trade types.Trade)) - OnOrderUpdate(cb func(order types.Order)) - EmitTradeUpdate(trade types.Trade) - EmitOrderUpdate(order types.Order) } type OrderExecutionRouter interface { @@ -28,8 +24,6 @@ type OrderExecutionRouter interface { } type ExchangeOrderExecutionRouter struct { - Notifiability - sessions map[string]*ExchangeSession executors map[string]OrderExecutor } @@ -44,12 +38,47 @@ func (e *ExchangeOrderExecutionRouter) SubmitOrdersTo(ctx context.Context, sessi return nil, fmt.Errorf("exchange session %s not found", session) } - formattedOrders, err := formatOrders(es, orders) + formattedOrders, err := es.FormatOrders(orders) if err != nil { return nil, err } - return es.Exchange.SubmitOrders(ctx, formattedOrders...) + createdOrders, _, err := BatchPlaceOrder(ctx, es.Exchange, formattedOrders...) + return createdOrders, err +} + +func BatchRetryPlaceOrder(ctx context.Context, exchange types.Exchange, errIdx []int, submitOrders ...types.SubmitOrder) (types.OrderSlice, error) { + var createdOrders types.OrderSlice + var err error + for _, idx := range errIdx { + createdOrder, err2 := exchange.SubmitOrder(ctx, submitOrders[idx]) + if err2 != nil { + err = multierr.Append(err, err2) + } else if createdOrder != nil { + createdOrders = append(createdOrders, *createdOrder) + } + } + + return createdOrders, err +} + +// BatchPlaceOrder +func BatchPlaceOrder(ctx context.Context, exchange types.Exchange, submitOrders ...types.SubmitOrder) (types.OrderSlice, []int, error) { + var createdOrders types.OrderSlice + var err error + var errIndexes []int + for i, submitOrder := range submitOrders { + createdOrder, err2 := exchange.SubmitOrder(ctx, submitOrder) + if err2 != nil { + err = multierr.Append(err, err2) + errIndexes = append(errIndexes, i) + } else if createdOrder != nil { + createdOrder.Tag = submitOrder.Tag + createdOrders = append(createdOrders, *createdOrder) + } + } + + return createdOrders, errIndexes, err } func (e *ExchangeOrderExecutionRouter) CancelOrdersTo(ctx context.Context, session string, orders ...types.Order) error { @@ -69,8 +98,6 @@ func (e *ExchangeOrderExecutionRouter) CancelOrdersTo(ctx context.Context, sessi type ExchangeOrderExecutor struct { // MinQuoteBalance fixedpoint.Value `json:"minQuoteBalance,omitempty" yaml:"minQuoteBalance,omitempty"` - Notifiability `json:"-" yaml:"-"` - Session *ExchangeSession `json:"-" yaml:"-"` // private trade update callbacks @@ -80,39 +107,18 @@ type ExchangeOrderExecutor struct { orderUpdateCallbacks []func(order types.Order) } -func (e *ExchangeOrderExecutor) notifySubmitOrders(orders ...types.SubmitOrder) { - for _, order := range orders { - // pass submit order as an interface object. - channel, ok := e.RouteObject(&order) - if ok { - e.NotifyTo(channel, ":memo: Submitting %s %s %s order with quantity: %f @ %f", order.Symbol, order.Type, order.Side, order.Quantity, order.Price, &order) - } else { - e.Notify(":memo: Submitting %s %s %s order with quantity: %f @ %f", order.Symbol, order.Type, order.Side, order.Quantity, order.Price, &order) - } - } -} - func (e *ExchangeOrderExecutor) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) (types.OrderSlice, error) { - formattedOrders, err := formatOrders(e.Session, orders) + formattedOrders, err := e.Session.FormatOrders(orders) if err != nil { return nil, err } for _, order := range formattedOrders { - // pass submit order as an interface object. - channel, ok := e.RouteObject(&order) - if ok { - e.NotifyTo(channel, ":memo: Submitting %s %s %s order with quantity: %f", order.Symbol, order.Type, order.Side, order.Quantity, order) - } else { - e.Notify(":memo: Submitting %s %s %s order with quantity: %f", order.Symbol, order.Type, order.Side, order.Quantity, order) - } - log.Infof("submitting order: %s", order.String()) } - e.notifySubmitOrders(formattedOrders...) - - return e.Session.Exchange.SubmitOrders(ctx, formattedOrders...) + createdOrders, _, err := BatchPlaceOrder(ctx, e.Session.Exchange, formattedOrders...) + return createdOrders, err } func (e *ExchangeOrderExecutor) CancelOrders(ctx context.Context, orders ...types.Order) error { @@ -314,18 +320,6 @@ func (c *BasicRiskController) ProcessOrders(session *ExchangeSession, orders ... return outOrders, nil } -func formatOrders(session *ExchangeSession, orders []types.SubmitOrder) (formattedOrders []types.SubmitOrder, err error) { - for _, order := range orders { - o, err := session.FormatOrder(order) - if err != nil { - return formattedOrders, err - } - formattedOrders = append(formattedOrders, o) - } - - return formattedOrders, err -} - func max(a, b int64) int64 { if a > b { return a diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go new file mode 100644 index 0000000000..1c7072246e --- /dev/null +++ b/pkg/bbgo/order_executor_general.go @@ -0,0 +1,479 @@ +package bbgo + +import ( + "context" + "errors" + "fmt" + "strings" + "sync/atomic" + "time" + + log "github.com/sirupsen/logrus" + "go.uber.org/multierr" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/util" +) + +var ErrExceededSubmitOrderRetryLimit = errors.New("exceeded submit order retry limit") + +// quantityReduceDelta is used to modify the order to submit, especially for the market order +var quantityReduceDelta = fixedpoint.NewFromFloat(0.005) + +// submitOrderRetryLimit is used when SubmitOrder failed, we will re-submit the order. +// This is for the maximum retries +const submitOrderRetryLimit = 5 + +// GeneralOrderExecutor implements the general order executor for strategy +type GeneralOrderExecutor struct { + session *ExchangeSession + symbol string + strategy string + strategyInstanceID string + position *types.Position + activeMakerOrders *ActiveOrderBook + orderStore *OrderStore + tradeCollector *TradeCollector + + marginBaseMaxBorrowable, marginQuoteMaxBorrowable fixedpoint.Value + + closing int64 +} + +func NewGeneralOrderExecutor(session *ExchangeSession, symbol, strategy, strategyInstanceID string, position *types.Position) *GeneralOrderExecutor { + // Always update the position fields + position.Strategy = strategy + position.StrategyInstanceID = strategyInstanceID + + orderStore := NewOrderStore(symbol) + + executor := &GeneralOrderExecutor{ + session: session, + symbol: symbol, + strategy: strategy, + strategyInstanceID: strategyInstanceID, + position: position, + activeMakerOrders: NewActiveOrderBook(symbol), + orderStore: orderStore, + tradeCollector: NewTradeCollector(symbol, position, orderStore), + } + + if session.Margin { + executor.startMarginAssetUpdater(context.Background()) + } + + return executor +} + +func (e *GeneralOrderExecutor) startMarginAssetUpdater(ctx context.Context) { + marginService, ok := e.session.Exchange.(types.MarginBorrowRepayService) + if !ok { + log.Warnf("session %s (%T) exchange does not support MarginBorrowRepayService", e.session.Name, e.session.Exchange) + return + } + + go e.marginAssetMaxBorrowableUpdater(ctx, 30*time.Minute, marginService, e.position.Market) +} + +func (e *GeneralOrderExecutor) updateMarginAssetMaxBorrowable(ctx context.Context, marginService types.MarginBorrowRepayService, market types.Market) { + maxBorrowable, err := marginService.QueryMarginAssetMaxBorrowable(ctx, market.BaseCurrency) + if err != nil { + log.WithError(err).Errorf("can not query margin base asset %s max borrowable", market.BaseCurrency) + } else { + log.Infof("updating margin base asset %s max borrowable amount: %f", market.BaseCurrency, maxBorrowable.Float64()) + e.marginBaseMaxBorrowable = maxBorrowable + } + + maxBorrowable, err = marginService.QueryMarginAssetMaxBorrowable(ctx, market.QuoteCurrency) + if err != nil { + log.WithError(err).Errorf("can not query margin quote asset %s max borrowable", market.QuoteCurrency) + } else { + log.Infof("updating margin quote asset %s max borrowable amount: %f", market.QuoteCurrency, maxBorrowable.Float64()) + e.marginQuoteMaxBorrowable = maxBorrowable + } +} + +func (e *GeneralOrderExecutor) marginAssetMaxBorrowableUpdater(ctx context.Context, interval time.Duration, marginService types.MarginBorrowRepayService, market types.Market) { + t := time.NewTicker(util.MillisecondsJitter(interval, 500)) + defer t.Stop() + + e.updateMarginAssetMaxBorrowable(ctx, marginService, market) + for { + select { + case <-ctx.Done(): + return + + case <-t.C: + e.updateMarginAssetMaxBorrowable(ctx, marginService, market) + } + } +} + +func (e *GeneralOrderExecutor) ActiveMakerOrders() *ActiveOrderBook { + return e.activeMakerOrders +} + +func (e *GeneralOrderExecutor) BindEnvironment(environ *Environment) { + e.tradeCollector.OnProfit(func(trade types.Trade, profit *types.Profit) { + environ.RecordPosition(e.position, trade, profit) + }) +} + +func (e *GeneralOrderExecutor) BindTradeStats(tradeStats *types.TradeStats) { + e.tradeCollector.OnProfit(func(trade types.Trade, profit *types.Profit) { + if profit == nil { + return + } + + tradeStats.Add(profit) + }) +} + +func (e *GeneralOrderExecutor) BindProfitStats(profitStats *types.ProfitStats) { + e.tradeCollector.OnProfit(func(trade types.Trade, profit *types.Profit) { + profitStats.AddTrade(trade) + if profit == nil { + return + } + + profitStats.AddProfit(*profit) + + Notify(profit) + Notify(profitStats) + }) +} + +func (e *GeneralOrderExecutor) Bind() { + e.activeMakerOrders.BindStream(e.session.UserDataStream) + e.orderStore.BindStream(e.session.UserDataStream) + + // trade notify + e.tradeCollector.OnTrade(func(trade types.Trade, profit, netProfit fixedpoint.Value) { + Notify(trade) + }) + + e.tradeCollector.OnPositionUpdate(func(position *types.Position) { + log.Infof("position changed: %s", position) + Notify(position) + }) + + e.tradeCollector.BindStream(e.session.UserDataStream) +} + +// CancelOrders cancels the given order objects directly +func (e *GeneralOrderExecutor) CancelOrders(ctx context.Context, orders ...types.Order) error { + err := e.session.Exchange.CancelOrders(ctx, orders...) + if err != nil { // Retry once + err = e.session.Exchange.CancelOrders(ctx, orders...) + } + return err +} + +func (e *GeneralOrderExecutor) SubmitOrders(ctx context.Context, submitOrders ...types.SubmitOrder) (types.OrderSlice, error) { + formattedOrders, err := e.session.FormatOrders(submitOrders) + if err != nil { + return nil, err + } + + createdOrders, errIdx, err := BatchPlaceOrder(ctx, e.session.Exchange, formattedOrders...) + if len(errIdx) > 0 { + createdOrders2, err2 := BatchRetryPlaceOrder(ctx, e.session.Exchange, errIdx, formattedOrders...) + if err2 != nil { + err = multierr.Append(err, err2) + } else { + createdOrders = append(createdOrders, createdOrders2...) + } + } + + e.orderStore.Add(createdOrders...) + e.activeMakerOrders.Add(createdOrders...) + e.tradeCollector.Process() + return createdOrders, err +} + +type OpenPositionOptions struct { + // Long is for open a long position + // Long or Short must be set, avoid loading it from the config file + // it should be set from the strategy code + Long bool `json:"-" yaml:"-"` + + // Short is for open a short position + // Long or Short must be set + Short bool `json:"-" yaml:"-"` + + // Leverage is used for leveraged position and account + // Leverage is not effected when using non-leverage spot account + Leverage fixedpoint.Value `json:"leverage,omitempty" modifiable:"true"` + + // Quantity will be used first, it will override the leverage if it's given + Quantity fixedpoint.Value `json:"quantity,omitempty" modifiable:"true"` + + // LimitOrder set to true to open a position with a limit order + // default is false, and will send MarketOrder + LimitOrder bool `json:"limitOrder,omitempty" modifiable:"true"` + + // LimitOrderTakerRatio is used when LimitOrder = true, it adjusts the price of the limit order with a ratio. + // So you can ensure that the limit order can be a taker order. Higher the ratio, higher the chance it could be a taker order. + // + // limitOrderTakerRatio is the price ratio to adjust your limit order as a taker order. e.g., 0.1% + // for sell order, 0.1% ratio means your final price = price * (1 - 0.1%) + // for buy order, 0.1% ratio means your final price = price * (1 + 0.1%) + // this is only enabled when the limitOrder option set to true + LimitOrderTakerRatio fixedpoint.Value `json:"limitOrderTakerRatio,omitempty"` + + Price fixedpoint.Value `json:"-" yaml:"-"` + Tags []string `json:"-" yaml:"-"` +} + +func (e *GeneralOrderExecutor) reduceQuantityAndSubmitOrder(ctx context.Context, price fixedpoint.Value, submitOrder types.SubmitOrder) (types.OrderSlice, error) { + var err error + for i := 0; i < submitOrderRetryLimit; i++ { + q := submitOrder.Quantity.Mul(fixedpoint.One.Sub(quantityReduceDelta)) + if !e.session.Futures { + if submitOrder.Side == types.SideTypeSell { + if baseBalance, ok := e.session.GetAccount().Balance(e.position.Market.BaseCurrency); ok { + q = fixedpoint.Min(q, baseBalance.Available) + } + } else { + if quoteBalance, ok := e.session.GetAccount().Balance(e.position.Market.QuoteCurrency); ok { + q = fixedpoint.Min(q, quoteBalance.Available.Div(price)) + } + } + } + log.Warnf("retrying order, adjusting order quantity: %v -> %v", submitOrder.Quantity, q) + + submitOrder.Quantity = q + if e.position.Market.IsDustQuantity(submitOrder.Quantity, price) { + return nil, types.NewZeroAssetError(nil) + } + + createdOrder, err2 := e.SubmitOrders(ctx, submitOrder) + if err2 != nil { + // collect the error object + err = multierr.Append(err, err2) + continue + } + + log.Infof("created order: %+v", createdOrder) + return createdOrder, nil + } + + return nil, multierr.Append(ErrExceededSubmitOrderRetryLimit, err) +} + +func (e *GeneralOrderExecutor) OpenPosition(ctx context.Context, options OpenPositionOptions) (types.OrderSlice, error) { + price := options.Price + submitOrder := types.SubmitOrder{ + Symbol: e.position.Symbol, + Type: types.OrderTypeMarket, + MarginSideEffect: types.SideEffectTypeMarginBuy, + Tag: strings.Join(options.Tags, ","), + } + + baseBalance, _ := e.session.GetAccount().Balance(e.position.Market.BaseCurrency) + + // FIXME: fix the max quote borrowing checking + // quoteBalance, _ := e.session.Account.Balance(e.position.Market.QuoteCurrency) + + if !options.LimitOrderTakerRatio.IsZero() { + if options.Price.IsZero() { + return nil, fmt.Errorf("OpenPositionOptions.Price is zero, can not adjust limit taker order price, options given: %+v", options) + } + + if options.Long { + // use higher price to buy (this ensures that our order will be filled) + price = price.Mul(one.Add(options.LimitOrderTakerRatio)) + } else if options.Short { + // use lower price to sell (this ensures that our order will be filled) + price = price.Mul(one.Sub(options.LimitOrderTakerRatio)) + } + } + + if options.LimitOrder { + submitOrder.Type = types.OrderTypeLimit + submitOrder.Price = price + } + + quantity := options.Quantity + + if options.Long { + if quantity.IsZero() { + quoteQuantity, err := CalculateQuoteQuantity(ctx, e.session, e.position.QuoteCurrency, options.Leverage) + if err != nil { + return nil, err + } + + quantity = quoteQuantity.Div(price) + } + if e.position.Market.IsDustQuantity(quantity, price) { + log.Warnf("dust quantity: %v", quantity) + return nil, nil + } + + quoteQuantity := quantity.Mul(price) + if e.session.Margin && !e.marginQuoteMaxBorrowable.IsZero() && quoteQuantity.Compare(e.marginQuoteMaxBorrowable) > 0 { + log.Warnf("adjusting quantity %f according to the max margin quote borrowable amount: %f", quantity.Float64(), e.marginQuoteMaxBorrowable.Float64()) + quantity = AdjustQuantityByMaxAmount(quantity, price, e.marginQuoteMaxBorrowable) + } + + submitOrder.Side = types.SideTypeBuy + submitOrder.Quantity = quantity + + Notify("Opening %s long position with quantity %v at price %v", e.position.Symbol, quantity, price) + + createdOrder, err := e.SubmitOrders(ctx, submitOrder) + if err == nil { + return createdOrder, nil + } + + return e.reduceQuantityAndSubmitOrder(ctx, price, submitOrder) + } else if options.Short { + if quantity.IsZero() { + var err error + quantity, err = CalculateBaseQuantity(e.session, e.position.Market, price, quantity, options.Leverage) + if err != nil { + return nil, err + } + } + if e.position.Market.IsDustQuantity(quantity, price) { + log.Warnf("dust quantity: %v", quantity) + return nil, nil + } + + if e.session.Margin && !e.marginBaseMaxBorrowable.IsZero() && quantity.Sub(baseBalance.Available).Compare(e.marginBaseMaxBorrowable) > 0 { + log.Warnf("adjusting %f quantity according to the max margin base borrowable amount: %f", quantity.Float64(), e.marginBaseMaxBorrowable.Float64()) + // quantity = fixedpoint.Min(quantity, e.marginBaseMaxBorrowable) + quantity = baseBalance.Available.Add(e.marginBaseMaxBorrowable) + } + + submitOrder.Side = types.SideTypeSell + submitOrder.Quantity = quantity + + Notify("Opening %s short position with quantity %v at price %v", e.position.Symbol, quantity, price) + return e.reduceQuantityAndSubmitOrder(ctx, price, submitOrder) + } + + return nil, errors.New("options Long or Short must be set") +} + +// GracefulCancelActiveOrderBook cancels the orders from the active orderbook. +func (e *GeneralOrderExecutor) GracefulCancelActiveOrderBook(ctx context.Context, activeOrders *ActiveOrderBook, orders ...types.Order) error { + if activeOrders.NumOfOrders() == 0 { + return nil + } + if err := activeOrders.GracefulCancel(ctx, e.session.Exchange, orders...); err != nil { + // Retry once + if err = activeOrders.GracefulCancel(ctx, e.session.Exchange); err != nil { + return fmt.Errorf("graceful cancel order error: %w", err) + } + } + + e.tradeCollector.Process() + return nil +} + +// CancelActiveOrderBookNoWait cancels the orders from the active orderbook without waiting +func (e *GeneralOrderExecutor) CancelActiveOrderBookNoWait(ctx context.Context, activeOrders *ActiveOrderBook, orders ...types.Order) error { + if activeOrders.NumOfOrders() == 0 { + return nil + } + if err := activeOrders.CancelNoWait(ctx, e.session.Exchange, orders...); err != nil { + return fmt.Errorf("cancel order error: %w", err) + } + return nil +} + +// GracefulCancel cancels all active maker orders if orders are not given, otherwise cancel all the given orders +func (e *GeneralOrderExecutor) GracefulCancel(ctx context.Context, orders ...types.Order) error { + return e.GracefulCancelActiveOrderBook(ctx, e.activeMakerOrders, orders...) +} + +// CancelNoWait cancels all active maker orders if orders is not given, otherwise cancel the given orders +func (e *GeneralOrderExecutor) CancelNoWait(ctx context.Context, orders ...types.Order) error { + return e.CancelActiveOrderBookNoWait(ctx, e.activeMakerOrders, orders...) +} + +// ClosePosition closes the current position by a percentage. +// percentage 0.1 means close 10% position +// tag is the order tag you want to attach, you may pass multiple tags, the tags will be combined into one tag string by commas. +func (e *GeneralOrderExecutor) ClosePosition(ctx context.Context, percentage fixedpoint.Value, tags ...string) error { + submitOrder := e.position.NewMarketCloseOrder(percentage) + if submitOrder == nil { + return nil + } + + if e.closing > 0 { + log.Errorf("position is already closing") + return nil + } + + atomic.AddInt64(&e.closing, 1) + defer atomic.StoreInt64(&e.closing, 0) + + if e.session.Futures { // Futures: Use base qty in e.position + submitOrder.Quantity = e.position.GetBase().Abs() + submitOrder.ReduceOnly = true + if e.position.IsLong() { + submitOrder.Side = types.SideTypeSell + } else if e.position.IsShort() { + submitOrder.Side = types.SideTypeBuy + } else { + submitOrder.Side = types.SideTypeSelf + submitOrder.Quantity = fixedpoint.Zero + } + + if submitOrder.Quantity.IsZero() { + return fmt.Errorf("no position to close: %+v", submitOrder) + } + } else { // Spot and spot margin + // check base balance and adjust the close position order + if e.position.IsLong() { + if baseBalance, ok := e.session.Account.Balance(e.position.Market.BaseCurrency); ok { + submitOrder.Quantity = fixedpoint.Min(submitOrder.Quantity, baseBalance.Available) + } + if submitOrder.Quantity.IsZero() { + return fmt.Errorf("insufficient base balance, can not sell: %+v", submitOrder) + } + } else if e.position.IsShort() { + // TODO: check quote balance here, we also need the current price to validate, need to design. + /* + if quoteBalance, ok := e.session.Account.Balance(e.position.Market.QuoteCurrency); ok { + // AdjustQuantityByMaxAmount(submitOrder.Quantity, quoteBalance.Available) + // submitOrder.Quantity = fixedpoint.Min(submitOrder.Quantity,) + } + */ + } + } + + tagStr := strings.Join(tags, ",") + submitOrder.Tag = tagStr + + Notify("Closing %s position %s with tags: %v", e.symbol, percentage.Percentage(), tagStr) + + _, err := e.SubmitOrders(ctx, *submitOrder) + return err +} + +func (e *GeneralOrderExecutor) TradeCollector() *TradeCollector { + return e.tradeCollector +} + +func (e *GeneralOrderExecutor) Session() *ExchangeSession { + return e.session +} + +func (e *GeneralOrderExecutor) Position() *types.Position { + return e.position +} + +// This implements PositionReader interface +func (e *GeneralOrderExecutor) CurrentPosition() *types.Position { + return e.position +} + +// This implements PositionResetter interface +func (e *GeneralOrderExecutor) ResetPosition() error { + e.position.Reset() + return nil +} diff --git a/pkg/bbgo/order_processor.go b/pkg/bbgo/order_processor.go index f61f134617..edf3844f17 100644 --- a/pkg/bbgo/order_processor.go +++ b/pkg/bbgo/order_processor.go @@ -1,8 +1,9 @@ package bbgo import ( - "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/pkg/errors" + + "github.com/c9s/bbgo/pkg/fixedpoint" ) var ( @@ -14,7 +15,7 @@ var ( ErrAssetBalanceLevelTooHigh = errors.New("asset balance level too high") ) -// AdjustQuantityByMaxAmount adjusts the quantity to make the amount greater than the given minAmount +// AdjustQuantityByMaxAmount adjusts the quantity to make the amount less than the given maxAmount func AdjustQuantityByMaxAmount(quantity, currentPrice, maxAmount fixedpoint.Value) fixedpoint.Value { // modify quantity for the min amount amount := currentPrice.Mul(quantity) diff --git a/pkg/bbgo/order_store.go b/pkg/bbgo/order_store.go index 46e4911c7f..473473e09e 100644 --- a/pkg/bbgo/order_store.go +++ b/pkg/bbgo/order_store.go @@ -104,8 +104,9 @@ func (s *OrderStore) Update(o types.Order) bool { s.mu.Lock() defer s.mu.Unlock() - _, ok := s.orders[o.OrderID] + old, ok := s.orders[o.OrderID] if ok { + o.Tag = old.Tag s.orders[o.OrderID] = o } return ok diff --git a/pkg/bbgo/persistence.go b/pkg/bbgo/persistence.go index 7dde77f61b..e87fd92a7e 100644 --- a/pkg/bbgo/persistence.go +++ b/pkg/bbgo/persistence.go @@ -1,76 +1,116 @@ package bbgo import ( - "fmt" + "context" + "os" + "reflect" + "github.com/codingconcepts/env" + "github.com/pkg/errors" log "github.com/sirupsen/logrus" + "github.com/c9s/bbgo/pkg/dynamic" "github.com/c9s/bbgo/pkg/service" ) -type PersistenceSelector struct { - // StoreID is the store you want to use. - StoreID string `json:"store" yaml:"store"` - - // Type is the persistence type - Type string `json:"type" yaml:"type"` +var defaultPersistenceServiceFacade = &service.PersistenceServiceFacade{ + Memory: service.NewMemoryService(), } -// Persistence is used for strategy to inject the persistence. -type Persistence struct { - PersistenceSelector *PersistenceSelector `json:"persistence,omitempty" yaml:"persistence,omitempty"` +var persistenceServiceFacade = defaultPersistenceServiceFacade + +// Sync syncs the object properties into the persistence layer +func Sync(ctx context.Context, obj interface{}) { + id := dynamic.CallID(obj) + if len(id) == 0 { + log.Warnf("InstanceID() is not provided, can not sync persistence") + return + } + + isolation := GetIsolationFromContext(ctx) - Facade *service.PersistenceServiceFacade `json:"-" yaml:"-"` + ps := isolation.persistenceServiceFacade.Get() + err := storePersistenceFields(obj, id, ps) + if err != nil { + log.WithError(err).Errorf("persistence sync failed") + } } -func (p *Persistence) backendService(t string) (service.PersistenceService, error) { - switch t { - case "json": - return p.Facade.Json, nil +func loadPersistenceFields(obj interface{}, id string, persistence service.PersistenceService) error { + return dynamic.IterateFieldsByTag(obj, "persistence", func(tag string, field reflect.StructField, value reflect.Value) error { + log.Debugf("[loadPersistenceFields] loading value into field %v, tag = %s, original value = %v", field, tag, value) - case "redis": - if p.Facade.Redis == nil { - log.Warn("redis persistence is not available, fallback to memory backend") - return p.Facade.Memory, nil + newValueInf := dynamic.NewTypeValueInterface(value.Type()) + // inf := value.Interface() + store := persistence.NewStore("state", id, tag) + if err := store.Load(&newValueInf); err != nil { + if err == service.ErrPersistenceNotExists { + log.Debugf("[loadPersistenceFields] state key does not exist, id = %v, tag = %s", id, tag) + return nil + } + + return err } - return p.Facade.Redis, nil - case "memory": - return p.Facade.Memory, nil + newValue := reflect.ValueOf(newValueInf) + if value.Kind() != reflect.Ptr && newValue.Kind() == reflect.Ptr { + newValue = newValue.Elem() + } - } + log.Debugf("[loadPersistenceFields] %v = %v -> %v\n", field, value, newValue) - return nil, fmt.Errorf("unsupported persistent type %s", t) + value.Set(newValue) + return nil + }) } -func (p *Persistence) Load(val interface{}, subIDs ...string) error { - ps, err := p.backendService(p.PersistenceSelector.Type) - if err != nil { - return err +func storePersistenceFields(obj interface{}, id string, persistence service.PersistenceService) error { + return dynamic.IterateFieldsByTag(obj, "persistence", func(tag string, ft reflect.StructField, fv reflect.Value) error { + log.Debugf("[storePersistenceFields] storing value from field %v, tag = %s, original value = %v", ft, tag, fv) + + inf := fv.Interface() + store := persistence.NewStore("state", id, tag) + return store.Save(inf) + }) +} + +func NewPersistenceServiceFacade(conf *PersistenceConfig) (*service.PersistenceServiceFacade, error) { + facade := &service.PersistenceServiceFacade{ + Memory: service.NewMemoryService(), + } + + if conf.Redis != nil { + if err := env.Set(conf.Redis); err != nil { + return nil, err + } + + redisPersistence := service.NewRedisPersistenceService(conf.Redis) + facade.Redis = redisPersistence } - log.Debugf("using persistence store %T for loading", ps) + if conf.Json != nil { + if _, err := os.Stat(conf.Json.Directory); os.IsNotExist(err) { + if err2 := os.MkdirAll(conf.Json.Directory, 0777); err2 != nil { + return nil, errors.Wrapf(err2, "can not create directory: %s", conf.Json.Directory) + } + } - if p.PersistenceSelector.StoreID == "" { - p.PersistenceSelector.StoreID = "default" + jsonPersistence := &service.JsonPersistenceService{Directory: conf.Json.Directory} + facade.Json = jsonPersistence } - store := ps.NewStore(p.PersistenceSelector.StoreID, subIDs...) - return store.Load(val) + return facade, nil } -func (p *Persistence) Save(val interface{}, subIDs ...string) error { - ps, err := p.backendService(p.PersistenceSelector.Type) +func ConfigurePersistence(ctx context.Context, conf *PersistenceConfig) error { + facade, err := NewPersistenceServiceFacade(conf) if err != nil { return err } - log.Debugf("using persistence store %T for storing", ps) - - if p.PersistenceSelector.StoreID == "" { - p.PersistenceSelector.StoreID = "default" - } + isolation := GetIsolationFromContext(ctx) + isolation.persistenceServiceFacade = facade - store := ps.NewStore(p.PersistenceSelector.StoreID, subIDs...) - return store.Save(val) + persistenceServiceFacade = facade + return nil } diff --git a/pkg/bbgo/persistence_test.go b/pkg/bbgo/persistence_test.go new file mode 100644 index 0000000000..dbd612ff28 --- /dev/null +++ b/pkg/bbgo/persistence_test.go @@ -0,0 +1,152 @@ +package bbgo + +import ( + "os" + "reflect" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/dynamic" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/service" + "github.com/c9s/bbgo/pkg/types" +) + +type TestStructWithoutInstanceID struct { + Symbol string +} + +func (s *TestStructWithoutInstanceID) ID() string { + return "test-struct-no-instance-id" +} + +type TestStruct struct { + *Environment + + Position *types.Position `persistence:"position"` + Integer int64 `persistence:"integer"` + Integer2 int64 `persistence:"integer2"` + Float int64 `persistence:"float"` + String string `persistence:"string"` +} + +func (t *TestStruct) InstanceID() string { + return "test-struct" +} + +func preparePersistentServices() []service.PersistenceService { + mem := service.NewMemoryService() + jsonDir := &service.JsonPersistenceService{Directory: "testoutput/persistence"} + pss := []service.PersistenceService{ + mem, + jsonDir, + } + + if _, ok := os.LookupEnv("TEST_REDIS"); ok { + redisP := service.NewRedisPersistenceService(&service.RedisPersistenceConfig{ + Host: "localhost", + Port: "6379", + DB: 0, + }) + pss = append(pss, redisP) + } + + return pss +} + +func Test_CallID(t *testing.T) { + t.Run("default", func(t *testing.T) { + id := dynamic.CallID(&TestStruct{}) + assert.NotEmpty(t, id) + assert.Equal(t, "test-struct", id) + }) + + t.Run("fallback", func(t *testing.T) { + id := dynamic.CallID(&TestStructWithoutInstanceID{Symbol: "BTCUSDT"}) + assert.Equal(t, "test-struct-no-instance-id:BTCUSDT", id) + }) +} + +func Test_loadPersistenceFields(t *testing.T) { + var pss = preparePersistentServices() + + for _, ps := range pss { + psName := reflect.TypeOf(ps).Elem().String() + t.Run(psName+"/empty", func(t *testing.T) { + b := &TestStruct{} + err := loadPersistenceFields(b, "test-empty", ps) + assert.NoError(t, err) + }) + + t.Run(psName+"/nil", func(t *testing.T) { + var b *TestStruct = nil + err := loadPersistenceFields(b, "test-nil", ps) + assert.Equal(t, dynamic.ErrCanNotIterateNilPointer, err) + }) + + t.Run(psName+"/pointer-field", func(t *testing.T) { + var a = &TestStruct{ + Position: types.NewPosition("BTCUSDT", "BTC", "USDT"), + } + a.Position.Base = fixedpoint.NewFromFloat(10.0) + a.Position.AverageCost = fixedpoint.NewFromFloat(3343.0) + err := storePersistenceFields(a, "pointer-field-test", ps) + assert.NoError(t, err) + + b := &TestStruct{} + err = loadPersistenceFields(b, "pointer-field-test", ps) + assert.NoError(t, err) + + assert.Equal(t, "10", a.Position.Base.String()) + assert.Equal(t, "3343", a.Position.AverageCost.String()) + }) + } +} + +func Test_storePersistenceFields(t *testing.T) { + var pss = preparePersistentServices() + + var a = &TestStruct{ + Integer: 1, + Integer2: 2, + Float: 3.0, + String: "foobar", + Position: types.NewPosition("BTCUSDT", "BTC", "USDT"), + } + + a.Position.Base = fixedpoint.NewFromFloat(10.0) + a.Position.AverageCost = fixedpoint.NewFromFloat(3343.0) + + for _, ps := range pss { + psName := reflect.TypeOf(ps).Elem().String() + t.Run("all/"+psName, func(t *testing.T) { + id := dynamic.CallID(a) + err := storePersistenceFields(a, id, ps) + assert.NoError(t, err) + + var i int64 + store := ps.NewStore("state", "test-struct", "integer") + err = store.Load(&i) + assert.NoError(t, err) + assert.Equal(t, int64(1), i) + + var p *types.Position + store = ps.NewStore("state", "test-struct", "position") + err = store.Load(&p) + assert.NoError(t, err) + assert.Equal(t, fixedpoint.NewFromFloat(10.0), p.Base) + assert.Equal(t, fixedpoint.NewFromFloat(3343.0), p.AverageCost) + + var b = &TestStruct{} + err = loadPersistenceFields(b, id, ps) + assert.NoError(t, err) + assert.Equal(t, a.Integer, b.Integer) + assert.Equal(t, a.Integer2, b.Integer2) + assert.Equal(t, a.Float, b.Float) + assert.Equal(t, a.String, b.String) + assert.Equal(t, a.Position, b.Position) + }) + } + +} diff --git a/pkg/bbgo/profitstats.go b/pkg/bbgo/profitstats.go index 920078f66e..f30d11b655 100644 --- a/pkg/bbgo/profitstats.go +++ b/pkg/bbgo/profitstats.go @@ -1,2 +1 @@ package bbgo - diff --git a/pkg/bbgo/reflect.go b/pkg/bbgo/reflect.go new file mode 100644 index 0000000000..920078f66e --- /dev/null +++ b/pkg/bbgo/reflect.go @@ -0,0 +1,2 @@ +package bbgo + diff --git a/pkg/bbgo/reflect_test.go b/pkg/bbgo/reflect_test.go new file mode 100644 index 0000000000..920078f66e --- /dev/null +++ b/pkg/bbgo/reflect_test.go @@ -0,0 +1,2 @@ +package bbgo + diff --git a/pkg/bbgo/reporter.go b/pkg/bbgo/reporter.go index e8bf85a442..910f1ba4fc 100644 --- a/pkg/bbgo/reporter.go +++ b/pkg/bbgo/reporter.go @@ -74,7 +74,7 @@ func (reporter *AverageCostPnLReporter) Run() { } for _, symbol := range reporter.Symbols { - report := calculator.Calculate(symbol, session.Trades[symbol].Copy(), session.lastPrices[symbol]) + report := calculator.NetValue(symbol, session.Trades[symbol].Copy(), session.lastPrices[symbol]) report.Print() } } diff --git a/pkg/bbgo/risk.go b/pkg/bbgo/risk.go new file mode 100644 index 0000000000..eb9230dd03 --- /dev/null +++ b/pkg/bbgo/risk.go @@ -0,0 +1,372 @@ +package bbgo + +import ( + "context" + "fmt" + "time" + + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/risk" + "github.com/c9s/bbgo/pkg/types" +) + +var defaultLeverage = fixedpoint.NewFromInt(3) + +var maxIsolatedMarginLeverage = fixedpoint.NewFromInt(10) + +var maxCrossMarginLeverage = fixedpoint.NewFromInt(3) + +type AccountValueCalculator struct { + session *ExchangeSession + quoteCurrency string + prices map[string]fixedpoint.Value + tickers map[string]types.Ticker + updateTime time.Time +} + +func NewAccountValueCalculator(session *ExchangeSession, quoteCurrency string) *AccountValueCalculator { + return &AccountValueCalculator{ + session: session, + quoteCurrency: quoteCurrency, + prices: make(map[string]fixedpoint.Value), + tickers: make(map[string]types.Ticker), + } +} + +func (c *AccountValueCalculator) UpdatePrices(ctx context.Context) error { + balances := c.session.Account.Balances() + currencies := balances.Currencies() + var symbols []string + for _, currency := range currencies { + if currency == c.quoteCurrency { + continue + } + + symbol := currency + c.quoteCurrency + symbols = append(symbols, symbol) + } + + tickers, err := c.session.Exchange.QueryTickers(ctx, symbols...) + if err != nil { + return err + } + + c.tickers = tickers + for symbol, ticker := range tickers { + c.prices[symbol] = ticker.Last + if ticker.Time.After(c.updateTime) { + c.updateTime = ticker.Time + } + } + return nil +} + +func (c *AccountValueCalculator) DebtValue(ctx context.Context) (fixedpoint.Value, error) { + debtValue := fixedpoint.Zero + + if len(c.prices) == 0 { + if err := c.UpdatePrices(ctx); err != nil { + return debtValue, err + } + } + + balances := c.session.Account.Balances() + for _, b := range balances { + symbol := b.Currency + c.quoteCurrency + price, ok := c.prices[symbol] + if !ok { + continue + } + + debtValue = debtValue.Add(b.Debt().Mul(price)) + } + + return debtValue, nil +} + +func (c *AccountValueCalculator) MarketValue(ctx context.Context) (fixedpoint.Value, error) { + marketValue := fixedpoint.Zero + + if len(c.prices) == 0 { + if err := c.UpdatePrices(ctx); err != nil { + return marketValue, err + } + } + + balances := c.session.Account.Balances() + for _, b := range balances { + if b.Currency == c.quoteCurrency { + marketValue = marketValue.Add(b.Total()) + continue + } + + symbol := b.Currency + c.quoteCurrency + price, ok := c.prices[symbol] + if !ok { + continue + } + + marketValue = marketValue.Add(b.Total().Mul(price)) + } + + return marketValue, nil +} + +func (c *AccountValueCalculator) NetValue(ctx context.Context) (fixedpoint.Value, error) { + if len(c.prices) == 0 { + if err := c.UpdatePrices(ctx); err != nil { + return fixedpoint.Zero, err + } + } + + balances := c.session.Account.Balances() + accountValue := calculateNetValueInQuote(balances, c.prices, c.quoteCurrency) + return accountValue, nil +} + +func calculateNetValueInQuote(balances types.BalanceMap, prices types.PriceMap, quoteCurrency string) (accountValue fixedpoint.Value) { + accountValue = fixedpoint.Zero + + for _, b := range balances { + if b.Currency == quoteCurrency { + accountValue = accountValue.Add(b.Net()) + continue + } + + symbol := b.Currency + quoteCurrency // for BTC/USDT, ETH/USDT pairs + symbolReverse := quoteCurrency + b.Currency // for USDT/USDC or USDT/TWD pairs + if price, ok := prices[symbol]; ok { + accountValue = accountValue.Add(b.Net().Mul(price)) + } else if priceReverse, ok2 := prices[symbolReverse]; ok2 { + accountValue = accountValue.Add(b.Net().Div(priceReverse)) + } + } + + return accountValue +} + +func (c *AccountValueCalculator) AvailableQuote(ctx context.Context) (fixedpoint.Value, error) { + accountValue := fixedpoint.Zero + + if len(c.prices) == 0 { + if err := c.UpdatePrices(ctx); err != nil { + return accountValue, err + } + } + + balances := c.session.Account.Balances() + for _, b := range balances { + if b.Currency == c.quoteCurrency { + accountValue = accountValue.Add(b.Net()) + continue + } + + symbol := b.Currency + c.quoteCurrency + price, ok := c.prices[symbol] + if !ok { + continue + } + + accountValue = accountValue.Add(b.Net().Mul(price)) + } + + return accountValue, nil +} + +// MarginLevel calculates the margin level from the asset market value and the debt value +// See https://www.binance.com/en/support/faq/360030493931 +func (c *AccountValueCalculator) MarginLevel(ctx context.Context) (fixedpoint.Value, error) { + marginLevel := fixedpoint.Zero + marketValue, err := c.MarketValue(ctx) + if err != nil { + return marginLevel, err + } + + debtValue, err := c.DebtValue(ctx) + if err != nil { + return marginLevel, err + } + + marginLevel = marketValue.Div(debtValue) + return marginLevel, nil +} + +func aggregateUsdNetValue(balances types.BalanceMap) fixedpoint.Value { + totalUsdValue := fixedpoint.Zero + // get all usd value if any + for currency, balance := range balances { + if types.IsUSDFiatCurrency(currency) { + totalUsdValue = totalUsdValue.Add(balance.Net()) + } + } + + return totalUsdValue +} + +func usdFiatBalances(balances types.BalanceMap) (fiats types.BalanceMap, rest types.BalanceMap) { + rest = make(types.BalanceMap) + fiats = make(types.BalanceMap) + for currency, balance := range balances { + if types.IsUSDFiatCurrency(currency) { + fiats[currency] = balance + } else { + rest[currency] = balance + } + } + + return fiats, rest +} + +func CalculateBaseQuantity(session *ExchangeSession, market types.Market, price, quantity, leverage fixedpoint.Value) (fixedpoint.Value, error) { + // default leverage guard + if leverage.IsZero() { + leverage = defaultLeverage + } + + baseBalance, hasBaseBalance := session.Account.Balance(market.BaseCurrency) + balances := session.Account.Balances() + + usingLeverage := session.Margin || session.IsolatedMargin || session.Futures || session.IsolatedFutures + if !usingLeverage { + // For spot, we simply sell the base quoteCurrency + if hasBaseBalance { + if quantity.IsZero() { + log.Warnf("sell quantity is not set, using all available base balance: %v", baseBalance) + if !baseBalance.Available.IsZero() { + return baseBalance.Available, nil + } + } else { + return fixedpoint.Min(quantity, baseBalance.Available), nil + } + } + + return quantity, types.NewZeroAssetError( + fmt.Errorf("quantity is zero, can not submit sell order, please check your quantity settings, your account balances: %+v", balances)) + } + + usdBalances, restBalances := usdFiatBalances(balances) + + // for isolated margin we can calculate from these two pair + totalUsdValue := fixedpoint.Zero + if len(restBalances) == 1 && types.IsUSDFiatCurrency(market.QuoteCurrency) { + totalUsdValue = aggregateUsdNetValue(balances) + } else if len(restBalances) > 1 { + accountValue := NewAccountValueCalculator(session, "USDT") + netValue, err := accountValue.NetValue(context.Background()) + if err != nil { + return quantity, err + } + + totalUsdValue = netValue + } else { + // TODO: translate quote currency like BTC of ETH/BTC to usd value + totalUsdValue = aggregateUsdNetValue(usdBalances) + } + + if !quantity.IsZero() { + return quantity, nil + } + + if price.IsZero() { + return quantity, fmt.Errorf("%s price can not be zero", market.Symbol) + } + + // using leverage -- starts from here + log.Infof("calculating available leveraged base quantity: base balance = %+v, total usd value %f", baseBalance, totalUsdValue.Float64()) + + // calculate the quantity automatically + if session.Margin || session.IsolatedMargin { + baseBalanceValue := baseBalance.Net().Mul(price) + accountUsdValue := baseBalanceValue.Add(totalUsdValue) + + // avoid using all account value since there will be some trade loss for interests and the fee + accountUsdValue = accountUsdValue.Mul(one.Sub(fixedpoint.NewFromFloat(0.01))) + + log.Infof("calculated account usd value %f %s", accountUsdValue.Float64(), market.QuoteCurrency) + + originLeverage := leverage + if session.IsolatedMargin { + leverage = fixedpoint.Min(leverage, maxIsolatedMarginLeverage) + log.Infof("using isolated margin, maxLeverage=%f originalLeverage=%f currentLeverage=%f", + maxIsolatedMarginLeverage.Float64(), + originLeverage.Float64(), + leverage.Float64()) + } else { + leverage = fixedpoint.Min(leverage, maxCrossMarginLeverage) + log.Infof("using cross margin, maxLeverage=%f originalLeverage=%f currentLeverage=%f", + maxCrossMarginLeverage.Float64(), + originLeverage.Float64(), + leverage.Float64()) + } + + // spot margin use the equity value, so we use the total quote balance here + maxPosition := risk.CalculateMaxPosition(price, accountUsdValue, leverage) + debt := baseBalance.Debt() + maxQuantity := maxPosition.Sub(debt) + + log.Infof("margin leverage: calculated maxQuantity=%f maxPosition=%f debt=%f price=%f accountValue=%f %s leverage=%f", + maxQuantity.Float64(), + maxPosition.Float64(), + debt.Float64(), + price.Float64(), + accountUsdValue.Float64(), + market.QuoteCurrency, + leverage.Float64()) + + return maxQuantity, nil + } + + if session.Futures || session.IsolatedFutures { + maxPositionQuantity := risk.CalculateMaxPosition(price, totalUsdValue, leverage) + + return maxPositionQuantity, nil + } + + return quantity, types.NewZeroAssetError( + errors.New("quantity is zero, can not submit sell order, please check your settings")) +} + +func CalculateQuoteQuantity(ctx context.Context, session *ExchangeSession, quoteCurrency string, leverage fixedpoint.Value) (fixedpoint.Value, error) { + // default leverage guard + if leverage.IsZero() { + leverage = defaultLeverage + } + + quoteBalance, _ := session.Account.Balance(quoteCurrency) + + usingLeverage := session.Margin || session.IsolatedMargin || session.Futures || session.IsolatedFutures + if !usingLeverage { + // For spot, we simply return the quote balance + return quoteBalance.Available.Mul(fixedpoint.Min(leverage, fixedpoint.One)), nil + } + + originLeverage := leverage + if session.IsolatedMargin { + leverage = fixedpoint.Min(leverage, maxIsolatedMarginLeverage) + log.Infof("using isolated margin, maxLeverage=%f originalLeverage=%f currentLeverage=%f", + maxIsolatedMarginLeverage.Float64(), + originLeverage.Float64(), + leverage.Float64()) + } else { + leverage = fixedpoint.Min(leverage, maxCrossMarginLeverage) + log.Infof("using cross margin, maxLeverage=%f originalLeverage=%f currentLeverage=%f", + maxCrossMarginLeverage.Float64(), + originLeverage.Float64(), + leverage.Float64()) + } + + // using leverage -- starts from here + accountValue := NewAccountValueCalculator(session, quoteCurrency) + availableQuote, err := accountValue.AvailableQuote(ctx) + if err != nil { + log.WithError(err).Errorf("can not update available quote") + return fixedpoint.Zero, err + } + + log.Infof("calculating available leveraged quote quantity: account available quote = %+v", availableQuote) + + return availableQuote.Mul(leverage), nil +} diff --git a/pkg/bbgo/risk_controls.go b/pkg/bbgo/risk_controls.go index 266c0ca012..121a6fc030 100644 --- a/pkg/bbgo/risk_controls.go +++ b/pkg/bbgo/risk_controls.go @@ -32,7 +32,7 @@ func (e *RiskControlOrderExecutor) SubmitOrders(ctx context.Context, orders ...t } } - formattedOrders, err := formatOrders(e.Session, orders) + formattedOrders, err := e.Session.FormatOrders(orders) if err != nil { return retOrders, err } diff --git a/pkg/bbgo/risk_test.go b/pkg/bbgo/risk_test.go new file mode 100644 index 0000000000..f33f5845d9 --- /dev/null +++ b/pkg/bbgo/risk_test.go @@ -0,0 +1,323 @@ +package bbgo + +import ( + "context" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/types/mocks" +) + +func newTestTicker() types.Ticker { + return types.Ticker{ + Time: time.Now(), + Volume: fixedpoint.Zero, + Last: fixedpoint.NewFromFloat(19000.0), + Open: fixedpoint.NewFromFloat(19500.0), + High: fixedpoint.NewFromFloat(19900.0), + Low: fixedpoint.NewFromFloat(18800.0), + Buy: fixedpoint.NewFromFloat(19500.0), + Sell: fixedpoint.NewFromFloat(18900.0), + } +} + +func TestAccountValueCalculator_NetValue(t *testing.T) { + + t.Run("borrow and available", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockEx := mocks.NewMockExchange(mockCtrl) + // for market data stream and user data stream + mockEx.EXPECT().NewStream().Return(&types.StandardStream{}).Times(2) + mockEx.EXPECT().QueryTickers(gomock.Any(), []string{"BTCUSDT"}).Return(map[string]types.Ticker{ + "BTCUSDT": newTestTicker(), + }, nil) + + session := NewExchangeSession("test", mockEx) + session.Account.UpdateBalances(types.BalanceMap{ + "BTC": { + Currency: "BTC", + Available: fixedpoint.NewFromFloat(2.0), + Locked: fixedpoint.Zero, + Borrowed: fixedpoint.NewFromFloat(1.0), + Interest: fixedpoint.Zero, + NetAsset: fixedpoint.Zero, + }, + "USDT": { + Currency: "USDT", + Available: fixedpoint.NewFromFloat(1000.0), + Locked: fixedpoint.Zero, + Borrowed: fixedpoint.Zero, + Interest: fixedpoint.Zero, + NetAsset: fixedpoint.Zero, + }, + }) + assert.NotNil(t, session) + + cal := NewAccountValueCalculator(session, "USDT") + assert.NotNil(t, cal) + + ctx := context.Background() + netValue, err := cal.NetValue(ctx) + assert.NoError(t, err) + assert.Equal(t, "20000", netValue.String()) + }) + + t.Run("borrowed and sold", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockEx := mocks.NewMockExchange(mockCtrl) + // for market data stream and user data stream + mockEx.EXPECT().NewStream().Return(&types.StandardStream{}).Times(2) + mockEx.EXPECT().QueryTickers(gomock.Any(), []string{"BTCUSDT"}).Return(map[string]types.Ticker{ + "BTCUSDT": newTestTicker(), + }, nil) + + session := NewExchangeSession("test", mockEx) + session.Account.UpdateBalances(types.BalanceMap{ + "BTC": { + Currency: "BTC", + Available: fixedpoint.Zero, + Locked: fixedpoint.Zero, + Borrowed: fixedpoint.NewFromFloat(1.0), + Interest: fixedpoint.Zero, + NetAsset: fixedpoint.Zero, + }, + "USDT": { + Currency: "USDT", + Available: fixedpoint.NewFromFloat(21000.0), + Locked: fixedpoint.Zero, + Borrowed: fixedpoint.Zero, + Interest: fixedpoint.Zero, + NetAsset: fixedpoint.Zero, + }, + }) + assert.NotNil(t, session) + + cal := NewAccountValueCalculator(session, "USDT") + assert.NotNil(t, cal) + + ctx := context.Background() + netValue, err := cal.NetValue(ctx) + assert.NoError(t, err) + assert.Equal(t, "2000", netValue.String()) // 21000-19000 + }) +} + +func TestNewAccountValueCalculator_MarginLevel(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockEx := mocks.NewMockExchange(mockCtrl) + // for market data stream and user data stream + mockEx.EXPECT().NewStream().Return(&types.StandardStream{}).Times(2) + mockEx.EXPECT().QueryTickers(gomock.Any(), []string{"BTCUSDT"}).Return(map[string]types.Ticker{ + "BTCUSDT": newTestTicker(), + }, nil) + + session := NewExchangeSession("test", mockEx) + session.Account.UpdateBalances(types.BalanceMap{ + "BTC": { + Currency: "BTC", + Available: fixedpoint.Zero, + Locked: fixedpoint.Zero, + Borrowed: fixedpoint.NewFromFloat(1.0), + Interest: fixedpoint.NewFromFloat(0.003), + NetAsset: fixedpoint.Zero, + }, + "USDT": { + Currency: "USDT", + Available: fixedpoint.NewFromFloat(21000.0), + Locked: fixedpoint.Zero, + Borrowed: fixedpoint.Zero, + Interest: fixedpoint.Zero, + NetAsset: fixedpoint.Zero, + }, + }) + assert.NotNil(t, session) + + cal := NewAccountValueCalculator(session, "USDT") + assert.NotNil(t, cal) + + ctx := context.Background() + marginLevel, err := cal.MarginLevel(ctx) + assert.NoError(t, err) + + // expected (21000 / 19000 * 1.003) + assert.Equal(t, + fixedpoint.NewFromFloat(21000.0).Div(fixedpoint.NewFromFloat(19000.0).Mul(fixedpoint.NewFromFloat(1.003))).FormatString(6), + marginLevel.FormatString(6)) +} + +func number(n float64) fixedpoint.Value { + return fixedpoint.NewFromFloat(n) +} + +func Test_aggregateUsdValue(t *testing.T) { + type args struct { + balances types.BalanceMap + } + tests := []struct { + name string + args args + want fixedpoint.Value + }{ + { + name: "mixed", + args: args{ + balances: types.BalanceMap{ + "USDC": types.Balance{Currency: "USDC", Available: number(70.0)}, + "USDT": types.Balance{Currency: "USDT", Available: number(100.0)}, + "BUSD": types.Balance{Currency: "BUSD", Available: number(80.0)}, + "BTC": types.Balance{Currency: "BTC", Available: number(0.01)}, + }, + }, + want: number(250.0), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, aggregateUsdNetValue(tt.args.balances), "aggregateUsdNetValue(%v)", tt.args.balances) + }) + } +} + +func Test_usdFiatBalances(t *testing.T) { + type args struct { + balances types.BalanceMap + } + tests := []struct { + name string + args args + wantFiats types.BalanceMap + wantRest types.BalanceMap + }{ + { + args: args{ + balances: types.BalanceMap{ + "USDC": types.Balance{Currency: "USDC", Available: number(70.0)}, + "USDT": types.Balance{Currency: "USDT", Available: number(100.0)}, + "BUSD": types.Balance{Currency: "BUSD", Available: number(80.0)}, + "BTC": types.Balance{Currency: "BTC", Available: number(0.01)}, + }, + }, + wantFiats: types.BalanceMap{ + "USDC": types.Balance{Currency: "USDC", Available: number(70.0)}, + "USDT": types.Balance{Currency: "USDT", Available: number(100.0)}, + "BUSD": types.Balance{Currency: "BUSD", Available: number(80.0)}, + }, + wantRest: types.BalanceMap{ + "BTC": types.Balance{Currency: "BTC", Available: number(0.01)}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotFiats, gotRest := usdFiatBalances(tt.args.balances) + assert.Equalf(t, tt.wantFiats, gotFiats, "usdFiatBalances(%v)", tt.args.balances) + assert.Equalf(t, tt.wantRest, gotRest, "usdFiatBalances(%v)", tt.args.balances) + }) + } +} + +func Test_calculateNetValueInQuote(t *testing.T) { + type args struct { + balances types.BalanceMap + prices types.PriceMap + quoteCurrency string + } + tests := []struct { + name string + args args + wantAccountValue fixedpoint.Value + }{ + { + name: "positive asset", + args: args{ + balances: types.BalanceMap{ + "USDC": types.Balance{Currency: "USDC", Available: number(70.0)}, + "USDT": types.Balance{Currency: "USDT", Available: number(100.0)}, + "BUSD": types.Balance{Currency: "BUSD", Available: number(80.0)}, + "BTC": types.Balance{Currency: "BTC", Available: number(0.01)}, + }, + prices: types.PriceMap{ + "USDCUSDT": number(1.0), + "BUSDUSDT": number(1.0), + "BTCUSDT": number(19000.0), + }, + quoteCurrency: "USDT", + }, + wantAccountValue: number(19000.0*0.01 + 100.0 + 80.0 + 70.0), + }, + { + name: "reversed usdt price", + args: args{ + balances: types.BalanceMap{ + "USDC": types.Balance{Currency: "USDC", Available: number(70.0)}, + "TWD": types.Balance{Currency: "TWD", Available: number(3000.0)}, + "USDT": types.Balance{Currency: "USDT", Available: number(100.0)}, + "BUSD": types.Balance{Currency: "BUSD", Available: number(80.0)}, + "BTC": types.Balance{Currency: "BTC", Available: number(0.01)}, + }, + prices: types.PriceMap{ + "USDTTWD": number(30.0), + "USDCUSDT": number(1.0), + "BUSDUSDT": number(1.0), + "BTCUSDT": number(19000.0), + }, + quoteCurrency: "USDT", + }, + wantAccountValue: number(19000.0*0.01 + 100.0 + 80.0 + 70.0 + (3000.0 / 30.0)), + }, + { + name: "borrow base asset", + args: args{ + balances: types.BalanceMap{ + "USDT": types.Balance{Currency: "USDT", Available: number(20000.0 * 2)}, + "USDC": types.Balance{Currency: "USDC", Available: number(70.0)}, + "BUSD": types.Balance{Currency: "BUSD", Available: number(80.0)}, + "BTC": types.Balance{Currency: "BTC", Available: number(0), Borrowed: number(2.0)}, + }, + prices: types.PriceMap{ + "USDCUSDT": number(1.0), + "BUSDUSDT": number(1.0), + "BTCUSDT": number(19000.0), + }, + quoteCurrency: "USDT", + }, + wantAccountValue: number(19000.0*-2.0 + 20000.0*2 + 80.0 + 70.0), + }, + { + name: "multi base asset", + args: args{ + balances: types.BalanceMap{ + "USDT": types.Balance{Currency: "USDT", Available: number(20000.0 * 2)}, + "USDC": types.Balance{Currency: "USDC", Available: number(70.0)}, + "BUSD": types.Balance{Currency: "BUSD", Available: number(80.0)}, + "ETH": types.Balance{Currency: "ETH", Available: number(10.0)}, + "BTC": types.Balance{Currency: "BTC", Available: number(0), Borrowed: number(2.0)}, + }, + prices: types.PriceMap{ + "USDCUSDT": number(1.0), + "BUSDUSDT": number(1.0), + "ETHUSDT": number(1700.0), + "BTCUSDT": number(19000.0), + }, + quoteCurrency: "USDT", + }, + wantAccountValue: number(19000.0*-2.0 + 1700.0*10.0 + 20000.0*2 + 80.0 + 70.0), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.wantAccountValue, calculateNetValueInQuote(tt.args.balances, tt.args.prices, tt.args.quoteCurrency), "calculateNetValueInQuote(%v, %v, %v)", tt.args.balances, tt.args.prices, tt.args.quoteCurrency) + }) + } +} diff --git a/pkg/bbgo/scale_test.go b/pkg/bbgo/scale_test.go index 66e8544e36..ab7ac2cf83 100644 --- a/pkg/bbgo/scale_test.go +++ b/pkg/bbgo/scale_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/assert" ) -const Delta = 1e-9 +const delta = 1e-9 func TestExponentialScale(t *testing.T) { // graph see: https://www.desmos.com/calculator/ip0ijbcbbf @@ -19,8 +19,8 @@ func TestExponentialScale(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "f(x) = 0.001000 * 1.002305 ^ (x - 1000.000000)", scale.String()) - assert.InDelta(t, 0.001, scale.Call(1000.0), Delta) - assert.InDelta(t, 0.01, scale.Call(2000.0), Delta) + assert.InDelta(t, 0.001, scale.Call(1000.0), delta) + assert.InDelta(t, 0.01, scale.Call(2000.0), delta) for x := 1000; x <= 2000; x += 100 { y := scale.Call(float64(x)) @@ -38,8 +38,8 @@ func TestExponentialScale_Reverse(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "f(x) = 0.100000 * 0.995405 ^ (x - 1000.000000)", scale.String()) - assert.InDelta(t, 0.1, scale.Call(1000.0), Delta) - assert.InDelta(t, 0.001, scale.Call(2000.0), Delta) + assert.InDelta(t, 0.1, scale.Call(1000.0), delta) + assert.InDelta(t, 0.001, scale.Call(2000.0), delta) for x := 1000; x <= 2000; x += 100 { y := scale.Call(float64(x)) @@ -57,8 +57,8 @@ func TestLogScale(t *testing.T) { err := scale.Solve() assert.NoError(t, err) assert.Equal(t, "f(x) = 0.001303 * log(x - 999.000000) + 0.001000", scale.String()) - assert.InDelta(t, 0.001, scale.Call(1000.0), Delta) - assert.InDelta(t, 0.01, scale.Call(2000.0), Delta) + assert.InDelta(t, 0.001, scale.Call(1000.0), delta) + assert.InDelta(t, 0.01, scale.Call(2000.0), delta) for x := 1000; x <= 2000; x += 100 { y := scale.Call(float64(x)) t.Logf("%s = %f", scale.FormulaOf(float64(x)), y) @@ -74,8 +74,8 @@ func TestLinearScale(t *testing.T) { err := scale.Solve() assert.NoError(t, err) assert.Equal(t, "f(x) = 0.007000 * x + -4.000000", scale.String()) - assert.InDelta(t, 3, scale.Call(1000), Delta) - assert.InDelta(t, 10, scale.Call(2000), Delta) + assert.InDelta(t, 3, scale.Call(1000), delta) + assert.InDelta(t, 10, scale.Call(2000), delta) for x := 1000; x <= 2000; x += 100 { y := scale.Call(float64(x)) t.Logf("%s = %f", scale.FormulaOf(float64(x)), y) @@ -91,8 +91,8 @@ func TestLinearScale2(t *testing.T) { err := scale.Solve() assert.NoError(t, err) assert.Equal(t, "f(x) = 0.150000 * x + -0.050000", scale.String()) - assert.InDelta(t, 0.1, scale.Call(1), Delta) - assert.InDelta(t, 0.4, scale.Call(3), Delta) + assert.InDelta(t, 0.1, scale.Call(1), delta) + assert.InDelta(t, 0.4, scale.Call(3), delta) } func TestQuadraticScale(t *testing.T) { @@ -105,9 +105,9 @@ func TestQuadraticScale(t *testing.T) { err := scale.Solve() assert.NoError(t, err) assert.Equal(t, "f(x) = 0.000550 * x ^ 2 + 0.135000 * x + 1.000000", scale.String()) - assert.InDelta(t, 1, scale.Call(0), Delta) - assert.InDelta(t, 20, scale.Call(100.0), Delta) - assert.InDelta(t, 50.0, scale.Call(200.0), Delta) + assert.InDelta(t, 1, scale.Call(0), delta) + assert.InDelta(t, 20, scale.Call(100.0), delta) + assert.InDelta(t, 50.0, scale.Call(200.0), delta) for x := 0; x <= 200; x += 1 { y := scale.Call(float64(x)) t.Logf("%s = %f", scale.FormulaOf(float64(x)), y) @@ -127,11 +127,11 @@ func TestPercentageScale(t *testing.T) { v, err := s.Scale(0.0) assert.NoError(t, err) - assert.InDelta(t, 1.0, v, Delta) + assert.InDelta(t, 1.0, v, delta) v, err = s.Scale(1.0) assert.NoError(t, err) - assert.InDelta(t, 100.0, v, Delta) + assert.InDelta(t, 100.0, v, delta) }) t.Run("from -1.0 to 1.0", func(t *testing.T) { @@ -146,11 +146,11 @@ func TestPercentageScale(t *testing.T) { v, err := s.Scale(-1.0) assert.NoError(t, err) - assert.InDelta(t, 10.0, v, Delta) + assert.InDelta(t, 10.0, v, delta) v, err = s.Scale(1.0) assert.NoError(t, err) - assert.InDelta(t, 100.0, v, Delta) + assert.InDelta(t, 100.0, v, delta) }) t.Run("reverse -1.0 to 1.0", func(t *testing.T) { @@ -165,19 +165,19 @@ func TestPercentageScale(t *testing.T) { v, err := s.Scale(-1.0) assert.NoError(t, err) - assert.InDelta(t, 100.0, v, Delta) + assert.InDelta(t, 100.0, v, delta) v, err = s.Scale(1.0) assert.NoError(t, err) - assert.InDelta(t, 10.0, v, Delta) + assert.InDelta(t, 10.0, v, delta) v, err = s.Scale(2.0) assert.NoError(t, err) - assert.InDelta(t, 10.0, v, Delta) + assert.InDelta(t, 10.0, v, delta) v, err = s.Scale(-2.0) assert.NoError(t, err) - assert.InDelta(t, 100.0, v, Delta) + assert.InDelta(t, 100.0, v, delta) }) t.Run("negative range", func(t *testing.T) { @@ -192,10 +192,10 @@ func TestPercentageScale(t *testing.T) { v, err := s.Scale(0.0) assert.NoError(t, err) - assert.InDelta(t, -100.0, v, Delta) + assert.InDelta(t, -100.0, v, delta) v, err = s.Scale(1.0) assert.NoError(t, err) - assert.InDelta(t, 100.0, v, Delta) + assert.InDelta(t, 100.0, v, delta) }) } diff --git a/pkg/bbgo/serialmarketdatastore.go b/pkg/bbgo/serialmarketdatastore.go new file mode 100644 index 0000000000..3bcf6b645f --- /dev/null +++ b/pkg/bbgo/serialmarketdatastore.go @@ -0,0 +1,69 @@ +package bbgo + +import ( + "time" + + "github.com/c9s/bbgo/pkg/types" +) + +type SerialMarketDataStore struct { + *MarketDataStore + KLines map[types.Interval]*types.KLine + Subscription []types.Interval +} + +func NewSerialMarketDataStore(symbol string) *SerialMarketDataStore { + return &SerialMarketDataStore{ + MarketDataStore: NewMarketDataStore(symbol), + KLines: make(map[types.Interval]*types.KLine), + Subscription: []types.Interval{}, + } +} + +func (store *SerialMarketDataStore) Subscribe(interval types.Interval) { + // dedup + for _, i := range store.Subscription { + if i == interval { + return + } + } + store.Subscription = append(store.Subscription, interval) +} + +func (store *SerialMarketDataStore) BindStream(stream types.Stream) { + stream.OnKLineClosed(store.handleKLineClosed) +} + +func (store *SerialMarketDataStore) handleKLineClosed(kline types.KLine) { + store.AddKLine(kline) +} + +func (store *SerialMarketDataStore) AddKLine(kline types.KLine) { + if kline.Symbol != store.Symbol { + return + } + // only consumes kline1m + if kline.Interval != types.Interval1m { + return + } + // endtime in minutes + timestamp := kline.StartTime.Time().Add(time.Minute) + for _, val := range store.Subscription { + k, ok := store.KLines[val] + if !ok { + k = &types.KLine{} + k.Set(&kline) + k.Interval = val + k.Closed = false + store.KLines[val] = k + } else { + k.Merge(&kline) + k.Closed = false + } + if timestamp.Truncate(val.Duration()) == timestamp { + k.Closed = true + store.MarketDataStore.AddKLine(*k) + delete(store.KLines, val) + } + } +} diff --git a/pkg/bbgo/session.go b/pkg/bbgo/session.go index e2fba67eeb..e62ac6a608 100644 --- a/pkg/bbgo/session.go +++ b/pkg/bbgo/session.go @@ -7,161 +7,27 @@ import ( "sync" "time" - "github.com/c9s/bbgo/pkg/cache" + "github.com/slack-go/slack" "github.com/prometheus/client_golang/prometheus" log "github.com/sirupsen/logrus" "github.com/spf13/viper" - "github.com/c9s/bbgo/pkg/cmd/cmdutil" + "github.com/c9s/bbgo/pkg/cache" + "github.com/c9s/bbgo/pkg/util/templateutil" + + exchange2 "github.com/c9s/bbgo/pkg/exchange" "github.com/c9s/bbgo/pkg/fixedpoint" - "github.com/c9s/bbgo/pkg/indicator" "github.com/c9s/bbgo/pkg/service" "github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/util" ) -var ( - debugEWMA = false - debugSMA = false -) - -func init() { - // when using --dotenv option, the dotenv is loaded from command.PersistentPreRunE, not init. - // hence here the env var won't enable the debug flag - util.SetEnvVarBool("DEBUG_EWMA", &debugEWMA) - util.SetEnvVarBool("DEBUG_SMA", &debugSMA) -} - -type StandardIndicatorSet struct { - Symbol string - // Standard indicators - // interval -> window - sma map[types.IntervalWindow]*indicator.SMA - ewma map[types.IntervalWindow]*indicator.EWMA - boll map[types.IntervalWindowBandWidth]*indicator.BOLL - stoch map[types.IntervalWindow]*indicator.STOCH - volatility map[types.IntervalWindow]*indicator.VOLATILITY - - store *MarketDataStore -} - -func NewStandardIndicatorSet(symbol string, store *MarketDataStore) *StandardIndicatorSet { - set := &StandardIndicatorSet{ - Symbol: symbol, - sma: make(map[types.IntervalWindow]*indicator.SMA), - ewma: make(map[types.IntervalWindow]*indicator.EWMA), - boll: make(map[types.IntervalWindowBandWidth]*indicator.BOLL), - stoch: make(map[types.IntervalWindow]*indicator.STOCH), - volatility: make(map[types.IntervalWindow]*indicator.VOLATILITY), - store: store, - } - - // let us pre-defined commonly used intervals - for interval := range types.SupportedIntervals { - for _, window := range []int{7, 25, 99} { - iw := types.IntervalWindow{Interval: interval, Window: window} - set.sma[iw] = &indicator.SMA{IntervalWindow: iw} - set.sma[iw].Bind(store) - if debugSMA { - set.sma[iw].OnUpdate(func(value float64) { - log.Infof("%s SMA %s: %f", symbol, iw.String(), value) - }) - } - - set.ewma[iw] = &indicator.EWMA{IntervalWindow: iw} - set.ewma[iw].Bind(store) - - // if debug EWMA is enabled, we add the debug handler - if debugEWMA { - set.ewma[iw].OnUpdate(func(value float64) { - log.Infof("%s EWMA %s: %f", symbol, iw.String(), value) - }) - } - - } - - // setup boll indicator, we may refactor boll indicator by subscribing SMA indicator, - // however, since general used BOLLINGER band use window 21, which is not in the existing SMA indicator sets. - // Pull out the bandwidth configuration as the boll Key - iw := types.IntervalWindow{Interval: interval, Window: 21} - - // set efault band width to 2.0 - iwb := types.IntervalWindowBandWidth{IntervalWindow: iw, BandWidth: 2.0} - set.boll[iwb] = &indicator.BOLL{IntervalWindow: iw, K: iwb.BandWidth} - set.boll[iwb].Bind(store) - } - - return set -} - -// BOLL returns the bollinger band indicator of the given interval, the window and bandwidth -func (set *StandardIndicatorSet) BOLL(iw types.IntervalWindow, bandWidth float64) *indicator.BOLL { - iwb := types.IntervalWindowBandWidth{IntervalWindow: iw, BandWidth: bandWidth} - inc, ok := set.boll[iwb] - if !ok { - inc = &indicator.BOLL{IntervalWindow: iw, K: bandWidth} - inc.Bind(set.store) - set.boll[iwb] = inc - } - - return inc -} - -// SMA returns the simple moving average indicator of the given interval and the window size. -func (set *StandardIndicatorSet) SMA(iw types.IntervalWindow) *indicator.SMA { - inc, ok := set.sma[iw] - if !ok { - inc = &indicator.SMA{IntervalWindow: iw} - inc.Bind(set.store) - set.sma[iw] = inc - } - - return inc -} - -// EWMA returns the exponential weighed moving average indicator of the given interval and the window size. -func (set *StandardIndicatorSet) EWMA(iw types.IntervalWindow) *indicator.EWMA { - inc, ok := set.ewma[iw] - if !ok { - inc = &indicator.EWMA{IntervalWindow: iw} - inc.Bind(set.store) - set.ewma[iw] = inc - } - - return inc -} - -func (set *StandardIndicatorSet) STOCH(iw types.IntervalWindow) *indicator.STOCH { - inc, ok := set.stoch[iw] - if !ok { - inc = &indicator.STOCH{IntervalWindow: iw} - inc.Bind(set.store) - set.stoch[iw] = inc - } - - return inc -} - -// VOLATILITY returns the volatility(stddev) indicator of the given interval and the window size. -func (set *StandardIndicatorSet) VOLATILITY(iw types.IntervalWindow) *indicator.VOLATILITY { - inc, ok := set.volatility[iw] - if !ok { - inc = &indicator.VOLATILITY{IntervalWindow: iw} - inc.Bind(set.store) - set.volatility[iw] = inc - } - - return inc -} +var KLinePreloadLimit int64 = 1000 // ExchangeSession presents the exchange connection Session // It also maintains and collects the data returned from the stream. type ExchangeSession struct { - // exchange Session based notification system - // we make it as a value field so that we can configure it separately - Notifiability `json:"-" yaml:"-"` - // --------------------------- // Session config fields // --------------------------- @@ -176,9 +42,10 @@ type ExchangeSession struct { SubAccount string `json:"subAccount,omitempty" yaml:"subAccount,omitempty"` // Withdrawal is used for enabling withdrawal functions - Withdrawal bool `json:"withdrawal,omitempty" yaml:"withdrawal,omitempty"` - MakerFeeRate fixedpoint.Value `json:"makerFeeRate" yaml:"makerFeeRate"` - TakerFeeRate fixedpoint.Value `json:"takerFeeRate" yaml:"takerFeeRate"` + Withdrawal bool `json:"withdrawal,omitempty" yaml:"withdrawal,omitempty"` + MakerFeeRate fixedpoint.Value `json:"makerFeeRate" yaml:"makerFeeRate"` + TakerFeeRate fixedpoint.Value `json:"takerFeeRate" yaml:"takerFeeRate"` + ModifyOrderAmountForFee bool `json:"modifyOrderAmountForFee" yaml:"modifyOrderAmountForFee"` PublicOnly bool `json:"publicOnly,omitempty" yaml:"publicOnly"` Margin bool `json:"margin,omitempty" yaml:"margin"` @@ -211,6 +78,8 @@ type ExchangeSession struct { Exchange types.Exchange `json:"-" yaml:"-"` + UseHeikinAshi bool `json:"heikinAshi,omitempty" yaml:"heikinAshi,omitempty"` + // Trades collects the executed trades from the exchange // map: symbol -> []trade Trades map[string]*types.TradeSlice `json:"-" yaml:"-"` @@ -249,12 +118,6 @@ func NewExchangeSession(name string, exchange types.Exchange) *ExchangeSession { marketDataStream.SetPublicOnly() session := &ExchangeSession{ - Notifiability: Notifiability{ - SymbolChannelRouter: NewPatternChannelRouter(nil), - SessionChannelRouter: NewPatternChannelRouter(nil), - ObjectChannelRouter: NewObjectChannelRouter(), - }, - Name: name, Exchange: exchange, UserDataStream: userDataStream, @@ -278,8 +141,7 @@ func NewExchangeSession(name string, exchange types.Exchange) *ExchangeSession { session.OrderExecutor = &ExchangeOrderExecutor{ // copy the notification system so that we can route - Notifiability: session.Notifiability, - Session: session, + Session: session, } return session @@ -293,15 +155,16 @@ func (session *ExchangeSession) GetAccount() (a *types.Account) { } // UpdateAccount locks the account mutex and update the account object -func (session *ExchangeSession) UpdateAccount(ctx context.Context) error { +func (session *ExchangeSession) UpdateAccount(ctx context.Context) (*types.Account, error) { account, err := session.Exchange.QueryAccount(ctx) if err != nil { - return err + return nil, err } + session.accountMutex.Lock() session.Account = account session.accountMutex.Unlock() - return nil + return account, nil } // Init initializes the basic data structure and market information by its exchange. @@ -333,6 +196,32 @@ func (session *ExchangeSession) Init(ctx context.Context, environ *Environment) session.markets = markets + if feeRateProvider, ok := session.Exchange.(types.ExchangeDefaultFeeRates); ok { + defaultFeeRates := feeRateProvider.DefaultFeeRates() + if session.MakerFeeRate.IsZero() { + session.MakerFeeRate = defaultFeeRates.MakerFeeRate + } + if session.TakerFeeRate.IsZero() { + session.TakerFeeRate = defaultFeeRates.TakerFeeRate + } + } + + if session.ModifyOrderAmountForFee { + amountProtectExchange, ok := session.Exchange.(types.ExchangeAmountFeeProtect) + if !ok { + return fmt.Errorf("exchange %s does not support order amount protection", session.ExchangeName.String()) + } + + fees := types.ExchangeFee{MakerFeeRate: session.MakerFeeRate, TakerFeeRate: session.TakerFeeRate} + amountProtectExchange.SetModifyOrderAmountForFee(fees) + } + + if session.UseHeikinAshi { + session.MarketDataStream = &types.HeikinAshiStream{ + StandardStreamEmitter: session.MarketDataStream.(types.StandardStreamEmitter), + } + } + // query and initialize the balances if !session.PublicOnly { account, err := session.Exchange.QueryAccount(ctx) @@ -387,13 +276,23 @@ func (session *ExchangeSession) Init(ctx context.Context, environ *Environment) } // update last prices - session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { - if _, ok := session.startPrices[kline.Symbol]; !ok { - session.startPrices[kline.Symbol] = kline.Open - } + if session.UseHeikinAshi { + session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { + if _, ok := session.startPrices[kline.Symbol]; !ok { + session.startPrices[kline.Symbol] = kline.Open + } - session.lastPrices[kline.Symbol] = kline.Close - }) + session.lastPrices[kline.Symbol] = session.MarketDataStream.(*types.HeikinAshiStream).LastOrigin[kline.Symbol][kline.Interval].Close + }) + } else { + session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { + if _, ok := session.startPrices[kline.Symbol]; !ok { + session.startPrices[kline.Symbol] = kline.Open + } + + session.lastPrices[kline.Symbol] = kline.Close + }) + } session.MarketDataStream.OnMarketTrade(func(trade types.Trade) { session.lastPrices[trade.Symbol] = trade.Price @@ -445,6 +344,8 @@ func (session *ExchangeSession) initSymbol(ctx context.Context, environ *Environ trades, err = environ.TradeService.Query(service.QueryTradesOptions{ Exchange: session.Exchange.Name(), Symbol: symbol, + Ordering: "DESC", + Limit: 100, }) } @@ -452,6 +353,7 @@ func (session *ExchangeSession) initSymbol(ctx context.Context, environ *Environ return err } + trades = types.SortTradesAscending(trades) log.Infof("symbol %s: %d trades loaded", symbol, len(trades)) } @@ -477,12 +379,18 @@ func (session *ExchangeSession) initSymbol(ctx context.Context, environ *Environ orderStore.BindStream(session.UserDataStream) session.orderStores[symbol] = orderStore - marketDataStore := NewMarketDataStore(symbol) - marketDataStore.BindStream(session.MarketDataStream) - session.marketDataStores[symbol] = marketDataStore + if _, ok := session.marketDataStores[symbol]; !ok { + marketDataStore := NewMarketDataStore(symbol) + marketDataStore.BindStream(session.MarketDataStream) + session.marketDataStores[symbol] = marketDataStore + } + + marketDataStore := session.marketDataStores[symbol] - standardIndicatorSet := NewStandardIndicatorSet(symbol, marketDataStore) - session.standardIndicatorSets[symbol] = standardIndicatorSet + if _, ok := session.standardIndicatorSets[symbol]; !ok { + standardIndicatorSet := NewStandardIndicatorSet(symbol, session.MarketDataStream, marketDataStore) + session.standardIndicatorSets[symbol] = standardIndicatorSet + } // used kline intervals by the given symbol var klineSubscriptions = map[types.Interval]struct{}{} @@ -512,29 +420,34 @@ func (session *ExchangeSession) initSymbol(ctx context.Context, environ *Environ for interval := range klineSubscriptions { // avoid querying the last unclosed kline endTime := environ.startTime - kLines, err := session.Exchange.QueryKLines(ctx, symbol, interval, types.KLineQueryOptions{ - EndTime: &endTime, - Limit: 1000, // indicators need at least 100 - }) - if err != nil { - return err - } + var i int64 + for i = 0; i < KLinePreloadLimit; i += 1000 { + var duration time.Duration = time.Duration(-i * int64(interval.Duration())) + e := endTime.Add(duration) + + kLines, err := session.Exchange.QueryKLines(ctx, symbol, interval, types.KLineQueryOptions{ + EndTime: &e, + Limit: 1000, // indicators need at least 100 + }) + if err != nil { + return err + } - if len(kLines) == 0 { - log.Warnf("no kline data for %s %s (end time <= %s)", symbol, interval, environ.startTime) - continue - } + if len(kLines) == 0 { + log.Warnf("no kline data for %s %s (end time <= %s)", symbol, interval, e) + continue + } - // update last prices by the given kline - lastKLine := kLines[len(kLines)-1] - if interval == types.Interval1m { - log.Infof("last kline %+v", lastKLine) - session.lastPrices[symbol] = lastKLine.Close - } + // update last prices by the given kline + lastKLine := kLines[len(kLines)-1] + if interval == types.Interval1m { + session.lastPrices[symbol] = lastKLine.Close + } - for _, k := range kLines { - // let market data store trigger the update, so that the indicator could be updated too. - marketDataStore.AddKLine(k) + for _, k := range kLines { + // let market data store trigger the update, so that the indicator could be updated too. + marketDataStore.AddKLine(k) + } } } @@ -544,9 +457,16 @@ func (session *ExchangeSession) initSymbol(ctx context.Context, environ *Environ return nil } -func (session *ExchangeSession) StandardIndicatorSet(symbol string) (*StandardIndicatorSet, bool) { +func (session *ExchangeSession) StandardIndicatorSet(symbol string) *StandardIndicatorSet { set, ok := session.standardIndicatorSets[symbol] - return set, ok + if ok { + return set + } + + store, _ := session.MarketDataStore(symbol) + set = NewStandardIndicatorSet(symbol, session.MarketDataStream, store) + session.standardIndicatorSets[symbol] = set + return set } func (session *ExchangeSession) Position(symbol string) (pos *types.Position, ok bool) { @@ -577,10 +497,38 @@ func (session *ExchangeSession) Positions() map[string]*types.Position { // MarketDataStore returns the market data store of a symbol func (session *ExchangeSession) MarketDataStore(symbol string) (s *MarketDataStore, ok bool) { s, ok = session.marketDataStores[symbol] + if !ok { + s = NewMarketDataStore(symbol) + s.BindStream(session.MarketDataStream) + session.marketDataStores[symbol] = s + return s, true + } return s, ok } -// MarketDataStore returns the market data store of a symbol +// KLine updates will be received in the order listend in intervals array +func (session *ExchangeSession) SerialMarketDataStore(symbol string, intervals []types.Interval) (store *SerialMarketDataStore, ok bool) { + st, ok := session.MarketDataStore(symbol) + if !ok { + return nil, false + } + store = NewSerialMarketDataStore(symbol) + klines, ok := st.KLinesOfInterval(types.Interval1m) + if !ok { + log.Errorf("SerialMarketDataStore: cannot get 1m history") + return nil, false + } + for _, interval := range intervals { + store.Subscribe(interval) + } + for _, kline := range *klines { + store.AddKLine(kline) + } + store.BindStream(session.MarketDataStream) + return store, true +} + +// OrderBook returns the personal orderbook of a symbol func (session *ExchangeSession) OrderBook(symbol string) (s *types.StreamOrderBook, ok bool) { s, ok = session.orderBooks[symbol] return s, ok @@ -596,6 +544,10 @@ func (session *ExchangeSession) LastPrice(symbol string) (price fixedpoint.Value return price, ok } +func (session *ExchangeSession) AllLastPrices() map[string]fixedpoint.Value { + return session.lastPrices +} + func (session *ExchangeSession) LastPrices() map[string]fixedpoint.Value { return session.lastPrices } @@ -646,21 +598,19 @@ func (session *ExchangeSession) FormatOrder(order types.SubmitOrder) (types.Subm return order, nil } -func (session *ExchangeSession) UpdatePrices(ctx context.Context) (err error) { - if session.lastPriceUpdatedAt.After(time.Now().Add(-time.Hour)) { - return nil - } - - balances := session.GetAccount().Balances() +func (session *ExchangeSession) UpdatePrices(ctx context.Context, currencies []string, fiat string) (err error) { + // TODO: move this cache check to the http routes + // if session.lastPriceUpdatedAt.After(time.Now().Add(-time.Hour)) { + // return nil + // } var symbols []string - for _, b := range balances { - symbols = append(symbols, b.Currency+"USDT") - symbols = append(symbols, "USDT"+b.Currency) + for _, c := range currencies { + symbols = append(symbols, c+fiat) // BTC/USDT + symbols = append(symbols, fiat+c) // USDT/TWD } tickers, err := session.Exchange.QueryTickers(ctx, symbols...) - if err != nil || len(tickers) == 0 { return err } @@ -668,12 +618,7 @@ func (session *ExchangeSession) UpdatePrices(ctx context.Context) (err error) { var lastTime time.Time for k, v := range tickers { // for {Crypto}/USDT markets - if strings.HasSuffix(k, "USDT") { - session.lastPrices[k] = v.Last - } else if strings.HasPrefix(k, "USDT") { - session.lastPrices[k] = fixedpoint.One.Div(v.Last) - } - + session.lastPrices[k] = v.Last if v.Time.After(lastTime) { lastTime = v.Time } @@ -727,17 +672,17 @@ func (session *ExchangeSession) FindPossibleSymbols() (symbols []string, err err // InitExchange initialize the exchange instance and allocate memory for fields // In this stage, the session var could be loaded from the JSON config, so the pointer fields are still nil // The Init method will be called after this stage, environment.Init will call the session.Init method later. -func (session *ExchangeSession) InitExchange(name string, exchange types.Exchange) error { +func (session *ExchangeSession) InitExchange(name string, ex types.Exchange) error { var err error var exchangeName = session.ExchangeName - if exchange == nil { + if ex == nil { if session.PublicOnly { - exchange, err = cmdutil.NewExchangePublic(exchangeName) + ex, err = exchange2.NewPublic(exchangeName) } else { if session.Key != "" && session.Secret != "" { - exchange, err = cmdutil.NewExchangeStandard(exchangeName, session.Key, session.Secret, session.Passphrase, session.SubAccount) + ex, err = exchange2.NewStandard(exchangeName, session.Key, session.Secret, session.Passphrase, session.SubAccount) } else { - exchange, err = cmdutil.NewExchangeWithEnvVarPrefix(exchangeName, session.EnvVarPrefix) + ex, err = exchange2.NewWithEnvVarPrefix(exchangeName, session.EnvVarPrefix) } } } @@ -748,7 +693,7 @@ func (session *ExchangeSession) InitExchange(name string, exchange types.Exchang // configure exchange if session.Margin { - marginExchange, ok := exchange.(types.MarginExchange) + marginExchange, ok := ex.(types.MarginExchange) if !ok { return fmt.Errorf("exchange %s does not support margin", exchangeName) } @@ -761,7 +706,7 @@ func (session *ExchangeSession) InitExchange(name string, exchange types.Exchang } if session.Futures { - futuresExchange, ok := exchange.(types.FuturesExchange) + futuresExchange, ok := ex.(types.FuturesExchange) if !ok { return fmt.Errorf("exchange %s does not support futures", exchangeName) } @@ -774,14 +719,9 @@ func (session *ExchangeSession) InitExchange(name string, exchange types.Exchang } session.Name = name - session.Notifiability = Notifiability{ - SymbolChannelRouter: NewPatternChannelRouter(nil), - SessionChannelRouter: NewPatternChannelRouter(nil), - ObjectChannelRouter: NewObjectChannelRouter(), - } - session.Exchange = exchange - session.UserDataStream = exchange.NewStream() - session.MarketDataStream = exchange.NewStream() + session.Exchange = ex + session.UserDataStream = ex.NewStream() + session.MarketDataStream = ex.NewStream() session.MarketDataStream.SetPublicOnly() // pointer fields @@ -799,8 +739,7 @@ func (session *ExchangeSession) InitExchange(name string, exchange types.Exchang session.orderStores = make(map[string]*OrderStore) session.OrderExecutor = &ExchangeOrderExecutor{ // copy the notification system so that we can route - Notifiability: session.Notifiability, - Session: session, + Session: session, } session.usedSymbols = make(map[string]struct{}) @@ -923,9 +862,34 @@ func (session *ExchangeSession) bindUserDataStreamMetrics(stream types.Stream) { func (session *ExchangeSession) bindConnectionStatusNotification(stream types.Stream, streamName string) { stream.OnDisconnect(func() { - session.Notifiability.Notify("session %s %s stream disconnected", session.Name, streamName) + Notify("session %s %s stream disconnected", session.Name, streamName) }) stream.OnConnect(func() { - session.Notifiability.Notify("session %s %s stream connected", session.Name, streamName) + Notify("session %s %s stream connected", session.Name, streamName) }) } + +func (session *ExchangeSession) SlackAttachment() slack.Attachment { + var fields []slack.AttachmentField + var footerIcon = types.ExchangeFooterIcon(session.ExchangeName) + return slack.Attachment{ + // Pretext: "", + // Text: text, + Title: session.Name, + Fields: fields, + FooterIcon: footerIcon, + Footer: templateutil.Render("update time {{ . }}", time.Now().Format(time.RFC822)), + } +} + +func (session *ExchangeSession) FormatOrders(orders []types.SubmitOrder) (formattedOrders []types.SubmitOrder, err error) { + for _, order := range orders { + o, err := session.FormatOrder(order) + if err != nil { + return formattedOrders, err + } + formattedOrders = append(formattedOrders, o) + } + + return formattedOrders, err +} diff --git a/pkg/bbgo/smart_stops.go b/pkg/bbgo/smart_stops.go deleted file mode 100644 index e3dd0ca267..0000000000 --- a/pkg/bbgo/smart_stops.go +++ /dev/null @@ -1,287 +0,0 @@ -package bbgo - -import ( - "context" - "errors" - - log "github.com/sirupsen/logrus" - - "github.com/c9s/bbgo/pkg/fixedpoint" - "github.com/c9s/bbgo/pkg/types" -) - -type TrailingStop struct { - // CallbackRate is the callback rate from the previous high price - CallbackRate fixedpoint.Value `json:"callbackRate,omitempty"` - - // ClosePosition is a percentage of the position to be closed - ClosePosition fixedpoint.Value `json:"closePosition,omitempty"` - - // MinProfit is the percentage of the minimum profit ratio. - // Stop order will be activiated only when the price reaches above this threshold. - MinProfit fixedpoint.Value `json:"minProfit,omitempty"` - - // Interval is the time resolution to update the stop order - // KLine per Interval will be used for updating the stop order - Interval types.Interval `json:"interval,omitempty"` - - // Virtual is used when you don't want to place the real order on the exchange and lock the balance. - // You want to handle the stop order by the strategy itself. - Virtual bool `json:"virtual,omitempty"` -} - -type TrailingStopController struct { - *TrailingStop - - Symbol string - - position *types.Position - latestHigh fixedpoint.Value - averageCost fixedpoint.Value - - // activated: when the price reaches the min profit price, we set the activated to true to enable trailing stop - activated bool -} - -func NewTrailingStopController(symbol string, config *TrailingStop) *TrailingStopController { - return &TrailingStopController{ - TrailingStop: config, - Symbol: symbol, - } -} - -func (c *TrailingStopController) Subscribe(session *ExchangeSession) { - session.Subscribe(types.KLineChannel, c.Symbol, types.SubscribeOptions{ - Interval: c.Interval.String(), - }) -} - -func (c *TrailingStopController) Run(ctx context.Context, session *ExchangeSession, tradeCollector *TradeCollector) { - // store the position - c.position = tradeCollector.Position() - c.averageCost = c.position.AverageCost - - // Use trade collector to get the position update event - tradeCollector.OnPositionUpdate(func(position *types.Position) { - // update average cost if we have it. - c.averageCost = position.AverageCost - }) - - session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { - if kline.Symbol != c.Symbol || kline.Interval != c.Interval { - return - } - - // if average cost is zero, we don't need trailing stop - if c.averageCost.IsZero() || c.position == nil { - return - } - - closePrice := kline.Close - - // if we don't hold position, we just skip dust position - if c.position.Base.Abs().Compare(c.position.Market.MinQuantity) < 0 || c.position.Base.Abs().Mul(closePrice).Compare(c.position.Market.MinNotional) < 0 { - return - } - - if c.MinProfit.Sign() <= 0 { - // when minProfit is not set, we should always activate the trailing stop order - c.activated = true - } else if closePrice.Compare(c.averageCost) > 0 || - changeRate(closePrice, c.averageCost).Compare(c.MinProfit) > 0 { - - if !c.activated { - log.Infof("%s trailing stop activated at price %s", c.Symbol, closePrice.String()) - c.activated = true - } - } else { - return - } - - if !c.activated { - return - } - - // if the trailing stop order is activated, we should update the latest high - // update the latest high - c.latestHigh = fixedpoint.Max(closePrice, c.latestHigh) - - // if it's in the callback rate, we don't want to trigger stop - if closePrice.Compare(c.latestHigh) < 0 && changeRate(closePrice, c.latestHigh).Compare(c.CallbackRate) < 0 { - return - } - - if c.Virtual { - // if the profit rate is defined, and it is less than our minimum profit rate, we skip stop - if c.MinProfit.Sign() > 0 && - closePrice.Compare(c.averageCost) < 0 || - changeRate(closePrice, c.averageCost).Compare(c.MinProfit) < 0 { - return - } - - log.Infof("%s trailing stop emitted, latest high: %s, closed price: %s, average cost: %s, profit spread: %s", - c.Symbol, - c.latestHigh.String(), - closePrice.String(), - c.averageCost.String(), - closePrice.Sub(c.averageCost).String()) - - log.Infof("current %s position: %s", c.Symbol, c.position.String()) - - marketOrder := c.position.NewClosePositionOrder(c.ClosePosition) - if marketOrder != nil { - log.Infof("submitting %s market order to stop: %+v", c.Symbol, marketOrder) - - // skip dust order - if marketOrder.Quantity.Mul(closePrice).Compare(c.position.Market.MinNotional) < 0 { - log.Warnf("%s market order quote quantity %s < min notional %s, skip placing order", c.Symbol, marketOrder.Quantity.Mul(closePrice).String(), c.position.Market.MinNotional.String()) - return - } - - createdOrders, err := session.Exchange.SubmitOrders(ctx, *marketOrder) - if err != nil { - log.WithError(err).Errorf("stop market order place error") - return - } - tradeCollector.OrderStore().Add(createdOrders...) - tradeCollector.Process() - - // reset the state - c.latestHigh = fixedpoint.Zero - c.activated = false - } - } else { - // place stop order only when the closed price is greater than the current average cost - if c.MinProfit.Sign() > 0 && closePrice.Compare(c.averageCost) > 0 && - changeRate(closePrice, c.averageCost).Compare(c.MinProfit) >= 0 { - - stopPrice := c.averageCost.Mul(fixedpoint.One.Add(c.MinProfit)) - orderForm := c.GenerateStopOrder(stopPrice, c.averageCost) - if orderForm != nil { - log.Infof("updating %s stop limit order to simulate trailing stop order...", c.Symbol) - - createdOrders, err := session.Exchange.SubmitOrders(ctx, *orderForm) - if err != nil { - log.WithError(err).Errorf("%s stop order place error", c.Symbol) - return - } - - tradeCollector.OrderStore().Add(createdOrders...) - tradeCollector.Process() - } - } - } - }) -} - -func (c *TrailingStopController) GenerateStopOrder(stopPrice, price fixedpoint.Value) *types.SubmitOrder { - base := c.position.GetBase() - if base.IsZero() { - return nil - } - - quantity := base.Abs() - quoteQuantity := price.Mul(quantity) - - if c.ClosePosition.Sign() > 0 { - quantity = quantity.Mul(c.ClosePosition) - } - - // skip dust orders - if quantity.Compare(c.position.Market.MinQuantity) < 0 || - quoteQuantity.Compare(c.position.Market.MinNotional) < 0 { - return nil - } - - side := types.SideTypeSell - if base.Sign() < 0 { - side = types.SideTypeBuy - } - - return &types.SubmitOrder{ - Symbol: c.Symbol, - Market: c.position.Market, - Type: types.OrderTypeStopLimit, - Side: side, - StopPrice: stopPrice, - Price: price, - Quantity: quantity, - } -} - -type FixedStop struct{} - -type Stop struct { - TrailingStop *TrailingStop `json:"trailingStop,omitempty"` - FixedStop *FixedStop `json:"fixedStop,omitempty"` -} - -// SmartStops shares the stop order logics between different strategies -// -// See also: -// - Stop-Loss order: https://www.investopedia.com/terms/s/stop-lossorder.asp -// - Trailing Stop-loss order: https://www.investopedia.com/articles/trading/08/trailing-stop-loss.asp -// -// How to integrate this into your strategy? -// -// To use the stop controllers, you can embed this struct into your Strategy struct -// -// func (s *Strategy) Initialize() error { -// return s.SmartStops.InitializeStopControllers(s.Symbol) -// } -// func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { -// s.SmartStops.Subscribe(session) -// } -// -// func (s *Strategy) Run() { -// s.SmartStops.RunStopControllers(ctx, session, s.tradeCollector) -// } -// -type SmartStops struct { - // Stops is the slice of the stop order config - Stops []Stop `json:"stops,omitempty"` - - // StopControllers are constructed from the stop config - StopControllers []StopController `json:"-"` -} - -type StopController interface { - Subscribe(session *ExchangeSession) - Run(ctx context.Context, session *ExchangeSession, tradeCollector *TradeCollector) -} - -func (s *SmartStops) newStopController(symbol string, config Stop) (StopController, error) { - if config.TrailingStop != nil { - return NewTrailingStopController(symbol, config.TrailingStop), nil - } - - return nil, errors.New("incorrect stop controller setup") -} - -func (s *SmartStops) InitializeStopControllers(symbol string) error { - for _, stop := range s.Stops { - controller, err := s.newStopController(symbol, stop) - if err != nil { - return err - } - - s.StopControllers = append(s.StopControllers, controller) - } - return nil -} - -func (s *SmartStops) Subscribe(session *ExchangeSession) { - for _, stopController := range s.StopControllers { - stopController.Subscribe(session) - } -} - -func (s *SmartStops) RunStopControllers(ctx context.Context, session *ExchangeSession, tradeCollector *TradeCollector) { - for _, stopController := range s.StopControllers { - stopController.Run(ctx, session, tradeCollector) - } -} - -func changeRate(a, b fixedpoint.Value) fixedpoint.Value { - return a.Sub(b).Div(b).Abs() -} diff --git a/pkg/bbgo/source.go b/pkg/bbgo/source.go new file mode 100644 index 0000000000..34a3de131d --- /dev/null +++ b/pkg/bbgo/source.go @@ -0,0 +1,82 @@ +package bbgo + +import ( + "encoding/json" + "strings" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" + log "github.com/sirupsen/logrus" +) + +type SourceFunc func(*types.KLine) fixedpoint.Value + +type selectorInternal struct { + Source string + sourceGetter SourceFunc +} + +func (s *selectorInternal) UnmarshalJSON(d []byte) error { + if err := json.Unmarshal(d, &s.Source); err != nil { + return err + } + s.init() + return nil +} + +func (s selectorInternal) MarshalJSON() ([]byte, error) { + if s.Source == "" { + s.Source = "close" + s.init() + } + return []byte("\"" + s.Source + "\""), nil +} + +type SourceSelector struct { + Source selectorInternal `json:"source,omitempty"` +} + +func (s *selectorInternal) init() { + switch strings.ToLower(s.Source) { + case "close": + s.sourceGetter = func(kline *types.KLine) fixedpoint.Value { return kline.Close } + case "high": + s.sourceGetter = func(kline *types.KLine) fixedpoint.Value { return kline.High } + case "low": + s.sourceGetter = func(kline *types.KLine) fixedpoint.Value { return kline.Low } + case "hl2": + s.sourceGetter = func(kline *types.KLine) fixedpoint.Value { return kline.High.Add(kline.Low).Div(fixedpoint.Two) } + case "hlc3": + s.sourceGetter = func(kline *types.KLine) fixedpoint.Value { + return kline.High.Add(kline.Low).Add(kline.Close).Div(fixedpoint.Three) + } + case "ohlc4": + s.sourceGetter = func(kline *types.KLine) fixedpoint.Value { + return kline.High.Add(kline.Low).Add(kline.Close).Add(kline.Open).Div(fixedpoint.Four) + } + case "open": + s.sourceGetter = func(kline *types.KLine) fixedpoint.Value { return kline.Open } + case "oc2": + s.sourceGetter = func(kline *types.KLine) fixedpoint.Value { return kline.Open.Add(kline.Close).Div(fixedpoint.Two) } + default: + log.Infof("source not set: %s, use hl2 by default", s.Source) + s.sourceGetter = func(kline *types.KLine) fixedpoint.Value { return kline.High.Add(kline.Low).Div(fixedpoint.Two) } + } +} + +func (s *selectorInternal) String() string { + if s.Source == "" { + s.Source = "close" + s.init() + } + return s.Source +} + +// lazy init if empty struct is passed in +func (s *SourceSelector) GetSource(kline *types.KLine) fixedpoint.Value { + if s.Source.Source == "" { + s.Source.Source = "close" + s.Source.init() + } + return s.Source.sourceGetter(kline) +} diff --git a/pkg/bbgo/source_test.go b/pkg/bbgo/source_test.go new file mode 100644 index 0000000000..cfcee3c081 --- /dev/null +++ b/pkg/bbgo/source_test.go @@ -0,0 +1,34 @@ +package bbgo + +import ( + "encoding/json" + "testing" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" + "github.com/stretchr/testify/assert" +) + +func TestSource(t *testing.T) { + input := "{\"source\":\"high\"}" + type Strategy struct { + SourceSelector + } + s := Strategy{} + assert.NoError(t, json.Unmarshal([]byte(input), &s)) + assert.Equal(t, s.Source.Source, "high") + assert.NotNil(t, s.Source.sourceGetter) + e, err := json.Marshal(&s) + assert.NoError(t, err) + assert.Equal(t, input, string(e)) + + input = "{}" + s = Strategy{} + assert.NoError(t, json.Unmarshal([]byte(input), &s)) + assert.Equal(t, fixedpoint.Zero, s.GetSource(&types.KLine{})) + + e, err = json.Marshal(&Strategy{}) + assert.NoError(t, err) + assert.Equal(t, "{\"source\":\"close\"}", string(e)) + +} diff --git a/pkg/bbgo/standard_indicator_set.go b/pkg/bbgo/standard_indicator_set.go new file mode 100644 index 0000000000..52ecdddce1 --- /dev/null +++ b/pkg/bbgo/standard_indicator_set.go @@ -0,0 +1,181 @@ +package bbgo + +import ( + "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/util" +) + +var ( + debugBOLL = false +) + +func init() { + // when using --dotenv option, the dotenv is loaded from command.PersistentPreRunE, not init. + // hence here the env var won't enable the debug flag + util.SetEnvVarBool("DEBUG_BOLL", &debugBOLL) +} + +type MACDConfig struct { + types.IntervalWindow +} + +type StandardIndicatorSet struct { + Symbol string + + // Standard indicators + // interval -> window + iwbIndicators map[types.IntervalWindowBandWidth]*indicator.BOLL + iwIndicators map[indicatorKey]indicator.KLinePusher + macdIndicators map[indicator.MACDConfig]*indicator.MACD + + stream types.Stream + store *MarketDataStore +} + +type indicatorKey struct { + iw types.IntervalWindow + id string +} + +func NewStandardIndicatorSet(symbol string, stream types.Stream, store *MarketDataStore) *StandardIndicatorSet { + return &StandardIndicatorSet{ + Symbol: symbol, + store: store, + stream: stream, + iwIndicators: make(map[indicatorKey]indicator.KLinePusher), + iwbIndicators: make(map[types.IntervalWindowBandWidth]*indicator.BOLL), + macdIndicators: make(map[indicator.MACDConfig]*indicator.MACD), + } +} + +func (s *StandardIndicatorSet) initAndBind(inc indicator.KLinePusher, interval types.Interval) { + if klines, ok := s.store.KLinesOfInterval(interval); ok { + for _, k := range *klines { + inc.PushK(k) + } + } + + s.stream.OnKLineClosed(types.KLineWith(s.Symbol, interval, inc.PushK)) +} + +func (s *StandardIndicatorSet) allocateSimpleIndicator(t indicator.KLinePusher, iw types.IntervalWindow, id string) indicator.KLinePusher { + k := indicatorKey{ + iw: iw, + id: id, + } + inc, ok := s.iwIndicators[k] + if ok { + return inc + } + + inc = t + s.initAndBind(inc, iw.Interval) + s.iwIndicators[k] = inc + return t +} + +// SMA is a helper function that returns the simple moving average indicator of the given interval and the window size. +func (s *StandardIndicatorSet) SMA(iw types.IntervalWindow) *indicator.SMA { + inc := s.allocateSimpleIndicator(&indicator.SMA{IntervalWindow: iw}, iw, "sma") + return inc.(*indicator.SMA) +} + +// EWMA is a helper function that returns the exponential weighed moving average indicator of the given interval and the window size. +func (s *StandardIndicatorSet) EWMA(iw types.IntervalWindow) *indicator.EWMA { + inc := s.allocateSimpleIndicator(&indicator.EWMA{IntervalWindow: iw}, iw, "ewma") + return inc.(*indicator.EWMA) +} + +// VWMA +func (s *StandardIndicatorSet) VWMA(iw types.IntervalWindow) *indicator.VWMA { + inc := s.allocateSimpleIndicator(&indicator.VWMA{IntervalWindow: iw}, iw, "vwma") + return inc.(*indicator.VWMA) +} + +func (s *StandardIndicatorSet) PivotHigh(iw types.IntervalWindow) *indicator.PivotHigh { + inc := s.allocateSimpleIndicator(&indicator.PivotHigh{IntervalWindow: iw}, iw, "pivothigh") + return inc.(*indicator.PivotHigh) +} + +func (s *StandardIndicatorSet) PivotLow(iw types.IntervalWindow) *indicator.PivotLow { + inc := s.allocateSimpleIndicator(&indicator.PivotLow{IntervalWindow: iw}, iw, "pivotlow") + return inc.(*indicator.PivotLow) +} + +func (s *StandardIndicatorSet) ATR(iw types.IntervalWindow) *indicator.ATR { + inc := s.allocateSimpleIndicator(&indicator.ATR{IntervalWindow: iw}, iw, "atr") + return inc.(*indicator.ATR) +} + +func (s *StandardIndicatorSet) ATRP(iw types.IntervalWindow) *indicator.ATRP { + inc := s.allocateSimpleIndicator(&indicator.ATRP{IntervalWindow: iw}, iw, "atrp") + return inc.(*indicator.ATRP) +} + +func (s *StandardIndicatorSet) EMV(iw types.IntervalWindow) *indicator.EMV { + inc := s.allocateSimpleIndicator(&indicator.EMV{IntervalWindow: iw}, iw, "emv") + return inc.(*indicator.EMV) +} + +func (s *StandardIndicatorSet) CCI(iw types.IntervalWindow) *indicator.CCI { + inc := s.allocateSimpleIndicator(&indicator.CCI{IntervalWindow: iw}, iw, "cci") + return inc.(*indicator.CCI) +} + +func (s *StandardIndicatorSet) HULL(iw types.IntervalWindow) *indicator.HULL { + inc := s.allocateSimpleIndicator(&indicator.HULL{IntervalWindow: iw}, iw, "hull") + return inc.(*indicator.HULL) +} + +func (s *StandardIndicatorSet) STOCH(iw types.IntervalWindow) *indicator.STOCH { + inc := s.allocateSimpleIndicator(&indicator.STOCH{IntervalWindow: iw}, iw, "stoch") + return inc.(*indicator.STOCH) +} + +// BOLL returns the bollinger band indicator of the given interval, the window and bandwidth +func (s *StandardIndicatorSet) BOLL(iw types.IntervalWindow, bandWidth float64) *indicator.BOLL { + iwb := types.IntervalWindowBandWidth{IntervalWindow: iw, BandWidth: bandWidth} + inc, ok := s.iwbIndicators[iwb] + if !ok { + inc = &indicator.BOLL{IntervalWindow: iw, K: bandWidth} + s.initAndBind(inc, iw.Interval) + + if debugBOLL { + inc.OnUpdate(func(sma float64, upBand float64, downBand float64) { + logrus.Infof("%s BOLL %s: sma=%f up=%f down=%f", s.Symbol, iw.String(), sma, upBand, downBand) + }) + } + s.iwbIndicators[iwb] = inc + } + + return inc +} + +func (s *StandardIndicatorSet) MACD(iw types.IntervalWindow, shortPeriod, longPeriod int) *indicator.MACD { + config := indicator.MACDConfig{IntervalWindow: iw, ShortPeriod: shortPeriod, LongPeriod: longPeriod} + + inc, ok := s.macdIndicators[config] + if ok { + return inc + } + inc = &indicator.MACD{MACDConfig: config} + s.macdIndicators[config] = inc + s.initAndBind(inc, config.IntervalWindow.Interval) + return inc +} + +// GHFilter is a helper function that returns the G-H (alpha beta) digital filter of the given interval and the window size. +func (s *StandardIndicatorSet) GHFilter(iw types.IntervalWindow) *indicator.GHFilter { + inc := s.allocateSimpleIndicator(&indicator.GHFilter{IntervalWindow: iw}, iw, "ghfilter") + return inc.(*indicator.GHFilter) +} + +// KalmanFilter is a helper function that returns the Kalman digital filter of the given interval and the window size. +// Note that the additional smooth window is set to zero in standard indicator set. Users have to create their own instance and push K-lines if a smoother filter is needed. +func (s *StandardIndicatorSet) KalmanFilter(iw types.IntervalWindow) *indicator.KalmanFilter { + inc := s.allocateSimpleIndicator(&indicator.KalmanFilter{IntervalWindow: iw, AdditionalSmoothWindow: 0}, iw, "kalmanfilter") + return inc.(*indicator.KalmanFilter) +} diff --git a/pkg/bbgo/stop_ema.go b/pkg/bbgo/stop_ema.go new file mode 100644 index 0000000000..11c0472b64 --- /dev/null +++ b/pkg/bbgo/stop_ema.go @@ -0,0 +1,42 @@ +package bbgo + +import ( + "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +type StopEMA struct { + types.IntervalWindow + Range fixedpoint.Value `json:"range"` + + stopEWMA *indicator.EWMA +} + +func (s *StopEMA) Bind(session *ExchangeSession, orderExecutor *GeneralOrderExecutor) { + symbol := orderExecutor.Position().Symbol + s.stopEWMA = session.StandardIndicatorSet(symbol).EWMA(s.IntervalWindow) +} + +func (s *StopEMA) Allowed(closePrice fixedpoint.Value) bool { + ema := fixedpoint.NewFromFloat(s.stopEWMA.Last()) + if ema.IsZero() { + logrus.Infof("stopEMA protection: value is zero, skip") + return false + } + + emaStopShortPrice := ema.Mul(fixedpoint.One.Sub(s.Range)) + if closePrice.Compare(emaStopShortPrice) < 0 { + Notify("stopEMA %s protection: close price %f less than stopEMA %f = EMA(%f) * (1 - RANGE %f)", + s.IntervalWindow.String(), + closePrice.Float64(), + emaStopShortPrice.Float64(), + ema.Float64(), + s.Range.Float64()) + return false + } + + return true +} diff --git a/pkg/bbgo/strategycontroller_callbacks.go b/pkg/bbgo/strategycontroller_callbacks.go index caa3e68fbf..dd432fcb57 100644 --- a/pkg/bbgo/strategycontroller_callbacks.go +++ b/pkg/bbgo/strategycontroller_callbacks.go @@ -1,4 +1,4 @@ -// Code generated by "callbackgen -type StrategyController strategy_controller.go"; DO NOT EDIT. +// Code generated by "callbackgen -type StrategyController -interface"; DO NOT EDIT. package bbgo @@ -33,3 +33,11 @@ func (s *StrategyController) EmitEmergencyStop() { cb() } } + +type StrategyControllerEventHub interface { + OnSuspend(cb func()) + + OnResume(cb func()) + + OnEmergencyStop(cb func()) +} diff --git a/pkg/bbgo/testdata/strategy.yaml b/pkg/bbgo/testdata/strategy.yaml index e04e630e1a..9c1ecc4829 100644 --- a/pkg/bbgo/testdata/strategy.yaml +++ b/pkg/bbgo/testdata/strategy.yaml @@ -5,11 +5,19 @@ sessions: envVarPrefix: MAX takerFeeRate: 0 makerFeeRate: 0 + modifyOrderAmountForFee: false binance: exchange: binance envVarPrefix: BINANCE takerFeeRate: 0 makerFeeRate: 0 + modifyOrderAmountForFee: false + ftx: + exchange: ftx + envVarPrefix: FTX + takerFeeRate: 0 + makerFeeRate: 0 + modifyOrderAmountForFee: true exchangeStrategies: - on: ["binance"] diff --git a/pkg/bbgo/trade_store.go b/pkg/bbgo/trade_store.go index eab6cca31c..a5ea2db380 100644 --- a/pkg/bbgo/trade_store.go +++ b/pkg/bbgo/trade_store.go @@ -12,15 +12,13 @@ type TradeStore struct { trades map[uint64]types.Trade - Symbol string RemoveCancelled bool RemoveFilled bool AddOrderUpdate bool } -func NewTradeStore(symbol string) *TradeStore { +func NewTradeStore() *TradeStore { return &TradeStore{ - Symbol: symbol, trades: make(map[uint64]types.Trade), } } diff --git a/pkg/bbgo/tradecollector.go b/pkg/bbgo/tradecollector.go index c648ec48c3..14c18d484e 100644 --- a/pkg/bbgo/tradecollector.go +++ b/pkg/bbgo/tradecollector.go @@ -2,6 +2,7 @@ package bbgo import ( "context" + "sync" "time" log "github.com/sirupsen/logrus" @@ -22,10 +23,14 @@ type TradeCollector struct { orderStore *OrderStore doneTrades map[types.TradeKey]struct{} - recoverCallbacks []func(trade types.Trade) - tradeCallbacks []func(trade types.Trade, profit, netProfit fixedpoint.Value) + mu sync.Mutex + + recoverCallbacks []func(trade types.Trade) + + tradeCallbacks []func(trade types.Trade, profit, netProfit fixedpoint.Value) + positionUpdateCallbacks []func(position *types.Position) - profitCallbacks []func(trade types.Trade, profit, netProfit fixedpoint.Value) + profitCallbacks []func(trade types.Trade, profit *types.Profit) } func NewTradeCollector(symbol string, position *types.Position, orderStore *OrderStore) *TradeCollector { @@ -34,7 +39,7 @@ func NewTradeCollector(symbol string, position *types.Position, orderStore *Orde orderSig: sigchan.New(1), tradeC: make(chan types.Trade, 100), - tradeStore: NewTradeStore(symbol), + tradeStore: NewTradeStore(), doneTrades: make(map[types.TradeKey]struct{}), position: position, orderStore: orderStore, @@ -51,6 +56,10 @@ func (c *TradeCollector) Position() *types.Position { return c.position } +func (c *TradeCollector) SetPosition(position *types.Position) { + c.position = position +} + // QueueTrade sends the trade object to the trade channel, // so that the goroutine can receive the trade and process in the background. func (c *TradeCollector) QueueTrade(trade types.Trade) { @@ -94,33 +103,52 @@ func (c *TradeCollector) Recover(ctx context.Context, ex types.ExchangeTradeHist return nil } +func (c *TradeCollector) setDone(key types.TradeKey) { + c.mu.Lock() + c.doneTrades[key] = struct{}{} + c.mu.Unlock() +} + // Process filters the received trades and see if there are orders matching the trades // if we have the order in the order store, then the trade will be considered for the position. // profit will also be calculated. func (c *TradeCollector) Process() bool { positionChanged := false + c.tradeStore.Filter(func(trade types.Trade) bool { key := trade.Key() + c.mu.Lock() + defer c.mu.Unlock() + // if it's already done, remove the trade from the trade store if _, done := c.doneTrades[key]; done { return true } if c.orderStore.Exists(trade.OrderID) { - c.doneTrades[key] = struct{}{} - if profit, netProfit, madeProfit := c.position.AddTrade(trade); madeProfit { - c.EmitTrade(trade, profit, netProfit) - c.EmitProfit(trade, profit, netProfit) + if c.position != nil { + profit, netProfit, madeProfit := c.position.AddTrade(trade) + if madeProfit { + p := c.position.NewProfit(trade, profit, netProfit) + c.EmitTrade(trade, profit, netProfit) + c.EmitProfit(trade, &p) + } else { + c.EmitTrade(trade, fixedpoint.Zero, fixedpoint.Zero) + c.EmitProfit(trade, nil) + } + positionChanged = true } else { c.EmitTrade(trade, fixedpoint.Zero, fixedpoint.Zero) } - positionChanged = true + + c.doneTrades[key] = struct{}{} return true } return false }) - if positionChanged { + + if positionChanged && c.position != nil { c.EmitPositionUpdate(c.position) } @@ -132,21 +160,32 @@ func (c *TradeCollector) Process() bool { // return true when the given trade is added // return false when the given trade is not added func (c *TradeCollector) processTrade(trade types.Trade) bool { - if c.orderStore.Exists(trade.OrderID) { - key := trade.Key() + c.mu.Lock() + defer c.mu.Unlock() - // if it's already done, remove the trade from the trade store - if _, done := c.doneTrades[key]; done { - return false - } + key := trade.Key() + + // if it's already done, remove the trade from the trade store + if _, done := c.doneTrades[key]; done { + return false + } - if profit, netProfit, madeProfit := c.position.AddTrade(trade); madeProfit { - c.EmitTrade(trade, profit, netProfit) - c.EmitProfit(trade, profit, netProfit) + if c.orderStore.Exists(trade.OrderID) { + if c.position != nil { + profit, netProfit, madeProfit := c.position.AddTrade(trade) + if madeProfit { + p := c.position.NewProfit(trade, profit, netProfit) + c.EmitTrade(trade, profit, netProfit) + c.EmitProfit(trade, &p) + } else { + c.EmitTrade(trade, fixedpoint.Zero, fixedpoint.Zero) + c.EmitProfit(trade, nil) + } + c.EmitPositionUpdate(c.position) } else { c.EmitTrade(trade, fixedpoint.Zero, fixedpoint.Zero) } - c.EmitPositionUpdate(c.position) + c.doneTrades[key] = struct{}{} return true } diff --git a/pkg/bbgo/tradecollector_callbacks.go b/pkg/bbgo/tradecollector_callbacks.go index af8bf1bd13..44756224f9 100644 --- a/pkg/bbgo/tradecollector_callbacks.go +++ b/pkg/bbgo/tradecollector_callbacks.go @@ -37,12 +37,12 @@ func (c *TradeCollector) EmitPositionUpdate(position *types.Position) { } } -func (c *TradeCollector) OnProfit(cb func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value)) { +func (c *TradeCollector) OnProfit(cb func(trade types.Trade, profit *types.Profit)) { c.profitCallbacks = append(c.profitCallbacks, cb) } -func (c *TradeCollector) EmitProfit(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { +func (c *TradeCollector) EmitProfit(trade types.Trade, profit *types.Profit) { for _, cb := range c.profitCallbacks { - cb(trade, profit, netProfit) + cb(trade, profit) } } diff --git a/pkg/bbgo/trader.go b/pkg/bbgo/trader.go index c6eefb2c1f..b6813909b0 100644 --- a/pkg/bbgo/trader.go +++ b/pkg/bbgo/trader.go @@ -11,19 +11,43 @@ import ( _ "github.com/go-sql-driver/mysql" + "github.com/c9s/bbgo/pkg/dynamic" "github.com/c9s/bbgo/pkg/interact" ) +// Strategy method calls: +// -> Defaults() (optional method) +// -> Initialize() (optional method) +// -> Validate() (optional method) +// -> Run() (optional method) +// -> Shutdown(shutdownCtx context.Context, wg *sync.WaitGroup) +type StrategyID interface { + ID() string +} + // SingleExchangeStrategy represents the single Exchange strategy type SingleExchangeStrategy interface { - ID() string + StrategyID Run(ctx context.Context, orderExecutor OrderExecutor, session *ExchangeSession) error } +// StrategyInitializer's Initialize method is called before the Subscribe method call. type StrategyInitializer interface { Initialize() error } +type StrategyDefaulter interface { + Defaults() error +} + +type StrategyValidator interface { + Validate() error +} + +type StrategyShutdown interface { + Shutdown(ctx context.Context, wg *sync.WaitGroup) +} + // ExchangeSessionSubscriber provides an interface for collecting subscriptions from different strategies // Subscribe method will be called before the user data stream connection is created. type ExchangeSessionSubscriber interface { @@ -35,28 +59,10 @@ type CrossExchangeSessionSubscriber interface { } type CrossExchangeStrategy interface { - ID() string + StrategyID CrossRun(ctx context.Context, orderExecutionRouter OrderExecutionRouter, sessions map[string]*ExchangeSession) error } -type Validator interface { - Validate() error -} - -//go:generate callbackgen -type Graceful -type Graceful struct { - shutdownCallbacks []func(ctx context.Context, wg *sync.WaitGroup) -} - -func (g *Graceful) Shutdown(ctx context.Context) { - var wg sync.WaitGroup - wg.Add(len(g.shutdownCallbacks)) - - go g.EmitShutdown(ctx, &wg) - - wg.Wait() -} - type Logging interface { EnableLogging() DisableLogging() @@ -82,9 +88,9 @@ type Trader struct { crossExchangeStrategies []CrossExchangeStrategy exchangeStrategies map[string][]SingleExchangeStrategy - logger Logger + gracefulShutdown GracefulShutdown - Graceful Graceful + logger Logger } func NewTrader(environ *Environment) *Trader { @@ -122,20 +128,6 @@ func (trader *Trader) Configure(userConfig *Config) error { trader.AttachCrossExchangeStrategy(strategy) } - for _, report := range userConfig.PnLReporters { - if len(report.AverageCostBySymbols) > 0 { - - log.Infof("setting up average cost pnl reporter on symbols: %v", report.AverageCostBySymbols) - trader.ReportPnL(). - AverageCostBySymbols(report.AverageCostBySymbols...). - Of(report.Of...). - When(report.When...) - - } else { - return fmt.Errorf("unsupported PnL reporter: %+v", report) - } - } - return nil } @@ -155,9 +147,8 @@ func (trader *Trader) AttachStrategyOn(session string, strategies ...SingleExcha return fmt.Errorf("session %s is not defined, valid sessions are: %v", session, keys) } - for _, s := range strategies { - trader.exchangeStrategies[session] = append(trader.exchangeStrategies[session], s) - } + trader.exchangeStrategies[session] = append( + trader.exchangeStrategies[session], strategies...) return nil } @@ -180,6 +171,12 @@ func (trader *Trader) Subscribe() { for sessionName, strategies := range trader.exchangeStrategies { session := trader.environment.sessions[sessionName] for _, strategy := range strategies { + if defaulter, ok := strategy.(StrategyDefaulter); ok { + if err := defaulter.Defaults(); err != nil { + panic(err) + } + } + if initializer, ok := strategy.(StrategyInitializer); ok { if err := initializer.Initialize(); err != nil { panic(err) @@ -195,6 +192,12 @@ func (trader *Trader) Subscribe() { } for _, strategy := range trader.crossExchangeStrategies { + if defaulter, ok := strategy.(StrategyDefaulter); ok { + if err := defaulter.Defaults(); err != nil { + panic(err) + } + } + if initializer, ok := strategy.(StrategyInitializer); ok { if err := initializer.Initialize(); err != nil { panic(err) @@ -210,59 +213,16 @@ func (trader *Trader) Subscribe() { } func (trader *Trader) RunSingleExchangeStrategy(ctx context.Context, strategy SingleExchangeStrategy, session *ExchangeSession, orderExecutor OrderExecutor) error { - rs := reflect.ValueOf(strategy) - - // get the struct element - rs = rs.Elem() - - if rs.Kind() != reflect.Struct { - return errors.New("strategy object is not a struct") - } - - if err := trader.injectCommonServices(strategy); err != nil { - return err - } - - if err := injectField(rs, "OrderExecutor", orderExecutor, false); err != nil { - return errors.Wrapf(err, "failed to inject OrderExecutor on %T", strategy) - } - - if symbol, ok := isSymbolBasedStrategy(rs); ok { - log.Infof("found symbol based strategy from %s", rs.Type()) - - market, ok := session.Market(symbol) - if !ok { - return fmt.Errorf("market of symbol %s not found", symbol) - } - - indicatorSet, ok := session.StandardIndicatorSet(symbol) - if !ok { - return fmt.Errorf("standardIndicatorSet of symbol %s not found", symbol) - } - - store, ok := session.MarketDataStore(symbol) - if !ok { - return fmt.Errorf("marketDataStore of symbol %s not found", symbol) - } - - if err := parseStructAndInject(strategy, - market, - indicatorSet, - store, - session, - session.OrderExecutor, - ); err != nil { - return errors.Wrapf(err, "failed to inject object into %T", strategy) - } - } - - // If the strategy has Validate() method, run it and check the error - if v, ok := strategy.(Validator); ok { + if v, ok := strategy.(StrategyValidator); ok { if err := v.Validate(); err != nil { return fmt.Errorf("failed to validate the config: %w", err) } } + if shutdown, ok := strategy.(StrategyShutdown); ok { + trader.gracefulShutdown.OnShutdown(shutdown.Shutdown) + } + return strategy.Run(ctx, orderExecutor, session) } @@ -302,12 +262,87 @@ func (trader *Trader) RunAllSingleExchangeStrategy(ctx context.Context) error { return nil } +func (trader *Trader) injectFields() error { + // load and run Session strategies + for sessionName, strategies := range trader.exchangeStrategies { + var session = trader.environment.sessions[sessionName] + var orderExecutor = trader.getSessionOrderExecutor(sessionName) + for _, strategy := range strategies { + rs := reflect.ValueOf(strategy) + + // get the struct element + rs = rs.Elem() + + if rs.Kind() != reflect.Struct { + return errors.New("strategy object is not a struct") + } + + if err := trader.injectCommonServices(strategy); err != nil { + return err + } + + if err := dynamic.InjectField(rs, "OrderExecutor", orderExecutor, false); err != nil { + return errors.Wrapf(err, "failed to inject OrderExecutor on %T", strategy) + } + + if symbol, ok := dynamic.LookupSymbolField(rs); ok { + log.Infof("found symbol based strategy from %s", rs.Type()) + + market, ok := session.Market(symbol) + if !ok { + return fmt.Errorf("market of symbol %s not found", symbol) + } + + indicatorSet := session.StandardIndicatorSet(symbol) + if !ok { + return fmt.Errorf("standardIndicatorSet of symbol %s not found", symbol) + } + + store, ok := session.MarketDataStore(symbol) + if !ok { + return fmt.Errorf("marketDataStore of symbol %s not found", symbol) + } + + if err := dynamic.ParseStructAndInject(strategy, + market, + session, + session.OrderExecutor, + indicatorSet, + store, + ); err != nil { + return errors.Wrapf(err, "failed to inject object into %T", strategy) + } + } + } + } + + for _, strategy := range trader.crossExchangeStrategies { + rs := reflect.ValueOf(strategy) + + // get the struct element from the struct pointer + rs = rs.Elem() + if rs.Kind() != reflect.Struct { + continue + } + + if err := trader.injectCommonServices(strategy); err != nil { + return err + } + } + + return nil +} + func (trader *Trader) Run(ctx context.Context) error { // before we start the interaction, // register the core interaction, because we can only get the strategies in this scope // trader.environment.Connect will call interact.Start interact.AddCustomInteraction(NewCoreInteraction(trader.environment, trader)) + if err := trader.injectFields(); err != nil { + return err + } + trader.Subscribe() if err := trader.environment.Start(ctx); err != nil { @@ -319,9 +354,8 @@ func (trader *Trader) Run(ctx context.Context) error { } router := &ExchangeOrderExecutionRouter{ - Notifiability: trader.environment.Notifiability, - sessions: trader.environment.sessions, - executors: make(map[string]OrderExecutor), + sessions: trader.environment.sessions, + executors: make(map[string]OrderExecutor), } for sessionID := range trader.environment.sessions { var orderExecutor = trader.getSessionOrderExecutor(sessionID) @@ -329,42 +363,82 @@ func (trader *Trader) Run(ctx context.Context) error { } for _, strategy := range trader.crossExchangeStrategies { - rs := reflect.ValueOf(strategy) - - // get the struct element from the struct pointer - rs = rs.Elem() - if rs.Kind() != reflect.Struct { - continue + if err := strategy.CrossRun(ctx, router, trader.environment.sessions); err != nil { + return err } + } - if err := trader.injectCommonServices(strategy); err != nil { - return err + return trader.environment.Connect(ctx) +} + +func (trader *Trader) LoadState() error { + if trader.environment.BacktestService != nil { + return nil + } + + if persistenceServiceFacade == nil { + return nil + } + + ps := persistenceServiceFacade.Get() + + log.Infof("loading strategies states...") + + return trader.IterateStrategies(func(strategy StrategyID) error { + id := dynamic.CallID(strategy) + return loadPersistenceFields(strategy, id, ps) + }) +} + +func (trader *Trader) IterateStrategies(f func(st StrategyID) error) error { + for _, strategies := range trader.exchangeStrategies { + for _, strategy := range strategies { + if err := f(strategy); err != nil { + return err + } } + } - if err := strategy.CrossRun(ctx, router, trader.environment.sessions); err != nil { + for _, strategy := range trader.crossExchangeStrategies { + if err := f(strategy); err != nil { return err } } - return trader.environment.Connect(ctx) + return nil } -var defaultPersistenceSelector = &PersistenceSelector{ - StoreID: "default", - Type: "memory", -} +func (trader *Trader) SaveState() error { + if trader.environment.BacktestService != nil { + return nil + } -func (trader *Trader) injectCommonServices(s interface{}) error { - persistenceFacade := trader.environment.PersistenceServiceFacade - persistence := &Persistence{ - PersistenceSelector: defaultPersistenceSelector, - Facade: persistenceFacade, + if persistenceServiceFacade == nil { + return nil } + ps := persistenceServiceFacade.Get() + + log.Infof("saving strategies states...") + return trader.IterateStrategies(func(strategy StrategyID) error { + id := dynamic.CallID(strategy) + if len(id) == 0 { + return nil + } + + return storePersistenceFields(strategy, id, ps) + }) +} + +func (trader *Trader) Shutdown(ctx context.Context) { + trader.gracefulShutdown.Shutdown(ctx) +} + +func (trader *Trader) injectCommonServices(s interface{}) error { // a special injection for persistence selector: // if user defined the selector, the facade pointer will be nil, hence we need to update the persistence facade pointer sv := reflect.ValueOf(s).Elem() - if field, ok := hasField(sv, "Persistence"); ok { + if field, ok := dynamic.HasField(sv, "Persistence"); ok { // the selector is set, but we need to update the facade pointer if !field.IsNil() { elem := field.Elem() @@ -372,33 +446,26 @@ func (trader *Trader) injectCommonServices(s interface{}) error { return fmt.Errorf("field Persistence is not a struct element, %s given", field) } - if err := injectField(elem, "Facade", persistenceFacade, true); err != nil { + if err := dynamic.InjectField(elem, "Facade", persistenceServiceFacade, true); err != nil { return err } /* - if err := parseStructAndInject(field.Interface(), persistenceFacade); err != nil { + if err := ParseStructAndInject(field.Interface(), persistenceFacade); err != nil { return err } */ } } - return parseStructAndInject(s, - &trader.Graceful, + return dynamic.ParseStructAndInject(s, &trader.logger, - &trader.environment.Notifiability, + Notification, trader.environment.TradeService, trader.environment.OrderService, trader.environment.DatabaseService, trader.environment.AccountService, trader.environment, - persistence, - persistenceFacade, // if the strategy use persistence facade separately + persistenceServiceFacade, // if the strategy use persistence facade separately ) } - -// ReportPnL configure and set the PnLReporter with the given notifier -func (trader *Trader) ReportPnL() *PnLReporterManager { - return NewPnLReporter(&trader.environment.Notifiability) -} diff --git a/pkg/bbgo/trader_test.go b/pkg/bbgo/trader_test.go index 920078f66e..f30d11b655 100644 --- a/pkg/bbgo/trader_test.go +++ b/pkg/bbgo/trader_test.go @@ -1,2 +1 @@ package bbgo - diff --git a/pkg/bbgo/trend_ema.go b/pkg/bbgo/trend_ema.go new file mode 100644 index 0000000000..c37e63aedb --- /dev/null +++ b/pkg/bbgo/trend_ema.go @@ -0,0 +1,66 @@ +package bbgo + +import ( + "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +type TrendEMA struct { + types.IntervalWindow + + // MaxGradient is the maximum gradient allowed for the entry. + MaxGradient float64 `json:"maxGradient"` + MinGradient float64 `json:"minGradient"` + + ewma *indicator.EWMA + + last, current float64 +} + +func (s *TrendEMA) Bind(session *ExchangeSession, orderExecutor *GeneralOrderExecutor) { + symbol := orderExecutor.Position().Symbol + s.ewma = session.StandardIndicatorSet(symbol).EWMA(s.IntervalWindow) + + session.MarketDataStream.OnStart(func() { + if s.ewma.Length() < 2 { + return + } + + s.last = s.ewma.Values[s.ewma.Length()-2] + s.current = s.ewma.Last() + }) + + session.MarketDataStream.OnKLineClosed(types.KLineWith(symbol, s.Interval, func(kline types.KLine) { + s.last = s.current + s.current = s.ewma.Last() + })) +} + +func (s *TrendEMA) Gradient() float64 { + if s.last > 0.0 && s.current > 0.0 { + return s.current / s.last + } + return 0.0 +} + +func (s *TrendEMA) GradientAllowed() bool { + gradient := s.Gradient() + + logrus.Infof("trendEMA %+v current=%f last=%f gradient=%f", s, s.current, s.last, gradient) + + if gradient == .0 { + return false + } + + if s.MaxGradient > 0.0 && gradient > s.MaxGradient { + return false + } + + if s.MinGradient > 0.0 && gradient < s.MinGradient { + return false + } + + return true +} diff --git a/pkg/bbgo/trend_ema_test.go b/pkg/bbgo/trend_ema_test.go new file mode 100644 index 0000000000..4e5c12d784 --- /dev/null +++ b/pkg/bbgo/trend_ema_test.go @@ -0,0 +1,21 @@ +package bbgo + +import ( + "testing" + + "github.com/c9s/bbgo/pkg/types" +) + +func Test_TrendEMA(t *testing.T) { + t.Run("Test Trend EMA", func(t *testing.T) { + trendEMA_test := TrendEMA{ + IntervalWindow: types.IntervalWindow{Window: 1}, + } + trendEMA_test.last = 1000.0 + trendEMA_test.current = 1200.0 + + if trendEMA_test.Gradient() != 1.2 { + t.Errorf("Gradient() = %v, want %v", trendEMA_test.Gradient(), 1.2) + } + }) +} diff --git a/pkg/bbgo/twap_order_executor.go b/pkg/bbgo/twap_order_executor.go index 6f8fad5a01..1a942cd88d 100644 --- a/pkg/bbgo/twap_order_executor.go +++ b/pkg/bbgo/twap_order_executor.go @@ -36,7 +36,7 @@ type TwapExecution struct { currentPrice fixedpoint.Value activePosition fixedpoint.Value - activeMakerOrders *LocalActiveOrderBook + activeMakerOrders *ActiveOrderBook orderStore *OrderStore position *types.Position @@ -408,7 +408,7 @@ func (e *TwapExecution) Run(parentCtx context.Context) error { e.orderStore = NewOrderStore(e.Symbol) e.orderStore.BindStream(e.userDataStream) - e.activeMakerOrders = NewLocalActiveOrderBook(e.Symbol) + e.activeMakerOrders = NewActiveOrderBook(e.Symbol) e.activeMakerOrders.OnFilled(e.handleFilledOrder) e.activeMakerOrders.BindStream(e.userDataStream) diff --git a/pkg/cmd/backtest.go b/pkg/cmd/backtest.go index 84f65a8418..192261e8b7 100644 --- a/pkg/cmd/backtest.go +++ b/pkg/cmd/backtest.go @@ -3,16 +3,19 @@ package cmd import ( "bufio" "context" - "encoding/json" "fmt" - "io/ioutil" + "github.com/c9s/bbgo/pkg/cmd/cmdutil" + "github.com/c9s/bbgo/pkg/data/tsv" + "github.com/c9s/bbgo/pkg/util" + "github.com/fatih/color" + "github.com/google/uuid" "os" "path/filepath" + "sort" "strings" "syscall" "time" - "github.com/fatih/color" "github.com/pkg/errors" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -21,25 +24,19 @@ import ( "github.com/c9s/bbgo/pkg/accounting/pnl" "github.com/c9s/bbgo/pkg/backtest" "github.com/c9s/bbgo/pkg/bbgo" - "github.com/c9s/bbgo/pkg/cmd/cmdutil" + "github.com/c9s/bbgo/pkg/exchange" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/service" "github.com/c9s/bbgo/pkg/types" ) -type BackTestReport struct { - Symbol string `json:"symbol,omitempty"` - LastPrice fixedpoint.Value `json:"lastPrice,omitempty"` - StartPrice fixedpoint.Value `json:"startPrice,omitempty"` - PnLReport *pnl.AverageCostPnlReport `json:"pnlReport,omitempty"` - InitialBalances types.BalanceMap `json:"initialBalances,omitempty"` - FinalBalances types.BalanceMap `json:"finalBalances,omitempty"` -} - func init() { BacktestCmd.Flags().Bool("sync", false, "sync backtest data") BacktestCmd.Flags().Bool("sync-only", false, "sync backtest data only, do not run backtest") BacktestCmd.Flags().String("sync-from", "", "sync backtest data from the given time, which will override the time range in the backtest config") + BacktestCmd.Flags().String("sync-exchange", "", "specify only one exchange to sync backtest data") + BacktestCmd.Flags().String("session", "", "specify only one exchange session to run backtest") + BacktestCmd.Flags().Bool("verify", false, "verify the kline back-test data") BacktestCmd.Flags().Bool("base-asset-baseline", false, "use base asset performance as the competitive baseline performance") @@ -47,12 +44,13 @@ func init() { BacktestCmd.Flags().String("config", "config/bbgo.yaml", "strategy config file") BacktestCmd.Flags().Bool("force", false, "force execution without confirm") BacktestCmd.Flags().String("output", "", "the report output directory") + BacktestCmd.Flags().Bool("subdir", false, "generate report in the sub-directory of the output directory") RootCmd.AddCommand(BacktestCmd) } var BacktestCmd = &cobra.Command{ Use: "backtest", - Short: "backtest your strategies", + Short: "run backtest with strategies", SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { verboseCnt, err := cmd.Flags().GetCount("verbose") @@ -83,6 +81,16 @@ var BacktestCmd = &cobra.Command{ return err } + syncExchangeName, err := cmd.Flags().GetString("sync-exchange") + if err != nil { + return err + } + + sessionName, err := cmd.Flags().GetString("session") + if err != nil { + return err + } + force, err := cmd.Flags().GetBool("force") if err != nil { return err @@ -93,7 +101,12 @@ var BacktestCmd = &cobra.Command{ return err } - jsonOutputEnabled := len(outputDirectory) > 0 + generatingReport := len(outputDirectory) > 0 + + reportFileInSubDir, err := cmd.Flags().GetBool("subdir") + if err != nil { + return err + } syncOnly, err := cmd.Flags().GetBool("sync-only") if err != nil { @@ -115,15 +128,6 @@ var BacktestCmd = &cobra.Command{ return err } - if verboseCnt == 2 { - log.SetLevel(log.DebugLevel) - } else if verboseCnt > 0 { - log.SetLevel(log.InfoLevel) - } else { - // default mode, disable strategy logging and order executor logging - log.SetLevel(log.ErrorLevel) - } - if userConfig.Backtest == nil { return errors.New("backtest config is not defined") } @@ -131,25 +135,27 @@ var BacktestCmd = &cobra.Command{ ctx, cancel := context.WithCancel(context.Background()) defer cancel() - var now = time.Now() + var now = time.Now().Local() var startTime, endTime time.Time - startTime = userConfig.Backtest.StartTime.Time() + startTime = userConfig.Backtest.StartTime.Time().Local() // set default start time to the past 6 months // userConfig.Backtest.StartTime = now.AddDate(0, -6, 0).Format("2006-01-02") - if userConfig.Backtest.EndTime != nil { - endTime = userConfig.Backtest.EndTime.Time() + endTime = userConfig.Backtest.EndTime.Time().Local() } else { endTime = now } - _ = endTime - log.Infof("starting backtest with startTime %s", startTime.Format(time.ANSIC)) + // ensure that we're using local time + startTime = startTime.Local() + endTime = endTime.Local() + + log.Infof("starting backtest with startTime %s", startTime.Format(time.RFC3339)) environ := bbgo.NewEnvironment() - if err := BootstrapBacktestEnvironment(ctx, environ, userConfig); err != nil { + if err := bbgo.BootstrapBacktestEnvironment(ctx, environ); err != nil { return err } @@ -159,94 +165,64 @@ var BacktestCmd = &cobra.Command{ backtestService := &service.BacktestService{DB: environ.DatabaseService.DB} environ.BacktestService = backtestService - - var sourceExchanges = make(map[types.ExchangeName]types.Exchange) - if len(userConfig.Backtest.Sessions) > 0 { - for _, name := range userConfig.Backtest.Sessions { - exName, err := types.ValidExchangeName(name) - if err != nil { - return err - } - - publicExchange, err := cmdutil.NewExchangePublic(exName) - if err != nil { - return err - } - sourceExchanges[exName] = publicExchange - } - } else { + bbgo.SetBackTesting(backtestService) + + if len(sessionName) > 0 { + userConfig.Backtest.Sessions = []string{sessionName} + } else if len(syncExchangeName) > 0 { + userConfig.Backtest.Sessions = []string{syncExchangeName} + } else if len(userConfig.Backtest.Sessions) == 0 { + log.Infof("backtest.sessions is not defined, using all supported exchanges: %v", types.SupportedExchanges) for _, exName := range types.SupportedExchanges { - publicExchange, err := cmdutil.NewExchangePublic(exName) - if err != nil { - return err - } - sourceExchanges[exName] = publicExchange + userConfig.Backtest.Sessions = append(userConfig.Backtest.Sessions, exName.String()) } } - if wantSync { - var syncFromTime time.Time - - // override the sync from time if the option is given - if len(syncFromDateStr) > 0 { - syncFromTime, err = time.Parse(types.DateFormat, syncFromDateStr) - if err != nil { - return err - } + var sourceExchanges = make(map[types.ExchangeName]types.Exchange) + for _, name := range userConfig.Backtest.Sessions { + exName, err := types.ValidExchangeName(name) + if err != nil { + return err + } - if syncFromTime.After(startTime) { - return fmt.Errorf("sync-from time %s can not be latter than the backtest start time %s", syncFromTime, startTime) - } - } else { - // we need at least 1 month backward data for EMA and last prices - syncFromTime = startTime.AddDate(0, -1, 0) - log.Infof("adjusted sync start time %s to %s for backward market data", startTime, syncFromTime) + publicExchange, err := exchange.NewPublic(exName) + if err != nil { + return err } + sourceExchanges[exName] = publicExchange + } - log.Infof("starting synchronization: %v", userConfig.Backtest.Symbols) - for _, symbol := range userConfig.Backtest.Symbols { + var syncFromTime time.Time - for _, sourceExchange := range sourceExchanges { - exCustom, ok := sourceExchange.(types.CustomIntervalProvider) + // user can override the sync from time if the option is given + if len(syncFromDateStr) > 0 { + syncFromTime, err = time.Parse(types.DateFormat, syncFromDateStr) + if err != nil { + return err + } - var supportIntervals map[types.Interval]int - if ok { - supportIntervals = exCustom.SupportedInterval() - } else { - supportIntervals = types.SupportedIntervals - } + if syncFromTime.After(startTime) { + return fmt.Errorf("sync-from time %s can not be latter than the backtest start time %s", syncFromTime, startTime) + } - for interval := range supportIntervals { - // if err := s.SyncKLineByInterval(ctx, exchange, symbol, interval, startTime, endTime); err != nil { - // return err - // } - firstKLine, err := backtestService.QueryFirstKLine(sourceExchange.Name(), symbol, interval) - if err != nil { - return errors.Wrapf(err, "failed to query backtest kline") - } + syncFromTime = syncFromTime.Local() + } else { + // we need at least 1 month backward data for EMA and last prices + syncFromTime = startTime.AddDate(0, -1, 0) + log.Infof("adjusted sync start time %s to %s for backward market data", startTime, syncFromTime) + } - // if we don't have klines before the start time endpoint, the back-test will fail. - // because the last price will be missing. - if firstKLine != nil { - if err := backtestService.SyncExist(ctx, sourceExchange, symbol, syncFromTime, time.Now(), interval); err != nil { - return err - } - } else { - if err := backtestService.Sync(ctx, sourceExchange, symbol, syncFromTime, time.Now(), interval); err != nil { - return err - } - } - } - } + if wantSync { + log.Infof("starting synchronization: %v", userConfig.Backtest.Symbols) + if err := sync(ctx, userConfig, backtestService, sourceExchanges, syncFromTime, endTime); err != nil { + return err } log.Info("synchronization done") - for _, sourceExchange := range sourceExchanges { - if shouldVerify { - err2, done := backtestService.Verify(userConfig.Backtest.Symbols, startTime, time.Now(), sourceExchange, verboseCnt) - if done { - return err2 - } + if shouldVerify { + err := verify(userConfig, backtestService, sourceExchanges, syncFromTime, endTime) + if err != nil { + return err } } @@ -270,6 +246,15 @@ var BacktestCmd = &cobra.Command{ } } + if verboseCnt == 2 { + log.SetLevel(log.DebugLevel) + } else if verboseCnt > 0 { + log.SetLevel(log.InfoLevel) + } else { + // default mode, disable strategy logging and order executor logging + log.SetLevel(log.ErrorLevel) + } + environ.SetStartTime(startTime) // exchangeNameStr is the session name. @@ -278,13 +263,24 @@ var BacktestCmd = &cobra.Command{ if err != nil { return errors.Wrap(err, "failed to create backtest exchange") } - environ.AddExchange(name.String(), backtestExchange) + session := environ.AddExchange(name.String(), backtestExchange) + exchangeFromConfig := userConfig.Sessions[name.String()] + if exchangeFromConfig != nil { + session.UseHeikinAshi = exchangeFromConfig.UseHeikinAshi + } } if err := environ.Init(ctx); err != nil { return err } + for _, session := range environ.Sessions() { + userDataStream := session.UserDataStream.(types.StandardStreamEmitter) + backtestEx := session.Exchange.(*backtest.Exchange) + backtestEx.MarketDataStream = session.MarketDataStream.(types.StandardStreamEmitter) + backtestEx.BindUserData(userDataStream) + } + trader := bbgo.NewTrader(environ) if verboseCnt == 0 { trader.DisableLogging() @@ -298,44 +294,189 @@ var BacktestCmd = &cobra.Command{ return err } - type KChanEx struct { - KChan chan types.KLine - Exchange *backtest.Exchange + allKLineIntervals, requiredInterval, backTestIntervals := collectSubscriptionIntervals(environ) + exchangeSources, err := toExchangeSources(environ.Sessions(), startTime, endTime, requiredInterval, backTestIntervals...) + if err != nil { + return err } - for _, session := range environ.Sessions() { - backtestExchange := session.Exchange.(*backtest.Exchange) - backtestExchange.InitMarketData() + + var kLineHandlers []func(k types.KLine, exSource *backtest.ExchangeDataSource) + var manifests backtest.Manifests + var runID = userConfig.GetSignature() + "_" + uuid.NewString() + var reportDir = outputDirectory + var sessionTradeStats = make(map[string]map[string]*types.TradeStats) + + var tradeCollectorList []*bbgo.TradeCollector + for _, exSource := range exchangeSources { + sessionName := exSource.Session.Name + tradeStatsMap := make(map[string]*types.TradeStats) + for usedSymbol := range exSource.Session.Positions() { + market, _ := exSource.Session.Market(usedSymbol) + position := types.NewPositionFromMarket(market) + orderStore := bbgo.NewOrderStore(usedSymbol) + orderStore.AddOrderUpdate = true + tradeCollector := bbgo.NewTradeCollector(usedSymbol, position, orderStore) + + tradeStats := types.NewTradeStats(usedSymbol) + tradeStats.SetIntervalProfitCollector(types.NewIntervalProfitCollector(types.Interval1d, startTime)) + tradeCollector.OnProfit(func(trade types.Trade, profit *types.Profit) { + if profit == nil { + return + } + tradeStats.Add(profit) + }) + tradeStatsMap[usedSymbol] = tradeStats + + orderStore.BindStream(exSource.Session.UserDataStream) + tradeCollector.BindStream(exSource.Session.UserDataStream) + tradeCollectorList = append(tradeCollectorList, tradeCollector) + } + sessionTradeStats[sessionName] = tradeStatsMap } + kLineHandlers = append(kLineHandlers, func(k types.KLine, _ *backtest.ExchangeDataSource) { + if k.Interval == types.Interval1d && k.Closed { + for _, collector := range tradeCollectorList { + collector.Process() + } + } + }) - var klineChans []KChanEx - for _, session := range environ.Sessions() { - exchange := session.Exchange.(*backtest.Exchange) - c, err := exchange.GetMarketData() + if generatingReport { + if reportFileInSubDir { + // reportDir = filepath.Join(reportDir, backtestSessionName) + reportDir = filepath.Join(reportDir, runID) + } + if err := util.SafeMkdirAll(reportDir); err != nil { + return err + } + + startTimeStr := startTime.Format("20060102") + endTimeStr := endTime.Format("20060102") + kLineSubDir := strings.Join([]string{"klines", "_", startTimeStr, "-", endTimeStr}, "") + kLineDataDir := filepath.Join(outputDirectory, "shared", kLineSubDir) + if err := util.SafeMkdirAll(kLineDataDir); err != nil { + return err + } + + stateRecorder := backtest.NewStateRecorder(reportDir) + err = trader.IterateStrategies(func(st bbgo.StrategyID) error { + return stateRecorder.Scan(st.(backtest.Instance)) + }) + if err != nil { + return err + } + + manifests = stateRecorder.Manifests() + manifests, err = rewriteManifestPaths(manifests, reportDir) + if err != nil { + return err + } + + // state snapshot + kLineHandlers = append(kLineHandlers, func(k types.KLine, _ *backtest.ExchangeDataSource) { + // snapshot per 1m + if k.Interval == types.Interval1m && k.Closed { + if _, err := stateRecorder.Snapshot(); err != nil { + log.WithError(err).Errorf("state record failed to snapshot the strategy state") + } + } + }) + + dumper := backtest.NewKLineDumper(kLineDataDir) + defer func() { + if err := dumper.Close(); err != nil { + log.WithError(err).Errorf("kline dumper can not close files") + } + }() + + kLineHandlers = append(kLineHandlers, func(k types.KLine, _ *backtest.ExchangeDataSource) { + if err := dumper.Record(k); err != nil { + log.WithError(err).Errorf("can not write kline to file") + } + }) + + // equity curve recording -- record per 1h kline + equityCurveTsv, err := tsv.NewWriterFile(filepath.Join(reportDir, "equity_curve.tsv")) if err != nil { return err } - klineChans = append(klineChans, KChanEx{KChan: c, Exchange: exchange}) + defer func() { _ = equityCurveTsv.Close() }() + + _ = equityCurveTsv.Write([]string{ + "time", + "in_usd", + }) + defer equityCurveTsv.Flush() + + kLineHandlers = append(kLineHandlers, func(k types.KLine, exSource *backtest.ExchangeDataSource) { + if k.Interval != types.Interval1h { + return + } + + balances, err := exSource.Exchange.QueryAccountBalances(ctx) + if err != nil { + log.WithError(err).Errorf("query back-test account balance error") + } else { + assets := balances.Assets(exSource.Session.AllLastPrices(), k.EndTime.Time()) + _ = equityCurveTsv.Write([]string{ + k.EndTime.Time().Format(time.RFC1123), + assets.InUSD().String(), + }) + } + }) + + ordersTsv, err := tsv.NewWriterFile(filepath.Join(reportDir, "orders.tsv")) + if err != nil { + return err + } + defer func() { _ = ordersTsv.Close() }() + _ = ordersTsv.Write(types.Order{}.CsvHeader()) + + for _, exSource := range exchangeSources { + exSource.Session.UserDataStream.OnOrderUpdate(func(order types.Order) { + if order.Status == types.OrderStatusFilled { + for _, record := range order.CsvRecords() { + _ = ordersTsv.Write(record) + } + } + }) + } } runCtx, cancelRun := context.WithCancel(ctx) + for _, exK := range exchangeSources { + exK.Callbacks = kLineHandlers + } go func() { defer cancelRun() + + // Optimize back-test speed for single exchange source + var numOfExchangeSources = len(exchangeSources) + if numOfExchangeSources == 1 { + exSource := exchangeSources[0] + for k := range exSource.C { + exSource.Exchange.ConsumeKLine(k, requiredInterval) + } + + if err := exSource.Exchange.CloseMarketData(); err != nil { + log.WithError(err).Errorf("close market data error") + } + return + } + + RunMultiExchangeData: for { - count := len(klineChans) - for _, kchanex := range klineChans { - kLine, more := <-kchanex.KChan - if more { - kchanex.Exchange.ConsumeKLine(kLine) - } else { - if err := kchanex.Exchange.CloseMarketData(); err != nil { - log.Errorf("%v", err) + for _, exK := range exchangeSources { + k, more := <-exK.C + if !more { + if err := exK.Exchange.CloseMarketData(); err != nil { + log.WithError(err).Errorf("close market data error") return } - count-- + break RunMultiExchangeData } - } - if count == 0 { - break + + exK.Exchange.ConsumeKLine(k, requiredInterval) } } }() @@ -343,121 +484,213 @@ var BacktestCmd = &cobra.Command{ cmdutil.WaitForSignal(runCtx, syscall.SIGINT, syscall.SIGTERM) log.Infof("shutting down trader...") - shutdownCtx, cancelShutdown := context.WithDeadline(runCtx, time.Now().Add(10*time.Second)) - trader.Graceful.Shutdown(shutdownCtx) + + gracefulShutdownPeriod := 30 * time.Second + shtCtx, cancelShutdown := context.WithTimeout(bbgo.NewTodoContextWithExistingIsolation(ctx), gracefulShutdownPeriod) + bbgo.Shutdown(shtCtx) cancelShutdown() // put the logger back to print the pnl log.SetLevel(log.InfoLevel) + + // aggregate total balances + initTotalBalances := types.BalanceMap{} + finalTotalBalances := types.BalanceMap{} + var sessionNames []string for _, session := range environ.Sessions() { - backtestExchange := session.Exchange.(*backtest.Exchange) - exchangeName := session.Exchange.Name().String() - for symbol, trades := range session.Trades { - market, ok := session.Market(symbol) - if !ok { - return fmt.Errorf("market not found: %s, %s", symbol, exchangeName) - } + sessionNames = append(sessionNames, session.Name) + accountConfig := userConfig.Backtest.GetAccount(session.Name) + initBalances := accountConfig.Balances.BalanceMap() + initTotalBalances = initTotalBalances.Add(initBalances) - calculator := &pnl.AverageCostCalculator{ - TradingFeeCurrency: backtestExchange.PlatformFeeCurrency(), - Market: market, - } + finalBalances := session.GetAccount().Balances() + finalTotalBalances = finalTotalBalances.Add(finalBalances) + } - startPrice, ok := session.StartPrice(symbol) - if !ok { - return fmt.Errorf("start price not found: %s, %s. run --sync first", symbol, exchangeName) + summaryReport := &backtest.SummaryReport{ + StartTime: startTime, + EndTime: endTime, + Sessions: sessionNames, + InitialTotalBalances: initTotalBalances, + FinalTotalBalances: finalTotalBalances, + Manifests: manifests, + Symbols: nil, + } + + for interval := range allKLineIntervals { + summaryReport.Intervals = append(summaryReport.Intervals, interval) + } + + for _, session := range environ.Sessions() { + for symbol, trades := range session.Trades { + intervalProfits := sessionTradeStats[session.Name][symbol].IntervalProfits[types.Interval1d] + symbolReport, err := createSymbolReport(userConfig, session, symbol, trades.Trades, intervalProfits) + if err != nil { + return err } - lastPrice, ok := session.LastPrice(symbol) - if !ok { - return fmt.Errorf("last price not found: %s, %s", symbol, exchangeName) + summaryReport.Symbols = append(summaryReport.Symbols, symbol) + summaryReport.SymbolReports = append(summaryReport.SymbolReports, *symbolReport) + summaryReport.TotalProfit = symbolReport.PnL.Profit + summaryReport.TotalUnrealizedProfit = symbolReport.PnL.UnrealizedProfit + summaryReport.InitialEquityValue = summaryReport.InitialEquityValue.Add(symbolReport.InitialEquityValue()) + summaryReport.FinalEquityValue = summaryReport.FinalEquityValue.Add(symbolReport.FinalEquityValue()) + summaryReport.TotalGrossProfit.Add(symbolReport.PnL.GrossProfit) + summaryReport.TotalGrossLoss.Add(symbolReport.PnL.GrossLoss) + + // write report to a file + if generatingReport { + reportFileName := fmt.Sprintf("symbol_report_%s_%s.json", session.Name, symbol) + if err := util.WriteJsonFile(filepath.Join(reportDir, reportFileName), &symbolReport); err != nil { + return err + } } + } + } - color.Green("%s %s PROFIT AND LOSS REPORT", strings.ToUpper(exchangeName), symbol) - color.Green("===============================================") + if generatingReport { + summaryReportFile := filepath.Join(reportDir, "summary.json") - report := calculator.Calculate(symbol, trades.Trades, lastPrice) - report.Print() + // output summary report filepath to stdout, so that our optimizer can read from it + fmt.Println(summaryReportFile) - initBalances := userConfig.Backtest.Account[exchangeName].Balances.BalanceMap() - finalBalances := session.GetAccount().Balances() + if err := util.WriteJsonFile(summaryReportFile, summaryReport); err != nil { + return errors.Wrapf(err, "can not write summary report json file: %s", summaryReportFile) + } - log.Infof("INITIAL BALANCES:") - initBalances.Print() + configJsonFile := filepath.Join(reportDir, "config.json") + if err := util.WriteJsonFile(configJsonFile, userConfig); err != nil { + return errors.Wrapf(err, "can not write config json file: %s", configJsonFile) + } - log.Infof("FINAL BALANCES:") - finalBalances.Print() + // append report index + if reportFileInSubDir { + if err := backtest.AddReportIndexRun(outputDirectory, backtest.Run{ + ID: runID, + Config: userConfig, + Time: time.Now(), + }); err != nil { + return err + } + } + } else { + color.Green("BACK-TEST REPORT") + color.Green("===============================================\n") + color.Green("START TIME: %s\n", startTime.Format(time.RFC1123)) + color.Green("END TIME: %s\n", endTime.Format(time.RFC1123)) + color.Green("INITIAL TOTAL BALANCE: %v\n", initTotalBalances) + color.Green("FINAL TOTAL BALANCE: %v\n", finalTotalBalances) + for _, symbolReport := range summaryReport.SymbolReports { + symbolReport.Print(wantBaseAssetBaseline) + } + } - if jsonOutputEnabled { - result := BackTestReport{ - Symbol: symbol, - LastPrice: lastPrice, - StartPrice: startPrice, - PnLReport: report, - InitialBalances: initBalances, - FinalBalances: finalBalances, - } + return nil + }, +} - jsonOutput, err := json.MarshalIndent(&result, "", " ") - if err != nil { - return err - } +func collectSubscriptionIntervals(environ *bbgo.Environment) (allKLineIntervals map[types.Interval]struct{}, requiredInterval types.Interval, backTestIntervals []types.Interval) { + // default extra back-test intervals + backTestIntervals = []types.Interval{types.Interval1h, types.Interval1d} + // all subscribed intervals + allKLineIntervals = make(map[types.Interval]struct{}) - if err := ioutil.WriteFile(filepath.Join(outputDirectory, symbol+".json"), jsonOutput, 0644); err != nil { - return err - } + for _, interval := range backTestIntervals { + allKLineIntervals[interval] = struct{}{} + } + // default interval is 1m for all exchanges + requiredInterval = types.Interval1m + for _, session := range environ.Sessions() { + for _, sub := range session.Subscriptions { + if sub.Channel == types.KLineChannel { + if sub.Options.Interval.Seconds()%60 > 0 { + // if any subscription interval is less than 60s, then we will use 1s for back-testing + requiredInterval = types.Interval1s + log.Warnf("found kline subscription interval less than 60s, modify default backtest interval to 1s") } + allKLineIntervals[sub.Options.Interval] = struct{}{} + } + } + } + return allKLineIntervals, requiredInterval, backTestIntervals +} - initQuoteAsset := inQuoteAsset(initBalances, market, startPrice) - finalQuoteAsset := inQuoteAsset(finalBalances, market, lastPrice) - log.Infof("INITIAL ASSET IN %s ~= %s %s (1 %s = %v)", market.QuoteCurrency, market.FormatQuantity(initQuoteAsset), market.QuoteCurrency, market.BaseCurrency, startPrice) - log.Infof("FINAL ASSET IN %s ~= %s %s (1 %s = %v)", market.QuoteCurrency, market.FormatQuantity(finalQuoteAsset), market.QuoteCurrency, market.BaseCurrency, lastPrice) +func createSymbolReport(userConfig *bbgo.Config, session *bbgo.ExchangeSession, symbol string, trades []types.Trade, intervalProfit *types.IntervalProfitCollector) ( + *backtest.SessionSymbolReport, + error, +) { + backtestExchange, ok := session.Exchange.(*backtest.Exchange) + if !ok { + return nil, fmt.Errorf("unexpected error, exchange instance is not a backtest exchange") + } - if report.Profit.Sign() > 0 { - color.Green("REALIZED PROFIT: +%v %s", report.Profit, market.QuoteCurrency) - } else { - color.Red("REALIZED PROFIT: %v %s", report.Profit, market.QuoteCurrency) - } + market, ok := session.Market(symbol) + if !ok { + return nil, fmt.Errorf("market not found: %s, %s", symbol, session.Exchange.Name()) + } - if report.UnrealizedProfit.Sign() > 0 { - color.Green("UNREALIZED PROFIT: +%v %s", report.UnrealizedProfit, market.QuoteCurrency) - } else { - color.Red("UNREALIZED PROFIT: %v %s", report.UnrealizedProfit, market.QuoteCurrency) - } + startPrice, ok := session.StartPrice(symbol) + if !ok { + return nil, fmt.Errorf("start price not found: %s, %s. run --sync first", symbol, session.Exchange.Name()) + } - if finalQuoteAsset.Compare(initQuoteAsset) > 0 { - color.Green("ASSET INCREASED: +%v %s (+%s)", finalQuoteAsset.Sub(initQuoteAsset), market.QuoteCurrency, finalQuoteAsset.Sub(initQuoteAsset).Div(initQuoteAsset).FormatPercentage(2)) - } else { - color.Red("ASSET DECREASED: %v %s (%s)", finalQuoteAsset.Sub(initQuoteAsset), market.QuoteCurrency, finalQuoteAsset.Sub(initQuoteAsset).Div(initQuoteAsset).FormatPercentage(2)) - } + lastPrice, ok := session.LastPrice(symbol) + if !ok { + return nil, fmt.Errorf("last price not found: %s, %s", symbol, session.Exchange.Name()) + } - if wantBaseAssetBaseline { - // initBaseAsset := inBaseAsset(initBalances, market, startPrice) - // finalBaseAsset := inBaseAsset(finalBalances, market, lastPrice) - // log.Infof("INITIAL ASSET IN %s ~= %s %s (1 %s = %f)", market.BaseCurrency, market.FormatQuantity(initBaseAsset), market.BaseCurrency, market.BaseCurrency, startPrice) - // log.Infof("FINAL ASSET IN %s ~= %s %s (1 %s = %f)", market.BaseCurrency, market.FormatQuantity(finalBaseAsset), market.BaseCurrency, market.BaseCurrency, lastPrice) - - if lastPrice.Compare(startPrice) > 0 { - color.Green("%s BASE ASSET PERFORMANCE: +%s (= (%s - %s) / %s)", - market.BaseCurrency, - lastPrice.Sub(startPrice).Div(startPrice).FormatPercentage(2), - lastPrice.FormatString(2), - startPrice.FormatString(2), - startPrice.FormatString(2)) - } else { - color.Red("%s BASE ASSET PERFORMANCE: %s (= (%s - %s) / %s)", - market.BaseCurrency, - lastPrice.Sub(startPrice).Div(startPrice).FormatPercentage(2), - lastPrice.FormatString(2), - startPrice.FormatString(2), - startPrice.FormatString(2)) - } - } - } + calculator := &pnl.AverageCostCalculator{ + TradingFeeCurrency: backtestExchange.PlatformFeeCurrency(), + Market: market, + } + + sharpeRatio := fixedpoint.NewFromFloat(intervalProfit.GetSharpe()) + sortinoRatio := fixedpoint.NewFromFloat(intervalProfit.GetSortino()) + + report := calculator.Calculate(symbol, trades, lastPrice) + accountConfig := userConfig.Backtest.GetAccount(session.Exchange.Name().String()) + initBalances := accountConfig.Balances.BalanceMap() + finalBalances := session.GetAccount().Balances() + symbolReport := backtest.SessionSymbolReport{ + Exchange: session.Exchange.Name(), + Symbol: symbol, + Market: market, + LastPrice: lastPrice, + StartPrice: startPrice, + PnL: report, + InitialBalances: initBalances, + FinalBalances: finalBalances, + // Manifests: manifests, + Sharpe: sharpeRatio, + Sortino: sortinoRatio, + } + + for _, s := range session.Subscriptions { + symbolReport.Subscriptions = append(symbolReport.Subscriptions, s) + } + + sessionKLineIntervals := map[types.Interval]struct{}{} + for _, sub := range session.Subscriptions { + if sub.Channel == types.KLineChannel { + sessionKLineIntervals[sub.Options.Interval] = struct{}{} } + } - return nil - }, + for interval := range sessionKLineIntervals { + symbolReport.Intervals = append(symbolReport.Intervals, interval) + } + + return &symbolReport, nil +} + +func verify(userConfig *bbgo.Config, backtestService *service.BacktestService, sourceExchanges map[types.ExchangeName]types.Exchange, startTime, endTime time.Time) error { + for _, sourceExchange := range sourceExchanges { + err := backtestService.Verify(sourceExchange, userConfig.Backtest.Symbols, startTime, endTime) + if err != nil { + return err + } + } + return nil } func confirmation(s string) bool { @@ -481,3 +714,70 @@ func confirmation(s string) bool { } } } + +func toExchangeSources(sessions map[string]*bbgo.ExchangeSession, startTime, endTime time.Time, requiredInterval types.Interval, extraIntervals ...types.Interval) (exchangeSources []*backtest.ExchangeDataSource, err error) { + for _, session := range sessions { + backtestEx := session.Exchange.(*backtest.Exchange) + + c, err := backtestEx.SubscribeMarketData(startTime, endTime, requiredInterval, extraIntervals...) + if err != nil { + return exchangeSources, err + } + + sessionCopy := session + src := &backtest.ExchangeDataSource{ + C: c, + Exchange: backtestEx, + Session: sessionCopy, + } + backtestEx.Src = src + exchangeSources = append(exchangeSources, src) + } + return exchangeSources, nil +} + +func sync(ctx context.Context, userConfig *bbgo.Config, backtestService *service.BacktestService, sourceExchanges map[types.ExchangeName]types.Exchange, syncFrom, syncTo time.Time) error { + for _, symbol := range userConfig.Backtest.Symbols { + for _, sourceExchange := range sourceExchanges { + exCustom, ok := sourceExchange.(types.CustomIntervalProvider) + + var supportIntervals map[types.Interval]int + if ok { + supportIntervals = exCustom.SupportedInterval() + } else { + supportIntervals = types.SupportedIntervals + } + if !userConfig.Backtest.SyncSecKLines { + delete(supportIntervals, types.Interval1s) + } + + // sort intervals + var intervals []types.Interval + for interval := range supportIntervals { + intervals = append(intervals, interval) + } + sort.Slice(intervals, func(i, j int) bool { + return intervals[i].Duration() < intervals[j].Duration() + }) + + for _, interval := range intervals { + if err := backtestService.Sync(ctx, sourceExchange, symbol, interval, syncFrom, syncTo); err != nil { + return err + } + } + } + } + return nil +} + +func rewriteManifestPaths(manifests backtest.Manifests, basePath string) (backtest.Manifests, error) { + var filterManifests = backtest.Manifests{} + for k, m := range manifests { + p, err := filepath.Rel(basePath, m) + if err != nil { + return nil, err + } + filterManifests[k] = p + } + return filterManifests, nil +} diff --git a/pkg/cmd/cmdutil/exchange.go b/pkg/cmd/cmdutil/exchange.go index a3c2c5e283..b6eaaab516 100644 --- a/pkg/cmd/cmdutil/exchange.go +++ b/pkg/cmd/cmdutil/exchange.go @@ -1,65 +1 @@ package cmdutil - -import ( - "fmt" - "os" - "strings" - - "github.com/c9s/bbgo/pkg/exchange/binance" - "github.com/c9s/bbgo/pkg/exchange/ftx" - "github.com/c9s/bbgo/pkg/exchange/kucoin" - "github.com/c9s/bbgo/pkg/exchange/max" - "github.com/c9s/bbgo/pkg/exchange/okex" - "github.com/c9s/bbgo/pkg/types" -) - -func NewExchangePublic(exchangeName types.ExchangeName) (types.Exchange, error) { - return NewExchangeStandard(exchangeName, "", "", "", "") -} - -func NewExchangeStandard(n types.ExchangeName, key, secret, passphrase, subAccount string) (types.Exchange, error) { - switch n { - - case types.ExchangeFTX: - return ftx.NewExchange(key, secret, subAccount), nil - - case types.ExchangeBinance: - return binance.New(key, secret), nil - - case types.ExchangeMax: - return max.New(key, secret), nil - - case types.ExchangeOKEx: - return okex.New(key, secret, passphrase), nil - - case types.ExchangeKucoin: - return kucoin.New(key, secret, passphrase), nil - - default: - return nil, fmt.Errorf("unsupported exchange: %v", n) - - } -} - -func NewExchangeWithEnvVarPrefix(n types.ExchangeName, varPrefix string) (types.Exchange, error) { - if len(varPrefix) == 0 { - varPrefix = n.String() - } - - varPrefix = strings.ToUpper(varPrefix) - - key := os.Getenv(varPrefix + "_API_KEY") - secret := os.Getenv(varPrefix + "_API_SECRET") - if len(key) == 0 || len(secret) == 0 { - return nil, fmt.Errorf("can not initialize exchange %s: empty key or secret, env var prefix: %s", n, varPrefix) - } - - passphrase := os.Getenv(varPrefix + "_API_PASSPHRASE") - subAccount := os.Getenv(varPrefix + "_SUBACCOUNT") - return NewExchangeStandard(n, key, secret, passphrase, subAccount) -} - -// NewExchange constructor exchange object from viper config. -func NewExchange(n types.ExchangeName) (types.Exchange, error) { - return NewExchangeWithEnvVarPrefix(n, "") -} diff --git a/pkg/cmd/deposit.go b/pkg/cmd/deposit.go index 9464c35da2..a43b12acfb 100644 --- a/pkg/cmd/deposit.go +++ b/pkg/cmd/deposit.go @@ -3,10 +3,8 @@ package cmd import ( "context" "fmt" - "os" "time" - "github.com/pkg/errors" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -29,34 +27,7 @@ var depositsCmd = &cobra.Command{ RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - configFile, err := cmd.Flags().GetString("config") - if err != nil { - return err - } - - if len(configFile) == 0 { - return errors.New("--config option is required") - } - - // if config file exists, use the config loaded from the config file. - // otherwise, use a empty config object - var userConfig *bbgo.Config - if _, err := os.Stat(configFile); err == nil { - // load successfully - userConfig, err = bbgo.Load(configFile, false) - if err != nil { - return err - } - } else if os.IsNotExist(err) { - // config file doesn't exist - userConfig = &bbgo.Config{} - } else { - // other error - return err - } - environ := bbgo.NewEnvironment() - if err := environ.ConfigureExchangeSessions(userConfig); err != nil { return err } diff --git a/pkg/cmd/hoptimize.go b/pkg/cmd/hoptimize.go new file mode 100644 index 0000000000..195ae39637 --- /dev/null +++ b/pkg/cmd/hoptimize.go @@ -0,0 +1,171 @@ +package cmd + +import ( + "context" + "encoding/json" + "fmt" + "github.com/c9s/bbgo/pkg/optimizer" + "github.com/fatih/color" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "gopkg.in/yaml.v3" + "io/ioutil" + "os" + "os/signal" + "syscall" + "time" +) + +func init() { + hoptimizeCmd.Flags().String("optimizer-config", "optimizer.yaml", "config file") + hoptimizeCmd.Flags().String("name", "", "assign an optimization session name") + hoptimizeCmd.Flags().Bool("json-keep-all", false, "keep all results of trials") + hoptimizeCmd.Flags().String("output", "output", "backtest report output directory") + hoptimizeCmd.Flags().Bool("json", false, "print optimizer metrics in json format") + hoptimizeCmd.Flags().Bool("tsv", false, "print optimizer metrics in csv format") + RootCmd.AddCommand(hoptimizeCmd) +} + +var hoptimizeCmd = &cobra.Command{ + Use: "hoptimize", + Short: "run hyperparameter optimizer (experimental)", + + // SilenceUsage is an option to silence usage when an error occurs. + SilenceUsage: true, + + RunE: func(cmd *cobra.Command, args []string) error { + optimizerConfigFilename, err := cmd.Flags().GetString("optimizer-config") + if err != nil { + return err + } + + configFile, err := cmd.Flags().GetString("config") + if err != nil { + return err + } + + optSessionName, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + + jsonKeepAll, err := cmd.Flags().GetBool("json-keep-all") + if err != nil { + return err + } + + outputDirectory, err := cmd.Flags().GetString("output") + if err != nil { + return err + } + + printJsonFormat, err := cmd.Flags().GetBool("json") + if err != nil { + return err + } + + printTsvFormat, err := cmd.Flags().GetBool("tsv") + if err != nil { + return err + } + + yamlBody, err := ioutil.ReadFile(configFile) + if err != nil { + return err + } + var obj map[string]interface{} + if err := yaml.Unmarshal(yamlBody, &obj); err != nil { + return err + } + delete(obj, "notifications") + delete(obj, "sync") + + optConfig, err := optimizer.LoadConfig(optimizerConfigFilename) + if err != nil { + return err + } + + // the config json template used for patch + configJson, err := json.MarshalIndent(obj, "", " ") + if err != nil { + return err + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + go func() { + c := make(chan os.Signal) + signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) + <-c + log.Info("Early stop by manual cancelation.") + cancel() + }() + + if len(optSessionName) == 0 { + optSessionName = fmt.Sprintf("bbgo-hpopt-%v", time.Now().UnixMilli()) + } + tempDirNameFormat := fmt.Sprintf("%s-config-*", optSessionName) + configDir, err := os.MkdirTemp("", tempDirNameFormat) + if err != nil { + return err + } + + executor := &optimizer.LocalProcessExecutor{ + Config: optConfig.Executor.LocalExecutorConfig, + Bin: os.Args[0], + WorkDir: ".", + ConfigDir: configDir, + OutputDir: outputDirectory, + } + + optz := &optimizer.HyperparameterOptimizer{ + SessionName: optSessionName, + Config: optConfig, + } + + if err := executor.Prepare(configJson); err != nil { + return err + } + + report, err := optz.Run(ctx, executor, configJson) + log.Info("All test trial finished.") + if err != nil { + return err + } + + if printJsonFormat { + if !jsonKeepAll { + report.Trials = nil + } + out, err := json.MarshalIndent(report, "", " ") + if err != nil { + return err + } + + // print report JSON to stdout + fmt.Println(string(out)) + } else if printTsvFormat { + if err := optimizer.FormatResultsTsv(os.Stdout, report.Parameters, report.Trials); err != nil { + return err + } + } else { + color.Green("OPTIMIZER REPORT") + color.Green("===============================================\n") + color.Green("SESSION NAME: %s\n", report.Name) + color.Green("OPTIMIZE OBJECTIVE: %s\n", report.Objective) + color.Green("BEST OBJECTIVE VALUE: %s\n", report.Best.Value) + color.Green("OPTIMAL PARAMETERS:") + for _, selectorConfig := range optConfig.Matrix { + label := selectorConfig.Label + if val, exist := report.Best.Parameters[label]; exist { + color.Green(" - %s: %v", label, val) + } else { + color.Red(" - %s: (invalid parameter definition)", label) + } + } + } + + return nil + }, +} diff --git a/pkg/cmd/import.go b/pkg/cmd/import.go new file mode 100644 index 0000000000..6e60754e4c --- /dev/null +++ b/pkg/cmd/import.go @@ -0,0 +1,6 @@ +package cmd + +// import built-in strategies +import ( + _ "github.com/c9s/bbgo/pkg/cmd/strategy" +) diff --git a/pkg/cmd/kline.go b/pkg/cmd/kline.go index ccdeb7c89b..b35abf05f5 100644 --- a/pkg/cmd/kline.go +++ b/pkg/cmd/kline.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "syscall" + "time" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -54,9 +55,22 @@ var klineCmd = &cobra.Command{ return err } + now := time.Now() + kLines, err := session.Exchange.QueryKLines(ctx, symbol, types.Interval(interval), types.KLineQueryOptions{ + Limit: 50, + EndTime: &now, + }) + if err != nil { + return err + } + log.Infof("kLines from RESTful API") + for _, k := range kLines { + log.Info(k.String()) + } + s := session.Exchange.NewStream() s.SetPublicOnly() - s.Subscribe(types.KLineChannel, symbol, types.SubscribeOptions{Interval: interval}) + s.Subscribe(types.KLineChannel, symbol, types.SubscribeOptions{Interval: types.Interval(interval)}) s.OnKLineClosed(func(kline types.KLine) { log.Infof("kline closed: %s", kline.String()) diff --git a/pkg/cmd/margin.go b/pkg/cmd/margin.go new file mode 100644 index 0000000000..1ad608f90d --- /dev/null +++ b/pkg/cmd/margin.go @@ -0,0 +1,189 @@ +package cmd + +import ( + "context" + "errors" + "fmt" + "time" + + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/types" +) + +var selectedSession *bbgo.ExchangeSession + +func init() { + marginLoansCmd.Flags().String("asset", "", "asset") + marginLoansCmd.Flags().String("session", "", "exchange session name") + marginCmd.AddCommand(marginLoansCmd) + + marginRepaysCmd.Flags().String("asset", "", "asset") + marginRepaysCmd.Flags().String("session", "", "exchange session name") + marginCmd.AddCommand(marginRepaysCmd) + + marginInterestsCmd.Flags().String("asset", "", "asset") + marginInterestsCmd.Flags().String("session", "", "exchange session name") + marginCmd.AddCommand(marginInterestsCmd) + + RootCmd.AddCommand(marginCmd) +} + +// go run ./cmd/bbgo margin --session=binance +var marginCmd = &cobra.Command{ + Use: "margin", + Short: "margin related history", + SilenceUsage: true, + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + if err := cobraLoadDotenv(cmd, args); err != nil { + return err + } + + if err := cobraLoadConfig(cmd, args); err != nil { + return err + } + + // ctx := context.Background() + environ := bbgo.NewEnvironment() + + if userConfig == nil { + return errors.New("user config is not loaded") + } + + if err := environ.ConfigureExchangeSessions(userConfig); err != nil { + return err + } + + sessionName, err := cmd.Flags().GetString("session") + if err != nil { + return err + } + + session, ok := environ.Session(sessionName) + if !ok { + return fmt.Errorf("session %s not found", sessionName) + } + + selectedSession = session + return nil + }, +} + +// go run ./cmd/bbgo margin loans --session=binance +var marginLoansCmd = &cobra.Command{ + Use: "loans --session=SESSION_NAME --asset=ASSET", + Short: "query loans history", + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + + asset, err := cmd.Flags().GetString("asset") + if err != nil { + return err + } + + if selectedSession == nil { + return errors.New("session is not set") + } + + marginHistoryService, ok := selectedSession.Exchange.(types.MarginHistory) + if !ok { + return fmt.Errorf("exchange %s does not support MarginHistory service", selectedSession.ExchangeName) + } + + now := time.Now() + startTime := now.AddDate(0, -5, 0) + endTime := now + loans, err := marginHistoryService.QueryLoanHistory(ctx, asset, &startTime, &endTime) + if err != nil { + return err + } + + log.Infof("%d loans", len(loans)) + for _, loan := range loans { + log.Infof("LOAN %+v", loan) + } + + return nil + }, +} + +// go run ./cmd/bbgo margin loans --session=binance +var marginRepaysCmd = &cobra.Command{ + Use: "repays --session=SESSION_NAME --asset=ASSET", + Short: "query repay history", + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + + asset, err := cmd.Flags().GetString("asset") + if err != nil { + return err + } + + if selectedSession == nil { + return errors.New("session is not set") + } + + marginHistoryService, ok := selectedSession.Exchange.(types.MarginHistory) + if !ok { + return fmt.Errorf("exchange %s does not support MarginHistory service", selectedSession.ExchangeName) + } + + now := time.Now() + startTime := now.AddDate(0, -5, 0) + endTime := now + repays, err := marginHistoryService.QueryLoanHistory(ctx, asset, &startTime, &endTime) + if err != nil { + return err + } + + log.Infof("%d repays", len(repays)) + for _, repay := range repays { + log.Infof("REPAY %+v", repay) + } + + return nil + }, +} + +// go run ./cmd/bbgo margin interests --session=binance +var marginInterestsCmd = &cobra.Command{ + Use: "interests --session=SESSION_NAME --asset=ASSET", + Short: "query interests history", + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + + asset, err := cmd.Flags().GetString("asset") + if err != nil { + return fmt.Errorf("can't get the symbol from flags: %w", err) + } + + if selectedSession == nil { + return errors.New("session is not set") + } + + marginHistoryService, ok := selectedSession.Exchange.(types.MarginHistory) + if !ok { + return fmt.Errorf("exchange %s does not support MarginHistory service", selectedSession.ExchangeName) + } + + now := time.Now() + startTime := now.AddDate(0, -5, 0) + endTime := now + interests, err := marginHistoryService.QueryInterestHistory(ctx, asset, &startTime, &endTime) + if err != nil { + return err + } + + log.Infof("%d interests", len(interests)) + for _, interest := range interests { + log.Infof("INTEREST %+v", interest) + } + + return nil + }, +} diff --git a/pkg/cmd/optimize.go b/pkg/cmd/optimize.go new file mode 100644 index 0000000000..a9af7f4e24 --- /dev/null +++ b/pkg/cmd/optimize.go @@ -0,0 +1,146 @@ +package cmd + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "os" + + "github.com/spf13/cobra" + "gopkg.in/yaml.v3" + + "github.com/c9s/bbgo/pkg/optimizer" +) + +func init() { + optimizeCmd.Flags().String("optimizer-config", "optimizer.yaml", "config file") + optimizeCmd.Flags().String("output", "output", "backtest report output directory") + optimizeCmd.Flags().Bool("json", false, "print optimizer metrics in json format") + optimizeCmd.Flags().Bool("tsv", false, "print optimizer metrics in csv format") + optimizeCmd.Flags().Int("limit", 50, "limit how many results to print pr metric") + RootCmd.AddCommand(optimizeCmd) +} + +var optimizeCmd = &cobra.Command{ + Use: "optimize", + Short: "run optimizer", + + // SilenceUsage is an option to silence usage when an error occurs. + SilenceUsage: true, + + RunE: func(cmd *cobra.Command, args []string) error { + optimizerConfigFilename, err := cmd.Flags().GetString("optimizer-config") + if err != nil { + return err + } + + configFile, err := cmd.Flags().GetString("config") + if err != nil { + return err + } + + printJsonFormat, err := cmd.Flags().GetBool("json") + if err != nil { + return err + } + + printTsvFormat, err := cmd.Flags().GetBool("tsv") + if err != nil { + return err + } + + outputDirectory, err := cmd.Flags().GetString("output") + if err != nil { + return err + } + + resultLimit, err := cmd.Flags().GetInt("limit") + if err != nil { + return err + } + + yamlBody, err := ioutil.ReadFile(configFile) + if err != nil { + return err + } + var obj map[string]interface{} + if err := yaml.Unmarshal(yamlBody, &obj); err != nil { + return err + } + delete(obj, "notifications") + delete(obj, "sync") + + optConfig, err := optimizer.LoadConfig(optimizerConfigFilename) + if err != nil { + return err + } + + // the config json template used for patch + configJson, err := json.MarshalIndent(obj, "", " ") + if err != nil { + return err + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + _ = ctx + + configDir, err := os.MkdirTemp("", "bbgo-config-*") + if err != nil { + return err + } + + executor := &optimizer.LocalProcessExecutor{ + Config: optConfig.Executor.LocalExecutorConfig, + Bin: os.Args[0], + WorkDir: ".", + ConfigDir: configDir, + OutputDir: outputDirectory, + } + + optz := &optimizer.GridOptimizer{ + Config: optConfig, + } + + if err := executor.Prepare(configJson); err != nil { + return err + } + + metrics, err := optz.Run(executor, configJson) + if err != nil { + return err + } + + if printJsonFormat { + out, err := json.MarshalIndent(metrics, "", " ") + if err != nil { + return err + } + + // print metrics JSON to stdout + fmt.Println(string(out)) + } else if printTsvFormat { + if err := optimizer.FormatMetricsTsv(os.Stdout, metrics); err != nil { + return err + } + } else { + for n, values := range metrics { + if len(values) == 0 { + continue + } + + if len(values) > resultLimit && resultLimit != 0 { + values = values[:resultLimit] + } + + fmt.Printf("%v => %s\n", values[0].Labels, n) + for _, m := range values { + fmt.Printf("%v => %s %v\n", m.Params, n, m.Value) + } + } + } + + return nil + }, +} diff --git a/pkg/cmd/orders.go b/pkg/cmd/orders.go index c75e8485de..3e7ac116ce 100644 --- a/pkg/cmd/orders.go +++ b/pkg/cmd/orders.go @@ -300,7 +300,6 @@ var submitOrderCmd = &cobra.Command{ "session", "symbol", "side", - "price", "quantity", }), RunE: func(cmd *cobra.Command, args []string) error { @@ -329,11 +328,21 @@ var submitOrderCmd = &cobra.Command{ return fmt.Errorf("can not get price: %w", err) } + asMarketOrder, err := cmd.Flags().GetBool("market") + if err != nil { + return err + } + quantity, err := cmd.Flags().GetString("quantity") if err != nil { return fmt.Errorf("can not get quantity: %w", err) } + marginOrderSideEffect, err := cmd.Flags().GetString("margin-side-effect") + if err != nil { + return fmt.Errorf("can not get quantity: %w", err) + } + environ := bbgo.NewEnvironment() if err := environ.ConfigureExchangeSessions(userConfig); err != nil { return err @@ -354,21 +363,33 @@ var submitOrderCmd = &cobra.Command{ } so := types.SubmitOrder{ - Symbol: symbol, - Side: types.SideType(strings.ToUpper(side)), - Type: types.OrderTypeLimit, - Quantity: fixedpoint.MustNewFromString(quantity), - Price: fixedpoint.MustNewFromString(price), - Market: market, - TimeInForce: "GTC", + Symbol: symbol, + Side: types.SideType(strings.ToUpper(side)), + Type: types.OrderTypeLimit, + Quantity: fixedpoint.MustNewFromString(quantity), + Market: market, + MarginSideEffect: types.MarginOrderSideEffectType(marginOrderSideEffect), + } + + if asMarketOrder { + so.Type = types.OrderTypeMarket + so.Price = fixedpoint.Zero + } else { + if len(price) == 0 { + return fmt.Errorf("price is required for limit order submission") + } + + so.Type = types.OrderTypeLimit + so.Price = fixedpoint.MustNewFromString(price) + so.TimeInForce = types.TimeInForceGTC } - co, err := session.Exchange.SubmitOrders(ctx, so) + co, err := session.Exchange.SubmitOrder(ctx, so) if err != nil { return err } - log.Infof("submitted order: %+v\ncreated order: %+v", so, co[0]) + log.Infof("submitted order: %+v\ncreated order: %+v", so, co) return nil }, } @@ -386,6 +407,8 @@ func init() { submitOrderCmd.Flags().String("side", "", "the trading side: buy or sell") submitOrderCmd.Flags().String("price", "", "the trading price") submitOrderCmd.Flags().String("quantity", "", "the trading quantity") + submitOrderCmd.Flags().Bool("market", false, "submit order as a market order") + submitOrderCmd.Flags().String("margin-side-effect", "", "margin order side effect") executeOrderCmd.Flags().String("session", "", "the exchange session name for sync") executeOrderCmd.Flags().String("symbol", "", "the trading pair, like btcusdt") diff --git a/pkg/cmd/pnl.go b/pkg/cmd/pnl.go index ffe927078c..8d3fd42317 100644 --- a/pkg/cmd/pnl.go +++ b/pkg/cmd/pnl.go @@ -2,16 +2,15 @@ package cmd import ( "context" + "errors" "fmt" - "os" + "sort" "strings" "time" - "github.com/pkg/errors" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/c9s/bbgo/pkg/accounting" "github.com/c9s/bbgo/pkg/accounting/pnl" "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/service" @@ -19,39 +18,33 @@ import ( ) func init() { - PnLCmd.Flags().String("session", "", "target exchange") + PnLCmd.Flags().StringArray("session", []string{}, "target exchange sessions") PnLCmd.Flags().String("symbol", "", "trading symbol") PnLCmd.Flags().Bool("include-transfer", false, "convert transfer records into trades") - PnLCmd.Flags().Int("limit", 500, "number of trades") + PnLCmd.Flags().Bool("sync", false, "sync before loading trades") + PnLCmd.Flags().String("since", "", "query trades from a time point") + PnLCmd.Flags().Uint64("limit", 0, "number of trades") RootCmd.AddCommand(PnLCmd) } var PnLCmd = &cobra.Command{ Use: "pnl", - Short: "pnl calculator", + Short: "Average Cost Based PnL Calculator", + Long: "This command calculates the average cost-based profit from your total trades", SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - configFile, err := cmd.Flags().GetString("config") + sessionNames, err := cmd.Flags().GetStringArray("session") if err != nil { return err } - if len(configFile) == 0 { - return errors.New("--config option is required") + if len(sessionNames) == 0 { + return errors.New("--session [SESSION] is required") } - if _, err := os.Stat(configFile); os.IsNotExist(err) { - return err - } - - userConfig, err := bbgo.Load(configFile, false) - if err != nil { - return err - } - - sessionName, err := cmd.Flags().GetString("session") + wantSync, err := cmd.Flags().GetBool("sync") if err != nil { return err } @@ -65,74 +58,96 @@ var PnLCmd = &cobra.Command{ return errors.New("--symbol [SYMBOL] is required") } - limit, err := cmd.Flags().GetInt("limit") - if err != nil { - return err - } - - environ := bbgo.NewEnvironment() + // this is the default since + since := time.Now().AddDate(-1, 0, 0) - if err := environ.ConfigureDatabase(ctx); err != nil { + sinceOpt, err := cmd.Flags().GetString("since") + if err != nil { return err } - if err := environ.ConfigureExchangeSessions(userConfig); err != nil { - return err + if sinceOpt != "" { + lt, err := types.ParseLooseFormatTime(sinceOpt) + if err != nil { + return err + } + since = lt.Time() } - session, ok := environ.Session(sessionName) - if !ok { - return fmt.Errorf("session %s not found", sessionName) - } + until := time.Now() - if err := environ.SyncSession(ctx, session); err != nil { + includeTransfer, err := cmd.Flags().GetBool("include-transfer") + if err != nil { return err } - if err = environ.Init(ctx); err != nil { + limit, err := cmd.Flags().GetUint64("limit") + if err != nil { return err } - exchange := session.Exchange + environ := bbgo.NewEnvironment() - market, ok := session.Market(symbol) - if !ok { - return fmt.Errorf("market config %s not found", symbol) + if err := environ.ConfigureDatabase(ctx); err != nil { + return err } - since := time.Now().AddDate(-1, 0, 0) - until := time.Now() - - includeTransfer, err := cmd.Flags().GetBool("include-transfer") - if err != nil { + if err := environ.ConfigureExchangeSessions(userConfig); err != nil { return err } - if includeTransfer { - transferService, ok := exchange.(types.ExchangeTransferService) + for _, sessionName := range sessionNames { + session, ok := environ.Session(sessionName) if !ok { - return fmt.Errorf("session exchange %s does not implement transfer service", sessionName) + return fmt.Errorf("session %s not found", sessionName) } - deposits, err := transferService.QueryDepositHistory(ctx, market.BaseCurrency, since, until) - if err != nil { - return err + if wantSync { + if err := environ.SyncSession(ctx, session, symbol); err != nil { + return err + } } - _ = deposits - withdrawals, err := transferService.QueryWithdrawHistory(ctx, market.BaseCurrency, since, until) - if err != nil { - return err + if includeTransfer { + exchange := session.Exchange + market, _ := session.Market(symbol) + transferService, ok := exchange.(types.ExchangeTransferService) + if !ok { + return fmt.Errorf("session exchange %s does not implement transfer service", sessionName) + } + + deposits, err := transferService.QueryDepositHistory(ctx, market.BaseCurrency, since, until) + if err != nil { + return err + } + _ = deposits + + withdrawals, err := transferService.QueryWithdrawHistory(ctx, market.BaseCurrency, since, until) + if err != nil { + return err + } + + sort.Slice(withdrawals, func(i, j int) bool { + a := withdrawals[i].ApplyTime.Time() + b := withdrawals[j].ApplyTime.Time() + return a.Before(b) + }) + + // we need the backtest klines for the daily prices + backtestService := &service.BacktestService{DB: environ.DatabaseService.DB} + if err := backtestService.Sync(ctx, exchange, symbol, types.Interval1d, since, until); err != nil { + return err + } } - _ = withdrawals + } - // we need the backtest klines for the daily prices - backtestService := &service.BacktestService{DB: environ.DatabaseService.DB} - if err := backtestService.SyncKLineByInterval(ctx, exchange, symbol, types.Interval1d, since, until); err != nil { - return err - } + if err = environ.Init(ctx); err != nil { + return err } + session, _ := environ.Session(sessionNames[0]) + exchange := session.Exchange + var trades []types.Trade tradingFeeCurrency := exchange.PlatformFeeCurrency() if strings.HasPrefix(symbol, tradingFeeCurrency) { @@ -140,9 +155,10 @@ var PnLCmd = &cobra.Command{ trades, err = environ.TradeService.QueryForTradingFeeCurrency(exchange.Name(), symbol, tradingFeeCurrency) } else { trades, err = environ.TradeService.Query(service.QueryTradesOptions{ - Exchange: exchange.Name(), Symbol: symbol, Limit: limit, + Sessions: sessionNames, + Since: &since, }) } @@ -150,41 +166,40 @@ var PnLCmd = &cobra.Command{ return err } - log.Infof("%d trades loaded", len(trades)) - - stockManager := &accounting.StockDistribution{ - Symbol: symbol, - TradingFeeCurrency: tradingFeeCurrency, + if len(trades) == 0 { + return errors.New("empty trades, you need to run sync command to sync the trades from the exchange first") } - checkpoints, err := stockManager.AddTrades(trades) - if err != nil { - return err - } + trades = types.SortTradesAscending(trades) - log.Infof("found checkpoints: %+v", checkpoints) - log.Infof("stock: %v", stockManager.Stocks.Quantity()) + log.Infof("%d trades loaded", len(trades)) tickers, err := exchange.QueryTickers(ctx, symbol) - if err != nil { return err } currentTick, ok := tickers[symbol] - if !ok { return errors.New("no ticker data for current price") } - currentPrice := currentTick.Last + market, ok := session.Market(symbol) + if !ok { + return fmt.Errorf("market not found: %s, %s", symbol, session.Exchange.Name()) + } + currentPrice := currentTick.Last calculator := &pnl.AverageCostCalculator{ TradingFeeCurrency: tradingFeeCurrency, + Market: market, } report := calculator.Calculate(symbol, trades, currentPrice) report.Print() + + log.Warnf("note that if you're using cross-exchange arbitrage, the PnL won't be accurate") + log.Warnf("withdrawal and deposits are not considered in the PnL") return nil }, } diff --git a/pkg/cmd/root.go b/pkg/cmd/root.go index 687f9a4bf0..27e22194af 100644 --- a/pkg/cmd/root.go +++ b/pkg/cmd/root.go @@ -4,6 +4,7 @@ import ( "net/http" "os" "path" + "runtime/pprof" "strings" "time" @@ -22,6 +23,8 @@ import ( _ "github.com/go-sql-driver/mysql" ) +var cpuProfileFile *os.File + var userConfig *bbgo.Config var RootCmd = &cobra.Command{ @@ -32,24 +35,10 @@ var RootCmd = &cobra.Command{ SilenceUsage: true, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - disableDotEnv, err := cmd.Flags().GetBool("no-dotenv") - if err != nil { + if err := cobraLoadDotenv(cmd, args); err != nil { return err } - if !disableDotEnv { - dotenvFile, err := cmd.Flags().GetString("dotenv") - if err != nil { - return err - } - - if _, err := os.Stat(dotenvFile); err == nil { - if err := godotenv.Load(dotenvFile); err != nil { - return errors.Wrap(err, "error loading dotenv file") - } - } - } - if viper.GetBool("debug") { log.Infof("debug mode is enabled") log.SetLevel(log.DebugLevel) @@ -67,39 +56,89 @@ var RootCmd = &cobra.Command{ }() } - configFile, err := cmd.Flags().GetString("config") + cpuProfile, err := cmd.Flags().GetString("cpu-profile") if err != nil { - return errors.Wrapf(err, "failed to get the config flag") + return err } - // load config file nicely - if len(configFile) > 0 { - // if config file exists, use the config loaded from the config file. - // otherwise, use a empty config object - if _, err := os.Stat(configFile); err == nil { - // load successfully - userConfig, err = bbgo.Load(configFile, false) - if err != nil { - return errors.Wrapf(err, "can not load config file: %s", configFile) - } + if cpuProfile != "" { + log.Infof("starting cpu profiler, recording at %s", cpuProfile) + + cpuProfileFile, err = os.Create(cpuProfile) + if err != nil { + return errors.Wrap(err, "can not create file for CPU profile") + } - } else if os.IsNotExist(err) { - // config file doesn't exist, we should use the empty config - userConfig = &bbgo.Config{} - } else { - // other error - return errors.Wrapf(err, "config file load error: %s", configFile) + if err := pprof.StartCPUProfile(cpuProfileFile); err != nil { + return errors.Wrap(err, "can not start CPU profile") } } - return nil + return cobraLoadConfig(cmd, args) }, + PersistentPostRunE: func(cmd *cobra.Command, args []string) error { + pprof.StopCPUProfile() + if cpuProfileFile != nil { + return cpuProfileFile.Close() // error handling omitted for example + } + return nil + }, RunE: func(cmd *cobra.Command, args []string) error { return nil }, } +func cobraLoadDotenv(cmd *cobra.Command, args []string) error { + disableDotEnv, err := cmd.Flags().GetBool("no-dotenv") + if err != nil { + return err + } + + if !disableDotEnv { + dotenvFile, err := cmd.Flags().GetString("dotenv") + if err != nil { + return err + } + + if _, err := os.Stat(dotenvFile); err == nil { + if err := godotenv.Load(dotenvFile); err != nil { + return errors.Wrap(err, "error loading dotenv file") + } + } + } + return nil +} + +func cobraLoadConfig(cmd *cobra.Command, args []string) error { + configFile, err := cmd.Flags().GetString("config") + if err != nil { + return errors.Wrapf(err, "failed to get the config flag") + } + + // load config file nicely + if len(configFile) > 0 { + // if config file exists, use the config loaded from the config file. + // otherwise, use an empty config object + if _, err := os.Stat(configFile); err == nil { + // load successfully + userConfig, err = bbgo.Load(configFile, false) + if err != nil { + return errors.Wrapf(err, "can not load config file: %s", configFile) + } + + } else if os.IsNotExist(err) { + // config file doesn't exist, we should use the empty config + userConfig = &bbgo.Config{} + } else { + // other error + return errors.Wrapf(err, "config file load error: %s", configFile) + } + } + + return nil +} + func init() { RootCmd.PersistentFlags().Bool("debug", false, "debug mode") RootCmd.PersistentFlags().Bool("metrics", false, "enable prometheus metrics") @@ -129,6 +168,7 @@ func init() { RootCmd.PersistentFlags().String("ftx-api-key", "", "ftx api key") RootCmd.PersistentFlags().String("ftx-api-secret", "", "ftx api secret") RootCmd.PersistentFlags().String("ftx-subaccount", "", "subaccount name. Specify it if the credential is for subaccount.") + RootCmd.PersistentFlags().String("cpu-profile", "", "cpu profile") viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) diff --git a/pkg/cmd/run.go b/pkg/cmd/run.go index b2415c3047..fafeda8aa6 100644 --- a/pkg/cmd/run.go +++ b/pkg/cmd/run.go @@ -6,7 +6,6 @@ import ( "os" "os/exec" "path/filepath" - "runtime/pprof" "syscall" "time" @@ -30,11 +29,11 @@ func init() { RunCmd.Flags().Bool("enable-webserver", false, "enable webserver") RunCmd.Flags().Bool("enable-web-server", false, "legacy option, this is renamed to --enable-webserver") RunCmd.Flags().String("webserver-bind", ":8080", "webserver binding") + RunCmd.Flags().Bool("lightweight", false, "lightweight mode") RunCmd.Flags().Bool("enable-grpc", false, "enable grpc server") RunCmd.Flags().String("grpc-bind", ":50051", "grpc server binding") - RunCmd.Flags().String("cpu-profile", "", "cpu profile") RunCmd.Flags().Bool("setup", false, "use setup mode") RootCmd.AddCommand(RunCmd) } @@ -79,47 +78,10 @@ func runSetup(baseCtx context.Context, userConfig *bbgo.Config, enableApiServer cmdutil.WaitForSignal(ctx, syscall.SIGINT, syscall.SIGTERM) cancelTrading() - // graceful period = 15 second - shutdownCtx, cancelShutdown := context.WithDeadline(ctx, time.Now().Add(15*time.Second)) - - log.Infof("shutting down...") - trader.Graceful.Shutdown(shutdownCtx) + gracefulShutdownPeriod := 30 * time.Second + shtCtx, cancelShutdown := context.WithTimeout(bbgo.NewTodoContextWithExistingIsolation(ctx), gracefulShutdownPeriod) + bbgo.Shutdown(shtCtx) cancelShutdown() - return nil -} - -func BootstrapBacktestEnvironment(ctx context.Context, environ *bbgo.Environment, userConfig *bbgo.Config) error { - if err := environ.ConfigureDatabase(ctx); err != nil { - return err - } - - environ.Notifiability = bbgo.Notifiability{ - SymbolChannelRouter: bbgo.NewPatternChannelRouter(nil), - SessionChannelRouter: bbgo.NewPatternChannelRouter(nil), - ObjectChannelRouter: bbgo.NewObjectChannelRouter(), - } - - return nil -} - -func BootstrapEnvironment(ctx context.Context, environ *bbgo.Environment, userConfig *bbgo.Config) error { - if err := environ.ConfigureDatabase(ctx); err != nil { - return err - } - - if err := environ.ConfigureExchangeSessions(userConfig); err != nil { - return errors.Wrap(err, "exchange session configure error") - } - - if userConfig.Persistence != nil { - if err := environ.ConfigurePersistence(userConfig.Persistence); err != nil { - return errors.Wrap(err, "persistence configure error") - } - } - - if err := environ.ConfigureNotificationSystem(userConfig); err != nil { - return errors.Wrap(err, "notification configure error") - } return nil } @@ -166,10 +128,22 @@ func runConfig(basectx context.Context, cmd *cobra.Command, userConfig *bbgo.Con defer cancelTrading() environ := bbgo.NewEnvironment() - if err := BootstrapEnvironment(ctx, environ, userConfig); err != nil { + + lightweight, err := cmd.Flags().GetBool("lightweight") + if err != nil { return err } + if lightweight { + if err := bbgo.BootstrapEnvironmentLightweight(ctx, environ, userConfig); err != nil { + return err + } + } else { + if err := bbgo.BootstrapEnvironment(ctx, environ, userConfig); err != nil { + return err + } + } + if err := environ.Init(ctx); err != nil { return err } @@ -189,6 +163,10 @@ func runConfig(basectx context.Context, cmd *cobra.Command, userConfig *bbgo.Con return err } + if err := trader.LoadState(); err != nil { + return err + } + if err := trader.Run(ctx); err != nil { return err } @@ -223,11 +201,15 @@ func runConfig(basectx context.Context, cmd *cobra.Command, userConfig *bbgo.Con cmdutil.WaitForSignal(ctx, syscall.SIGINT, syscall.SIGTERM) cancelTrading() - log.Infof("shutting down...") - shutdownCtx, cancelShutdown := context.WithDeadline(ctx, time.Now().Add(30*time.Second)) - trader.Graceful.Shutdown(shutdownCtx) + gracefulShutdownPeriod := 30 * time.Second + shtCtx, cancelShutdown := context.WithTimeout(bbgo.NewTodoContextWithExistingIsolation(ctx), gracefulShutdownPeriod) + bbgo.Shutdown(shtCtx) cancelShutdown() + if err := trader.SaveState(); err != nil { + log.WithError(err).Errorf("can not save strategy states") + } + for _, session := range environ.Sessions() { if err := session.MarketDataStream.Close(); err != nil { log.WithError(err).Errorf("[%s] market data stream close error", session.Name) @@ -256,11 +238,6 @@ func run(cmd *cobra.Command, args []string) error { return err } - cpuProfile, err := cmd.Flags().GetString("cpu-profile") - if err != nil { - return err - } - if !setup { // if it's not setup, then the config file option is required. if len(configFile) == 0 { @@ -292,20 +269,6 @@ func run(cmd *cobra.Command, args []string) error { return err } - if cpuProfile != "" { - f, err := os.Create(cpuProfile) - if err != nil { - log.Fatal("could not create CPU profile: ", err) - } - defer f.Close() // error handling omitted for example - - if err := pprof.StartCPUProfile(f); err != nil { - log.Fatal("could not start CPU profile: ", err) - } - - defer pprof.StopCPUProfile() - } - return runConfig(ctx, cmd, userConfig) } diff --git a/pkg/cmd/builtin.go b/pkg/cmd/strategy/builtin.go similarity index 64% rename from pkg/cmd/builtin.go rename to pkg/cmd/strategy/builtin.go index cee00c6e66..1c29ab684c 100644 --- a/pkg/cmd/builtin.go +++ b/pkg/cmd/strategy/builtin.go @@ -1,26 +1,39 @@ -package cmd +package strategy // import built-in strategies import ( + _ "github.com/c9s/bbgo/pkg/strategy/audacitymaker" _ "github.com/c9s/bbgo/pkg/strategy/autoborrow" _ "github.com/c9s/bbgo/pkg/strategy/bollgrid" _ "github.com/c9s/bbgo/pkg/strategy/bollmaker" + _ "github.com/c9s/bbgo/pkg/strategy/dca" + _ "github.com/c9s/bbgo/pkg/strategy/drift" + _ "github.com/c9s/bbgo/pkg/strategy/elliottwave" _ "github.com/c9s/bbgo/pkg/strategy/emastop" _ "github.com/c9s/bbgo/pkg/strategy/etf" _ "github.com/c9s/bbgo/pkg/strategy/ewoDgtrd" _ "github.com/c9s/bbgo/pkg/strategy/factorzoo" _ "github.com/c9s/bbgo/pkg/strategy/flashcrash" + _ "github.com/c9s/bbgo/pkg/strategy/fmaker" _ "github.com/c9s/bbgo/pkg/strategy/funding" _ "github.com/c9s/bbgo/pkg/strategy/grid" + _ "github.com/c9s/bbgo/pkg/strategy/harmonic" + _ "github.com/c9s/bbgo/pkg/strategy/irr" _ "github.com/c9s/bbgo/pkg/strategy/kline" + _ "github.com/c9s/bbgo/pkg/strategy/marketcap" + _ "github.com/c9s/bbgo/pkg/strategy/pivotshort" _ "github.com/c9s/bbgo/pkg/strategy/pricealert" _ "github.com/c9s/bbgo/pkg/strategy/pricedrop" _ "github.com/c9s/bbgo/pkg/strategy/rebalance" + _ "github.com/c9s/bbgo/pkg/strategy/rsmaker" _ "github.com/c9s/bbgo/pkg/strategy/schedule" _ "github.com/c9s/bbgo/pkg/strategy/skeleton" + _ "github.com/c9s/bbgo/pkg/strategy/supertrend" _ "github.com/c9s/bbgo/pkg/strategy/support" _ "github.com/c9s/bbgo/pkg/strategy/swing" _ "github.com/c9s/bbgo/pkg/strategy/techsignal" + _ "github.com/c9s/bbgo/pkg/strategy/trendtrader" + _ "github.com/c9s/bbgo/pkg/strategy/wall" _ "github.com/c9s/bbgo/pkg/strategy/xbalance" _ "github.com/c9s/bbgo/pkg/strategy/xgap" _ "github.com/c9s/bbgo/pkg/strategy/xmaker" diff --git a/pkg/cmd/sync.go b/pkg/cmd/sync.go index d3bb001db8..a898c3fe4a 100644 --- a/pkg/cmd/sync.go +++ b/pkg/cmd/sync.go @@ -6,21 +6,20 @@ import ( "time" "github.com/pkg/errors" - log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/c9s/bbgo/pkg/bbgo" ) func init() { - SyncCmd.Flags().String("session", "", "the exchange session name for sync") + SyncCmd.Flags().StringArray("session", []string{}, "the exchange session name for sync") SyncCmd.Flags().String("symbol", "", "symbol of market for syncing") SyncCmd.Flags().String("since", "", "sync from time") RootCmd.AddCommand(SyncCmd) } var SyncCmd = &cobra.Command{ - Use: "sync --session=[exchange_name] --symbol=[pair_name] [--since=yyyy/mm/dd]", + Use: "sync [--session=[exchange_name]] [--symbol=[pair_name]] [[--since=yyyy/mm/dd]]", Short: "sync trades and orders history", SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { @@ -58,7 +57,7 @@ var SyncCmd = &cobra.Command{ return err } - sessionName, err := cmd.Flags().GetString("session") + sessionNames, err := cmd.Flags().GetStringArray("session") if err != nil { return err } @@ -88,35 +87,20 @@ var SyncCmd = &cobra.Command{ environ.SetSyncStartTime(syncStartTime) - // syncSymbols is the symbol list to sync - var syncSymbols []string - - if userConfig.Sync != nil && len(userConfig.Sync.Symbols) > 0 { - syncSymbols = userConfig.Sync.Symbols - } - if len(symbol) > 0 { - syncSymbols = []string{symbol} - } - - var selectedSessions []string - - if userConfig.Sync != nil && len(userConfig.Sync.Sessions) > 0 { - selectedSessions = userConfig.Sync.Sessions - } - if len(sessionName) > 0 { - selectedSessions = []string{sessionName} + if userConfig.Sync != nil && len(userConfig.Sync.Symbols) > 0 { + userConfig.Sync.Symbols = []bbgo.SyncSymbol{ + {Symbol: symbol}, + } + } } - sessions := environ.SelectSessions(selectedSessions...) - for _, session := range sessions { - if err := environ.SyncSession(ctx, session, syncSymbols...); err != nil { - return err + if len(sessionNames) > 0 { + if userConfig.Sync != nil && len(userConfig.Sync.Sessions) > 0 { + userConfig.Sync.Sessions = sessionNames } - - log.Infof("exchange session %s synchronization done", session.Name) } - return nil + return environ.Sync(ctx, userConfig) }, } diff --git a/pkg/cmd/transfer.go b/pkg/cmd/transfer.go index 9932db3747..de57970710 100644 --- a/pkg/cmd/transfer.go +++ b/pkg/cmd/transfer.go @@ -59,7 +59,7 @@ var TransferHistoryCmd = &cobra.Command{ } environ := bbgo.NewEnvironment() - if err := BootstrapEnvironment(ctx, environ, userConfig); err != nil { + if err := bbgo.BootstrapEnvironment(ctx, environ, userConfig); err != nil { return err } diff --git a/pkg/cmd/utils.go b/pkg/cmd/utils.go index efff19eb31..dda83d2ff9 100644 --- a/pkg/cmd/utils.go +++ b/pkg/cmd/utils.go @@ -5,11 +5,12 @@ import ( "github.com/spf13/viper" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/c9s/bbgo/pkg/exchange/ftx" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" - log "github.com/sirupsen/logrus" - "github.com/spf13/cobra" ) func cobraInitRequired(required []string) func(cmd *cobra.Command, args []string) error { @@ -23,6 +24,7 @@ func cobraInitRequired(required []string) func(cmd *cobra.Command, args []string } } +// inQuoteAsset converts all balances in quote asset func inQuoteAsset(balances types.BalanceMap, market types.Market, price fixedpoint.Value) fixedpoint.Value { quote := balances[market.QuoteCurrency] base := balances[market.BaseCurrency] diff --git a/pkg/data/tsv/writer.go b/pkg/data/tsv/writer.go new file mode 100644 index 0000000000..f91df9ac5c --- /dev/null +++ b/pkg/data/tsv/writer.go @@ -0,0 +1,45 @@ +package tsv + +import ( + "encoding/csv" + "io" + "os" +) + +type Writer struct { + file io.WriteCloser + + *csv.Writer +} + +func NewWriterFile(filename string) (*Writer, error) { + f, err := os.Create(filename) + if err != nil { + return nil, err + } + + return NewWriter(f), nil +} + +func AppendWriterFile(filename string) (*Writer, error) { + f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644) + if err != nil { + return nil, err + } + + return NewWriter(f), nil +} + +func NewWriter(file io.WriteCloser) *Writer { + tsv := csv.NewWriter(file) + tsv.Comma = '\t' + return &Writer{ + Writer: tsv, + file: file, + } +} + +func (w *Writer) Close() error { + w.Writer.Flush() + return w.file.Close() +} diff --git a/pkg/datasource/coinmarketcap/datasource.go b/pkg/datasource/coinmarketcap/datasource.go new file mode 100644 index 0000000000..50e5b42385 --- /dev/null +++ b/pkg/datasource/coinmarketcap/datasource.go @@ -0,0 +1,36 @@ +package coinmarketcap + +import ( + "context" + + v1 "github.com/c9s/bbgo/pkg/datasource/coinmarketcap/v1" +) + +type DataSource struct { + client *v1.RestClient +} + +func New(apiKey string) *DataSource { + client := v1.New() + client.Auth(apiKey) + return &DataSource{client: client} +} + +func (d *DataSource) QueryMarketCapInUSD(ctx context.Context, limit int) (map[string]float64, error) { + req := v1.ListingsLatestRequest{ + Client: d.client, + Limit: &limit, + } + + resp, err := req.Do(ctx) + if err != nil { + return nil, err + } + + marketcaps := make(map[string]float64) + for _, data := range resp { + marketcaps[data.Symbol] = data.Quote["USD"].MarketCap + } + + return marketcaps, nil +} diff --git a/pkg/datasource/coinmarketcap/v1/client.go b/pkg/datasource/coinmarketcap/v1/client.go new file mode 100644 index 0000000000..be4f3d181e --- /dev/null +++ b/pkg/datasource/coinmarketcap/v1/client.go @@ -0,0 +1,55 @@ +package v1 + +import ( + "context" + "net/http" + "net/url" + "time" + + "github.com/c9s/requestgen" +) + +const baseURL = "https://pro-api.coinmarketcap.com" +const defaultHTTPTimeout = time.Second * 15 + +type RestClient struct { + requestgen.BaseAPIClient + + apiKey string +} + +func New() *RestClient { + u, err := url.Parse(baseURL) + if err != nil { + panic(err) + } + + return &RestClient{ + BaseAPIClient: requestgen.BaseAPIClient{ + BaseURL: u, + HttpClient: &http.Client{ + Timeout: defaultHTTPTimeout, + }, + }, + } +} + +func (c *RestClient) Auth(apiKey string) { + // pragma: allowlist nextline secret + c.apiKey = apiKey +} + +func (c *RestClient) NewAuthenticatedRequest(ctx context.Context, method, refURL string, params url.Values, payload interface{}) (*http.Request, error) { + req, err := c.NewRequest(ctx, method, refURL, params, payload) + if err != nil { + return nil, err + } + + req.Header.Add("Accept", "application/json") + req.Header.Add("Content-Type", "application/json") + + // Attach API Key to header. https://coinmarketcap.com/api/documentation/v1/#section/Authentication + req.Header.Add("X-CMC_PRO_API_KEY", c.apiKey) + + return req, nil +} diff --git a/pkg/datasource/coinmarketcap/v1/listings.go b/pkg/datasource/coinmarketcap/v1/listings.go new file mode 100644 index 0000000000..35c8532c5e --- /dev/null +++ b/pkg/datasource/coinmarketcap/v1/listings.go @@ -0,0 +1,56 @@ +package v1 + +import ( + "github.com/c9s/requestgen" +) + +//go:generate requestgen -method GET -url "/v1/cryptocurrency/listings/historical" -type ListingsHistoricalRequest -responseType Response -responseDataField Data -responseDataType []Data +type ListingsHistoricalRequest struct { + Client requestgen.AuthenticatedAPIClient + + Date string `param:"date,query,required"` + Start *int `param:"start,query" default:"1"` + Limit *int `param:"limit,query" default:"100"` + Convert *string `param:"convert,query"` + ConvertID *string `param:"convert_id,query"` + Sort *string `param:"sort,query" default:"cmc_rank" validValues:"cmc_rank,name,symbol,market_cap,price,circulating_supply,total_supply,max_supply,num_market_pairs,volume_24h,percent_change_1h,percent_change_24h,percent_change_7d"` + SortDir *string `param:"sort_dir,query" validValues:"asc,desc"` + CryptocurrencyType *string `param:"cryptocurrency_type,query" default:"all" validValues:"all,coins,tokens"` + Aux *string `param:"aux,query" default:"platform,tags,date_added,circulating_supply,total_supply,max_supply,cmc_rank,num_market_pairs"` +} + +//go:generate requestgen -method GET -url "/v1/cryptocurrency/listings/latest" -type ListingsLatestRequest -responseType Response -responseDataField Data -responseDataType []Data +type ListingsLatestRequest struct { + Client requestgen.AuthenticatedAPIClient + + Start *int `param:"start,query" default:"1"` + Limit *int `param:"limit,query" default:"100"` + PriceMin *float64 `param:"price_min,query"` + PriceMax *float64 `param:"price_max,query"` + MarketCapMin *float64 `param:"market_cap_min,query"` + MarketCapMax *float64 `param:"market_cap_max,query"` + Volume24HMin *float64 `param:"volume_24h_min,query"` + Volume24HMax *float64 `param:"volume_24h_max,query"` + CirculatingSupplyMin *float64 `param:"circulating_supply_min,query"` + CirculatingSupplyMax *float64 `param:"circulating_supply_max,query"` + PercentChange24HMin *float64 `param:"percent_change_24h_min,query"` + PercentChange24HMax *float64 `param:"percent_change_24h_max,query"` + Convert *string `param:"convert,query"` + ConvertID *string `param:"convert_id,query"` + Sort *string `param:"sort,query" default:"market_cap" validValues:"name,symbol,date_added,market_cap,market_cap_strict,price,circulating_supply,total_supply,max_supply,num_market_pairs,volume_24h,percent_change_1h,percent_change_24h,percent_change_7d,market_cap_by_total_supply_strict,volume_7d,volume_30d"` + SortDir *string `param:"sort_dir,query" validValues:"asc,desc"` + CryptocurrencyType *string `param:"cryptocurrency_type,query" default:"all" validValues:"all,coins,tokens"` + Tag *string `param:"tag,query" default:"all" validValues:"all,defi,filesharing"` + Aux *string `param:"aux,query" default:"num_market_pairs,cmc_rank,date_added,tags,platform,max_supply,circulating_supply,total_supply"` +} + +//go:generate requestgen -method GET -url "/v1/cryptocurrency/listings/new" -type ListingsNewRequest -responseType Response -responseDataField Data -responseDataType []Data +type ListingsNewRequest struct { + Client requestgen.AuthenticatedAPIClient + + Start *int `param:"start,query" default:"1"` + Limit *int `param:"limit,query" default:"100"` + Convert *string `param:"convert,query"` + ConvertID *string `param:"convert_id,query"` + SortDir *string `param:"sort_dir,query" validValues:"asc,desc"` +} diff --git a/pkg/datasource/coinmarketcap/v1/listings_historical_request_requestgen.go b/pkg/datasource/coinmarketcap/v1/listings_historical_request_requestgen.go new file mode 100644 index 0000000000..c71651cdd4 --- /dev/null +++ b/pkg/datasource/coinmarketcap/v1/listings_historical_request_requestgen.go @@ -0,0 +1,315 @@ +// Code generated by "requestgen -method GET -url /v1/cryptocurrency/listings/historical -type ListingsHistoricalRequest -responseType Response -responseDataField Data -responseDataType []Data"; DO NOT EDIT. + +package v1 + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +func (l *ListingsHistoricalRequest) SetDate(Date string) *ListingsHistoricalRequest { + l.Date = Date + return l +} + +func (l *ListingsHistoricalRequest) SetStart(Start int) *ListingsHistoricalRequest { + l.Start = &Start + return l +} + +func (l *ListingsHistoricalRequest) SetLimit(Limit int) *ListingsHistoricalRequest { + l.Limit = &Limit + return l +} + +func (l *ListingsHistoricalRequest) SetConvert(Convert string) *ListingsHistoricalRequest { + l.Convert = &Convert + return l +} + +func (l *ListingsHistoricalRequest) SetConvertID(ConvertID string) *ListingsHistoricalRequest { + l.ConvertID = &ConvertID + return l +} + +func (l *ListingsHistoricalRequest) SetSort(Sort string) *ListingsHistoricalRequest { + l.Sort = &Sort + return l +} + +func (l *ListingsHistoricalRequest) SetSortDir(SortDir string) *ListingsHistoricalRequest { + l.SortDir = &SortDir + return l +} + +func (l *ListingsHistoricalRequest) SetCryptocurrencyType(CryptocurrencyType string) *ListingsHistoricalRequest { + l.CryptocurrencyType = &CryptocurrencyType + return l +} + +func (l *ListingsHistoricalRequest) SetAux(Aux string) *ListingsHistoricalRequest { + l.Aux = &Aux + return l +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (l *ListingsHistoricalRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + // check Date field -> json key date + Date := l.Date + + // TEMPLATE check-required + if len(Date) == 0 { + return nil, fmt.Errorf("date is required, empty string given") + } + // END TEMPLATE check-required + + // assign parameter of Date + params["date"] = Date + // check Start field -> json key start + if l.Start != nil { + Start := *l.Start + + // assign parameter of Start + params["start"] = Start + } else { + Start := 1 + + // assign parameter of Start + params["start"] = Start + } + // check Limit field -> json key limit + if l.Limit != nil { + Limit := *l.Limit + + // assign parameter of Limit + params["limit"] = Limit + } else { + Limit := 100 + + // assign parameter of Limit + params["limit"] = Limit + } + // check Convert field -> json key convert + if l.Convert != nil { + Convert := *l.Convert + + // assign parameter of Convert + params["convert"] = Convert + } else { + } + // check ConvertID field -> json key convert_id + if l.ConvertID != nil { + ConvertID := *l.ConvertID + + // assign parameter of ConvertID + params["convert_id"] = ConvertID + } else { + } + // check Sort field -> json key sort + if l.Sort != nil { + Sort := *l.Sort + + // TEMPLATE check-valid-values + switch Sort { + case "cmc_rank", "name", "symbol", "market_cap", "price", "circulating_supply", "total_supply", "max_supply", "num_market_pairs", "volume_24h", "percent_change_1h", "percent_change_24h", "percent_change_7d": + params["sort"] = Sort + + default: + return nil, fmt.Errorf("sort value %v is invalid", Sort) + + } + // END TEMPLATE check-valid-values + + // assign parameter of Sort + params["sort"] = Sort + } else { + Sort := "cmc_rank" + + // assign parameter of Sort + params["sort"] = Sort + } + // check SortDir field -> json key sort_dir + if l.SortDir != nil { + SortDir := *l.SortDir + + // TEMPLATE check-valid-values + switch SortDir { + case "asc", "desc": + params["sort_dir"] = SortDir + + default: + return nil, fmt.Errorf("sort_dir value %v is invalid", SortDir) + + } + // END TEMPLATE check-valid-values + + // assign parameter of SortDir + params["sort_dir"] = SortDir + } else { + } + // check CryptocurrencyType field -> json key cryptocurrency_type + if l.CryptocurrencyType != nil { + CryptocurrencyType := *l.CryptocurrencyType + + // TEMPLATE check-valid-values + switch CryptocurrencyType { + case "all", "coins", "tokens": + params["cryptocurrency_type"] = CryptocurrencyType + + default: + return nil, fmt.Errorf("cryptocurrency_type value %v is invalid", CryptocurrencyType) + + } + // END TEMPLATE check-valid-values + + // assign parameter of CryptocurrencyType + params["cryptocurrency_type"] = CryptocurrencyType + } else { + CryptocurrencyType := "all" + + // assign parameter of CryptocurrencyType + params["cryptocurrency_type"] = CryptocurrencyType + } + // check Aux field -> json key aux + if l.Aux != nil { + Aux := *l.Aux + + // assign parameter of Aux + params["aux"] = Aux + } else { + Aux := "platform,tags,date_added,circulating_supply,total_supply,max_supply,cmc_rank,num_market_pairs" + + // assign parameter of Aux + params["aux"] = Aux + } + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (l *ListingsHistoricalRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (l *ListingsHistoricalRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := l.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if l.isVarSlice(_v) { + l.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (l *ListingsHistoricalRequest) GetParametersJSON() ([]byte, error) { + params, err := l.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (l *ListingsHistoricalRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (l *ListingsHistoricalRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (l *ListingsHistoricalRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (l *ListingsHistoricalRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (l *ListingsHistoricalRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := l.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (l *ListingsHistoricalRequest) Do(ctx context.Context) ([]Data, error) { + + // no body params + var params interface{} + query, err := l.GetQueryParameters() + if err != nil { + return nil, err + } + + apiURL := "/v1/cryptocurrency/listings/historical" + + req, err := l.Client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := l.Client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse Response + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + var data []Data + if err := json.Unmarshal(apiResponse.Data, &data); err != nil { + return nil, err + } + return data, nil +} diff --git a/pkg/datasource/coinmarketcap/v1/listings_latest_request_requestgen.go b/pkg/datasource/coinmarketcap/v1/listings_latest_request_requestgen.go new file mode 100644 index 0000000000..fe453c8b7f --- /dev/null +++ b/pkg/datasource/coinmarketcap/v1/listings_latest_request_requestgen.go @@ -0,0 +1,457 @@ +// Code generated by "requestgen -method GET -url /v1/cryptocurrency/listings/latest -type ListingsLatestRequest -responseType Response -responseDataField Data -responseDataType []Data"; DO NOT EDIT. + +package v1 + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +func (l *ListingsLatestRequest) SetStart(Start int) *ListingsLatestRequest { + l.Start = &Start + return l +} + +func (l *ListingsLatestRequest) SetLimit(Limit int) *ListingsLatestRequest { + l.Limit = &Limit + return l +} + +func (l *ListingsLatestRequest) SetPriceMin(PriceMin float64) *ListingsLatestRequest { + l.PriceMin = &PriceMin + return l +} + +func (l *ListingsLatestRequest) SetPriceMax(PriceMax float64) *ListingsLatestRequest { + l.PriceMax = &PriceMax + return l +} + +func (l *ListingsLatestRequest) SetMarketCapMin(MarketCapMin float64) *ListingsLatestRequest { + l.MarketCapMin = &MarketCapMin + return l +} + +func (l *ListingsLatestRequest) SetMarketCapMax(MarketCapMax float64) *ListingsLatestRequest { + l.MarketCapMax = &MarketCapMax + return l +} + +func (l *ListingsLatestRequest) SetVolume24HMin(Volume24HMin float64) *ListingsLatestRequest { + l.Volume24HMin = &Volume24HMin + return l +} + +func (l *ListingsLatestRequest) SetVolume24HMax(Volume24HMax float64) *ListingsLatestRequest { + l.Volume24HMax = &Volume24HMax + return l +} + +func (l *ListingsLatestRequest) SetCirculatingSupplyMin(CirculatingSupplyMin float64) *ListingsLatestRequest { + l.CirculatingSupplyMin = &CirculatingSupplyMin + return l +} + +func (l *ListingsLatestRequest) SetCirculatingSupplyMax(CirculatingSupplyMax float64) *ListingsLatestRequest { + l.CirculatingSupplyMax = &CirculatingSupplyMax + return l +} + +func (l *ListingsLatestRequest) SetPercentChange24HMin(PercentChange24HMin float64) *ListingsLatestRequest { + l.PercentChange24HMin = &PercentChange24HMin + return l +} + +func (l *ListingsLatestRequest) SetPercentChange24HMax(PercentChange24HMax float64) *ListingsLatestRequest { + l.PercentChange24HMax = &PercentChange24HMax + return l +} + +func (l *ListingsLatestRequest) SetConvert(Convert string) *ListingsLatestRequest { + l.Convert = &Convert + return l +} + +func (l *ListingsLatestRequest) SetConvertID(ConvertID string) *ListingsLatestRequest { + l.ConvertID = &ConvertID + return l +} + +func (l *ListingsLatestRequest) SetSort(Sort string) *ListingsLatestRequest { + l.Sort = &Sort + return l +} + +func (l *ListingsLatestRequest) SetSortDir(SortDir string) *ListingsLatestRequest { + l.SortDir = &SortDir + return l +} + +func (l *ListingsLatestRequest) SetCryptocurrencyType(CryptocurrencyType string) *ListingsLatestRequest { + l.CryptocurrencyType = &CryptocurrencyType + return l +} + +func (l *ListingsLatestRequest) SetTag(Tag string) *ListingsLatestRequest { + l.Tag = &Tag + return l +} + +func (l *ListingsLatestRequest) SetAux(Aux string) *ListingsLatestRequest { + l.Aux = &Aux + return l +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (l *ListingsLatestRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + // check Start field -> json key start + if l.Start != nil { + Start := *l.Start + + // assign parameter of Start + params["start"] = Start + } else { + Start := 1 + + // assign parameter of Start + params["start"] = Start + } + // check Limit field -> json key limit + if l.Limit != nil { + Limit := *l.Limit + + // assign parameter of Limit + params["limit"] = Limit + } else { + Limit := 100 + + // assign parameter of Limit + params["limit"] = Limit + } + // check PriceMin field -> json key price_min + if l.PriceMin != nil { + PriceMin := *l.PriceMin + + // assign parameter of PriceMin + params["price_min"] = PriceMin + } else { + } + // check PriceMax field -> json key price_max + if l.PriceMax != nil { + PriceMax := *l.PriceMax + + // assign parameter of PriceMax + params["price_max"] = PriceMax + } else { + } + // check MarketCapMin field -> json key market_cap_min + if l.MarketCapMin != nil { + MarketCapMin := *l.MarketCapMin + + // assign parameter of MarketCapMin + params["market_cap_min"] = MarketCapMin + } else { + } + // check MarketCapMax field -> json key market_cap_max + if l.MarketCapMax != nil { + MarketCapMax := *l.MarketCapMax + + // assign parameter of MarketCapMax + params["market_cap_max"] = MarketCapMax + } else { + } + // check Volume24HMin field -> json key volume_24h_min + if l.Volume24HMin != nil { + Volume24HMin := *l.Volume24HMin + + // assign parameter of Volume24HMin + params["volume_24h_min"] = Volume24HMin + } else { + } + // check Volume24HMax field -> json key volume_24h_max + if l.Volume24HMax != nil { + Volume24HMax := *l.Volume24HMax + + // assign parameter of Volume24HMax + params["volume_24h_max"] = Volume24HMax + } else { + } + // check CirculatingSupplyMin field -> json key circulating_supply_min + if l.CirculatingSupplyMin != nil { + CirculatingSupplyMin := *l.CirculatingSupplyMin + + // assign parameter of CirculatingSupplyMin + params["circulating_supply_min"] = CirculatingSupplyMin + } else { + } + // check CirculatingSupplyMax field -> json key circulating_supply_max + if l.CirculatingSupplyMax != nil { + CirculatingSupplyMax := *l.CirculatingSupplyMax + + // assign parameter of CirculatingSupplyMax + params["circulating_supply_max"] = CirculatingSupplyMax + } else { + } + // check PercentChange24HMin field -> json key percent_change_24h_min + if l.PercentChange24HMin != nil { + PercentChange24HMin := *l.PercentChange24HMin + + // assign parameter of PercentChange24HMin + params["percent_change_24h_min"] = PercentChange24HMin + } else { + } + // check PercentChange24HMax field -> json key percent_change_24h_max + if l.PercentChange24HMax != nil { + PercentChange24HMax := *l.PercentChange24HMax + + // assign parameter of PercentChange24HMax + params["percent_change_24h_max"] = PercentChange24HMax + } else { + } + // check Convert field -> json key convert + if l.Convert != nil { + Convert := *l.Convert + + // assign parameter of Convert + params["convert"] = Convert + } else { + } + // check ConvertID field -> json key convert_id + if l.ConvertID != nil { + ConvertID := *l.ConvertID + + // assign parameter of ConvertID + params["convert_id"] = ConvertID + } else { + } + // check Sort field -> json key sort + if l.Sort != nil { + Sort := *l.Sort + + // TEMPLATE check-valid-values + switch Sort { + case "name", "symbol", "date_added", "market_cap", "market_cap_strict", "price", "circulating_supply", "total_supply", "max_supply", "num_market_pairs", "volume_24h", "percent_change_1h", "percent_change_24h", "percent_change_7d", "market_cap_by_total_supply_strict", "volume_7d", "volume_30d": + params["sort"] = Sort + + default: + return nil, fmt.Errorf("sort value %v is invalid", Sort) + + } + // END TEMPLATE check-valid-values + + // assign parameter of Sort + params["sort"] = Sort + } else { + Sort := "market_cap" + + // assign parameter of Sort + params["sort"] = Sort + } + // check SortDir field -> json key sort_dir + if l.SortDir != nil { + SortDir := *l.SortDir + + // TEMPLATE check-valid-values + switch SortDir { + case "asc", "desc": + params["sort_dir"] = SortDir + + default: + return nil, fmt.Errorf("sort_dir value %v is invalid", SortDir) + + } + // END TEMPLATE check-valid-values + + // assign parameter of SortDir + params["sort_dir"] = SortDir + } else { + } + // check CryptocurrencyType field -> json key cryptocurrency_type + if l.CryptocurrencyType != nil { + CryptocurrencyType := *l.CryptocurrencyType + + // TEMPLATE check-valid-values + switch CryptocurrencyType { + case "all", "coins", "tokens": + params["cryptocurrency_type"] = CryptocurrencyType + + default: + return nil, fmt.Errorf("cryptocurrency_type value %v is invalid", CryptocurrencyType) + + } + // END TEMPLATE check-valid-values + + // assign parameter of CryptocurrencyType + params["cryptocurrency_type"] = CryptocurrencyType + } else { + CryptocurrencyType := "all" + + // assign parameter of CryptocurrencyType + params["cryptocurrency_type"] = CryptocurrencyType + } + // check Tag field -> json key tag + if l.Tag != nil { + Tag := *l.Tag + + // TEMPLATE check-valid-values + switch Tag { + case "all", "defi", "filesharing": + params["tag"] = Tag + + default: + return nil, fmt.Errorf("tag value %v is invalid", Tag) + + } + // END TEMPLATE check-valid-values + + // assign parameter of Tag + params["tag"] = Tag + } else { + Tag := "all" + + // assign parameter of Tag + params["tag"] = Tag + } + // check Aux field -> json key aux + if l.Aux != nil { + Aux := *l.Aux + + // assign parameter of Aux + params["aux"] = Aux + } else { + Aux := "num_market_pairs,cmc_rank,date_added,tags,platform,max_supply,circulating_supply,total_supply" + + // assign parameter of Aux + params["aux"] = Aux + } + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (l *ListingsLatestRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (l *ListingsLatestRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := l.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if l.isVarSlice(_v) { + l.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (l *ListingsLatestRequest) GetParametersJSON() ([]byte, error) { + params, err := l.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (l *ListingsLatestRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (l *ListingsLatestRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (l *ListingsLatestRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (l *ListingsLatestRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (l *ListingsLatestRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := l.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (l *ListingsLatestRequest) Do(ctx context.Context) ([]Data, error) { + + // no body params + var params interface{} + query, err := l.GetQueryParameters() + if err != nil { + return nil, err + } + + apiURL := "/v1/cryptocurrency/listings/latest" + + req, err := l.Client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := l.Client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse Response + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + var data []Data + if err := json.Unmarshal(apiResponse.Data, &data); err != nil { + return nil, err + } + return data, nil +} diff --git a/pkg/datasource/coinmarketcap/v1/listings_new_request_requestgen.go b/pkg/datasource/coinmarketcap/v1/listings_new_request_requestgen.go new file mode 100644 index 0000000000..a23e47d345 --- /dev/null +++ b/pkg/datasource/coinmarketcap/v1/listings_new_request_requestgen.go @@ -0,0 +1,226 @@ +// Code generated by "requestgen -method GET -url /v1/cryptocurrency/listings/new -type ListingsNewRequest -responseType Response -responseDataField Data -responseDataType []Data"; DO NOT EDIT. + +package v1 + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +func (l *ListingsNewRequest) SetStart(Start int) *ListingsNewRequest { + l.Start = &Start + return l +} + +func (l *ListingsNewRequest) SetLimit(Limit int) *ListingsNewRequest { + l.Limit = &Limit + return l +} + +func (l *ListingsNewRequest) SetConvert(Convert string) *ListingsNewRequest { + l.Convert = &Convert + return l +} + +func (l *ListingsNewRequest) SetConvertID(ConvertID string) *ListingsNewRequest { + l.ConvertID = &ConvertID + return l +} + +func (l *ListingsNewRequest) SetSortDir(SortDir string) *ListingsNewRequest { + l.SortDir = &SortDir + return l +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (l *ListingsNewRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + // check Start field -> json key start + if l.Start != nil { + Start := *l.Start + + // assign parameter of Start + params["start"] = Start + } else { + Start := 1 + + // assign parameter of Start + params["start"] = Start + } + // check Limit field -> json key limit + if l.Limit != nil { + Limit := *l.Limit + + // assign parameter of Limit + params["limit"] = Limit + } else { + Limit := 100 + + // assign parameter of Limit + params["limit"] = Limit + } + // check Convert field -> json key convert + if l.Convert != nil { + Convert := *l.Convert + + // assign parameter of Convert + params["convert"] = Convert + } else { + } + // check ConvertID field -> json key convert_id + if l.ConvertID != nil { + ConvertID := *l.ConvertID + + // assign parameter of ConvertID + params["convert_id"] = ConvertID + } else { + } + // check SortDir field -> json key sort_dir + if l.SortDir != nil { + SortDir := *l.SortDir + + // TEMPLATE check-valid-values + switch SortDir { + case "asc", "desc": + params["sort_dir"] = SortDir + + default: + return nil, fmt.Errorf("sort_dir value %v is invalid", SortDir) + + } + // END TEMPLATE check-valid-values + + // assign parameter of SortDir + params["sort_dir"] = SortDir + } else { + } + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (l *ListingsNewRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (l *ListingsNewRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := l.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if l.isVarSlice(_v) { + l.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (l *ListingsNewRequest) GetParametersJSON() ([]byte, error) { + params, err := l.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (l *ListingsNewRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (l *ListingsNewRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (l *ListingsNewRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (l *ListingsNewRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (l *ListingsNewRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := l.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (l *ListingsNewRequest) Do(ctx context.Context) ([]json.RawMessage, error) { + + // no body params + var params interface{} + query, err := l.GetQueryParameters() + if err != nil { + return nil, err + } + + apiURL := "/v1/cryptocurrency/listings/new" + + req, err := l.Client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := l.Client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse Response + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + var data []json.RawMessage + if err := json.Unmarshal(apiResponse.Data, &data); err != nil { + return nil, err + } + return data, nil +} diff --git a/pkg/datasource/coinmarketcap/v1/types.go b/pkg/datasource/coinmarketcap/v1/types.go new file mode 100644 index 0000000000..a505625ea1 --- /dev/null +++ b/pkg/datasource/coinmarketcap/v1/types.go @@ -0,0 +1,61 @@ +package v1 + +import ( + "encoding/json" + "time" +) + +type Response struct { + Data json.RawMessage `json:"data"` + Status Status `json:"status"` +} + +type Data struct { + ID int64 `json:"id"` + Name string `json:"name"` + Symbol string `json:"symbol"` + Slug string `json:"slug"` + CmcRank int64 `json:"cmc_rank,omitempty"` + IsActive bool `json:"is_active,omitempty"` + IsFiat int64 `json:"is_fiat,omitempty"` + NumMarketPairs int64 `json:"num_market_pairs"` + CirculatingSupply float64 `json:"circulating_supply"` + TotalSupply float64 `json:"total_supply"` + MaxSupply float64 `json:"max_supply"` + LastUpdated time.Time `json:"last_updated"` + DateAdded time.Time `json:"date_added"` + Tags []string `json:"tags"` + SelfReportedCirculatingSupply float64 `json:"self_reported_circulating_supply,omitempty"` + SelfReportedMarketCap float64 `json:"self_reported_market_cap,omitempty"` + Platform Platform `json:"platform"` + Quote map[string]Quote `json:"quote"` +} + +type Quote struct { + Price float64 `json:"price"` + Volume24H float64 `json:"volume_24h"` + VolumeChange24H float64 `json:"volume_change_24h"` + PercentChange1H float64 `json:"percent_change_1h"` + PercentChange24H float64 `json:"percent_change_24h"` + PercentChange7D float64 `json:"percent_change_7d"` + MarketCap float64 `json:"market_cap"` + MarketCapDominance float64 `json:"market_cap_dominance"` + FullyDilutedMarketCap float64 `json:"fully_diluted_market_cap"` + LastUpdated time.Time `json:"last_updated"` +} + +type Status struct { + Timestamp time.Time `json:"timestamp"` + ErrorCode int `json:"error_code"` + ErrorMessage string `json:"error_message"` + Elapsed int `json:"elapsed"` + CreditCount int `json:"credit_count"` +} + +type Platform struct { + ID int `json:"id"` + Name string `json:"name"` + Symbol string `json:"symbol"` + Slug string `json:"slug"` + TokenAddress string `json:"token_address"` +} diff --git a/pkg/datasource/glassnode/datasource.go b/pkg/datasource/glassnode/datasource.go index fa6ff51cc9..bde301772c 100644 --- a/pkg/datasource/glassnode/datasource.go +++ b/pkg/datasource/glassnode/datasource.go @@ -18,19 +18,38 @@ func New(apiKey string) *DataSource { return &DataSource{client: client} } +func (d *DataSource) Query(ctx context.Context, category, metric, asset string, options QueryOptions) (glassnodeapi.DataSlice, error) { + req := glassnodeapi.Request{ + Client: d.client, + Asset: asset, + Since: options.Since, + Until: options.Until, + Interval: options.Interval, + + Category: category, + Metric: metric, + } + + resp, err := req.Do(ctx) + if err != nil { + return nil, err + } + + return glassnodeapi.DataSlice(resp), nil +} + // query last futures open interest // https://docs.glassnode.com/api/derivatives#futures-open-interest func (d *DataSource) QueryFuturesOpenInterest(ctx context.Context, currency string) (float64, error) { - req := glassnodeapi.DerivativesRequest{ - Client: d.client, - Asset: currency, - // 25 hours ago - Since: time.Now().Add(-25 * time.Hour).Unix(), - Interval: glassnodeapi.Interval24h, - Metric: "futures_open_interest_sum", + until := time.Now() + since := until.Add(-24 * time.Hour) + + options := QueryOptions{ + Since: &since, + Until: &until, } + resp, err := d.Query(ctx, "derivatives", "futures_open_interest_sum", currency, options) - resp, err := req.Do(ctx) if err != nil { return 0, err } @@ -41,16 +60,16 @@ func (d *DataSource) QueryFuturesOpenInterest(ctx context.Context, currency stri // query last market cap in usd // https://docs.glassnode.com/api/market#market-cap func (d *DataSource) QueryMarketCapInUSD(ctx context.Context, currency string) (float64, error) { - req := glassnodeapi.MarketRequest{ - Client: d.client, - Asset: currency, - // 25 hours ago - Since: time.Now().Add(-25 * time.Hour).Unix(), - Interval: glassnodeapi.Interval24h, - Metric: "marketcap_usd", + until := time.Now() + since := until.Add(-24 * time.Hour) + + options := QueryOptions{ + Since: &since, + Until: &until, } - resp, err := req.Do(ctx) + resp, err := d.Query(ctx, "market", "marketcap_usd", currency, options) + if err != nil { return 0, err } diff --git a/pkg/datasource/glassnode/glassnodeapi/addresses.go b/pkg/datasource/glassnode/glassnodeapi/addresses.go deleted file mode 100644 index 261880fe0d..0000000000 --- a/pkg/datasource/glassnode/glassnodeapi/addresses.go +++ /dev/null @@ -1,17 +0,0 @@ -package glassnodeapi - -import "github.com/c9s/requestgen" - -//go:generate requestgen -method GET -type AddressesRequest -url "/v1/metrics/addresses/:metric" -responseType Response -type AddressesRequest struct { - Client requestgen.AuthenticatedAPIClient - - Asset string `param:"a,required,query"` - Since int64 `param:"s,query"` - Until int64 `param:"u,query"` - Interval Interval `param:"i,query"` - Format Format `param:"f,query"` - TimestampFormat string `param:"timestamp_format,query"` - - Metric string `param:"metric,slug"` -} diff --git a/pkg/datasource/glassnode/glassnodeapi/addresses_request_requestgen.go b/pkg/datasource/glassnode/glassnodeapi/addresses_request_requestgen.go deleted file mode 100644 index 5af5f7724f..0000000000 --- a/pkg/datasource/glassnode/glassnodeapi/addresses_request_requestgen.go +++ /dev/null @@ -1,196 +0,0 @@ -// Code generated by "requestgen -method GET -type AddressesRequest -url /v1/metrics/addresses/:metric -responseType Response"; DO NOT EDIT. - -package glassnodeapi - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "regexp" -) - -func (a *AddressesRequest) SetAsset(Asset string) *AddressesRequest { - a.Asset = Asset - return a -} - -func (a *AddressesRequest) SetSince(Since int64) *AddressesRequest { - a.Since = Since - return a -} - -func (a *AddressesRequest) SetUntil(Until int64) *AddressesRequest { - a.Until = Until - return a -} - -func (a *AddressesRequest) SetInterval(Interval Interval) *AddressesRequest { - a.Interval = Interval - return a -} - -func (a *AddressesRequest) SetFormat(Format Format) *AddressesRequest { - a.Format = Format - return a -} - -func (a *AddressesRequest) SetTimestampFormat(TimestampFormat string) *AddressesRequest { - a.TimestampFormat = TimestampFormat - return a -} - -func (a *AddressesRequest) SetMetric(Metric string) *AddressesRequest { - a.Metric = Metric - return a -} - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (a *AddressesRequest) GetQueryParameters() (url.Values, error) { - var params = map[string]interface{}{} - // check Asset field -> json key a - Asset := a.Asset - - // TEMPLATE check-required - if len(Asset) == 0 { - return nil, fmt.Errorf("a is required, empty string given") - } - // END TEMPLATE check-required - - // assign parameter of Asset - params["a"] = Asset - // check Since field -> json key s - Since := a.Since - - // assign parameter of Since - params["s"] = Since - // check Until field -> json key u - Until := a.Until - - // assign parameter of Until - params["u"] = Until - // check Interval field -> json key i - Interval := a.Interval - - // assign parameter of Interval - params["i"] = Interval - // check Format field -> json key f - Format := a.Format - - // assign parameter of Format - params["f"] = Format - // check TimestampFormat field -> json key timestamp_format - TimestampFormat := a.TimestampFormat - - // assign parameter of TimestampFormat - params["timestamp_format"] = TimestampFormat - - query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParameters builds and checks the parameters and return the result in a map object -func (a *AddressesRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (a *AddressesRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := a.GetParameters() - if err != nil { - return query, err - } - - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParametersJSON converts the parameters from GetParameters into the JSON format -func (a *AddressesRequest) GetParametersJSON() ([]byte, error) { - params, err := a.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} - -// GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (a *AddressesRequest) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - // check Metric field -> json key metric - Metric := a.Metric - - // assign parameter of Metric - params["metric"] = Metric - - return params, nil -} - -func (a *AddressesRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) - } - - return url -} - -func (a *AddressesRequest) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := a.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) - } - - return slugs, nil -} - -func (a *AddressesRequest) Do(ctx context.Context) (Response, error) { - - // no body params - var params interface{} - query, err := a.GetQueryParameters() - if err != nil { - return nil, err - } - - apiURL := "/v1/metrics/addresses/:metric" - slugs, err := a.GetSlugsMap() - if err != nil { - return nil, err - } - - apiURL = a.applySlugsToUrl(apiURL, slugs) - - req, err := a.Client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := a.Client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse Response - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - return apiResponse, nil -} diff --git a/pkg/datasource/glassnode/glassnodeapi/blockchain.go b/pkg/datasource/glassnode/glassnodeapi/blockchain.go deleted file mode 100644 index a9370f725a..0000000000 --- a/pkg/datasource/glassnode/glassnodeapi/blockchain.go +++ /dev/null @@ -1,17 +0,0 @@ -package glassnodeapi - -import "github.com/c9s/requestgen" - -//go:generate requestgen -method GET -type BlockchainRequest -url "/v1/metrics/blockchain/:metric" -responseType Response -type BlockchainRequest struct { - Client requestgen.AuthenticatedAPIClient - - Asset string `param:"a,required,query"` - Since int64 `param:"s,query"` - Until int64 `param:"u,query"` - Interval Interval `param:"i,query"` - Format Format `param:"f,query"` - TimestampFormat string `param:"timestamp_format,query"` - - Metric string `param:"metric,slug"` -} diff --git a/pkg/datasource/glassnode/glassnodeapi/blockchain_request_requestgen.go b/pkg/datasource/glassnode/glassnodeapi/blockchain_request_requestgen.go deleted file mode 100644 index d9978d3d87..0000000000 --- a/pkg/datasource/glassnode/glassnodeapi/blockchain_request_requestgen.go +++ /dev/null @@ -1,196 +0,0 @@ -// Code generated by "requestgen -method GET -type BlockchainRequest -url /v1/metrics/blockchain/:metric -responseType Response"; DO NOT EDIT. - -package glassnodeapi - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "regexp" -) - -func (b *BlockchainRequest) SetAsset(Asset string) *BlockchainRequest { - b.Asset = Asset - return b -} - -func (b *BlockchainRequest) SetSince(Since int64) *BlockchainRequest { - b.Since = Since - return b -} - -func (b *BlockchainRequest) SetUntil(Until int64) *BlockchainRequest { - b.Until = Until - return b -} - -func (b *BlockchainRequest) SetInterval(Interval Interval) *BlockchainRequest { - b.Interval = Interval - return b -} - -func (b *BlockchainRequest) SetFormat(Format Format) *BlockchainRequest { - b.Format = Format - return b -} - -func (b *BlockchainRequest) SetTimestampFormat(TimestampFormat string) *BlockchainRequest { - b.TimestampFormat = TimestampFormat - return b -} - -func (b *BlockchainRequest) SetMetric(Metric string) *BlockchainRequest { - b.Metric = Metric - return b -} - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (b *BlockchainRequest) GetQueryParameters() (url.Values, error) { - var params = map[string]interface{}{} - // check Asset field -> json key a - Asset := b.Asset - - // TEMPLATE check-required - if len(Asset) == 0 { - return nil, fmt.Errorf("a is required, empty string given") - } - // END TEMPLATE check-required - - // assign parameter of Asset - params["a"] = Asset - // check Since field -> json key s - Since := b.Since - - // assign parameter of Since - params["s"] = Since - // check Until field -> json key u - Until := b.Until - - // assign parameter of Until - params["u"] = Until - // check Interval field -> json key i - Interval := b.Interval - - // assign parameter of Interval - params["i"] = Interval - // check Format field -> json key f - Format := b.Format - - // assign parameter of Format - params["f"] = Format - // check TimestampFormat field -> json key timestamp_format - TimestampFormat := b.TimestampFormat - - // assign parameter of TimestampFormat - params["timestamp_format"] = TimestampFormat - - query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParameters builds and checks the parameters and return the result in a map object -func (b *BlockchainRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (b *BlockchainRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := b.GetParameters() - if err != nil { - return query, err - } - - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParametersJSON converts the parameters from GetParameters into the JSON format -func (b *BlockchainRequest) GetParametersJSON() ([]byte, error) { - params, err := b.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} - -// GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (b *BlockchainRequest) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - // check Metric field -> json key metric - Metric := b.Metric - - // assign parameter of Metric - params["metric"] = Metric - - return params, nil -} - -func (b *BlockchainRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) - } - - return url -} - -func (b *BlockchainRequest) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := b.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) - } - - return slugs, nil -} - -func (b *BlockchainRequest) Do(ctx context.Context) (Response, error) { - - // no body params - var params interface{} - query, err := b.GetQueryParameters() - if err != nil { - return nil, err - } - - apiURL := "/v1/metrics/blockchain/:metric" - slugs, err := b.GetSlugsMap() - if err != nil { - return nil, err - } - - apiURL = b.applySlugsToUrl(apiURL, slugs) - - req, err := b.Client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := b.Client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse Response - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - return apiResponse, nil -} diff --git a/pkg/datasource/glassnode/glassnodeapi/client.go b/pkg/datasource/glassnode/glassnodeapi/client.go index 3370854d6f..fcc71d0b13 100644 --- a/pkg/datasource/glassnode/glassnodeapi/client.go +++ b/pkg/datasource/glassnode/glassnodeapi/client.go @@ -1,10 +1,7 @@ package glassnodeapi import ( - "bytes" "context" - "encoding/json" - "errors" "net/http" "net/url" "time" @@ -16,8 +13,7 @@ const defaultHTTPTimeout = time.Second * 15 const glassnodeBaseURL = "https://api.glassnode.com" type RestClient struct { - BaseURL *url.URL - Client *http.Client + requestgen.BaseAPIClient apiKey string } @@ -28,82 +24,23 @@ func NewRestClient() *RestClient { panic(err) } - client := &RestClient{ - BaseURL: u, - Client: &http.Client{ - Timeout: defaultHTTPTimeout, + return &RestClient{ + BaseAPIClient: requestgen.BaseAPIClient{ + BaseURL: u, + HttpClient: &http.Client{ + Timeout: defaultHTTPTimeout, + }, }, } - - return client } func (c *RestClient) Auth(apiKey string) { + // pragma: allowlist nextline secret c.apiKey = apiKey } -func (c *RestClient) NewRequest(ctx context.Context, method string, refURL string, params url.Values, payload interface{}) (*http.Request, error) { - body, err := castPayload(payload) - if err != nil { - return nil, err - } - - rel, err := url.Parse(refURL) - if err != nil { - return nil, err - } - - if params != nil { - rel.RawQuery = params.Encode() - } - - pathURL := c.BaseURL.ResolveReference(rel) - return http.NewRequestWithContext(ctx, method, pathURL.String(), bytes.NewReader(body)) -} - -func (c *RestClient) SendRequest(req *http.Request) (*requestgen.Response, error) { - resp, err := c.Client.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - response, err := requestgen.NewResponse(resp) - if err != nil { - return response, err - } - - // Check error, if there is an error, return the ErrorResponse struct type - if response.IsError() { - return response, errors.New(string(response.Body)) - } - - return response, nil -} - func (c *RestClient) NewAuthenticatedRequest(ctx context.Context, method, refURL string, params url.Values, payload interface{}) (*http.Request, error) { - rel, err := url.Parse(refURL) - if err != nil { - return nil, err - } - - if params != nil { - rel.RawQuery = params.Encode() - } - - pathURL := c.BaseURL.ResolveReference(rel) - - path := pathURL.Path - if rel.RawQuery != "" { - path += "?" + rel.RawQuery - } - - body, err := castPayload(payload) - if err != nil { - return nil, err - } - - req, err := http.NewRequestWithContext(ctx, method, pathURL.String(), bytes.NewReader(body)) + req, err := c.NewRequest(ctx, method, refURL, params, payload) if err != nil { return nil, err } @@ -111,30 +48,8 @@ func (c *RestClient) NewAuthenticatedRequest(ctx context.Context, method, refURL req.Header.Add("Content-Type", "application/json") req.Header.Add("Accept", "application/json") - // Build authentication headers - c.attachAuthHeaders(req, method, path, body) - return req, nil -} - -func (c *RestClient) attachAuthHeaders(req *http.Request, method string, path string, body []byte) { // Attch API Key to header. https://docs.glassnode.com/basic-api/api-key#usage req.Header.Add("X-Api-Key", c.apiKey) -} -func castPayload(payload interface{}) ([]byte, error) { - if payload != nil { - switch v := payload.(type) { - case string: - return []byte(v), nil - - case []byte: - return v, nil - - default: - body, err := json.Marshal(v) - return body, err - } - } - - return nil, nil + return req, nil } diff --git a/pkg/datasource/glassnode/glassnodeapi/defi.go b/pkg/datasource/glassnode/glassnodeapi/defi.go deleted file mode 100644 index 44534256c6..0000000000 --- a/pkg/datasource/glassnode/glassnodeapi/defi.go +++ /dev/null @@ -1,17 +0,0 @@ -package glassnodeapi - -import "github.com/c9s/requestgen" - -//go:generate requestgen -method GET -type DefiRequest -url "/v1/metrics/defi/:metric" -responseType Response -type DefiRequest struct { - Client requestgen.AuthenticatedAPIClient - - Asset string `param:"a,required,query"` - Since int64 `param:"s,query"` - Until int64 `param:"u,query"` - Interval Interval `param:"i,query"` - Format Format `param:"f,query"` - TimestampFormat string `param:"timestamp_format,query"` - - Metric string `param:"metric,slug"` -} diff --git a/pkg/datasource/glassnode/glassnodeapi/defi_request_requestgen.go b/pkg/datasource/glassnode/glassnodeapi/defi_request_requestgen.go deleted file mode 100644 index df39ba1fb8..0000000000 --- a/pkg/datasource/glassnode/glassnodeapi/defi_request_requestgen.go +++ /dev/null @@ -1,196 +0,0 @@ -// Code generated by "requestgen -method GET -type DefiRequest -url /v1/metrics/defi/:metric -responseType Response"; DO NOT EDIT. - -package glassnodeapi - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "regexp" -) - -func (d *DefiRequest) SetAsset(Asset string) *DefiRequest { - d.Asset = Asset - return d -} - -func (d *DefiRequest) SetSince(Since int64) *DefiRequest { - d.Since = Since - return d -} - -func (d *DefiRequest) SetUntil(Until int64) *DefiRequest { - d.Until = Until - return d -} - -func (d *DefiRequest) SetInterval(Interval Interval) *DefiRequest { - d.Interval = Interval - return d -} - -func (d *DefiRequest) SetFormat(Format Format) *DefiRequest { - d.Format = Format - return d -} - -func (d *DefiRequest) SetTimestampFormat(TimestampFormat string) *DefiRequest { - d.TimestampFormat = TimestampFormat - return d -} - -func (d *DefiRequest) SetMetric(Metric string) *DefiRequest { - d.Metric = Metric - return d -} - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (d *DefiRequest) GetQueryParameters() (url.Values, error) { - var params = map[string]interface{}{} - // check Asset field -> json key a - Asset := d.Asset - - // TEMPLATE check-required - if len(Asset) == 0 { - return nil, fmt.Errorf("a is required, empty string given") - } - // END TEMPLATE check-required - - // assign parameter of Asset - params["a"] = Asset - // check Since field -> json key s - Since := d.Since - - // assign parameter of Since - params["s"] = Since - // check Until field -> json key u - Until := d.Until - - // assign parameter of Until - params["u"] = Until - // check Interval field -> json key i - Interval := d.Interval - - // assign parameter of Interval - params["i"] = Interval - // check Format field -> json key f - Format := d.Format - - // assign parameter of Format - params["f"] = Format - // check TimestampFormat field -> json key timestamp_format - TimestampFormat := d.TimestampFormat - - // assign parameter of TimestampFormat - params["timestamp_format"] = TimestampFormat - - query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParameters builds and checks the parameters and return the result in a map object -func (d *DefiRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (d *DefiRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := d.GetParameters() - if err != nil { - return query, err - } - - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParametersJSON converts the parameters from GetParameters into the JSON format -func (d *DefiRequest) GetParametersJSON() ([]byte, error) { - params, err := d.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} - -// GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (d *DefiRequest) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - // check Metric field -> json key metric - Metric := d.Metric - - // assign parameter of Metric - params["metric"] = Metric - - return params, nil -} - -func (d *DefiRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) - } - - return url -} - -func (d *DefiRequest) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := d.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) - } - - return slugs, nil -} - -func (d *DefiRequest) Do(ctx context.Context) (Response, error) { - - // no body params - var params interface{} - query, err := d.GetQueryParameters() - if err != nil { - return nil, err - } - - apiURL := "/v1/metrics/defi/:metric" - slugs, err := d.GetSlugsMap() - if err != nil { - return nil, err - } - - apiURL = d.applySlugsToUrl(apiURL, slugs) - - req, err := d.Client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := d.Client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse Response - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - return apiResponse, nil -} diff --git a/pkg/datasource/glassnode/glassnodeapi/derivatives.go b/pkg/datasource/glassnode/glassnodeapi/derivatives.go deleted file mode 100644 index 00f0f0e69b..0000000000 --- a/pkg/datasource/glassnode/glassnodeapi/derivatives.go +++ /dev/null @@ -1,17 +0,0 @@ -package glassnodeapi - -import "github.com/c9s/requestgen" - -//go:generate requestgen -method GET -type DerivativesRequest -url "/v1/metrics/derivatives/:metric" -responseType Response -type DerivativesRequest struct { - Client requestgen.AuthenticatedAPIClient - - Asset string `param:"a,required,query"` - Since int64 `param:"s,query"` - Until int64 `param:"u,query"` - Interval Interval `param:"i,query"` - Format Format `param:"f,query"` - TimestampFormat string `param:"timestamp_format,query"` - - Metric string `param:"metric,slug"` -} diff --git a/pkg/datasource/glassnode/glassnodeapi/derivatives_request_requestgen.go b/pkg/datasource/glassnode/glassnodeapi/derivatives_request_requestgen.go deleted file mode 100644 index 75c861a5c1..0000000000 --- a/pkg/datasource/glassnode/glassnodeapi/derivatives_request_requestgen.go +++ /dev/null @@ -1,196 +0,0 @@ -// Code generated by "requestgen -method GET -type DerivativesRequest -url /v1/metrics/derivatives/:metric -responseType Response"; DO NOT EDIT. - -package glassnodeapi - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "regexp" -) - -func (d *DerivativesRequest) SetAsset(Asset string) *DerivativesRequest { - d.Asset = Asset - return d -} - -func (d *DerivativesRequest) SetSince(Since int64) *DerivativesRequest { - d.Since = Since - return d -} - -func (d *DerivativesRequest) SetUntil(Until int64) *DerivativesRequest { - d.Until = Until - return d -} - -func (d *DerivativesRequest) SetInterval(Interval Interval) *DerivativesRequest { - d.Interval = Interval - return d -} - -func (d *DerivativesRequest) SetFormat(Format Format) *DerivativesRequest { - d.Format = Format - return d -} - -func (d *DerivativesRequest) SetTimestampFormat(TimestampFormat string) *DerivativesRequest { - d.TimestampFormat = TimestampFormat - return d -} - -func (d *DerivativesRequest) SetMetric(Metric string) *DerivativesRequest { - d.Metric = Metric - return d -} - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (d *DerivativesRequest) GetQueryParameters() (url.Values, error) { - var params = map[string]interface{}{} - // check Asset field -> json key a - Asset := d.Asset - - // TEMPLATE check-required - if len(Asset) == 0 { - return nil, fmt.Errorf("a is required, empty string given") - } - // END TEMPLATE check-required - - // assign parameter of Asset - params["a"] = Asset - // check Since field -> json key s - Since := d.Since - - // assign parameter of Since - params["s"] = Since - // check Until field -> json key u - Until := d.Until - - // assign parameter of Until - params["u"] = Until - // check Interval field -> json key i - Interval := d.Interval - - // assign parameter of Interval - params["i"] = Interval - // check Format field -> json key f - Format := d.Format - - // assign parameter of Format - params["f"] = Format - // check TimestampFormat field -> json key timestamp_format - TimestampFormat := d.TimestampFormat - - // assign parameter of TimestampFormat - params["timestamp_format"] = TimestampFormat - - query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParameters builds and checks the parameters and return the result in a map object -func (d *DerivativesRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (d *DerivativesRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := d.GetParameters() - if err != nil { - return query, err - } - - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParametersJSON converts the parameters from GetParameters into the JSON format -func (d *DerivativesRequest) GetParametersJSON() ([]byte, error) { - params, err := d.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} - -// GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (d *DerivativesRequest) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - // check Metric field -> json key metric - Metric := d.Metric - - // assign parameter of Metric - params["metric"] = Metric - - return params, nil -} - -func (d *DerivativesRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) - } - - return url -} - -func (d *DerivativesRequest) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := d.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) - } - - return slugs, nil -} - -func (d *DerivativesRequest) Do(ctx context.Context) (Response, error) { - - // no body params - var params interface{} - query, err := d.GetQueryParameters() - if err != nil { - return nil, err - } - - apiURL := "/v1/metrics/derivatives/:metric" - slugs, err := d.GetSlugsMap() - if err != nil { - return nil, err - } - - apiURL = d.applySlugsToUrl(apiURL, slugs) - - req, err := d.Client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := d.Client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse Response - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - return apiResponse, nil -} diff --git a/pkg/datasource/glassnode/glassnodeapi/distribution.go b/pkg/datasource/glassnode/glassnodeapi/distribution.go deleted file mode 100644 index 35b47736d6..0000000000 --- a/pkg/datasource/glassnode/glassnodeapi/distribution.go +++ /dev/null @@ -1,17 +0,0 @@ -package glassnodeapi - -import "github.com/c9s/requestgen" - -//go:generate requestgen -method GET -type DistributionRequest -url "/v1/metrics/distribution/:metric" -responseType Response -type DistributionRequest struct { - Client requestgen.AuthenticatedAPIClient - - Asset string `param:"a,required,query"` - Since int64 `param:"s,query"` - Until int64 `param:"u,query"` - Interval Interval `param:"i,query"` - Format Format `param:"f,query"` - TimestampFormat string `param:"timestamp_format,query"` - - Metric string `param:"metric,slug"` -} diff --git a/pkg/datasource/glassnode/glassnodeapi/distribution_request_requestgen.go b/pkg/datasource/glassnode/glassnodeapi/distribution_request_requestgen.go deleted file mode 100644 index aa45e22cef..0000000000 --- a/pkg/datasource/glassnode/glassnodeapi/distribution_request_requestgen.go +++ /dev/null @@ -1,196 +0,0 @@ -// Code generated by "requestgen -method GET -type DistributionRequest -url /v1/metrics/distribution/:metric -responseType Response"; DO NOT EDIT. - -package glassnodeapi - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "regexp" -) - -func (d *DistributionRequest) SetAsset(Asset string) *DistributionRequest { - d.Asset = Asset - return d -} - -func (d *DistributionRequest) SetSince(Since int64) *DistributionRequest { - d.Since = Since - return d -} - -func (d *DistributionRequest) SetUntil(Until int64) *DistributionRequest { - d.Until = Until - return d -} - -func (d *DistributionRequest) SetInterval(Interval Interval) *DistributionRequest { - d.Interval = Interval - return d -} - -func (d *DistributionRequest) SetFormat(Format Format) *DistributionRequest { - d.Format = Format - return d -} - -func (d *DistributionRequest) SetTimestampFormat(TimestampFormat string) *DistributionRequest { - d.TimestampFormat = TimestampFormat - return d -} - -func (d *DistributionRequest) SetMetric(Metric string) *DistributionRequest { - d.Metric = Metric - return d -} - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (d *DistributionRequest) GetQueryParameters() (url.Values, error) { - var params = map[string]interface{}{} - // check Asset field -> json key a - Asset := d.Asset - - // TEMPLATE check-required - if len(Asset) == 0 { - return nil, fmt.Errorf("a is required, empty string given") - } - // END TEMPLATE check-required - - // assign parameter of Asset - params["a"] = Asset - // check Since field -> json key s - Since := d.Since - - // assign parameter of Since - params["s"] = Since - // check Until field -> json key u - Until := d.Until - - // assign parameter of Until - params["u"] = Until - // check Interval field -> json key i - Interval := d.Interval - - // assign parameter of Interval - params["i"] = Interval - // check Format field -> json key f - Format := d.Format - - // assign parameter of Format - params["f"] = Format - // check TimestampFormat field -> json key timestamp_format - TimestampFormat := d.TimestampFormat - - // assign parameter of TimestampFormat - params["timestamp_format"] = TimestampFormat - - query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParameters builds and checks the parameters and return the result in a map object -func (d *DistributionRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (d *DistributionRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := d.GetParameters() - if err != nil { - return query, err - } - - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParametersJSON converts the parameters from GetParameters into the JSON format -func (d *DistributionRequest) GetParametersJSON() ([]byte, error) { - params, err := d.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} - -// GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (d *DistributionRequest) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - // check Metric field -> json key metric - Metric := d.Metric - - // assign parameter of Metric - params["metric"] = Metric - - return params, nil -} - -func (d *DistributionRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) - } - - return url -} - -func (d *DistributionRequest) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := d.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) - } - - return slugs, nil -} - -func (d *DistributionRequest) Do(ctx context.Context) (Response, error) { - - // no body params - var params interface{} - query, err := d.GetQueryParameters() - if err != nil { - return nil, err - } - - apiURL := "/v1/metrics/distribution/:metric" - slugs, err := d.GetSlugsMap() - if err != nil { - return nil, err - } - - apiURL = d.applySlugsToUrl(apiURL, slugs) - - req, err := d.Client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := d.Client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse Response - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - return apiResponse, nil -} diff --git a/pkg/datasource/glassnode/glassnodeapi/entities.go b/pkg/datasource/glassnode/glassnodeapi/entities.go deleted file mode 100644 index 5e32dad26a..0000000000 --- a/pkg/datasource/glassnode/glassnodeapi/entities.go +++ /dev/null @@ -1,17 +0,0 @@ -package glassnodeapi - -import "github.com/c9s/requestgen" - -//go:generate requestgen -method GET -type EntitiesRequest -url "/v1/metrics/entities/:metric" -responseType Response -type EntitiesRequest struct { - Client requestgen.AuthenticatedAPIClient - - Asset string `param:"a,required,query"` - Since int64 `param:"s,query"` - Until int64 `param:"u,query"` - Interval Interval `param:"i,query"` - Format Format `param:"f,query"` - TimestampFormat string `param:"timestamp_format,query"` - - Metric string `param:"metric,slug"` -} diff --git a/pkg/datasource/glassnode/glassnodeapi/entities_request_requestgen.go b/pkg/datasource/glassnode/glassnodeapi/entities_request_requestgen.go deleted file mode 100644 index 3fd3e39297..0000000000 --- a/pkg/datasource/glassnode/glassnodeapi/entities_request_requestgen.go +++ /dev/null @@ -1,196 +0,0 @@ -// Code generated by "requestgen -method GET -type EntitiesRequest -url /v1/metrics/entities/:metric -responseType Response"; DO NOT EDIT. - -package glassnodeapi - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "regexp" -) - -func (e *EntitiesRequest) SetAsset(Asset string) *EntitiesRequest { - e.Asset = Asset - return e -} - -func (e *EntitiesRequest) SetSince(Since int64) *EntitiesRequest { - e.Since = Since - return e -} - -func (e *EntitiesRequest) SetUntil(Until int64) *EntitiesRequest { - e.Until = Until - return e -} - -func (e *EntitiesRequest) SetInterval(Interval Interval) *EntitiesRequest { - e.Interval = Interval - return e -} - -func (e *EntitiesRequest) SetFormat(Format Format) *EntitiesRequest { - e.Format = Format - return e -} - -func (e *EntitiesRequest) SetTimestampFormat(TimestampFormat string) *EntitiesRequest { - e.TimestampFormat = TimestampFormat - return e -} - -func (e *EntitiesRequest) SetMetric(Metric string) *EntitiesRequest { - e.Metric = Metric - return e -} - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (e *EntitiesRequest) GetQueryParameters() (url.Values, error) { - var params = map[string]interface{}{} - // check Asset field -> json key a - Asset := e.Asset - - // TEMPLATE check-required - if len(Asset) == 0 { - return nil, fmt.Errorf("a is required, empty string given") - } - // END TEMPLATE check-required - - // assign parameter of Asset - params["a"] = Asset - // check Since field -> json key s - Since := e.Since - - // assign parameter of Since - params["s"] = Since - // check Until field -> json key u - Until := e.Until - - // assign parameter of Until - params["u"] = Until - // check Interval field -> json key i - Interval := e.Interval - - // assign parameter of Interval - params["i"] = Interval - // check Format field -> json key f - Format := e.Format - - // assign parameter of Format - params["f"] = Format - // check TimestampFormat field -> json key timestamp_format - TimestampFormat := e.TimestampFormat - - // assign parameter of TimestampFormat - params["timestamp_format"] = TimestampFormat - - query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParameters builds and checks the parameters and return the result in a map object -func (e *EntitiesRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (e *EntitiesRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := e.GetParameters() - if err != nil { - return query, err - } - - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParametersJSON converts the parameters from GetParameters into the JSON format -func (e *EntitiesRequest) GetParametersJSON() ([]byte, error) { - params, err := e.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} - -// GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (e *EntitiesRequest) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - // check Metric field -> json key metric - Metric := e.Metric - - // assign parameter of Metric - params["metric"] = Metric - - return params, nil -} - -func (e *EntitiesRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) - } - - return url -} - -func (e *EntitiesRequest) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := e.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) - } - - return slugs, nil -} - -func (e *EntitiesRequest) Do(ctx context.Context) (Response, error) { - - // no body params - var params interface{} - query, err := e.GetQueryParameters() - if err != nil { - return nil, err - } - - apiURL := "/v1/metrics/entities/:metric" - slugs, err := e.GetSlugsMap() - if err != nil { - return nil, err - } - - apiURL = e.applySlugsToUrl(apiURL, slugs) - - req, err := e.Client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := e.Client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse Response - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - return apiResponse, nil -} diff --git a/pkg/datasource/glassnode/glassnodeapi/eth2.go b/pkg/datasource/glassnode/glassnodeapi/eth2.go deleted file mode 100644 index 241ab4d170..0000000000 --- a/pkg/datasource/glassnode/glassnodeapi/eth2.go +++ /dev/null @@ -1,17 +0,0 @@ -package glassnodeapi - -import "github.com/c9s/requestgen" - -//go:generate requestgen -method GET -type ETH2Request -url "/v1/metrics/eth2/:metric" -responseType Response -type ETH2Request struct { - Client requestgen.AuthenticatedAPIClient - - Asset string `param:"a,required,query"` - Since int64 `param:"s,query"` - Until int64 `param:"u,query"` - Interval Interval `param:"i,query"` - Format Format `param:"f,query"` - TimestampFormat string `param:"timestamp_format,query"` - - Metric string `param:"metric,slug"` -} diff --git a/pkg/datasource/glassnode/glassnodeapi/eth_2_request_requestgen.go b/pkg/datasource/glassnode/glassnodeapi/eth_2_request_requestgen.go deleted file mode 100644 index 3d9502a330..0000000000 --- a/pkg/datasource/glassnode/glassnodeapi/eth_2_request_requestgen.go +++ /dev/null @@ -1,196 +0,0 @@ -// Code generated by "requestgen -method GET -type ETH2Request -url /v1/metrics/eth2/:metric -responseType Response"; DO NOT EDIT. - -package glassnodeapi - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "regexp" -) - -func (e *ETH2Request) SetAsset(Asset string) *ETH2Request { - e.Asset = Asset - return e -} - -func (e *ETH2Request) SetSince(Since int64) *ETH2Request { - e.Since = Since - return e -} - -func (e *ETH2Request) SetUntil(Until int64) *ETH2Request { - e.Until = Until - return e -} - -func (e *ETH2Request) SetInterval(Interval Interval) *ETH2Request { - e.Interval = Interval - return e -} - -func (e *ETH2Request) SetFormat(Format Format) *ETH2Request { - e.Format = Format - return e -} - -func (e *ETH2Request) SetTimestampFormat(TimestampFormat string) *ETH2Request { - e.TimestampFormat = TimestampFormat - return e -} - -func (e *ETH2Request) SetMetric(Metric string) *ETH2Request { - e.Metric = Metric - return e -} - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (e *ETH2Request) GetQueryParameters() (url.Values, error) { - var params = map[string]interface{}{} - // check Asset field -> json key a - Asset := e.Asset - - // TEMPLATE check-required - if len(Asset) == 0 { - return nil, fmt.Errorf("a is required, empty string given") - } - // END TEMPLATE check-required - - // assign parameter of Asset - params["a"] = Asset - // check Since field -> json key s - Since := e.Since - - // assign parameter of Since - params["s"] = Since - // check Until field -> json key u - Until := e.Until - - // assign parameter of Until - params["u"] = Until - // check Interval field -> json key i - Interval := e.Interval - - // assign parameter of Interval - params["i"] = Interval - // check Format field -> json key f - Format := e.Format - - // assign parameter of Format - params["f"] = Format - // check TimestampFormat field -> json key timestamp_format - TimestampFormat := e.TimestampFormat - - // assign parameter of TimestampFormat - params["timestamp_format"] = TimestampFormat - - query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParameters builds and checks the parameters and return the result in a map object -func (e *ETH2Request) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (e *ETH2Request) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := e.GetParameters() - if err != nil { - return query, err - } - - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParametersJSON converts the parameters from GetParameters into the JSON format -func (e *ETH2Request) GetParametersJSON() ([]byte, error) { - params, err := e.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} - -// GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (e *ETH2Request) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - // check Metric field -> json key metric - Metric := e.Metric - - // assign parameter of Metric - params["metric"] = Metric - - return params, nil -} - -func (e *ETH2Request) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) - } - - return url -} - -func (e *ETH2Request) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := e.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) - } - - return slugs, nil -} - -func (e *ETH2Request) Do(ctx context.Context) (Response, error) { - - // no body params - var params interface{} - query, err := e.GetQueryParameters() - if err != nil { - return nil, err - } - - apiURL := "/v1/metrics/eth2/:metric" - slugs, err := e.GetSlugsMap() - if err != nil { - return nil, err - } - - apiURL = e.applySlugsToUrl(apiURL, slugs) - - req, err := e.Client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := e.Client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse Response - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - return apiResponse, nil -} diff --git a/pkg/datasource/glassnode/glassnodeapi/fees.go b/pkg/datasource/glassnode/glassnodeapi/fees.go deleted file mode 100644 index 449b567765..0000000000 --- a/pkg/datasource/glassnode/glassnodeapi/fees.go +++ /dev/null @@ -1,17 +0,0 @@ -package glassnodeapi - -import "github.com/c9s/requestgen" - -//go:generate requestgen -method GET -type FeesRequest -url "/v1/metrics/fees/:metric" -responseType Response -type FeesRequest struct { - Client requestgen.AuthenticatedAPIClient - - Asset string `param:"a,required,query"` - Since int64 `param:"s,query"` - Until int64 `param:"u,query"` - Interval Interval `param:"i,query"` - Format Format `param:"f,query"` - TimestampFormat string `param:"timestamp_format,query"` - - Metric string `param:"metric,slug"` -} diff --git a/pkg/datasource/glassnode/glassnodeapi/fees_request_requestgen.go b/pkg/datasource/glassnode/glassnodeapi/fees_request_requestgen.go deleted file mode 100644 index e5071cc5e4..0000000000 --- a/pkg/datasource/glassnode/glassnodeapi/fees_request_requestgen.go +++ /dev/null @@ -1,196 +0,0 @@ -// Code generated by "requestgen -method GET -type FeesRequest -url /v1/metrics/fees/:metric -responseType Response"; DO NOT EDIT. - -package glassnodeapi - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "regexp" -) - -func (f *FeesRequest) SetAsset(Asset string) *FeesRequest { - f.Asset = Asset - return f -} - -func (f *FeesRequest) SetSince(Since int64) *FeesRequest { - f.Since = Since - return f -} - -func (f *FeesRequest) SetUntil(Until int64) *FeesRequest { - f.Until = Until - return f -} - -func (f *FeesRequest) SetInterval(Interval Interval) *FeesRequest { - f.Interval = Interval - return f -} - -func (f *FeesRequest) SetFormat(Format Format) *FeesRequest { - f.Format = Format - return f -} - -func (f *FeesRequest) SetTimestampFormat(TimestampFormat string) *FeesRequest { - f.TimestampFormat = TimestampFormat - return f -} - -func (f *FeesRequest) SetMetric(Metric string) *FeesRequest { - f.Metric = Metric - return f -} - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (f *FeesRequest) GetQueryParameters() (url.Values, error) { - var params = map[string]interface{}{} - // check Asset field -> json key a - Asset := f.Asset - - // TEMPLATE check-required - if len(Asset) == 0 { - return nil, fmt.Errorf("a is required, empty string given") - } - // END TEMPLATE check-required - - // assign parameter of Asset - params["a"] = Asset - // check Since field -> json key s - Since := f.Since - - // assign parameter of Since - params["s"] = Since - // check Until field -> json key u - Until := f.Until - - // assign parameter of Until - params["u"] = Until - // check Interval field -> json key i - Interval := f.Interval - - // assign parameter of Interval - params["i"] = Interval - // check Format field -> json key f - Format := f.Format - - // assign parameter of Format - params["f"] = Format - // check TimestampFormat field -> json key timestamp_format - TimestampFormat := f.TimestampFormat - - // assign parameter of TimestampFormat - params["timestamp_format"] = TimestampFormat - - query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParameters builds and checks the parameters and return the result in a map object -func (f *FeesRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (f *FeesRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := f.GetParameters() - if err != nil { - return query, err - } - - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParametersJSON converts the parameters from GetParameters into the JSON format -func (f *FeesRequest) GetParametersJSON() ([]byte, error) { - params, err := f.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} - -// GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (f *FeesRequest) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - // check Metric field -> json key metric - Metric := f.Metric - - // assign parameter of Metric - params["metric"] = Metric - - return params, nil -} - -func (f *FeesRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) - } - - return url -} - -func (f *FeesRequest) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := f.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) - } - - return slugs, nil -} - -func (f *FeesRequest) Do(ctx context.Context) (Response, error) { - - // no body params - var params interface{} - query, err := f.GetQueryParameters() - if err != nil { - return nil, err - } - - apiURL := "/v1/metrics/fees/:metric" - slugs, err := f.GetSlugsMap() - if err != nil { - return nil, err - } - - apiURL = f.applySlugsToUrl(apiURL, slugs) - - req, err := f.Client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := f.Client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse Response - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - return apiResponse, nil -} diff --git a/pkg/datasource/glassnode/glassnodeapi/indicators.go b/pkg/datasource/glassnode/glassnodeapi/indicators.go deleted file mode 100644 index d8382545e3..0000000000 --- a/pkg/datasource/glassnode/glassnodeapi/indicators.go +++ /dev/null @@ -1,17 +0,0 @@ -package glassnodeapi - -import "github.com/c9s/requestgen" - -//go:generate requestgen -method GET -type IndicatorsRequest -url "/v1/metrics/indicators/:metric" -responseType Response -type IndicatorsRequest struct { - Client requestgen.AuthenticatedAPIClient - - Asset string `param:"a,required,query"` - Since int64 `param:"s,query"` - Until int64 `param:"u,query"` - Interval Interval `param:"i,query"` - Format Format `param:"f,query"` - TimestampFormat string `param:"timestamp_format,query"` - - Metric string `param:"metric,slug"` -} diff --git a/pkg/datasource/glassnode/glassnodeapi/indicators_request_requestgen.go b/pkg/datasource/glassnode/glassnodeapi/indicators_request_requestgen.go deleted file mode 100644 index 0df98c8036..0000000000 --- a/pkg/datasource/glassnode/glassnodeapi/indicators_request_requestgen.go +++ /dev/null @@ -1,196 +0,0 @@ -// Code generated by "requestgen -method GET -type IndicatorsRequest -url /v1/metrics/indicators/:metric -responseType Response"; DO NOT EDIT. - -package glassnodeapi - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "regexp" -) - -func (i *IndicatorsRequest) SetAsset(Asset string) *IndicatorsRequest { - i.Asset = Asset - return i -} - -func (i *IndicatorsRequest) SetSince(Since int64) *IndicatorsRequest { - i.Since = Since - return i -} - -func (i *IndicatorsRequest) SetUntil(Until int64) *IndicatorsRequest { - i.Until = Until - return i -} - -func (i *IndicatorsRequest) SetInterval(Interval Interval) *IndicatorsRequest { - i.Interval = Interval - return i -} - -func (i *IndicatorsRequest) SetFormat(Format Format) *IndicatorsRequest { - i.Format = Format - return i -} - -func (i *IndicatorsRequest) SetTimestampFormat(TimestampFormat string) *IndicatorsRequest { - i.TimestampFormat = TimestampFormat - return i -} - -func (i *IndicatorsRequest) SetMetric(Metric string) *IndicatorsRequest { - i.Metric = Metric - return i -} - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (i *IndicatorsRequest) GetQueryParameters() (url.Values, error) { - var params = map[string]interface{}{} - // check Asset field -> json key a - Asset := i.Asset - - // TEMPLATE check-required - if len(Asset) == 0 { - return nil, fmt.Errorf("a is required, empty string given") - } - // END TEMPLATE check-required - - // assign parameter of Asset - params["a"] = Asset - // check Since field -> json key s - Since := i.Since - - // assign parameter of Since - params["s"] = Since - // check Until field -> json key u - Until := i.Until - - // assign parameter of Until - params["u"] = Until - // check Interval field -> json key i - Interval := i.Interval - - // assign parameter of Interval - params["i"] = Interval - // check Format field -> json key f - Format := i.Format - - // assign parameter of Format - params["f"] = Format - // check TimestampFormat field -> json key timestamp_format - TimestampFormat := i.TimestampFormat - - // assign parameter of TimestampFormat - params["timestamp_format"] = TimestampFormat - - query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParameters builds and checks the parameters and return the result in a map object -func (i *IndicatorsRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (i *IndicatorsRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := i.GetParameters() - if err != nil { - return query, err - } - - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParametersJSON converts the parameters from GetParameters into the JSON format -func (i *IndicatorsRequest) GetParametersJSON() ([]byte, error) { - params, err := i.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} - -// GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (i *IndicatorsRequest) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - // check Metric field -> json key metric - Metric := i.Metric - - // assign parameter of Metric - params["metric"] = Metric - - return params, nil -} - -func (i *IndicatorsRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) - } - - return url -} - -func (i *IndicatorsRequest) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := i.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) - } - - return slugs, nil -} - -func (i *IndicatorsRequest) Do(ctx context.Context) (Response, error) { - - // no body params - var params interface{} - query, err := i.GetQueryParameters() - if err != nil { - return nil, err - } - - apiURL := "/v1/metrics/indicators/:metric" - slugs, err := i.GetSlugsMap() - if err != nil { - return nil, err - } - - apiURL = i.applySlugsToUrl(apiURL, slugs) - - req, err := i.Client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := i.Client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse Response - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - return apiResponse, nil -} diff --git a/pkg/datasource/glassnode/glassnodeapi/institutions.go b/pkg/datasource/glassnode/glassnodeapi/institutions.go deleted file mode 100644 index 671c054594..0000000000 --- a/pkg/datasource/glassnode/glassnodeapi/institutions.go +++ /dev/null @@ -1,17 +0,0 @@ -package glassnodeapi - -import "github.com/c9s/requestgen" - -//go:generate requestgen -method GET -type InstitutionsRequest -url "/v1/metrics/institutions/:metric" -responseType Response -type InstitutionsRequest struct { - Client requestgen.AuthenticatedAPIClient - - Asset string `param:"a,required,query"` - Since int64 `param:"s,query"` - Until int64 `param:"u,query"` - Interval Interval `param:"i,query"` - Format Format `param:"f,query"` - TimestampFormat string `param:"timestamp_format,query"` - - Metric string `param:"metric,slug"` -} diff --git a/pkg/datasource/glassnode/glassnodeapi/institutions_request_requestgen.go b/pkg/datasource/glassnode/glassnodeapi/institutions_request_requestgen.go deleted file mode 100644 index b5ef3ff2b8..0000000000 --- a/pkg/datasource/glassnode/glassnodeapi/institutions_request_requestgen.go +++ /dev/null @@ -1,196 +0,0 @@ -// Code generated by "requestgen -method GET -type InstitutionsRequest -url /v1/metrics/institutions/:metric -responseType Response"; DO NOT EDIT. - -package glassnodeapi - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "regexp" -) - -func (i *InstitutionsRequest) SetAsset(Asset string) *InstitutionsRequest { - i.Asset = Asset - return i -} - -func (i *InstitutionsRequest) SetSince(Since int64) *InstitutionsRequest { - i.Since = Since - return i -} - -func (i *InstitutionsRequest) SetUntil(Until int64) *InstitutionsRequest { - i.Until = Until - return i -} - -func (i *InstitutionsRequest) SetInterval(Interval Interval) *InstitutionsRequest { - i.Interval = Interval - return i -} - -func (i *InstitutionsRequest) SetFormat(Format Format) *InstitutionsRequest { - i.Format = Format - return i -} - -func (i *InstitutionsRequest) SetTimestampFormat(TimestampFormat string) *InstitutionsRequest { - i.TimestampFormat = TimestampFormat - return i -} - -func (i *InstitutionsRequest) SetMetric(Metric string) *InstitutionsRequest { - i.Metric = Metric - return i -} - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (i *InstitutionsRequest) GetQueryParameters() (url.Values, error) { - var params = map[string]interface{}{} - // check Asset field -> json key a - Asset := i.Asset - - // TEMPLATE check-required - if len(Asset) == 0 { - return nil, fmt.Errorf("a is required, empty string given") - } - // END TEMPLATE check-required - - // assign parameter of Asset - params["a"] = Asset - // check Since field -> json key s - Since := i.Since - - // assign parameter of Since - params["s"] = Since - // check Until field -> json key u - Until := i.Until - - // assign parameter of Until - params["u"] = Until - // check Interval field -> json key i - Interval := i.Interval - - // assign parameter of Interval - params["i"] = Interval - // check Format field -> json key f - Format := i.Format - - // assign parameter of Format - params["f"] = Format - // check TimestampFormat field -> json key timestamp_format - TimestampFormat := i.TimestampFormat - - // assign parameter of TimestampFormat - params["timestamp_format"] = TimestampFormat - - query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParameters builds and checks the parameters and return the result in a map object -func (i *InstitutionsRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (i *InstitutionsRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := i.GetParameters() - if err != nil { - return query, err - } - - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParametersJSON converts the parameters from GetParameters into the JSON format -func (i *InstitutionsRequest) GetParametersJSON() ([]byte, error) { - params, err := i.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} - -// GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (i *InstitutionsRequest) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - // check Metric field -> json key metric - Metric := i.Metric - - // assign parameter of Metric - params["metric"] = Metric - - return params, nil -} - -func (i *InstitutionsRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) - } - - return url -} - -func (i *InstitutionsRequest) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := i.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) - } - - return slugs, nil -} - -func (i *InstitutionsRequest) Do(ctx context.Context) (Response, error) { - - // no body params - var params interface{} - query, err := i.GetQueryParameters() - if err != nil { - return nil, err - } - - apiURL := "/v1/metrics/institutions/:metric" - slugs, err := i.GetSlugsMap() - if err != nil { - return nil, err - } - - apiURL = i.applySlugsToUrl(apiURL, slugs) - - req, err := i.Client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := i.Client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse Response - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - return apiResponse, nil -} diff --git a/pkg/datasource/glassnode/glassnodeapi/lightning.go b/pkg/datasource/glassnode/glassnodeapi/lightning.go deleted file mode 100644 index d8040db5eb..0000000000 --- a/pkg/datasource/glassnode/glassnodeapi/lightning.go +++ /dev/null @@ -1,17 +0,0 @@ -package glassnodeapi - -import "github.com/c9s/requestgen" - -//go:generate requestgen -method GET -type LightningRequest -url "/v1/metrics/lightning/:metric" -responseType Response -type LightningRequest struct { - Client requestgen.AuthenticatedAPIClient - - Asset string `param:"a,required,query"` - Since int64 `param:"s,query"` - Until int64 `param:"u,query"` - Interval Interval `param:"i,query"` - Format Format `param:"f,query"` - TimestampFormat string `param:"timestamp_format,query"` - - Metric string `param:"metric,slug"` -} diff --git a/pkg/datasource/glassnode/glassnodeapi/lightning_request_requestgen.go b/pkg/datasource/glassnode/glassnodeapi/lightning_request_requestgen.go deleted file mode 100644 index 961866226a..0000000000 --- a/pkg/datasource/glassnode/glassnodeapi/lightning_request_requestgen.go +++ /dev/null @@ -1,196 +0,0 @@ -// Code generated by "requestgen -method GET -type LightningRequest -url /v1/metrics/lightning/:metric -responseType Response"; DO NOT EDIT. - -package glassnodeapi - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "regexp" -) - -func (l *LightningRequest) SetAsset(Asset string) *LightningRequest { - l.Asset = Asset - return l -} - -func (l *LightningRequest) SetSince(Since int64) *LightningRequest { - l.Since = Since - return l -} - -func (l *LightningRequest) SetUntil(Until int64) *LightningRequest { - l.Until = Until - return l -} - -func (l *LightningRequest) SetInterval(Interval Interval) *LightningRequest { - l.Interval = Interval - return l -} - -func (l *LightningRequest) SetFormat(Format Format) *LightningRequest { - l.Format = Format - return l -} - -func (l *LightningRequest) SetTimestampFormat(TimestampFormat string) *LightningRequest { - l.TimestampFormat = TimestampFormat - return l -} - -func (l *LightningRequest) SetMetric(Metric string) *LightningRequest { - l.Metric = Metric - return l -} - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (l *LightningRequest) GetQueryParameters() (url.Values, error) { - var params = map[string]interface{}{} - // check Asset field -> json key a - Asset := l.Asset - - // TEMPLATE check-required - if len(Asset) == 0 { - return nil, fmt.Errorf("a is required, empty string given") - } - // END TEMPLATE check-required - - // assign parameter of Asset - params["a"] = Asset - // check Since field -> json key s - Since := l.Since - - // assign parameter of Since - params["s"] = Since - // check Until field -> json key u - Until := l.Until - - // assign parameter of Until - params["u"] = Until - // check Interval field -> json key i - Interval := l.Interval - - // assign parameter of Interval - params["i"] = Interval - // check Format field -> json key f - Format := l.Format - - // assign parameter of Format - params["f"] = Format - // check TimestampFormat field -> json key timestamp_format - TimestampFormat := l.TimestampFormat - - // assign parameter of TimestampFormat - params["timestamp_format"] = TimestampFormat - - query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParameters builds and checks the parameters and return the result in a map object -func (l *LightningRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (l *LightningRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := l.GetParameters() - if err != nil { - return query, err - } - - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParametersJSON converts the parameters from GetParameters into the JSON format -func (l *LightningRequest) GetParametersJSON() ([]byte, error) { - params, err := l.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} - -// GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (l *LightningRequest) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - // check Metric field -> json key metric - Metric := l.Metric - - // assign parameter of Metric - params["metric"] = Metric - - return params, nil -} - -func (l *LightningRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) - } - - return url -} - -func (l *LightningRequest) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := l.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) - } - - return slugs, nil -} - -func (l *LightningRequest) Do(ctx context.Context) (Response, error) { - - // no body params - var params interface{} - query, err := l.GetQueryParameters() - if err != nil { - return nil, err - } - - apiURL := "/v1/metrics/lightning/:metric" - slugs, err := l.GetSlugsMap() - if err != nil { - return nil, err - } - - apiURL = l.applySlugsToUrl(apiURL, slugs) - - req, err := l.Client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := l.Client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse Response - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - return apiResponse, nil -} diff --git a/pkg/datasource/glassnode/glassnodeapi/market.go b/pkg/datasource/glassnode/glassnodeapi/market.go deleted file mode 100644 index aeefd0d383..0000000000 --- a/pkg/datasource/glassnode/glassnodeapi/market.go +++ /dev/null @@ -1,17 +0,0 @@ -package glassnodeapi - -import "github.com/c9s/requestgen" - -//go:generate requestgen -method GET -type MarketRequest -url "/v1/metrics/market/:metric" -responseType Response -type MarketRequest struct { - Client requestgen.AuthenticatedAPIClient - - Asset string `param:"a,required,query"` - Since int64 `param:"s,query"` - Until int64 `param:"u,query"` - Interval Interval `param:"i,query"` - Format Format `param:"f,query"` - TimestampFormat string `param:"timestamp_format,query"` - - Metric string `param:"metric,slug"` -} diff --git a/pkg/datasource/glassnode/glassnodeapi/market_request_requestgen.go b/pkg/datasource/glassnode/glassnodeapi/market_request_requestgen.go deleted file mode 100644 index da35ff3d80..0000000000 --- a/pkg/datasource/glassnode/glassnodeapi/market_request_requestgen.go +++ /dev/null @@ -1,196 +0,0 @@ -// Code generated by "requestgen -method GET -type MarketRequest -url /v1/metrics/market/:metric -responseType Response"; DO NOT EDIT. - -package glassnodeapi - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "regexp" -) - -func (m *MarketRequest) SetAsset(Asset string) *MarketRequest { - m.Asset = Asset - return m -} - -func (m *MarketRequest) SetSince(Since int64) *MarketRequest { - m.Since = Since - return m -} - -func (m *MarketRequest) SetUntil(Until int64) *MarketRequest { - m.Until = Until - return m -} - -func (m *MarketRequest) SetInterval(Interval Interval) *MarketRequest { - m.Interval = Interval - return m -} - -func (m *MarketRequest) SetFormat(Format Format) *MarketRequest { - m.Format = Format - return m -} - -func (m *MarketRequest) SetTimestampFormat(TimestampFormat string) *MarketRequest { - m.TimestampFormat = TimestampFormat - return m -} - -func (m *MarketRequest) SetMetric(Metric string) *MarketRequest { - m.Metric = Metric - return m -} - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (m *MarketRequest) GetQueryParameters() (url.Values, error) { - var params = map[string]interface{}{} - // check Asset field -> json key a - Asset := m.Asset - - // TEMPLATE check-required - if len(Asset) == 0 { - return nil, fmt.Errorf("a is required, empty string given") - } - // END TEMPLATE check-required - - // assign parameter of Asset - params["a"] = Asset - // check Since field -> json key s - Since := m.Since - - // assign parameter of Since - params["s"] = Since - // check Until field -> json key u - Until := m.Until - - // assign parameter of Until - params["u"] = Until - // check Interval field -> json key i - Interval := m.Interval - - // assign parameter of Interval - params["i"] = Interval - // check Format field -> json key f - Format := m.Format - - // assign parameter of Format - params["f"] = Format - // check TimestampFormat field -> json key timestamp_format - TimestampFormat := m.TimestampFormat - - // assign parameter of TimestampFormat - params["timestamp_format"] = TimestampFormat - - query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParameters builds and checks the parameters and return the result in a map object -func (m *MarketRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (m *MarketRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := m.GetParameters() - if err != nil { - return query, err - } - - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParametersJSON converts the parameters from GetParameters into the JSON format -func (m *MarketRequest) GetParametersJSON() ([]byte, error) { - params, err := m.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} - -// GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (m *MarketRequest) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - // check Metric field -> json key metric - Metric := m.Metric - - // assign parameter of Metric - params["metric"] = Metric - - return params, nil -} - -func (m *MarketRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) - } - - return url -} - -func (m *MarketRequest) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := m.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) - } - - return slugs, nil -} - -func (m *MarketRequest) Do(ctx context.Context) (Response, error) { - - // no body params - var params interface{} - query, err := m.GetQueryParameters() - if err != nil { - return nil, err - } - - apiURL := "/v1/metrics/market/:metric" - slugs, err := m.GetSlugsMap() - if err != nil { - return nil, err - } - - apiURL = m.applySlugsToUrl(apiURL, slugs) - - req, err := m.Client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := m.Client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse Response - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - return apiResponse, nil -} diff --git a/pkg/datasource/glassnode/glassnodeapi/mempool.go b/pkg/datasource/glassnode/glassnodeapi/mempool.go deleted file mode 100644 index 059c1c67b2..0000000000 --- a/pkg/datasource/glassnode/glassnodeapi/mempool.go +++ /dev/null @@ -1,17 +0,0 @@ -package glassnodeapi - -import "github.com/c9s/requestgen" - -//go:generate requestgen -method GET -type MempoolRequest -url "/v1/metrics/mempool/:metric" -responseType Response -type MempoolRequest struct { - Client requestgen.AuthenticatedAPIClient - - Asset string `param:"a,required,query"` - Since int64 `param:"s,query"` - Until int64 `param:"u,query"` - Interval Interval `param:"i,query"` - Format Format `param:"f,query"` - TimestampFormat string `param:"timestamp_format,query"` - - Metric string `param:"metric,slug"` -} diff --git a/pkg/datasource/glassnode/glassnodeapi/mempool_request_requestgen.go b/pkg/datasource/glassnode/glassnodeapi/mempool_request_requestgen.go deleted file mode 100644 index 1b49b93f89..0000000000 --- a/pkg/datasource/glassnode/glassnodeapi/mempool_request_requestgen.go +++ /dev/null @@ -1,196 +0,0 @@ -// Code generated by "requestgen -method GET -type MempoolRequest -url /v1/metrics/mempool/:metric -responseType Response"; DO NOT EDIT. - -package glassnodeapi - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "regexp" -) - -func (m *MempoolRequest) SetAsset(Asset string) *MempoolRequest { - m.Asset = Asset - return m -} - -func (m *MempoolRequest) SetSince(Since int64) *MempoolRequest { - m.Since = Since - return m -} - -func (m *MempoolRequest) SetUntil(Until int64) *MempoolRequest { - m.Until = Until - return m -} - -func (m *MempoolRequest) SetInterval(Interval Interval) *MempoolRequest { - m.Interval = Interval - return m -} - -func (m *MempoolRequest) SetFormat(Format Format) *MempoolRequest { - m.Format = Format - return m -} - -func (m *MempoolRequest) SetTimestampFormat(TimestampFormat string) *MempoolRequest { - m.TimestampFormat = TimestampFormat - return m -} - -func (m *MempoolRequest) SetMetric(Metric string) *MempoolRequest { - m.Metric = Metric - return m -} - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (m *MempoolRequest) GetQueryParameters() (url.Values, error) { - var params = map[string]interface{}{} - // check Asset field -> json key a - Asset := m.Asset - - // TEMPLATE check-required - if len(Asset) == 0 { - return nil, fmt.Errorf("a is required, empty string given") - } - // END TEMPLATE check-required - - // assign parameter of Asset - params["a"] = Asset - // check Since field -> json key s - Since := m.Since - - // assign parameter of Since - params["s"] = Since - // check Until field -> json key u - Until := m.Until - - // assign parameter of Until - params["u"] = Until - // check Interval field -> json key i - Interval := m.Interval - - // assign parameter of Interval - params["i"] = Interval - // check Format field -> json key f - Format := m.Format - - // assign parameter of Format - params["f"] = Format - // check TimestampFormat field -> json key timestamp_format - TimestampFormat := m.TimestampFormat - - // assign parameter of TimestampFormat - params["timestamp_format"] = TimestampFormat - - query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParameters builds and checks the parameters and return the result in a map object -func (m *MempoolRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (m *MempoolRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := m.GetParameters() - if err != nil { - return query, err - } - - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParametersJSON converts the parameters from GetParameters into the JSON format -func (m *MempoolRequest) GetParametersJSON() ([]byte, error) { - params, err := m.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} - -// GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (m *MempoolRequest) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - // check Metric field -> json key metric - Metric := m.Metric - - // assign parameter of Metric - params["metric"] = Metric - - return params, nil -} - -func (m *MempoolRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) - } - - return url -} - -func (m *MempoolRequest) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := m.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) - } - - return slugs, nil -} - -func (m *MempoolRequest) Do(ctx context.Context) (Response, error) { - - // no body params - var params interface{} - query, err := m.GetQueryParameters() - if err != nil { - return nil, err - } - - apiURL := "/v1/metrics/mempool/:metric" - slugs, err := m.GetSlugsMap() - if err != nil { - return nil, err - } - - apiURL = m.applySlugsToUrl(apiURL, slugs) - - req, err := m.Client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := m.Client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse Response - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - return apiResponse, nil -} diff --git a/pkg/datasource/glassnode/glassnodeapi/mining.go b/pkg/datasource/glassnode/glassnodeapi/mining.go deleted file mode 100644 index 44cda95a1d..0000000000 --- a/pkg/datasource/glassnode/glassnodeapi/mining.go +++ /dev/null @@ -1,17 +0,0 @@ -package glassnodeapi - -import "github.com/c9s/requestgen" - -//go:generate requestgen -method GET -type MiningRequest -url "/v1/metrics/mining/:metric" -responseType Response -type MiningRequest struct { - Client requestgen.AuthenticatedAPIClient - - Asset string `param:"a,required,query"` - Since int64 `param:"s,query"` - Until int64 `param:"u,query"` - Interval Interval `param:"i,query"` - Format Format `param:"f,query"` - TimestampFormat string `param:"timestamp_format,query"` - - Metric string `param:"metric,slug"` -} diff --git a/pkg/datasource/glassnode/glassnodeapi/mining_request_requestgen.go b/pkg/datasource/glassnode/glassnodeapi/mining_request_requestgen.go deleted file mode 100644 index b448ba87d7..0000000000 --- a/pkg/datasource/glassnode/glassnodeapi/mining_request_requestgen.go +++ /dev/null @@ -1,196 +0,0 @@ -// Code generated by "requestgen -method GET -type MiningRequest -url /v1/metrics/mining/:metric -responseType Response"; DO NOT EDIT. - -package glassnodeapi - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "regexp" -) - -func (m *MiningRequest) SetAsset(Asset string) *MiningRequest { - m.Asset = Asset - return m -} - -func (m *MiningRequest) SetSince(Since int64) *MiningRequest { - m.Since = Since - return m -} - -func (m *MiningRequest) SetUntil(Until int64) *MiningRequest { - m.Until = Until - return m -} - -func (m *MiningRequest) SetInterval(Interval Interval) *MiningRequest { - m.Interval = Interval - return m -} - -func (m *MiningRequest) SetFormat(Format Format) *MiningRequest { - m.Format = Format - return m -} - -func (m *MiningRequest) SetTimestampFormat(TimestampFormat string) *MiningRequest { - m.TimestampFormat = TimestampFormat - return m -} - -func (m *MiningRequest) SetMetric(Metric string) *MiningRequest { - m.Metric = Metric - return m -} - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (m *MiningRequest) GetQueryParameters() (url.Values, error) { - var params = map[string]interface{}{} - // check Asset field -> json key a - Asset := m.Asset - - // TEMPLATE check-required - if len(Asset) == 0 { - return nil, fmt.Errorf("a is required, empty string given") - } - // END TEMPLATE check-required - - // assign parameter of Asset - params["a"] = Asset - // check Since field -> json key s - Since := m.Since - - // assign parameter of Since - params["s"] = Since - // check Until field -> json key u - Until := m.Until - - // assign parameter of Until - params["u"] = Until - // check Interval field -> json key i - Interval := m.Interval - - // assign parameter of Interval - params["i"] = Interval - // check Format field -> json key f - Format := m.Format - - // assign parameter of Format - params["f"] = Format - // check TimestampFormat field -> json key timestamp_format - TimestampFormat := m.TimestampFormat - - // assign parameter of TimestampFormat - params["timestamp_format"] = TimestampFormat - - query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParameters builds and checks the parameters and return the result in a map object -func (m *MiningRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (m *MiningRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := m.GetParameters() - if err != nil { - return query, err - } - - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParametersJSON converts the parameters from GetParameters into the JSON format -func (m *MiningRequest) GetParametersJSON() ([]byte, error) { - params, err := m.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} - -// GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (m *MiningRequest) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - // check Metric field -> json key metric - Metric := m.Metric - - // assign parameter of Metric - params["metric"] = Metric - - return params, nil -} - -func (m *MiningRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) - } - - return url -} - -func (m *MiningRequest) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := m.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) - } - - return slugs, nil -} - -func (m *MiningRequest) Do(ctx context.Context) (Response, error) { - - // no body params - var params interface{} - query, err := m.GetQueryParameters() - if err != nil { - return nil, err - } - - apiURL := "/v1/metrics/mining/:metric" - slugs, err := m.GetSlugsMap() - if err != nil { - return nil, err - } - - apiURL = m.applySlugsToUrl(apiURL, slugs) - - req, err := m.Client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := m.Client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse Response - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - return apiResponse, nil -} diff --git a/pkg/datasource/glassnode/glassnodeapi/protocols.go b/pkg/datasource/glassnode/glassnodeapi/protocols.go deleted file mode 100644 index 3a0e62a0a1..0000000000 --- a/pkg/datasource/glassnode/glassnodeapi/protocols.go +++ /dev/null @@ -1,17 +0,0 @@ -package glassnodeapi - -import "github.com/c9s/requestgen" - -//go:generate requestgen -method GET -type ProtocolsRequest -url "/v1/metrics/protocols/:metric" -responseType Response -type ProtocolsRequest struct { - Client requestgen.AuthenticatedAPIClient - - Asset string `param:"a,required,query"` - Since int64 `param:"s,query"` - Until int64 `param:"u,query"` - Interval Interval `param:"i,query"` - Format Format `param:"f,query"` - TimestampFormat string `param:"timestamp_format,query"` - - Metric string `param:"metric,slug"` -} diff --git a/pkg/datasource/glassnode/glassnodeapi/protocols_request_requestgen.go b/pkg/datasource/glassnode/glassnodeapi/protocols_request_requestgen.go deleted file mode 100644 index 2b8fb6b0d5..0000000000 --- a/pkg/datasource/glassnode/glassnodeapi/protocols_request_requestgen.go +++ /dev/null @@ -1,196 +0,0 @@ -// Code generated by "requestgen -method GET -type ProtocolsRequest -url /v1/metrics/protocols/:metric -responseType Response"; DO NOT EDIT. - -package glassnodeapi - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "regexp" -) - -func (p *ProtocolsRequest) SetAsset(Asset string) *ProtocolsRequest { - p.Asset = Asset - return p -} - -func (p *ProtocolsRequest) SetSince(Since int64) *ProtocolsRequest { - p.Since = Since - return p -} - -func (p *ProtocolsRequest) SetUntil(Until int64) *ProtocolsRequest { - p.Until = Until - return p -} - -func (p *ProtocolsRequest) SetInterval(Interval Interval) *ProtocolsRequest { - p.Interval = Interval - return p -} - -func (p *ProtocolsRequest) SetFormat(Format Format) *ProtocolsRequest { - p.Format = Format - return p -} - -func (p *ProtocolsRequest) SetTimestampFormat(TimestampFormat string) *ProtocolsRequest { - p.TimestampFormat = TimestampFormat - return p -} - -func (p *ProtocolsRequest) SetMetric(Metric string) *ProtocolsRequest { - p.Metric = Metric - return p -} - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (p *ProtocolsRequest) GetQueryParameters() (url.Values, error) { - var params = map[string]interface{}{} - // check Asset field -> json key a - Asset := p.Asset - - // TEMPLATE check-required - if len(Asset) == 0 { - return nil, fmt.Errorf("a is required, empty string given") - } - // END TEMPLATE check-required - - // assign parameter of Asset - params["a"] = Asset - // check Since field -> json key s - Since := p.Since - - // assign parameter of Since - params["s"] = Since - // check Until field -> json key u - Until := p.Until - - // assign parameter of Until - params["u"] = Until - // check Interval field -> json key i - Interval := p.Interval - - // assign parameter of Interval - params["i"] = Interval - // check Format field -> json key f - Format := p.Format - - // assign parameter of Format - params["f"] = Format - // check TimestampFormat field -> json key timestamp_format - TimestampFormat := p.TimestampFormat - - // assign parameter of TimestampFormat - params["timestamp_format"] = TimestampFormat - - query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParameters builds and checks the parameters and return the result in a map object -func (p *ProtocolsRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (p *ProtocolsRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := p.GetParameters() - if err != nil { - return query, err - } - - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParametersJSON converts the parameters from GetParameters into the JSON format -func (p *ProtocolsRequest) GetParametersJSON() ([]byte, error) { - params, err := p.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} - -// GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (p *ProtocolsRequest) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - // check Metric field -> json key metric - Metric := p.Metric - - // assign parameter of Metric - params["metric"] = Metric - - return params, nil -} - -func (p *ProtocolsRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) - } - - return url -} - -func (p *ProtocolsRequest) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := p.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) - } - - return slugs, nil -} - -func (p *ProtocolsRequest) Do(ctx context.Context) (Response, error) { - - // no body params - var params interface{} - query, err := p.GetQueryParameters() - if err != nil { - return nil, err - } - - apiURL := "/v1/metrics/protocols/:metric" - slugs, err := p.GetSlugsMap() - if err != nil { - return nil, err - } - - apiURL = p.applySlugsToUrl(apiURL, slugs) - - req, err := p.Client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := p.Client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse Response - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - return apiResponse, nil -} diff --git a/pkg/datasource/glassnode/glassnodeapi/request.go b/pkg/datasource/glassnode/glassnodeapi/request.go new file mode 100644 index 0000000000..f8addc5ad5 --- /dev/null +++ b/pkg/datasource/glassnode/glassnodeapi/request.go @@ -0,0 +1,23 @@ +package glassnodeapi + +import ( + "time" + + "github.com/c9s/requestgen" +) + +//go:generate requestgen -method GET -type Request -url "/v1/metrics/:category/:metric" -responseType DataSlice +type Request struct { + Client requestgen.AuthenticatedAPIClient + + Asset string `param:"a,required,query"` + Since *time.Time `param:"s,query,seconds"` + Until *time.Time `param:"u,query,seconds"` + Interval *Interval `param:"i,query"` + Format *Format `param:"f,query" default:"JSON"` + Currency *string `param:"c,query"` + TimestampFormat *string `param:"timestamp_format,query"` + + Category string `param:"category,slug"` + Metric string `param:"metric,slug"` +} diff --git a/pkg/datasource/glassnode/glassnodeapi/request_requestgen.go b/pkg/datasource/glassnode/glassnodeapi/request_requestgen.go new file mode 100644 index 0000000000..0949ad10f7 --- /dev/null +++ b/pkg/datasource/glassnode/glassnodeapi/request_requestgen.go @@ -0,0 +1,288 @@ +// Code generated by "requestgen -method GET -type Request -url /v1/metrics/:category/:metric -responseType DataSlice"; DO NOT EDIT. + +package glassnodeapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" + "strconv" + "time" +) + +func (r *Request) SetAsset(Asset string) *Request { + r.Asset = Asset + return r +} + +func (r *Request) SetSince(Since time.Time) *Request { + r.Since = &Since + return r +} + +func (r *Request) SetUntil(Until time.Time) *Request { + r.Until = &Until + return r +} + +func (r *Request) SetInterval(Interval Interval) *Request { + r.Interval = &Interval + return r +} + +func (r *Request) SetFormat(Format Format) *Request { + r.Format = &Format + return r +} + +func (r *Request) SetCurrency(Currency string) *Request { + r.Currency = &Currency + return r +} + +func (r *Request) SetTimestampFormat(TimestampFormat string) *Request { + r.TimestampFormat = &TimestampFormat + return r +} + +func (r *Request) SetCategory(Category string) *Request { + r.Category = Category + return r +} + +func (r *Request) SetMetric(Metric string) *Request { + r.Metric = Metric + return r +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (r *Request) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + // check Asset field -> json key a + Asset := r.Asset + + // TEMPLATE check-required + if len(Asset) == 0 { + return nil, fmt.Errorf("a is required, empty string given") + } + // END TEMPLATE check-required + + // assign parameter of Asset + params["a"] = Asset + // check Since field -> json key s + if r.Since != nil { + Since := *r.Since + + // assign parameter of Since + // convert time.Time to seconds time stamp + params["s"] = strconv.FormatInt(Since.Unix(), 10) + } else { + } + // check Until field -> json key u + if r.Until != nil { + Until := *r.Until + + // assign parameter of Until + // convert time.Time to seconds time stamp + params["u"] = strconv.FormatInt(Until.Unix(), 10) + } else { + } + // check Interval field -> json key i + if r.Interval != nil { + Interval := *r.Interval + + // TEMPLATE check-valid-values + switch Interval { + case Interval1h, Interval24h, Interval10m, Interval1w, Interval1m: + params["i"] = Interval + + default: + return nil, fmt.Errorf("i value %v is invalid", Interval) + + } + // END TEMPLATE check-valid-values + + // assign parameter of Interval + params["i"] = Interval + } else { + } + // check Format field -> json key f + if r.Format != nil { + Format := *r.Format + + // TEMPLATE check-valid-values + switch Format { + case FormatJSON, FormatCSV: + params["f"] = Format + + default: + return nil, fmt.Errorf("f value %v is invalid", Format) + + } + // END TEMPLATE check-valid-values + + // assign parameter of Format + params["f"] = Format + } else { + Format := "JSON" + + // assign parameter of Format + params["f"] = Format + } + // check Currency field -> json key c + if r.Currency != nil { + Currency := *r.Currency + + // assign parameter of Currency + params["c"] = Currency + } else { + } + // check TimestampFormat field -> json key timestamp_format + if r.TimestampFormat != nil { + TimestampFormat := *r.TimestampFormat + + // assign parameter of TimestampFormat + params["timestamp_format"] = TimestampFormat + } else { + } + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (r *Request) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (r *Request) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := r.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if r.isVarSlice(_v) { + r.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (r *Request) GetParametersJSON() ([]byte, error) { + params, err := r.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (r *Request) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check Category field -> json key category + Category := r.Category + + // assign parameter of Category + params["category"] = Category + // check Metric field -> json key metric + Metric := r.Metric + + // assign parameter of Metric + params["metric"] = Metric + + return params, nil +} + +func (r *Request) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (r *Request) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (r *Request) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (r *Request) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := r.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (r *Request) Do(ctx context.Context) (DataSlice, error) { + + // no body params + var params interface{} + query, err := r.GetQueryParameters() + if err != nil { + return nil, err + } + + apiURL := "/v1/metrics/:category/:metric" + slugs, err := r.GetSlugsMap() + if err != nil { + return nil, err + } + + apiURL = r.applySlugsToUrl(apiURL, slugs) + + req, err := r.Client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := r.Client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse DataSlice + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return apiResponse, nil +} diff --git a/pkg/datasource/glassnode/glassnodeapi/supply.go b/pkg/datasource/glassnode/glassnodeapi/supply.go deleted file mode 100644 index 542f03b237..0000000000 --- a/pkg/datasource/glassnode/glassnodeapi/supply.go +++ /dev/null @@ -1,17 +0,0 @@ -package glassnodeapi - -import "github.com/c9s/requestgen" - -//go:generate requestgen -method GET -type SupplyRequest -url "/v1/metrics/supply/:metric" -responseType Response -type SupplyRequest struct { - Client requestgen.AuthenticatedAPIClient - - Asset string `param:"a,required,query"` - Since int64 `param:"s,query"` - Until int64 `param:"u,query"` - Interval Interval `param:"i,query"` - Format Format `param:"f,query"` - TimestampFormat string `param:"timestamp_format,query"` - - Metric string `param:"metric,slug"` -} diff --git a/pkg/datasource/glassnode/glassnodeapi/supply_request_requestgen.go b/pkg/datasource/glassnode/glassnodeapi/supply_request_requestgen.go deleted file mode 100644 index 2577d7da16..0000000000 --- a/pkg/datasource/glassnode/glassnodeapi/supply_request_requestgen.go +++ /dev/null @@ -1,196 +0,0 @@ -// Code generated by "requestgen -method GET -type SupplyRequest -url /v1/metrics/supply/:metric -responseType Response"; DO NOT EDIT. - -package glassnodeapi - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "regexp" -) - -func (s *SupplyRequest) SetAsset(Asset string) *SupplyRequest { - s.Asset = Asset - return s -} - -func (s *SupplyRequest) SetSince(Since int64) *SupplyRequest { - s.Since = Since - return s -} - -func (s *SupplyRequest) SetUntil(Until int64) *SupplyRequest { - s.Until = Until - return s -} - -func (s *SupplyRequest) SetInterval(Interval Interval) *SupplyRequest { - s.Interval = Interval - return s -} - -func (s *SupplyRequest) SetFormat(Format Format) *SupplyRequest { - s.Format = Format - return s -} - -func (s *SupplyRequest) SetTimestampFormat(TimestampFormat string) *SupplyRequest { - s.TimestampFormat = TimestampFormat - return s -} - -func (s *SupplyRequest) SetMetric(Metric string) *SupplyRequest { - s.Metric = Metric - return s -} - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (s *SupplyRequest) GetQueryParameters() (url.Values, error) { - var params = map[string]interface{}{} - // check Asset field -> json key a - Asset := s.Asset - - // TEMPLATE check-required - if len(Asset) == 0 { - return nil, fmt.Errorf("a is required, empty string given") - } - // END TEMPLATE check-required - - // assign parameter of Asset - params["a"] = Asset - // check Since field -> json key s - Since := s.Since - - // assign parameter of Since - params["s"] = Since - // check Until field -> json key u - Until := s.Until - - // assign parameter of Until - params["u"] = Until - // check Interval field -> json key i - Interval := s.Interval - - // assign parameter of Interval - params["i"] = Interval - // check Format field -> json key f - Format := s.Format - - // assign parameter of Format - params["f"] = Format - // check TimestampFormat field -> json key timestamp_format - TimestampFormat := s.TimestampFormat - - // assign parameter of TimestampFormat - params["timestamp_format"] = TimestampFormat - - query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParameters builds and checks the parameters and return the result in a map object -func (s *SupplyRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (s *SupplyRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := s.GetParameters() - if err != nil { - return query, err - } - - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParametersJSON converts the parameters from GetParameters into the JSON format -func (s *SupplyRequest) GetParametersJSON() ([]byte, error) { - params, err := s.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} - -// GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (s *SupplyRequest) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - // check Metric field -> json key metric - Metric := s.Metric - - // assign parameter of Metric - params["metric"] = Metric - - return params, nil -} - -func (s *SupplyRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) - } - - return url -} - -func (s *SupplyRequest) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := s.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) - } - - return slugs, nil -} - -func (s *SupplyRequest) Do(ctx context.Context) (Response, error) { - - // no body params - var params interface{} - query, err := s.GetQueryParameters() - if err != nil { - return nil, err - } - - apiURL := "/v1/metrics/supply/:metric" - slugs, err := s.GetSlugsMap() - if err != nil { - return nil, err - } - - apiURL = s.applySlugsToUrl(apiURL, slugs) - - req, err := s.Client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := s.Client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse Response - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - return apiResponse, nil -} diff --git a/pkg/datasource/glassnode/glassnodeapi/transactions.go b/pkg/datasource/glassnode/glassnodeapi/transactions.go deleted file mode 100644 index 57e22412f5..0000000000 --- a/pkg/datasource/glassnode/glassnodeapi/transactions.go +++ /dev/null @@ -1,17 +0,0 @@ -package glassnodeapi - -import "github.com/c9s/requestgen" - -//go:generate requestgen -method GET -type TransactionsRequest -url "/v1/metrics/transactions/:metric" -responseType Response -type TransactionsRequest struct { - Client requestgen.AuthenticatedAPIClient - - Asset string `param:"a,required,query"` - Since int64 `param:"s,query"` - Until int64 `param:"u,query"` - Interval Interval `param:"i,query"` - Format Format `param:"f,query"` - TimestampFormat string `param:"timestamp_format,query"` - - Metric string `param:"metric,slug"` -} diff --git a/pkg/datasource/glassnode/glassnodeapi/transactions_request_requestgen.go b/pkg/datasource/glassnode/glassnodeapi/transactions_request_requestgen.go deleted file mode 100644 index 7ebcd983bf..0000000000 --- a/pkg/datasource/glassnode/glassnodeapi/transactions_request_requestgen.go +++ /dev/null @@ -1,196 +0,0 @@ -// Code generated by "requestgen -method GET -type TransactionsRequest -url /v1/metrics/transactions/:metric -responseType Response"; DO NOT EDIT. - -package glassnodeapi - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "regexp" -) - -func (t *TransactionsRequest) SetAsset(Asset string) *TransactionsRequest { - t.Asset = Asset - return t -} - -func (t *TransactionsRequest) SetSince(Since int64) *TransactionsRequest { - t.Since = Since - return t -} - -func (t *TransactionsRequest) SetUntil(Until int64) *TransactionsRequest { - t.Until = Until - return t -} - -func (t *TransactionsRequest) SetInterval(Interval Interval) *TransactionsRequest { - t.Interval = Interval - return t -} - -func (t *TransactionsRequest) SetFormat(Format Format) *TransactionsRequest { - t.Format = Format - return t -} - -func (t *TransactionsRequest) SetTimestampFormat(TimestampFormat string) *TransactionsRequest { - t.TimestampFormat = TimestampFormat - return t -} - -func (t *TransactionsRequest) SetMetric(Metric string) *TransactionsRequest { - t.Metric = Metric - return t -} - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (t *TransactionsRequest) GetQueryParameters() (url.Values, error) { - var params = map[string]interface{}{} - // check Asset field -> json key a - Asset := t.Asset - - // TEMPLATE check-required - if len(Asset) == 0 { - return nil, fmt.Errorf("a is required, empty string given") - } - // END TEMPLATE check-required - - // assign parameter of Asset - params["a"] = Asset - // check Since field -> json key s - Since := t.Since - - // assign parameter of Since - params["s"] = Since - // check Until field -> json key u - Until := t.Until - - // assign parameter of Until - params["u"] = Until - // check Interval field -> json key i - Interval := t.Interval - - // assign parameter of Interval - params["i"] = Interval - // check Format field -> json key f - Format := t.Format - - // assign parameter of Format - params["f"] = Format - // check TimestampFormat field -> json key timestamp_format - TimestampFormat := t.TimestampFormat - - // assign parameter of TimestampFormat - params["timestamp_format"] = TimestampFormat - - query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParameters builds and checks the parameters and return the result in a map object -func (t *TransactionsRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (t *TransactionsRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := t.GetParameters() - if err != nil { - return query, err - } - - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParametersJSON converts the parameters from GetParameters into the JSON format -func (t *TransactionsRequest) GetParametersJSON() ([]byte, error) { - params, err := t.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} - -// GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (t *TransactionsRequest) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - // check Metric field -> json key metric - Metric := t.Metric - - // assign parameter of Metric - params["metric"] = Metric - - return params, nil -} - -func (t *TransactionsRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) - } - - return url -} - -func (t *TransactionsRequest) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := t.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) - } - - return slugs, nil -} - -func (t *TransactionsRequest) Do(ctx context.Context) (Response, error) { - - // no body params - var params interface{} - query, err := t.GetQueryParameters() - if err != nil { - return nil, err - } - - apiURL := "/v1/metrics/transactions/:metric" - slugs, err := t.GetSlugsMap() - if err != nil { - return nil, err - } - - apiURL = t.applySlugsToUrl(apiURL, slugs) - - req, err := t.Client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := t.Client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse Response - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - return apiResponse, nil -} diff --git a/pkg/datasource/glassnode/glassnodeapi/types.go b/pkg/datasource/glassnode/glassnodeapi/types.go index 0d055e9904..0e7183f3af 100644 --- a/pkg/datasource/glassnode/glassnodeapi/types.go +++ b/pkg/datasource/glassnode/glassnodeapi/types.go @@ -72,56 +72,50 @@ and ... ] -both can be stored into the Response structure. +both can be stored into the DataSlice structure. Note: use `HasOptions` to verify the type of response. */ -type Response []Data +type DataSlice []Data type Data struct { Timestamp Timestamp `json:"t"` Value float64 `json:"v"` Options map[string]float64 `json:"o"` } -func (s Response) IsEmpty() bool { - if len(s) == 0 { - return true - } - return false +func (s DataSlice) IsEmpty() bool { + return len(s) == 0 } -func (s Response) First() Data { +func (s DataSlice) First() Data { if s.IsEmpty() { return Data{} } return s[0] } -func (s Response) FirstValue() float64 { +func (s DataSlice) FirstValue() float64 { return s.First().Value } -func (s Response) FirstOptions() map[string]float64 { +func (s DataSlice) FirstOptions() map[string]float64 { return s.First().Options } -func (s Response) Last() Data { +func (s DataSlice) Last() Data { if s.IsEmpty() { return Data{} } return s[len(s)-1] } -func (s Response) LastValue() float64 { +func (s DataSlice) LastValue() float64 { return s.Last().Value } -func (s Response) LastOptions() map[string]float64 { +func (s DataSlice) LastOptions() map[string]float64 { return s.Last().Options } -func (s Response) HasOptions() bool { - if len(s.First().Options) == 0 { - return false - } - return true +func (s DataSlice) HasOptions() bool { + return len(s.First().Options) != 0 } diff --git a/pkg/datasource/glassnode/types.go b/pkg/datasource/glassnode/types.go new file mode 100644 index 0000000000..473eb8f994 --- /dev/null +++ b/pkg/datasource/glassnode/types.go @@ -0,0 +1,14 @@ +package glassnode + +import ( + "time" + + "github.com/c9s/bbgo/pkg/datasource/glassnode/glassnodeapi" +) + +type QueryOptions struct { + Since *time.Time + Until *time.Time + Interval *glassnodeapi.Interval + Currency *string +} diff --git a/pkg/datatype/floats/funcs.go b/pkg/datatype/floats/funcs.go new file mode 100644 index 0000000000..7942781681 --- /dev/null +++ b/pkg/datatype/floats/funcs.go @@ -0,0 +1,167 @@ +package floats + +import "sort" + +func Lower(arr []float64, x float64) []float64 { + sort.Float64s(arr) + + var rst []float64 + for _, a := range arr { + // filter prices that are Lower than the current closed price + if a >= x { + continue + } + + rst = append(rst, a) + } + + return rst +} + +func Higher(arr []float64, x float64) []float64 { + sort.Float64s(arr) + + var rst []float64 + for _, a := range arr { + // filter prices that are Lower than the current closed price + if a <= x { + continue + } + rst = append(rst, a) + } + + return rst +} + +func Group(arr []float64, minDistance float64) []float64 { + if len(arr) == 0 { + return nil + } + + var groups []float64 + var grp = []float64{arr[0]} + for _, price := range arr { + avg := Average(grp) + if (price / avg) > (1.0 + minDistance) { + groups = append(groups, avg) + grp = []float64{price} + } else { + grp = append(grp, price) + } + } + + if len(grp) > 0 { + groups = append(groups, Average(grp)) + } + + return groups +} + +func Average(arr []float64) float64 { + s := 0.0 + for _, a := range arr { + s += a + } + return s / float64(len(arr)) +} + +// Multiply multiplies two float series +func Multiply(inReal0 []float64, inReal1 []float64) []float64 { + outReal := make([]float64, len(inReal0)) + for i := 0; i < len(inReal0); i++ { + outReal[i] = inReal0[i] * inReal1[i] + } + return outReal +} + +// CrossOver returns true if series1 is crossing over series2. +// +// NOTE: Usually this is used with Media Average Series to check if it crosses for buy signals. +// It assumes first values are the most recent. +// The crossover function does not use most recent value, since usually it's not a complete candle. +// The second recent values and the previous are used, instead. +// +// ported from https://github.com/markcheno/go-talib/blob/master/talib.go +func CrossOver(series1 []float64, series2 []float64) bool { + if len(series1) < 3 || len(series2) < 3 { + return false + } + + N := len(series1) + + return series1[N-2] <= series2[N-2] && series1[N-1] > series2[N-1] +} + +// CrossUnder returns true if series1 is crossing under series2. +// +// NOTE: Usually this is used with Media Average Series to check if it crosses for sell signals. +// +// ported from https://github.com/markcheno/go-talib/blob/master/talib.go +func CrossUnder(series1 []float64, series2 []float64) bool { + if len(series1) < 3 || len(series2) < 3 { + return false + } + + N := len(series1) + + return series1[N-1] <= series2[N-1] && series1[N-2] > series2[N-2] +} + +// MinMax - Lowest and highest values over a specified period +// ported from https://github.com/markcheno/go-talib/blob/master/talib.go +func MinMax(inReal []float64, inTimePeriod int) (outMin []float64, outMax []float64) { + outMin = make([]float64, len(inReal)) + outMax = make([]float64, len(inReal)) + nbInitialElementNeeded := inTimePeriod - 1 + startIdx := nbInitialElementNeeded + outIdx := startIdx + today := startIdx + trailingIdx := startIdx - nbInitialElementNeeded + highestIdx := -1 + highest := 0.0 + lowestIdx := -1 + lowest := 0.0 + for today < len(inReal) { + tmpLow, tmpHigh := inReal[today], inReal[today] + if highestIdx < trailingIdx { + highestIdx = trailingIdx + highest = inReal[highestIdx] + i := highestIdx + i++ + for i <= today { + tmpHigh = inReal[i] + if tmpHigh > highest { + highestIdx = i + highest = tmpHigh + } + i++ + } + } else if tmpHigh >= highest { + highestIdx = today + highest = tmpHigh + } + if lowestIdx < trailingIdx { + lowestIdx = trailingIdx + lowest = inReal[lowestIdx] + i := lowestIdx + i++ + for i <= today { + tmpLow = inReal[i] + if tmpLow < lowest { + lowestIdx = i + lowest = tmpLow + } + i++ + } + } else if tmpLow <= lowest { + lowestIdx = today + lowest = tmpLow + } + outMax[outIdx] = highest + outMin[outIdx] = lowest + outIdx++ + trailingIdx++ + today++ + } + return outMin, outMax +} diff --git a/pkg/datatype/floats/funcs_test.go b/pkg/datatype/floats/funcs_test.go new file mode 100644 index 0000000000..25c37f908a --- /dev/null +++ b/pkg/datatype/floats/funcs_test.go @@ -0,0 +1,17 @@ +package floats + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestLower(t *testing.T) { + out := Lower([]float64{10.0, 11.0, 12.0, 13.0, 15.0}, 12.0) + assert.Equal(t, []float64{10.0, 11.0}, out) +} + +func TestHigher(t *testing.T) { + out := Higher([]float64{10.0, 11.0, 12.0, 13.0, 15.0}, 12.0) + assert.Equal(t, []float64{13.0, 15.0}, out) +} diff --git a/pkg/datatype/floats/map.go b/pkg/datatype/floats/map.go new file mode 100644 index 0000000000..efdcf82fe6 --- /dev/null +++ b/pkg/datatype/floats/map.go @@ -0,0 +1,42 @@ +package floats + +type Map map[string]float64 + +func (m Map) Sum() float64 { + sum := 0.0 + for _, v := range m { + sum += v + } + return sum +} + +func (m Map) MulScalar(x float64) Map { + o := Map{} + for k, v := range m { + o[k] = v * x + } + + return o +} +func (m Map) DivScalar(x float64) Map { + o := Map{} + for k, v := range m { + o[k] = v / x + } + + return o +} + +func (m Map) Normalize() Map { + sum := m.Sum() + if sum == 0 { + panic("zero sum") + } + + o := Map{} + for k, v := range m { + o[k] = v / sum + } + + return o +} diff --git a/pkg/datatype/floats/pivot.go b/pkg/datatype/floats/pivot.go new file mode 100644 index 0000000000..f794030477 --- /dev/null +++ b/pkg/datatype/floats/pivot.go @@ -0,0 +1,34 @@ +package floats + +func (s Slice) Pivot(left, right int, f func(a, pivot float64) bool) (float64, bool) { + return CalculatePivot(s, left, right, f) +} + +func CalculatePivot(values Slice, left, right int, f func(a, pivot float64) bool) (float64, bool) { + length := len(values) + + if right == 0 { + right = left + } + + if length == 0 || length < left+right+1 { + return 0.0, false + } + + end := length - 1 + index := end - right + val := values[index] + + for i := index - left; i <= index+right; i++ { + if i == index { + continue + } + + // return if we found lower value + if !f(values[i], val) { + return 0.0, false + } + } + + return val, true +} diff --git a/pkg/datatype/floats/slice.go b/pkg/datatype/floats/slice.go new file mode 100644 index 0000000000..4aab25eb87 --- /dev/null +++ b/pkg/datatype/floats/slice.go @@ -0,0 +1,151 @@ +package floats + +import ( + "math" + + "gonum.org/v1/gonum/floats" +) + +type Slice []float64 + +func New(a ...float64) Slice { + return Slice(a) +} + +func (s *Slice) Push(v float64) { + *s = append(*s, v) +} + +func (s *Slice) Update(v float64) { + *s = append(*s, v) +} + +func (s *Slice) Pop(i int64) (v float64) { + v = (*s)[i] + *s = append((*s)[:i], (*s)[i+1:]...) + return v +} + +func (s Slice) Max() float64 { + return floats.Max(s) +} + +func (s Slice) Min() float64 { + return floats.Min(s) +} + +func (s Slice) Sum() (sum float64) { + return floats.Sum(s) +} + +func (s Slice) Mean() (mean float64) { + length := len(s) + if length == 0 { + panic("zero length slice") + } + return s.Sum() / float64(length) +} + +func (s Slice) Tail(size int) Slice { + length := len(s) + if length <= size { + win := make(Slice, length) + copy(win, s) + return win + } + + win := make(Slice, size) + copy(win, s[length-size:]) + return win +} + +func (s Slice) Diff() (values Slice) { + for i, v := range s { + if i == 0 { + values.Push(0) + continue + } + values.Push(v - s[i-1]) + } + return values +} + +func (s Slice) PositiveValuesOrZero() (values Slice) { + for _, v := range s { + values.Push(math.Max(v, 0)) + } + return values +} + +func (s Slice) NegativeValuesOrZero() (values Slice) { + for _, v := range s { + values.Push(math.Min(v, 0)) + } + return values +} + +func (s Slice) Abs() (values Slice) { + for _, v := range s { + values.Push(math.Abs(v)) + } + return values +} + +func (s Slice) MulScalar(x float64) (values Slice) { + for _, v := range s { + values.Push(v * x) + } + return values +} + +func (s Slice) DivScalar(x float64) (values Slice) { + for _, v := range s { + values.Push(v / x) + } + return values +} + +func (s Slice) Mul(other Slice) (values Slice) { + if len(s) != len(other) { + panic("slice lengths do not match") + } + + for i, v := range s { + values.Push(v * other[i]) + } + + return values +} + +func (s Slice) Dot(other Slice) float64 { + return floats.Dot(s, other) +} + +func (s Slice) Normalize() Slice { + return s.DivScalar(s.Sum()) +} + +func (s *Slice) Last() float64 { + length := len(*s) + if length > 0 { + return (*s)[length-1] + } + return 0.0 +} + +func (s *Slice) Index(i int) float64 { + length := len(*s) + if length-i <= 0 || i < 0 { + return 0.0 + } + return (*s)[length-i-1] +} + +func (s *Slice) Length() int { + return len(*s) +} + +func (s Slice) Addr() *Slice { + return &s +} + diff --git a/pkg/depth/buffer.go b/pkg/depth/buffer.go index d753f81202..c960dfb2f9 100644 --- a/pkg/depth/buffer.go +++ b/pkg/depth/buffer.go @@ -3,6 +3,7 @@ package depth import ( "fmt" "sync" + "sync/atomic" "time" log "github.com/sirupsen/logrus" @@ -40,7 +41,7 @@ type Buffer struct { updateTimeout time.Duration // bufferingPeriod is used to buffer the update message before we get the full depth - bufferingPeriod time.Duration + bufferingPeriod atomic.Value } func NewBuffer(fetcher SnapshotFetcher) *Buffer { @@ -55,7 +56,7 @@ func (b *Buffer) SetUpdateTimeout(d time.Duration) { } func (b *Buffer) SetBufferingPeriod(d time.Duration) { - b.bufferingPeriod = d + b.bufferingPeriod.Store(d) } func (b *Buffer) resetSnapshot() { @@ -91,9 +92,6 @@ func (b *Buffer) AddUpdate(o types.SliceOrderBook, firstUpdateID int64, finalArg Object: o, } - // we lock here because there might be 2+ calls to the AddUpdate method - // we don't want to reset sync.Once 2 times here - b.mu.Lock() select { case <-b.resetC: log.Warnf("received depth reset signal, resetting...") @@ -104,6 +102,7 @@ func (b *Buffer) AddUpdate(o types.SliceOrderBook, firstUpdateID int64, finalArg } // if the snapshot is set to nil, we need to buffer the message + b.mu.Lock() if b.snapshot == nil { b.buffer = append(b.buffer, u) b.once.Do(func() { @@ -117,19 +116,21 @@ func (b *Buffer) AddUpdate(o types.SliceOrderBook, firstUpdateID int64, finalArg if u.FirstUpdateID > b.finalUpdateID+1 { // emitReset will reset the once outside the mutex lock section b.buffer = []Update{u} + finalUpdateID = b.finalUpdateID b.resetSnapshot() b.emitReset() b.mu.Unlock() return fmt.Errorf("found missing update between finalUpdateID %d and firstUpdateID %d, diff: %d", - b.finalUpdateID+1, + finalUpdateID+1, u.FirstUpdateID, - u.FirstUpdateID-b.finalUpdateID) + u.FirstUpdateID-finalUpdateID) } log.Debugf("depth update id %d -> %d", b.finalUpdateID, u.FinalUpdateID) b.finalUpdateID = u.FinalUpdateID - b.EmitPush(u) b.mu.Unlock() + + b.EmitPush(u) return nil } @@ -139,9 +140,9 @@ func (b *Buffer) fetchAndPush() error { return err } + b.mu.Lock() log.Debugf("fetched depth snapshot, final update id %d", finalUpdateID) - b.mu.Lock() if len(b.buffer) > 0 { // the snapshot is too early if finalUpdateID < b.buffer[0].FirstUpdateID { @@ -182,14 +183,16 @@ func (b *Buffer) fetchAndPush() error { b.snapshot = &book b.mu.Unlock() + + // should unlock first then call ready b.EmitReady(book, pushUpdates) return nil } func (b *Buffer) tryFetch() { for { - if b.bufferingPeriod > 0 { - <-time.After(b.bufferingPeriod) + if period := b.bufferingPeriod.Load(); period != nil { + <-time.After(period.(time.Duration)) } err := b.fetchAndPush() diff --git a/pkg/depth/buffer_test.go b/pkg/depth/buffer_test.go index 89f75d68bc..949db0f216 100644 --- a/pkg/depth/buffer_test.go +++ b/pkg/depth/buffer_test.go @@ -1,3 +1,6 @@ +//go:build !race +// +build !race + package depth import ( @@ -5,9 +8,10 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" - "github.com/stretchr/testify/assert" ) var itov = fixedpoint.NewFromInt @@ -23,6 +27,7 @@ func TestDepthBuffer_ReadyState(t *testing.T) { }, }, 33, nil }) + buf.SetBufferingPeriod(time.Millisecond * 5) readyC := make(chan struct{}) buf.OnReady(func(snapshot types.SliceOrderBook, updates []Update) { diff --git a/pkg/dynamic/call.go b/pkg/dynamic/call.go new file mode 100644 index 0000000000..4ba59484d4 --- /dev/null +++ b/pkg/dynamic/call.go @@ -0,0 +1,155 @@ +package dynamic + +import ( + "errors" + "reflect" +) + +// CallStructFieldsMethod iterates field from the given struct object +// check if the field object implements the interface, if it's implemented, then we call a specific method +func CallStructFieldsMethod(m interface{}, method string, args ...interface{}) error { + rv := reflect.ValueOf(m) + rt := reflect.TypeOf(m) + + if rt.Kind() != reflect.Ptr { + return errors.New("the given object needs to be a pointer") + } + + rv = rv.Elem() + rt = rt.Elem() + + if rt.Kind() != reflect.Struct { + return errors.New("the given object needs to be struct") + } + + argValues := ToReflectValues(args...) + for i := 0; i < rt.NumField(); i++ { + fieldType := rt.Field(i) + fieldValue := rv.Field(i) + + // skip non-exported fields + if !fieldType.IsExported() { + continue + } + + if fieldType.Type.Kind() == reflect.Ptr && fieldValue.IsNil() { + continue + } + + methodType, ok := fieldType.Type.MethodByName(method) + if !ok { + continue + } + + if len(argValues) < methodType.Type.NumIn() { + // return fmt.Errorf("method %v require %d args, %d given", methodType, methodType.Type.NumIn(), len(argValues)) + } + + refMethod := fieldValue.MethodByName(method) + refMethod.Call(argValues) + } + + return nil +} + +// CallMatch calls the function with the matched argument automatically +// you can define multiple parameter factory function to inject the return value as the function argument. +// e.g., +// CallMatch(targetFunction, 1, 10, true, func() *ParamType { .... }) +// +func CallMatch(f interface{}, objects ...interface{}) ([]reflect.Value, error) { + fv := reflect.ValueOf(f) + ft := reflect.TypeOf(f) + + var startIndex = 0 + var fArgs []reflect.Value + + var factoryParams = findFactoryParams(objects...) + +nextDynamicInputArg: + for i := 0; i < ft.NumIn(); i++ { + at := ft.In(i) + + // uat == underlying argument type + uat := at + if at.Kind() == reflect.Ptr { + uat = at.Elem() + } + + for oi := startIndex; oi < len(objects); oi++ { + var obj = objects[oi] + var objT = reflect.TypeOf(obj) + if objT == at { + fArgs = append(fArgs, reflect.ValueOf(obj)) + startIndex = oi + 1 + continue nextDynamicInputArg + } + + // get the kind of argument + switch k := uat.Kind(); k { + + case reflect.Interface: + if objT.Implements(at) { + fArgs = append(fArgs, reflect.ValueOf(obj)) + startIndex = oi + 1 + continue nextDynamicInputArg + } + } + } + + // factory param can be reused + for _, fp := range factoryParams { + fpt := fp.Type() + outType := fpt.Out(0) + if outType == at { + fOut := fp.Call(nil) + fArgs = append(fArgs, fOut[0]) + continue nextDynamicInputArg + } + } + + fArgs = append(fArgs, reflect.Zero(at)) + } + + out := fv.Call(fArgs) + if ft.NumOut() == 0 { + return out, nil + } + + // try to get the error object from the return value (if any) + var err error + for i := 0; i < ft.NumOut(); i++ { + outType := ft.Out(i) + switch outType.Kind() { + case reflect.Interface: + o := out[i].Interface() + switch ov := o.(type) { + case error: + err = ov + + } + + } + } + return out, err +} + +func findFactoryParams(objs ...interface{}) (fs []reflect.Value) { + for i := range objs { + obj := objs[i] + + objT := reflect.TypeOf(obj) + + if objT.Kind() != reflect.Func { + continue + } + + if objT.NumOut() == 0 || objT.NumIn() > 0 { + continue + } + + fs = append(fs, reflect.ValueOf(obj)) + } + + return fs +} diff --git a/pkg/dynamic/call_test.go b/pkg/dynamic/call_test.go new file mode 100644 index 0000000000..b65029ded8 --- /dev/null +++ b/pkg/dynamic/call_test.go @@ -0,0 +1,115 @@ +package dynamic + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +type callTest struct { + ChildCall1 *childCall1 + ChildCall2 *childCall2 +} + +type childCall1 struct{} + +func (c *childCall1) Subscribe(a int) {} + +type childCall2 struct{} + +func (c *childCall2) Subscribe(a int) {} + +func TestCallStructFieldsMethod(t *testing.T) { + c := &callTest{ + ChildCall1: &childCall1{}, + ChildCall2: &childCall2{}, + } + err := CallStructFieldsMethod(c, "Subscribe", 10) + assert.NoError(t, err) +} + +type S struct { + ID string +} + +func (s *S) String() string { return s.ID } + +func TestCallMatch(t *testing.T) { + t.Run("simple", func(t *testing.T) { + f := func(a int, b int) { + assert.Equal(t, 1, a) + assert.Equal(t, 2, b) + } + _, err := CallMatch(f, 1, 2) + assert.NoError(t, err) + }) + + t.Run("interface", func(t *testing.T) { + type A interface { + String() string + } + f := func(foo int, a A) { + assert.Equal(t, "foo", a.String()) + } + _, err := CallMatch(f, 10, &S{ID: "foo"}) + assert.NoError(t, err) + }) + + t.Run("nil interface", func(t *testing.T) { + type A interface { + String() string + } + f := func(foo int, a A) { + assert.Equal(t, 10, foo) + assert.Nil(t, a) + } + _, err := CallMatch(f, 10) + assert.NoError(t, err) + }) + + t.Run("struct pointer", func(t *testing.T) { + f := func(foo int, s *S) { + assert.Equal(t, 10, foo) + assert.NotNil(t, s) + } + _, err := CallMatch(f, 10, &S{}) + assert.NoError(t, err) + }) + + t.Run("struct pointer x 2", func(t *testing.T) { + f := func(foo int, s1, s2 *S) { + assert.Equal(t, 10, foo) + assert.Equal(t, "s1", s1.String()) + assert.Equal(t, "s2", s2.String()) + } + _, err := CallMatch(f, 10, &S{ID: "s1"}, &S{ID: "s2"}) + assert.NoError(t, err) + }) + + t.Run("func factory", func(t *testing.T) { + f := func(s *S) { + assert.Equal(t, "factory", s.String()) + } + _, err := CallMatch(f, func() *S { + return &S{ID: "factory"} + }) + assert.NoError(t, err) + }) + + t.Run("nil", func(t *testing.T) { + f := func(s *S) { + assert.Nil(t, s) + } + _, err := CallMatch(f) + assert.NoError(t, err) + }) + + t.Run("zero struct", func(t *testing.T) { + f := func(s S) { + assert.Equal(t, S{}, s) + } + _, err := CallMatch(f) + assert.NoError(t, err) + }) + +} diff --git a/pkg/dynamic/can.go b/pkg/dynamic/can.go new file mode 100644 index 0000000000..532a69df1f --- /dev/null +++ b/pkg/dynamic/can.go @@ -0,0 +1,14 @@ +package dynamic + +import "reflect" + +// For backward compatibility of reflect.Value.CanInt in go1.17 +func CanInt(v reflect.Value) bool { + k := v.Type().Kind() + switch k { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return true + default: + return false + } +} diff --git a/pkg/dynamic/field.go b/pkg/dynamic/field.go new file mode 100644 index 0000000000..c004458dec --- /dev/null +++ b/pkg/dynamic/field.go @@ -0,0 +1,113 @@ +package dynamic + +import ( + "errors" + "reflect" + "strings" +) + +func HasField(rs reflect.Value, fieldName string) (field reflect.Value, ok bool) { + field = rs.FieldByName(fieldName) + return field, field.IsValid() +} + +func LookupSymbolField(rs reflect.Value) (string, bool) { + if rs.Kind() == reflect.Ptr { + rs = rs.Elem() + } + + field := rs.FieldByName("Symbol") + if !field.IsValid() { + return "", false + } + + if field.Kind() != reflect.String { + return "", false + } + + return field.String(), true +} + +// Used by bbgo/interact_modify.go +func GetModifiableFields(val reflect.Value, callback func(tagName, name string)) { + if val.Kind() == reflect.Ptr { + val = val.Elem() + } + if !val.IsValid() { + return + } + num := val.Type().NumField() + for i := 0; i < num; i++ { + t := val.Type().Field(i) + if !t.IsExported() { + continue + } + if t.Anonymous { + GetModifiableFields(val.Field(i), callback) + } + modifiable := t.Tag.Get("modifiable") + if modifiable != "true" { + continue + } + jsonTag := t.Tag.Get("json") + if jsonTag == "" || jsonTag == "-" { + continue + } + name := strings.Split(jsonTag, ",")[0] + callback(name, t.Name) + } +} + +var zeroValue reflect.Value = reflect.Zero(reflect.TypeOf(0)) + +func GetModifiableField(val reflect.Value, name string) (reflect.Value, bool) { + if val.Kind() == reflect.Ptr { + if val.IsNil() { + return zeroValue, false + } + } + if val.Kind() != reflect.Struct { + return zeroValue, false + } + if !val.IsValid() { + return zeroValue, false + } + field, ok := val.Type().FieldByName(name) + if !ok { + return zeroValue, ok + } + if field.Tag.Get("modifiable") != "true" { + return zeroValue, false + } + jsonTag := field.Tag.Get("json") + if jsonTag == "" || jsonTag == "-" { + return zeroValue, false + } + value, err := FieldByIndexErr(val, field.Index) + if err != nil { + return zeroValue, false + } + return value, true +} + +// Modified from golang 1.19.1 reflect to eliminate all possible panic +func FieldByIndexErr(v reflect.Value, index []int) (reflect.Value, error) { + if len(index) == 1 { + return v.Field(index[0]), nil + } + if v.Kind() != reflect.Struct { + return zeroValue, errors.New("should receive a Struct") + } + for i, x := range index { + if i > 0 { + if v.Kind() == reflect.Ptr && v.Type().Elem().Kind() == reflect.Struct { + if v.IsNil() { + return zeroValue, errors.New("reflect: indirection through nil pointer to embedded struct field ") + } + v = v.Elem() + } + } + v = v.Field(x) + } + return v, nil +} diff --git a/pkg/dynamic/field_test.go b/pkg/dynamic/field_test.go new file mode 100644 index 0000000000..a57ae9228c --- /dev/null +++ b/pkg/dynamic/field_test.go @@ -0,0 +1,73 @@ +package dynamic + +import ( + "encoding/json" + "reflect" + "testing" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/stretchr/testify/assert" +) + +type Inner struct { + Field5 float64 `json:"field5,omitempty" modifiable:"true"` +} + +type InnerPointer struct { + Field6 float64 `json:"field6" modifiable:"true"` +} + +type Strategy struct { + Inner + *InnerPointer + Field1 fixedpoint.Value `json:"field1" modifiable:"true"` + Field2 float64 `json:"field2"` + field3 float64 `json:"field3" modifiable:"true"` + Field4 *fixedpoint.Value `json:"field4" modifiable:"true"` +} + +func TestGetModifiableFields(t *testing.T) { + s := Strategy{} + val := reflect.ValueOf(s) + GetModifiableFields(val, func(tagName, name string) { + assert.NotEqual(t, tagName, "field2") + assert.NotEqual(t, name, "Field2") + assert.NotEqual(t, tagName, "field3") + assert.NotEqual(t, name, "Field3") + }) +} + +func TestGetModifiableField(t *testing.T) { + // val must be get from pointer.Elem(), otherwise the fields will be unaddressable + s := &Strategy{Field1: fixedpoint.NewFromInt(1)} + val := reflect.ValueOf(s).Elem() + _, ok := GetModifiableField(val, "Field1") + assert.True(t, ok) + _, ok = GetModifiableField(val, "Field5") + assert.True(t, ok) + _, ok = GetModifiableField(val, "Field6") + assert.False(t, ok) + s.InnerPointer = &InnerPointer{} + _, ok = GetModifiableField(val, "Field6") + assert.True(t, ok) + _, ok = GetModifiableField(val, "Field2") + assert.False(t, ok) + _, ok = GetModifiableField(val, "Field3") + assert.False(t, ok) + _, ok = GetModifiableField(val, "Random") + assert.False(t, ok) + field, ok := GetModifiableField(val, "Field1") + assert.True(t, ok) + x := reflect.New(field.Type()) + xi := x.Interface() + assert.NoError(t, json.Unmarshal([]byte("\"3.1415%\""), &xi)) + assert.True(t, field.CanAddr()) + field.Set(x.Elem()) + assert.Equal(t, s.Field1.String(), "0.031415") + field, _ = GetModifiableField(val, "Field4") + x = reflect.New(field.Type()) + xi = x.Interface() + assert.NoError(t, json.Unmarshal([]byte("311"), &xi)) + field.Set(x.Elem()) + assert.Equal(t, s.Field4.String(), "311") +} diff --git a/pkg/dynamic/id.go b/pkg/dynamic/id.go new file mode 100644 index 0000000000..b7b4217e6b --- /dev/null +++ b/pkg/dynamic/id.go @@ -0,0 +1,30 @@ +package dynamic + +import ( + "reflect" +) + +type InstanceIDProvider interface { + InstanceID() string +} + +func CallID(obj interface{}) string { + sv := reflect.ValueOf(obj) + st := reflect.TypeOf(obj) + if st.Implements(reflect.TypeOf((*InstanceIDProvider)(nil)).Elem()) { + m := sv.MethodByName("InstanceID") + ret := m.Call(nil) + return ret[0].String() + } + + if symbol, ok := LookupSymbolField(sv); ok { + m := sv.MethodByName("ID") + ret := m.Call(nil) + return ret[0].String() + ":" + symbol + } + + // fallback to just ID + m := sv.MethodByName("ID") + ret := m.Call(nil) + return ret[0].String() + ":" +} diff --git a/pkg/bbgo/injection.go b/pkg/dynamic/inject.go similarity index 54% rename from pkg/bbgo/injection.go rename to pkg/dynamic/inject.go index cf9c47d42a..04a48599b0 100644 --- a/pkg/bbgo/injection.go +++ b/pkg/dynamic/inject.go @@ -1,31 +1,23 @@ -package bbgo +package dynamic import ( "fmt" "reflect" + "testing" + "time" "github.com/sirupsen/logrus" -) - -func isSymbolBasedStrategy(rs reflect.Value) (string, bool) { - field := rs.FieldByName("Symbol") - if !field.IsValid() { - return "", false - } - - if field.Kind() != reflect.String { - return "", false - } + "github.com/stretchr/testify/assert" - return field.String(), true -} + "github.com/c9s/bbgo/pkg/service" + "github.com/c9s/bbgo/pkg/types" +) -func hasField(rs reflect.Value, fieldName string) (field reflect.Value, ok bool) { - field = rs.FieldByName(fieldName) - return field, field.IsValid() +type testEnvironment struct { + startTime time.Time } -func injectField(rs reflect.Value, fieldName string, obj interface{}, pointerOnly bool) error { +func InjectField(rs reflect.Value, fieldName string, obj interface{}, pointerOnly bool) error { field := rs.FieldByName(fieldName) if !field.IsValid() { return nil @@ -56,10 +48,10 @@ func injectField(rs reflect.Value, fieldName string, obj interface{}, pointerOnl return nil } -// parseStructAndInject parses the struct fields and injects the objects into the corresponding fields by its type. +// ParseStructAndInject parses the struct fields and injects the objects into the corresponding fields by its type. // if the given object is a reference of an object, the type of the target field MUST BE a pointer field. // if the given object is a struct value, the type of the target field CAN BE a pointer field or a struct value field. -func parseStructAndInject(f interface{}, objects ...interface{}) error { +func ParseStructAndInject(f interface{}, objects ...interface{}) error { sv := reflect.ValueOf(f) st := reflect.TypeOf(f) @@ -139,3 +131,96 @@ func parseStructAndInject(f interface{}, objects ...interface{}) error { return nil } + +func Test_injectField(t *testing.T) { + type TT struct { + TradeService *service.TradeService + } + + // only pointer object can be set. + var tt = &TT{} + + // get the value of the pointer, or it can not be set. + var rv = reflect.ValueOf(tt).Elem() + + _, ret := HasField(rv, "TradeService") + assert.True(t, ret) + + ts := &service.TradeService{} + + err := InjectField(rv, "TradeService", ts, true) + assert.NoError(t, err) +} + +func Test_parseStructAndInject(t *testing.T) { + t.Run("skip nil", func(t *testing.T) { + ss := struct { + a int + Env *testEnvironment + }{ + a: 1, + Env: nil, + } + err := ParseStructAndInject(&ss, nil) + assert.NoError(t, err) + assert.Nil(t, ss.Env) + }) + t.Run("pointer", func(t *testing.T) { + ss := struct { + a int + Env *testEnvironment + }{ + a: 1, + Env: nil, + } + err := ParseStructAndInject(&ss, &testEnvironment{}) + assert.NoError(t, err) + assert.NotNil(t, ss.Env) + }) + + t.Run("composition", func(t *testing.T) { + type TT struct { + *service.TradeService + } + ss := TT{} + err := ParseStructAndInject(&ss, &service.TradeService{}) + assert.NoError(t, err) + assert.NotNil(t, ss.TradeService) + }) + + t.Run("struct", func(t *testing.T) { + ss := struct { + a int + Env testEnvironment + }{ + a: 1, + } + err := ParseStructAndInject(&ss, testEnvironment{ + startTime: time.Now(), + }) + assert.NoError(t, err) + assert.NotEqual(t, time.Time{}, ss.Env.startTime) + }) + t.Run("interface/any", func(t *testing.T) { + ss := struct { + Any interface{} // anything + }{ + Any: nil, + } + err := ParseStructAndInject(&ss, &testEnvironment{ + startTime: time.Now(), + }) + assert.NoError(t, err) + assert.NotNil(t, ss.Any) + }) + t.Run("interface/stringer", func(t *testing.T) { + ss := struct { + Stringer types.Stringer // stringer interface + }{ + Stringer: nil, + } + err := ParseStructAndInject(&ss, &types.Trade{}) + assert.NoError(t, err) + assert.NotNil(t, ss.Stringer) + }) +} diff --git a/pkg/dynamic/iterate.go b/pkg/dynamic/iterate.go new file mode 100644 index 0000000000..33a1283632 --- /dev/null +++ b/pkg/dynamic/iterate.go @@ -0,0 +1,96 @@ +package dynamic + +import ( + "errors" + "fmt" + "reflect" +) + +type StructFieldIterator func(tag string, ft reflect.StructField, fv reflect.Value) error + +var ErrCanNotIterateNilPointer = errors.New("can not iterate struct on a nil pointer") + +func IterateFields(obj interface{}, cb func(ft reflect.StructField, fv reflect.Value) error) error { + if obj == nil { + return errors.New("can not iterate field, given object is nil") + } + + sv := reflect.ValueOf(obj) + st := reflect.TypeOf(obj) + + if st.Kind() != reflect.Ptr { + return fmt.Errorf("f should be a pointer of a struct, %s given", st) + } + + // for pointer, check if it's nil + if sv.IsNil() { + return ErrCanNotIterateNilPointer + } + + // solve the reference + st = st.Elem() + sv = sv.Elem() + + if st.Kind() != reflect.Struct { + return fmt.Errorf("f should be a struct, %s given", st) + } + + for i := 0; i < sv.NumField(); i++ { + fv := sv.Field(i) + ft := st.Field(i) + + // skip unexported fields + if !st.Field(i).IsExported() { + continue + } + + if err := cb(ft, fv); err != nil { + return err + } + } + + return nil +} + +func IterateFieldsByTag(obj interface{}, tagName string, cb StructFieldIterator) error { + sv := reflect.ValueOf(obj) + st := reflect.TypeOf(obj) + + if st.Kind() != reflect.Ptr { + return fmt.Errorf("f should be a pointer of a struct, %s given", st) + } + + // for pointer, check if it's nil + if sv.IsNil() { + return ErrCanNotIterateNilPointer + } + + // solve the reference + st = st.Elem() + sv = sv.Elem() + + if st.Kind() != reflect.Struct { + return fmt.Errorf("f should be a struct, %s given", st) + } + + for i := 0; i < sv.NumField(); i++ { + fv := sv.Field(i) + ft := st.Field(i) + + // skip unexported fields + if !st.Field(i).IsExported() { + continue + } + + tag, ok := ft.Tag.Lookup(tagName) + if !ok { + continue + } + + if err := cb(tag, ft, fv); err != nil { + return err + } + } + + return nil +} diff --git a/pkg/dynamic/iterate_test.go b/pkg/dynamic/iterate_test.go new file mode 100644 index 0000000000..b15b74bf80 --- /dev/null +++ b/pkg/dynamic/iterate_test.go @@ -0,0 +1,44 @@ +package dynamic + +import ( + "os" + "reflect" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIterateFields(t *testing.T) { + + t.Run("basic", func(t *testing.T) { + var a = struct { + A int + B float64 + C *os.File + }{} + + cnt := 0 + err := IterateFields(&a, func(ft reflect.StructField, fv reflect.Value) error { + cnt++ + return nil + }) + assert.NoError(t, err) + assert.Equal(t, 3, cnt) + }) + + t.Run("non-ptr", func(t *testing.T) { + err := IterateFields(struct{}{}, func(ft reflect.StructField, fv reflect.Value) error { + return nil + }) + assert.Error(t, err) + }) + + t.Run("nil", func(t *testing.T) { + err := IterateFields(nil, func(ft reflect.StructField, fv reflect.Value) error { + return nil + }) + assert.Error(t, err) + }) + + +} diff --git a/pkg/dynamic/merge.go b/pkg/dynamic/merge.go new file mode 100644 index 0000000000..8e44bd3335 --- /dev/null +++ b/pkg/dynamic/merge.go @@ -0,0 +1,41 @@ +package dynamic + +import "reflect" + +// InheritStructValues merges the field value from the source struct to the dest struct. +// Only fields with the same type and the same name will be updated. +func InheritStructValues(dst, src interface{}) { + if dst == nil { + return + } + + rtA := reflect.TypeOf(dst) + srcStructType := reflect.TypeOf(src) + + rtA = rtA.Elem() + srcStructType = srcStructType.Elem() + + for i := 0; i < rtA.NumField(); i++ { + fieldType := rtA.Field(i) + fieldName := fieldType.Name + + if !fieldType.IsExported() { + continue + } + + // if there is a field with the same name + fieldSrcType, found := srcStructType.FieldByName(fieldName) + if !found { + continue + } + + // ensure that the type is the same + if fieldSrcType.Type == fieldType.Type { + srcValue := reflect.ValueOf(src).Elem().FieldByName(fieldName) + dstValue := reflect.ValueOf(dst).Elem().FieldByName(fieldName) + if (fieldType.Type.Kind() == reflect.Ptr && dstValue.IsNil()) || dstValue.IsZero() { + dstValue.Set(srcValue) + } + } + } +} diff --git a/pkg/dynamic/merge_test.go b/pkg/dynamic/merge_test.go new file mode 100644 index 0000000000..ca61355b0c --- /dev/null +++ b/pkg/dynamic/merge_test.go @@ -0,0 +1,82 @@ +package dynamic + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +type TestStrategy struct { + Symbol string `json:"symbol"` + Interval string `json:"interval"` + BaseQuantity fixedpoint.Value `json:"baseQuantity"` + MaxAssetQuantity fixedpoint.Value `json:"maxAssetQuantity"` + MinDropPercentage fixedpoint.Value `json:"minDropPercentage"` +} + +func Test_reflectMergeStructFields(t *testing.T) { + t.Run("zero value", func(t *testing.T) { + a := &TestStrategy{Symbol: "BTCUSDT"} + b := &struct{ Symbol string }{Symbol: ""} + InheritStructValues(b, a) + assert.Equal(t, "BTCUSDT", b.Symbol) + }) + + t.Run("non-zero value", func(t *testing.T) { + a := &TestStrategy{Symbol: "BTCUSDT"} + b := &struct{ Symbol string }{Symbol: "ETHUSDT"} + InheritStructValues(b, a) + assert.Equal(t, "ETHUSDT", b.Symbol, "should be the original value") + }) + + t.Run("zero embedded struct", func(t *testing.T) { + iw := types.IntervalWindow{Interval: types.Interval1h, Window: 30} + a := &struct { + types.IntervalWindow + Symbol string + }{ + IntervalWindow: iw, + Symbol: "BTCUSDT", + } + b := &struct { + Symbol string + types.IntervalWindow + }{} + InheritStructValues(b, a) + assert.Equal(t, iw, b.IntervalWindow) + assert.Equal(t, "BTCUSDT", b.Symbol) + }) + + t.Run("non-zero embedded struct", func(t *testing.T) { + iw := types.IntervalWindow{Interval: types.Interval1h, Window: 30} + a := &struct { + types.IntervalWindow + }{ + IntervalWindow: iw, + } + b := &struct { + types.IntervalWindow + }{ + IntervalWindow: types.IntervalWindow{Interval: types.Interval5m, Window: 9}, + } + InheritStructValues(b, a) + assert.Equal(t, types.IntervalWindow{Interval: types.Interval5m, Window: 9}, b.IntervalWindow) + }) + + t.Run("skip different type but the same name", func(t *testing.T) { + a := &struct { + A float64 + }{ + A: 1.99, + } + b := &struct { + A string + }{} + InheritStructValues(b, a) + assert.Equal(t, "", b.A) + assert.Equal(t, 1.99, a.A) + }) +} diff --git a/pkg/dynamic/print_config.go b/pkg/dynamic/print_config.go new file mode 100644 index 0000000000..a60d30618c --- /dev/null +++ b/pkg/dynamic/print_config.go @@ -0,0 +1,132 @@ +package dynamic + +import ( + "encoding/json" + "fmt" + "io" + "reflect" + "sort" + "strings" + + "github.com/fatih/color" + "github.com/jedib0t/go-pretty/v6/table" + "github.com/jedib0t/go-pretty/v6/text" + + "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/util" +) + +func DefaultWhiteList() []string { + return []string{"Window", "RightWindow", "Interval", "Symbol", "Source"} +} + +// @param s: strategy object +// @param f: io.Writer used for writing the config dump +// @param style: pretty print table style. Use NewDefaultTableStyle() to get default one. +// @param withColor: whether to print with color +// @param whiteLists: fields to be printed out from embedded struct (1st layer only) +func PrintConfig(s interface{}, f io.Writer, style *table.Style, withColor bool, whiteLists ...string) { + t := table.NewWriter() + var write func(io.Writer, string, ...interface{}) + + if withColor { + write = color.New(color.FgHiYellow).FprintfFunc() + } else { + write = func(a io.Writer, format string, args ...interface{}) { + fmt.Fprintf(a, format, args...) + } + } + if style != nil { + t.SetOutputMirror(f) + t.SetStyle(*style) + t.SetColumnConfigs([]table.ColumnConfig{ + {Number: 4, WidthMax: 50, WidthMaxEnforcer: text.WrapText}, + }) + t.AppendHeader(table.Row{"json", "struct field name", "type", "value"}) + } + write(f, "---- %s Settings ---\n", CallID(s)) + + embeddedWhiteSet := map[string]struct{}{} + for _, whiteList := range whiteLists { + embeddedWhiteSet[whiteList] = struct{}{} + } + + redundantSet := map[string]struct{}{} + + var rows []table.Row + + val := reflect.ValueOf(s) + + if val.Type().Kind() == util.Pointer { + val = val.Elem() + } + var values types.JsonArr + for i := 0; i < val.Type().NumField(); i++ { + t := val.Type().Field(i) + if !t.IsExported() { + continue + } + fieldName := t.Name + switch jsonTag := t.Tag.Get("json"); jsonTag { + case "-": + case "": + // we only fetch fields from the first layer of the embedded struct + if t.Anonymous { + var target reflect.Type + var field reflect.Value + if t.Type.Kind() == util.Pointer { + target = t.Type.Elem() + field = val.Field(i).Elem() + } else { + target = t.Type + field = val.Field(i) + } + for j := 0; j < target.NumField(); j++ { + tt := target.Field(j) + if !tt.IsExported() { + continue + } + fieldName := tt.Name + if _, ok := embeddedWhiteSet[fieldName]; !ok { + continue + } + if jtag := tt.Tag.Get("json"); jtag != "" && jtag != "-" { + name := strings.Split(jtag, ",")[0] + if _, ok := redundantSet[name]; ok { + continue + } + redundantSet[name] = struct{}{} + value := field.Field(j).Interface() + if e, err := json.Marshal(value); err == nil { + value = string(e) + } + values = append(values, types.JsonStruct{Key: fieldName, Json: name, Type: tt.Type.String(), Value: value}) + } + } + } + default: + name := strings.Split(jsonTag, ",")[0] + if _, ok := redundantSet[name]; ok { + continue + } + redundantSet[name] = struct{}{} + value := val.Field(i).Interface() + if e, err := json.Marshal(value); err == nil { + value = string(e) + } + values = append(values, types.JsonStruct{Key: fieldName, Json: name, Type: t.Type.String(), Value: value}) + } + } + sort.Sort(values) + for _, value := range values { + if style != nil { + rows = append(rows, table.Row{value.Json, value.Key, value.Type, value.Value}) + } else { + write(f, "%s: %v\n", value.Json, value.Value) + } + } + if style != nil { + t.AppendRows(rows) + t.Render() + } +} diff --git a/pkg/dynamic/print_strategy.go b/pkg/dynamic/print_strategy.go new file mode 100644 index 0000000000..5e1ff70620 --- /dev/null +++ b/pkg/dynamic/print_strategy.go @@ -0,0 +1,96 @@ +package dynamic + +import ( + "fmt" + "io" + "reflect" + "unsafe" + + "github.com/c9s/bbgo/pkg/util" +) + +// @param s: strategy object +// @param f: io.Writer used for writing the strategy dump +// @param seriesLength: if exist, the first value will be chosen to be the length of data from series to be printed out +// default to 1 when not exist or the value is invalid +func ParamDump(s interface{}, f io.Writer, seriesLength ...int) { + length := 1 + if len(seriesLength) > 0 && seriesLength[0] > 0 { + length = seriesLength[0] + } + val := reflect.ValueOf(s) + if val.Type().Kind() == util.Pointer { + val = val.Elem() + } + for i := 0; i < val.Type().NumField(); i++ { + t := val.Type().Field(i) + if ig := t.Tag.Get("ignore"); ig == "true" { + continue + } + field := val.Field(i) + if t.IsExported() || t.Anonymous || t.Type.Kind() == reflect.Func || t.Type.Kind() == reflect.Chan { + continue + } + fieldName := t.Name + typeName := field.Type().String() + value := reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem() + isSeries := true + lastFunc := value.MethodByName("Last") + isSeries = isSeries && lastFunc.IsValid() + lengthFunc := value.MethodByName("Length") + isSeries = isSeries && lengthFunc.IsValid() + indexFunc := value.MethodByName("Index") + isSeries = isSeries && indexFunc.IsValid() + + stringFunc := value.MethodByName("String") + canString := stringFunc.IsValid() + + if isSeries { + l := int(lengthFunc.Call(nil)[0].Int()) + if l >= length { + fmt.Fprintf(f, "%s: Series[..., %.4f", fieldName, indexFunc.Call([]reflect.Value{reflect.ValueOf(length - 1)})[0].Float()) + for j := length - 2; j >= 0; j-- { + fmt.Fprintf(f, ", %.4f", indexFunc.Call([]reflect.Value{reflect.ValueOf(j)})[0].Float()) + } + fmt.Fprintf(f, "]\n") + } else if l > 0 { + fmt.Fprintf(f, "%s: Series[%.4f", fieldName, indexFunc.Call([]reflect.Value{reflect.ValueOf(l - 1)})[0].Float()) + for j := l - 2; j >= 0; j-- { + fmt.Fprintf(f, ", %.4f", indexFunc.Call([]reflect.Value{reflect.ValueOf(j)})[0].Float()) + } + fmt.Fprintf(f, "]\n") + } else { + fmt.Fprintf(f, "%s: Series[]\n", fieldName) + } + } else if canString { + fmt.Fprintf(f, "%s: %s\n", fieldName, stringFunc.Call(nil)[0].String()) + } else if CanInt(field) { + fmt.Fprintf(f, "%s: %d\n", fieldName, field.Int()) + } else if field.CanConvert(reflect.TypeOf(float64(0))) { + fmt.Fprintf(f, "%s: %.4f\n", fieldName, field.Float()) + } else if field.CanInterface() { + fmt.Fprintf(f, "%s: %v", fieldName, field.Interface()) + } else if field.Type().Kind() == reflect.Map { + fmt.Fprintf(f, "%s: {", fieldName) + iter := value.MapRange() + for iter.Next() { + k := iter.Key().Interface() + v := iter.Value().Interface() + fmt.Fprintf(f, "%v: %v, ", k, v) + } + fmt.Fprintf(f, "}\n") + } else if field.Type().Kind() == reflect.Slice { + fmt.Fprintf(f, "%s: [", fieldName) + l := field.Len() + if l > 0 { + fmt.Fprintf(f, "%v", field.Index(0)) + } + for j := 1; j < field.Len(); j++ { + fmt.Fprintf(f, ", %v", field.Index(j)) + } + fmt.Fprintf(f, "]\n") + } else { + fmt.Fprintf(f, "%s(%s): %s\n", fieldName, typeName, field.String()) + } + } +} diff --git a/pkg/dynamic/typevalue.go b/pkg/dynamic/typevalue.go new file mode 100644 index 0000000000..3ca3f1c83f --- /dev/null +++ b/pkg/dynamic/typevalue.go @@ -0,0 +1,24 @@ +package dynamic + +import "reflect" + +// https://github.com/xiaojun207/go-base-utils/blob/master/utils/Clone.go +func NewTypeValueInterface(typ reflect.Type) interface{} { + if typ.Kind() == reflect.Ptr { + typ = typ.Elem() + dst := reflect.New(typ).Elem() + return dst.Addr().Interface() + } + dst := reflect.New(typ) + return dst.Interface() +} + +// ToReflectValues convert the go objects into reflect.Value slice +func ToReflectValues(args ...interface{}) (values []reflect.Value) { + for i := range args { + arg := args[i] + values = append(values, reflect.ValueOf(arg)) + } + + return values +} diff --git a/pkg/exchange/batch/batch.go b/pkg/exchange/batch/batch.go deleted file mode 100644 index 325c508b2d..0000000000 --- a/pkg/exchange/batch/batch.go +++ /dev/null @@ -1,165 +0,0 @@ -package batch - -import ( - "context" - "sort" - "time" - - "github.com/pkg/errors" - - "github.com/sirupsen/logrus" - "golang.org/x/time/rate" - - "github.com/c9s/bbgo/pkg/types" -) - -type KLineBatchQuery struct { - types.Exchange -} - -func (e KLineBatchQuery) Query(ctx context.Context, symbol string, interval types.Interval, startTime, endTime time.Time) (c chan []types.KLine, errC chan error) { - c = make(chan []types.KLine, 1000) - errC = make(chan error, 1) - - go func() { - defer close(c) - defer close(errC) - - tryQueryKlineTimes := 0 - - var currentTime = startTime - for currentTime.Before(endTime) { - kLines, err := e.QueryKLines(ctx, symbol, interval, types.KLineQueryOptions{ - StartTime: ¤tTime, - EndTime: &endTime, - }) - - if err != nil { - errC <- err - return - } - - sort.Slice(kLines, func(i, j int) bool { return kLines[i].StartTime.Unix() < kLines[j].StartTime.Unix() }) - - if len(kLines) == 0 { - return - } else if len(kLines) == 1 && kLines[0].StartTime.Unix() == currentTime.Unix() { - return - } - - tryQueryKlineTimes++ - const BatchSize = 200 - - var batchKLines = make([]types.KLine, 0, BatchSize) - for _, kline := range kLines { - // ignore any kline before the given start time of the batch query - if currentTime.Unix() != startTime.Unix() && kline.StartTime.Before(currentTime) { - continue - } - - // if there is a kline after the endTime of the batch query, it means the data is out of scope, we should exit - if kline.StartTime.After(endTime) || kline.EndTime.After(endTime) { - if len(batchKLines) != 0 { - c <- batchKLines - batchKLines = nil - } - return - } - - batchKLines = append(batchKLines, kline) - - if len(batchKLines) == BatchSize { - c <- batchKLines - batchKLines = nil - } - - // The issue is in FTX, prev endtime = next start time , so if add 1 ms , it would query forever. - currentTime = kline.StartTime.Time() - tryQueryKlineTimes = 0 - } - - // push the rest klines in the buffer - if len(batchKLines) > 0 { - c <- batchKLines - batchKLines = nil - } - - if tryQueryKlineTimes > 10 { // it means loop 10 times - errC <- errors.Errorf("there's a dead loop in batch.go#Query , symbol: %s , interval: %s, startTime:%s ", symbol, interval, startTime.String()) - return - } - } - }() - - return c, errC -} - -type RewardBatchQuery struct { - Service types.ExchangeRewardService -} - -func (q *RewardBatchQuery) Query(ctx context.Context, startTime, endTime time.Time) (c chan types.Reward, errC chan error) { - c = make(chan types.Reward, 500) - errC = make(chan error, 1) - - go func() { - limiter := rate.NewLimiter(rate.Every(5*time.Second), 2) // from binance (original 1200, use 1000 for safety) - - defer close(c) - defer close(errC) - - lastID := "" - rewardKeys := make(map[string]struct{}, 500) - - for startTime.Before(endTime) { - if err := limiter.Wait(ctx); err != nil { - logrus.WithError(err).Error("rate limit error") - } - - logrus.Infof("batch querying rewards %s <=> %s", startTime, endTime) - - rewards, err := q.Service.QueryRewards(ctx, startTime) - if err != nil { - errC <- err - return - } - - // empty data - if len(rewards) == 0 { - return - } - - // there is no new data - if len(rewards) == 1 && rewards[0].UUID == lastID { - return - } - - newCnt := 0 - for _, o := range rewards { - if _, ok := rewardKeys[o.UUID]; ok { - continue - } - - if o.CreatedAt.Time().After(endTime) { - // stop batch query - return - } - - newCnt++ - c <- o - rewardKeys[o.UUID] = struct{}{} - } - - if newCnt == 0 { - return - } - - end := len(rewards) - 1 - startTime = rewards[end].CreatedAt.Time() - lastID = rewards[end].UUID - } - - }() - - return c, errC -} diff --git a/pkg/exchange/batch/batch_test.go b/pkg/exchange/batch/batch_test.go new file mode 100644 index 0000000000..389c4ab2a8 --- /dev/null +++ b/pkg/exchange/batch/batch_test.go @@ -0,0 +1 @@ +package batch diff --git a/pkg/exchange/batch/closedorders.go b/pkg/exchange/batch/closedorders.go index e0f78867c7..3af58e2d5f 100644 --- a/pkg/exchange/batch/closedorders.go +++ b/pkg/exchange/batch/closedorders.go @@ -2,93 +2,36 @@ package batch import ( "context" - "sort" + "strconv" "time" - "github.com/sirupsen/logrus" - "golang.org/x/time/rate" - "github.com/c9s/bbgo/pkg/types" ) type ClosedOrderBatchQuery struct { - types.Exchange + types.ExchangeTradeHistoryService } -func (e ClosedOrderBatchQuery) Query(ctx context.Context, symbol string, startTime, endTime time.Time, lastOrderID uint64) (c chan types.Order, errC chan error) { - c = make(chan types.Order, 500) - errC = make(chan error, 1) - - tradeHistoryService, ok := e.Exchange.(types.ExchangeTradeHistoryService) - if !ok { - defer close(c) - defer close(errC) - // skip exchanges that does not support trading history services - logrus.Warnf("exchange %s does not implement ExchangeTradeHistoryService, skip syncing closed orders (ClosedOrderBatchQuery.Query) ", e.Exchange.Name()) - return c, errC - } - - go func() { - limiter := rate.NewLimiter(rate.Every(5*time.Second), 2) // from binance (original 1200, use 1000 for safety) - - defer close(c) - defer close(errC) - - orderIDs := make(map[uint64]struct{}, 500) - if lastOrderID > 0 { - orderIDs[lastOrderID] = struct{}{} - } - - for startTime.Before(endTime) { - if err := limiter.Wait(ctx); err != nil { - logrus.WithError(err).Error("rate limit error") - } - - logrus.Infof("batch querying %s closed orders %s <=> %s", symbol, startTime, endTime) - - orders, err := tradeHistoryService.QueryClosedOrders(ctx, symbol, startTime, endTime, lastOrderID) - if err != nil { - errC <- err - return +func (q *ClosedOrderBatchQuery) Query(ctx context.Context, symbol string, startTime, endTime time.Time, lastOrderID uint64) (c chan types.Order, errC chan error) { + query := &AsyncTimeRangedBatchQuery{ + Type: types.Order{}, + Q: func(startTime, endTime time.Time) (interface{}, error) { + orders, err := q.ExchangeTradeHistoryService.QueryClosedOrders(ctx, symbol, startTime, endTime, lastOrderID) + return orders, err + }, + T: func(obj interface{}) time.Time { + return time.Time(obj.(types.Order).CreationTime) + }, + ID: func(obj interface{}) string { + order := obj.(types.Order) + if order.OrderID > lastOrderID { + lastOrderID = order.OrderID } - for _, o := range orders { - logrus.Infof("%+v", o) - } - - if len(orders) == 0 { - return - } else if len(orders) > 0 { - allExists := true - for _, o := range orders { - if _, exists := orderIDs[o.OrderID]; !exists { - allExists = false - break - } - } - if allExists { - return - } - } - - // sort orders by time in ascending order - sort.Slice(orders, func(i, j int) bool { - return orders[i].CreationTime.Before(time.Time(orders[j].CreationTime)) - }) - - for _, o := range orders { - if _, ok := orderIDs[o.OrderID]; ok { - continue - } - - c <- o - startTime = o.CreationTime.Time() - lastOrderID = o.OrderID - orderIDs[o.OrderID] = struct{}{} - } - } - - }() + return strconv.FormatUint(order.OrderID, 10) + }, + } + c = make(chan types.Order, 100) + errC = query.Query(ctx, c, startTime, endTime) return c, errC } - diff --git a/pkg/exchange/batch/deposit.go b/pkg/exchange/batch/deposit.go new file mode 100644 index 0000000000..fdb471782b --- /dev/null +++ b/pkg/exchange/batch/deposit.go @@ -0,0 +1,36 @@ +package batch + +import ( + "context" + "time" + + "golang.org/x/time/rate" + + "github.com/c9s/bbgo/pkg/types" +) + +type DepositBatchQuery struct { + types.ExchangeTransferService +} + +func (e *DepositBatchQuery) Query(ctx context.Context, asset string, startTime, endTime time.Time) (c chan types.Deposit, errC chan error) { + query := &AsyncTimeRangedBatchQuery{ + Type: types.Deposit{}, + Limiter: rate.NewLimiter(rate.Every(5*time.Second), 2), + JumpIfEmpty: time.Hour * 24 * 80, + Q: func(startTime, endTime time.Time) (interface{}, error) { + return e.ExchangeTransferService.QueryDepositHistory(ctx, asset, startTime, endTime) + }, + T: func(obj interface{}) time.Time { + return time.Time(obj.(types.Deposit).Time) + }, + ID: func(obj interface{}) string { + deposit := obj.(types.Deposit) + return deposit.TransactionID + }, + } + + c = make(chan types.Deposit, 100) + errC = query.Query(ctx, c, startTime, endTime) + return c, errC +} diff --git a/pkg/exchange/batch/kline.go b/pkg/exchange/batch/kline.go new file mode 100644 index 0000000000..a4053fe342 --- /dev/null +++ b/pkg/exchange/batch/kline.go @@ -0,0 +1,37 @@ +package batch + +import ( + "context" + "strconv" + "time" + + "github.com/c9s/bbgo/pkg/types" +) + +type KLineBatchQuery struct { + types.Exchange +} + +func (e *KLineBatchQuery) Query(ctx context.Context, symbol string, interval types.Interval, startTime, endTime time.Time) (c chan types.KLine, errC chan error) { + query := &AsyncTimeRangedBatchQuery{ + Type: types.KLine{}, + Limiter: nil, // the rate limiter is handled in the exchange query method + Q: func(startTime, endTime time.Time) (interface{}, error) { + return e.Exchange.QueryKLines(ctx, symbol, interval, types.KLineQueryOptions{ + StartTime: &startTime, + EndTime: &endTime, + }) + }, + T: func(obj interface{}) time.Time { + return time.Time(obj.(types.KLine).StartTime) + }, + ID: func(obj interface{}) string { + kline := obj.(types.KLine) + return strconv.FormatInt(kline.StartTime.UnixMilli(), 10) + }, + } + + c = make(chan types.KLine, 3000) + errC = query.Query(ctx, c, startTime, endTime) + return c, errC +} diff --git a/pkg/exchange/batch/margin_interest.go b/pkg/exchange/batch/margin_interest.go new file mode 100644 index 0000000000..4e8224bc9e --- /dev/null +++ b/pkg/exchange/batch/margin_interest.go @@ -0,0 +1,36 @@ +package batch + +import ( + "context" + "time" + + "golang.org/x/time/rate" + + "github.com/c9s/bbgo/pkg/types" +) + +type MarginInterestBatchQuery struct { + types.MarginHistory +} + +func (e *MarginInterestBatchQuery) Query(ctx context.Context, asset string, startTime, endTime time.Time) (c chan types.MarginInterest, errC chan error) { + query := &AsyncTimeRangedBatchQuery{ + Type: types.MarginInterest{}, + Limiter: rate.NewLimiter(rate.Every(5*time.Second), 2), + JumpIfEmpty: time.Hour * 24 * 30, + Q: func(startTime, endTime time.Time) (interface{}, error) { + return e.QueryInterestHistory(ctx, asset, &startTime, &endTime) + }, + T: func(obj interface{}) time.Time { + return time.Time(obj.(types.MarginInterest).Time) + }, + ID: func(obj interface{}) string { + interest := obj.(types.MarginInterest) + return interest.Time.String() + }, + } + + c = make(chan types.MarginInterest, 100) + errC = query.Query(ctx, c, startTime, endTime) + return c, errC +} diff --git a/pkg/exchange/batch/margin_liquidation.go b/pkg/exchange/batch/margin_liquidation.go new file mode 100644 index 0000000000..3726d18913 --- /dev/null +++ b/pkg/exchange/batch/margin_liquidation.go @@ -0,0 +1,37 @@ +package batch + +import ( + "context" + "strconv" + "time" + + "golang.org/x/time/rate" + + "github.com/c9s/bbgo/pkg/types" +) + +type MarginLiquidationBatchQuery struct { + types.MarginHistory +} + +func (e *MarginLiquidationBatchQuery) Query(ctx context.Context, startTime, endTime time.Time) (c chan types.MarginLiquidation, errC chan error) { + query := &AsyncTimeRangedBatchQuery{ + Type: types.MarginLiquidation{}, + Limiter: rate.NewLimiter(rate.Every(5*time.Second), 2), + JumpIfEmpty: time.Hour * 24 * 30, + Q: func(startTime, endTime time.Time) (interface{}, error) { + return e.QueryLiquidationHistory(ctx, &startTime, &endTime) + }, + T: func(obj interface{}) time.Time { + return time.Time(obj.(types.MarginLiquidation).UpdatedTime) + }, + ID: func(obj interface{}) string { + liquidation := obj.(types.MarginLiquidation) + return strconv.FormatUint(liquidation.OrderID, 10) + }, + } + + c = make(chan types.MarginLiquidation, 100) + errC = query.Query(ctx, c, startTime, endTime) + return c, errC +} diff --git a/pkg/exchange/batch/margin_loan.go b/pkg/exchange/batch/margin_loan.go new file mode 100644 index 0000000000..a32c7ea15e --- /dev/null +++ b/pkg/exchange/batch/margin_loan.go @@ -0,0 +1,37 @@ +package batch + +import ( + "context" + "strconv" + "time" + + "golang.org/x/time/rate" + + "github.com/c9s/bbgo/pkg/types" +) + +type MarginLoanBatchQuery struct { + types.MarginHistory +} + +func (e *MarginLoanBatchQuery) Query(ctx context.Context, asset string, startTime, endTime time.Time) (c chan types.MarginLoan, errC chan error) { + query := &AsyncTimeRangedBatchQuery{ + Type: types.MarginLoan{}, + Limiter: rate.NewLimiter(rate.Every(5*time.Second), 2), + JumpIfEmpty: time.Hour * 24 * 30, + Q: func(startTime, endTime time.Time) (interface{}, error) { + return e.QueryLoanHistory(ctx, asset, &startTime, &endTime) + }, + T: func(obj interface{}) time.Time { + return time.Time(obj.(types.MarginLoan).Time) + }, + ID: func(obj interface{}) string { + loan := obj.(types.MarginLoan) + return strconv.FormatUint(loan.TransactionID, 10) + }, + } + + c = make(chan types.MarginLoan, 100) + errC = query.Query(ctx, c, startTime, endTime) + return c, errC +} diff --git a/pkg/exchange/batch/margin_repay.go b/pkg/exchange/batch/margin_repay.go new file mode 100644 index 0000000000..a30ea12085 --- /dev/null +++ b/pkg/exchange/batch/margin_repay.go @@ -0,0 +1,37 @@ +package batch + +import ( + "context" + "strconv" + "time" + + "golang.org/x/time/rate" + + "github.com/c9s/bbgo/pkg/types" +) + +type MarginRepayBatchQuery struct { + types.MarginHistory +} + +func (e *MarginRepayBatchQuery) Query(ctx context.Context, asset string, startTime, endTime time.Time) (c chan types.MarginRepay, errC chan error) { + query := &AsyncTimeRangedBatchQuery{ + Type: types.MarginRepay{}, + Limiter: rate.NewLimiter(rate.Every(5*time.Second), 2), + JumpIfEmpty: time.Hour * 24 * 30, + Q: func(startTime, endTime time.Time) (interface{}, error) { + return e.QueryRepayHistory(ctx, asset, &startTime, &endTime) + }, + T: func(obj interface{}) time.Time { + return time.Time(obj.(types.MarginRepay).Time) + }, + ID: func(obj interface{}) string { + loan := obj.(types.MarginRepay) + return strconv.FormatUint(loan.TransactionID, 10) + }, + } + + c = make(chan types.MarginRepay, 100) + errC = query.Query(ctx, c, startTime, endTime) + return c, errC +} diff --git a/pkg/exchange/batch/reward.go b/pkg/exchange/batch/reward.go new file mode 100644 index 0000000000..07d39a11dc --- /dev/null +++ b/pkg/exchange/batch/reward.go @@ -0,0 +1,34 @@ +package batch + +import ( + "context" + "time" + + "golang.org/x/time/rate" + + "github.com/c9s/bbgo/pkg/types" +) + +type RewardBatchQuery struct { + Service types.ExchangeRewardService +} + +func (q *RewardBatchQuery) Query(ctx context.Context, startTime, endTime time.Time) (c chan types.Reward, errC chan error) { + query := &AsyncTimeRangedBatchQuery{ + Type: types.Reward{}, + Limiter: rate.NewLimiter(rate.Every(5*time.Second), 2), + Q: func(startTime, endTime time.Time) (interface{}, error) { + return q.Service.QueryRewards(ctx, startTime) + }, + T: func(obj interface{}) time.Time { + return time.Time(obj.(types.Reward).CreatedAt) + }, + ID: func(obj interface{}) string { + return obj.(types.Reward).UUID + }, + } + + c = make(chan types.Reward, 500) + errC = query.Query(ctx, c, startTime, endTime) + return c, errC +} diff --git a/pkg/exchange/batch/time_range_query.go b/pkg/exchange/batch/time_range_query.go new file mode 100644 index 0000000000..f78afbcac5 --- /dev/null +++ b/pkg/exchange/batch/time_range_query.go @@ -0,0 +1,124 @@ +package batch + +import ( + "context" + "reflect" + "sort" + "time" + + "github.com/sirupsen/logrus" + "golang.org/x/time/rate" + + "github.com/c9s/bbgo/pkg/util" +) + +var log = logrus.WithField("component", "batch") + +type AsyncTimeRangedBatchQuery struct { + // Type is the object type of the result + Type interface{} + + // Limiter is the rate limiter for each query + Limiter *rate.Limiter + + // Q is the remote query function + Q func(startTime, endTime time.Time) (interface{}, error) + + // T function returns time of an object + T func(obj interface{}) time.Time + + // ID returns the ID of the object + ID func(obj interface{}) string + + // JumpIfEmpty jump the startTime + duration when the result is empty + JumpIfEmpty time.Duration +} + +func (q *AsyncTimeRangedBatchQuery) Query(ctx context.Context, ch interface{}, since, until time.Time) chan error { + errC := make(chan error, 1) + cRef := reflect.ValueOf(ch) + // cRef := reflect.MakeChan(reflect.TypeOf(q.Type), 100) + startTime := since + endTime := until + + go func() { + defer cRef.Close() + defer close(errC) + + idMap := make(map[string]struct{}, 100) + for startTime.Before(endTime) { + if q.Limiter != nil { + if err := q.Limiter.Wait(ctx); err != nil { + errC <- err + return + } + } + + log.Debugf("batch querying %T: %v <=> %v", q.Type, startTime, endTime) + + queryProfiler := util.StartTimeProfile("remoteQuery") + + sliceInf, err := q.Q(startTime, endTime) + if err != nil { + errC <- err + return + } + + listRef := reflect.ValueOf(sliceInf) + listLen := listRef.Len() + log.Debugf("batch querying %T: %d remote records", q.Type, listLen) + + queryProfiler.StopAndLog(log.Debugf) + + if listLen == 0 { + if q.JumpIfEmpty > 0 { + startTime = startTime.Add(q.JumpIfEmpty) + + log.Debugf("batch querying %T: empty records jump to %s", q.Type, startTime) + continue + } + + log.Debugf("batch querying %T: empty records, query is completed", q.Type) + return + } + + // sort by time + sort.Slice(listRef.Interface(), func(i, j int) bool { + a := listRef.Index(i) + b := listRef.Index(j) + tA := q.T(a.Interface()) + tB := q.T(b.Interface()) + return tA.Before(tB) + }) + + sentAny := false + for i := 0; i < listLen; i++ { + item := listRef.Index(i) + entryTime := q.T(item.Interface()) + if entryTime.Before(since) || entryTime.After(until) { + continue + } + + obj := item.Interface() + id := q.ID(obj) + if _, exists := idMap[id]; exists { + log.Debugf("batch querying %T: ignore duplicated record, id = %s", q.Type, id) + continue + } + + idMap[id] = struct{}{} + + cRef.Send(item) + sentAny = true + startTime = entryTime + } + + if !sentAny { + log.Debugf("batch querying %T: %d/%d records are not sent", q.Type, listLen, listLen) + return + } + } + }() + + return errC +} diff --git a/pkg/exchange/batch/time_range_query_test.go b/pkg/exchange/batch/time_range_query_test.go new file mode 100644 index 0000000000..e3d6634e0b --- /dev/null +++ b/pkg/exchange/batch/time_range_query_test.go @@ -0,0 +1,45 @@ +package batch + +import ( + "context" + "strconv" + "testing" + "time" +) + +func Test_TimeRangedQuery(t *testing.T) { + startTime := time.Date(2021, time.January, 1, 0, 0, 0, 0, time.UTC) + endTime := time.Date(2021, time.January, 2, 0, 0, 0, 0, time.UTC) + q := &AsyncTimeRangedBatchQuery{ + Type: time.Time{}, + T: func(obj interface{}) time.Time { + return obj.(time.Time) + }, + ID: func(obj interface{}) string { + return strconv.FormatInt(obj.(time.Time).UnixMilli(), 10) + }, + Q: func(startTime, endTime time.Time) (interface{}, error) { + var cnt = 0 + var data []time.Time + for startTime.Before(endTime) && cnt < 5 { + d := startTime + data = append(data, d) + cnt++ + startTime = startTime.Add(time.Minute) + } + t.Logf("data: %v", data) + return data, nil + }, + } + + ch := make(chan time.Time, 100) + + // consumer + go func() { + for d := range ch { + _ = d + } + }() + errC := q.Query(context.Background(), ch, startTime, endTime) + <-errC +} diff --git a/pkg/exchange/batch/trade.go b/pkg/exchange/batch/trade.go index 3757424e73..39fc5bd410 100644 --- a/pkg/exchange/batch/trade.go +++ b/pkg/exchange/batch/trade.go @@ -2,113 +2,47 @@ package batch import ( "context" - "errors" "time" - "github.com/sirupsen/logrus" - "golang.org/x/time/rate" - "github.com/c9s/bbgo/pkg/types" ) +var closedErrChan = make(chan error) + +func init() { + close(closedErrChan) +} + type TradeBatchQuery struct { - types.Exchange + types.ExchangeTradeHistoryService } func (e TradeBatchQuery) Query(ctx context.Context, symbol string, options *types.TradeQueryOptions) (c chan types.Trade, errC chan error) { - c = make(chan types.Trade, 500) - errC = make(chan error, 1) - - tradeHistoryService, ok := e.Exchange.(types.ExchangeTradeHistoryService) - if !ok { - close(errC) - close(c) - // skip exchanges that does not support trading history services - logrus.Warnf("exchange %s does not implement ExchangeTradeHistoryService, skip syncing closed orders (TradeBatchQuery.Query)", e.Exchange.Name()) - return c, errC - } - - if options.StartTime == nil { - - errC <- errors.New("start time is required for syncing trades") - close(errC) - close(c) - return c, errC + if options.EndTime == nil { + now := time.Now() + options.EndTime = &now } - var lastTradeID = options.LastTradeID - var startTime = *options.StartTime - var endTime = *options.EndTime - - go func() { - limiter := rate.NewLimiter(rate.Every(5*time.Second), 2) // from binance (original 1200, use 1000 for safety) - - defer close(c) - defer close(errC) - - var tradeKeys = map[types.TradeKey]struct{}{} - - for startTime.Before(endTime) { - if err := limiter.Wait(ctx); err != nil { - logrus.WithError(err).Error("rate limit error") + startTime := *options.StartTime + endTime := *options.EndTime + query := &AsyncTimeRangedBatchQuery{ + Type: types.Trade{}, + Q: func(startTime, endTime time.Time) (interface{}, error) { + return e.ExchangeTradeHistoryService.QueryTrades(ctx, symbol, options) + }, + T: func(obj interface{}) time.Time { + return time.Time(obj.(types.Trade).Time) + }, + ID: func(obj interface{}) string { + trade := obj.(types.Trade) + if trade.ID > options.LastTradeID { + options.LastTradeID = trade.ID } + return trade.Key().String() + }, + } - logrus.Infof("querying %s trades from id=%d limit=%d between %s <=> %s", symbol, lastTradeID, options.Limit, startTime, endTime) - - var err error - var trades []types.Trade - - trades, err = tradeHistoryService.QueryTrades(ctx, symbol, &types.TradeQueryOptions{ - StartTime: options.StartTime, - LastTradeID: lastTradeID, - }) - - // sort trades by time in ascending order - types.SortTradesAscending(trades) - - if err != nil { - errC <- err - return - } - - // if all trades are duplicated or empty, we end the batch query - if len(trades) == 0 { - return - } - - if len(trades) > 0 { - allExists := true - for _, td := range trades { - k := td.Key() - if _, exists := tradeKeys[k]; !exists { - allExists = false - break - } - } - if allExists { - return - } - } - - for _, td := range trades { - key := td.Key() - - logrus.Debugf("checking trade key: %v trade: %+v", key, td) - - if _, ok := tradeKeys[key]; ok { - logrus.Debugf("ignore duplicated trade: %+v", key) - continue - } - - lastTradeID = td.ID - startTime = time.Time(td.Time) - tradeKeys[key] = struct{}{} - - // ignore the first trade if last TradeID is given - c <- td - } - } - }() - + c = make(chan types.Trade, 100) + errC = query.Query(ctx, c, startTime, endTime) return c, errC } diff --git a/pkg/exchange/batch/withdraw.go b/pkg/exchange/batch/withdraw.go new file mode 100644 index 0000000000..36fc374896 --- /dev/null +++ b/pkg/exchange/batch/withdraw.go @@ -0,0 +1,36 @@ +package batch + +import ( + "context" + "time" + + "golang.org/x/time/rate" + + "github.com/c9s/bbgo/pkg/types" +) + +type WithdrawBatchQuery struct { + types.ExchangeTransferService +} + +func (e *WithdrawBatchQuery) Query(ctx context.Context, asset string, startTime, endTime time.Time) (c chan types.Withdraw, errC chan error) { + query := &AsyncTimeRangedBatchQuery{ + Type: types.Withdraw{}, + Limiter: rate.NewLimiter(rate.Every(5*time.Second), 2), + JumpIfEmpty: time.Hour * 24 * 80, + Q: func(startTime, endTime time.Time) (interface{}, error) { + return e.ExchangeTransferService.QueryWithdrawHistory(ctx, asset, startTime, endTime) + }, + T: func(obj interface{}) time.Time { + return time.Time(obj.(types.Withdraw).ApplyTime) + }, + ID: func(obj interface{}) string { + withdraw := obj.(types.Withdraw) + return withdraw.TransactionID + }, + } + + c = make(chan types.Withdraw, 100) + errC = query.Query(ctx, c, startTime, endTime) + return c, errC +} diff --git a/pkg/exchange/batch/withdraw_test.go b/pkg/exchange/batch/withdraw_test.go new file mode 100644 index 0000000000..68e67e9f2f --- /dev/null +++ b/pkg/exchange/batch/withdraw_test.go @@ -0,0 +1,38 @@ +package batch + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/exchange/binance" + "github.com/c9s/bbgo/pkg/testutil" +) + +func TestWithdrawBatchQuery(t *testing.T) { + key, secret, ok := testutil.IntegrationTestConfigured(t, "BINANCE") + if !ok { + t.Skip("binance api is not set") + } + + ex := binance.New(key, secret) + q := WithdrawBatchQuery{ + ExchangeTransferService: ex, + } + + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) + defer cancel() + now := time.Now() + startTime := now.AddDate(0, -6, 0) + endTime := now + dataC, errC := q.Query(ctx, "", startTime, endTime) + + for withdraw := range dataC { + t.Logf("%+v", withdraw) + } + + err := <-errC + assert.NoError(t, err) +} diff --git a/pkg/exchange/binance/binanceapi/alias.go b/pkg/exchange/binance/binanceapi/alias.go new file mode 100644 index 0000000000..f17052bdf3 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/alias.go @@ -0,0 +1,34 @@ +package binanceapi + +import ( + "github.com/adshao/go-binance/v2" +) + +type SideType = binance.SideType + +const SideTypeBuy = binance.SideTypeBuy +const SideTypeSell = binance.SideTypeSell + +type OrderType = binance.OrderType + +const ( + OrderTypeLimit OrderType = binance.OrderTypeLimit + OrderTypeMarket OrderType = binance.OrderTypeMarket + OrderTypeLimitMaker OrderType = binance.OrderTypeLimitMaker + OrderTypeStopLoss OrderType = binance.OrderTypeStopLoss + OrderTypeStopLossLimit OrderType = binance.OrderTypeStopLossLimit + OrderTypeTakeProfit OrderType = binance.OrderTypeTakeProfit + OrderTypeTakeProfitLimit OrderType = binance.OrderTypeTakeProfitLimit +) + +type OrderStatusType = binance.OrderStatusType + +const ( + OrderStatusTypeNew OrderStatusType = binance.OrderStatusTypeNew + OrderStatusTypePartiallyFilled OrderStatusType = binance.OrderStatusTypePartiallyFilled + OrderStatusTypeFilled OrderStatusType = binance.OrderStatusTypeFilled + OrderStatusTypeCanceled OrderStatusType = binance.OrderStatusTypeCanceled + OrderStatusTypePendingCancel OrderStatusType = binance.OrderStatusTypePendingCancel + OrderStatusTypeRejected OrderStatusType = binance.OrderStatusTypeRejected + OrderStatusTypeExpired OrderStatusType = binance.OrderStatusTypeExpired +) diff --git a/pkg/exchange/binance/binanceapi/client.go b/pkg/exchange/binance/binanceapi/client.go new file mode 100644 index 0000000000..0fa05db422 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/client.go @@ -0,0 +1,229 @@ +package binanceapi + +import ( + "bytes" + "context" + "crypto/hmac" + "crypto/sha256" + "encoding/json" + "fmt" + "net/http" + "net/url" + "strconv" + "time" + + "github.com/c9s/requestgen" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/types" +) + +const defaultHTTPTimeout = time.Second * 15 +const RestBaseURL = "https://api.binance.com" +const SandboxRestBaseURL = "https://testnet.binance.vision" +const DebugRequestResponse = false + +var DefaultHttpClient = &http.Client{ + Timeout: defaultHTTPTimeout, +} + +type RestClient struct { + requestgen.BaseAPIClient + + Key, Secret string + + recvWindow int + timeOffset int64 +} + +func NewClient(baseURL string) *RestClient { + if len(baseURL) == 0 { + baseURL = RestBaseURL + } + + u, err := url.Parse(baseURL) + if err != nil { + panic(err) + } + + client := &RestClient{ + BaseAPIClient: requestgen.BaseAPIClient{ + BaseURL: u, + HttpClient: DefaultHttpClient, + }, + } + + // client.AccountService = &AccountService{client: client} + return client +} + +func (c *RestClient) Auth(key, secret string) { + c.Key = key + // pragma: allowlist nextline secret + c.Secret = secret +} + +// NewRequest create new API request. Relative url can be provided in refURL. +func (c *RestClient) NewRequest(ctx context.Context, method, refURL string, params url.Values, payload interface{}) (*http.Request, error) { + rel, err := url.Parse(refURL) + if err != nil { + return nil, err + } + + if params != nil { + rel.RawQuery = params.Encode() + } + + body, err := castPayload(payload) + if err != nil { + return nil, err + } + + pathURL := c.BaseURL.ResolveReference(rel) + return http.NewRequestWithContext(ctx, method, pathURL.String(), bytes.NewReader(body)) +} + +func (c *RestClient) SetTimeOffsetFromServer(ctx context.Context) error { + req, err := c.NewRequest(ctx, "GET", "/api/v3/time", nil, nil) + if err != nil { + return err + } + + resp, err := c.SendRequest(req) + if err != nil { + return err + } + + var a struct { + ServerTime types.MillisecondTimestamp `json:"serverTime"` + } + + err = resp.DecodeJSON(&a) + if err != nil { + return err + } + + c.timeOffset = currentTimestamp() - a.ServerTime.Time().UnixMilli() + return nil +} + +func (c *RestClient) SendRequest(req *http.Request) (*requestgen.Response, error) { + if DebugRequestResponse { + logrus.Debugf("-> request: %+v", req) + response, err := c.BaseAPIClient.SendRequest(req) + logrus.Debugf("<- response: %s", string(response.Body)) + return response, err + } + + return c.BaseAPIClient.SendRequest(req) +} + +// newAuthenticatedRequest creates new http request for authenticated routes. +func (c *RestClient) NewAuthenticatedRequest(ctx context.Context, method, refURL string, params url.Values, payload interface{}) (*http.Request, error) { + if len(c.Key) == 0 { + return nil, errors.New("empty api key") + } + + if len(c.Secret) == 0 { + return nil, errors.New("empty api secret") + } + + rel, err := url.Parse(refURL) + if err != nil { + return nil, err + } + + if params == nil { + params = url.Values{} + } + + if c.recvWindow > 0 { + params.Set("recvWindow", strconv.Itoa(c.recvWindow)) + } + + params.Set("timestamp", strconv.FormatInt(currentTimestamp()-c.timeOffset, 10)) + rawQuery := params.Encode() + + pathURL := c.BaseURL.ResolveReference(rel) + body, err := castPayload(payload) + if err != nil { + return nil, err + } + + toSign := rawQuery + string(body) + signature := sign(c.Secret, toSign) + + // sv is the extra url parameters that we need to attach to the request + sv := url.Values{} + sv.Set("signature", signature) + if rawQuery == "" { + rawQuery = sv.Encode() + } else { + rawQuery = rawQuery + "&" + sv.Encode() + } + + if rawQuery != "" { + pathURL.RawQuery = rawQuery + } + + req, err := http.NewRequestWithContext(ctx, method, pathURL.String(), bytes.NewReader(body)) + if err != nil { + return nil, err + } + + // if our payload body is not an empty string + if len(body) > 0 { + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + } + + req.Header.Add("Accept", "application/json") + + // Build authentication headers + req.Header.Add("X-MBX-APIKEY", c.Key) + return req, nil +} + +// sign uses sha256 to sign the payload with the given secret +func sign(secret, payload string) string { + var sig = hmac.New(sha256.New, []byte(secret)) + _, err := sig.Write([]byte(payload)) + if err != nil { + return "" + } + + return fmt.Sprintf("%x", sig.Sum(nil)) +} + +func currentTimestamp() int64 { + return FormatTimestamp(time.Now()) +} + +// FormatTimestamp formats a time into Unix timestamp in milliseconds, as requested by Binance. +func FormatTimestamp(t time.Time) int64 { + return t.UnixNano() / int64(time.Millisecond) +} + +func castPayload(payload interface{}) ([]byte, error) { + if payload != nil { + switch v := payload.(type) { + case string: + return []byte(v), nil + + case []byte: + return v, nil + + default: + body, err := json.Marshal(v) + return body, err + } + } + + return nil, nil +} + +type APIResponse struct { + Code string `json:"code"` + Message string `json:"msg"` + Data json.RawMessage `json:"data"` +} diff --git a/pkg/exchange/binance/binanceapi/client_test.go b/pkg/exchange/binance/binanceapi/client_test.go new file mode 100644 index 0000000000..e86bba7832 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/client_test.go @@ -0,0 +1,142 @@ +package binanceapi + +import ( + "context" + "log" + "net/http/httputil" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/testutil" +) + +func getTestClientOrSkip(t *testing.T) *RestClient { + key, secret, ok := testutil.IntegrationTestConfigured(t, "BINANCE") + if !ok { + t.SkipNow() + return nil + } + + client := NewClient("") + client.Auth(key, secret) + return client +} + +func TestClient_GetTradeFeeRequest(t *testing.T) { + client := getTestClientOrSkip(t) + ctx := context.Background() + + err := client.SetTimeOffsetFromServer(ctx) + assert.NoError(t, err) + + req := client.NewGetTradeFeeRequest() + tradeFees, err := req.Do(ctx) + assert.NoError(t, err) + assert.NotEmpty(t, tradeFees) + t.Logf("tradeFees: %+v", tradeFees) +} + +func TestClient_GetDepositAddressRequest(t *testing.T) { + client := getTestClientOrSkip(t) + ctx := context.Background() + + err := client.SetTimeOffsetFromServer(ctx) + assert.NoError(t, err) + + req := client.NewGetDepositAddressRequest() + req.Coin("BTC") + address, err := req.Do(ctx) + assert.NoError(t, err) + assert.NotNil(t, address) + assert.NotEmpty(t, address.Url) + assert.NotEmpty(t, address.Address) + t.Logf("deposit address: %+v", address) +} + +func TestClient_GetDepositHistoryRequest(t *testing.T) { + client := getTestClientOrSkip(t) + ctx := context.Background() + + err := client.SetTimeOffsetFromServer(ctx) + assert.NoError(t, err) + + req := client.NewGetDepositHistoryRequest() + history, err := req.Do(ctx) + assert.NoError(t, err) + assert.NotNil(t, history) + assert.NotEmpty(t, history) + t.Logf("deposit history: %+v", history) +} + +func TestClient_NewSpotRebateHistoryRequest(t *testing.T) { + client := getTestClientOrSkip(t) + ctx := context.Background() + + err := client.SetTimeOffsetFromServer(ctx) + assert.NoError(t, err) + + req := client.NewGetSpotRebateHistoryRequest() + history, err := req.Do(ctx) + assert.NoError(t, err) + assert.NotNil(t, history) + assert.NotEmpty(t, history) + t.Logf("spot rebate history: %+v", history) +} + +func TestClient_NewGetMarginInterestRateHistoryRequest(t *testing.T) { + client := getTestClientOrSkip(t) + ctx := context.Background() + + err := client.SetTimeOffsetFromServer(ctx) + assert.NoError(t, err) + + req := client.NewGetMarginInterestRateHistoryRequest() + req.Asset("BTC") + history, err := req.Do(ctx) + assert.NoError(t, err) + assert.NotNil(t, history) + assert.NotEmpty(t, history) + t.Logf("interest rate history: %+v", history) +} + +func TestClient_privateCall(t *testing.T) { + key, secret, ok := testutil.IntegrationTestConfigured(t, "BINANCE") + if !ok { + t.SkipNow() + } + + client := NewClient("") + client.Auth(key, secret) + + ctx := context.Background() + + err := client.SetTimeOffsetFromServer(ctx) + assert.NoError(t, err) + + req, err := client.NewAuthenticatedRequest(ctx, "GET", "/sapi/v1/asset/tradeFee", nil, nil) + assert.NoError(t, err) + assert.NotNil(t, req) + + resp, err := client.SendRequest(req) + if assert.NoError(t, err) { + var feeStructs []struct { + Symbol string `json:"symbol"` + MakerCommission string `json:"makerCommission"` + TakerCommission string `json:"takerCommission"` + } + err = resp.DecodeJSON(&feeStructs) + if assert.NoError(t, err) { + assert.NotEmpty(t, feeStructs) + } + } else { + dump, _ := httputil.DumpRequest(req, true) + log.Printf("request: %s", dump) + } +} + +func TestClient_setTimeOffsetFromServer(t *testing.T) { + client := NewClient("") + err := client.SetTimeOffsetFromServer(context.Background()) + assert.NoError(t, err) +} diff --git a/pkg/exchange/binance/binanceapi/get_api_referral_if_new_user_request.go b/pkg/exchange/binance/binanceapi/get_api_referral_if_new_user_request.go new file mode 100644 index 0000000000..07d8857b5e --- /dev/null +++ b/pkg/exchange/binance/binanceapi/get_api_referral_if_new_user_request.go @@ -0,0 +1,19 @@ +package binanceapi + +import "github.com/c9s/requestgen" + +type ApiReferralIfNewUserResponse struct { + ApiAgentCode string `json:"apiAgentCode"` + RebateWorking bool `json:"rebateWorking"` + IfNewUser bool `json:"ifNewUser"` + ReferrerId int `json:"referrerId"` +} + +//go:generate requestgen -method GET -url "/sapi/v1/apiReferral/ifNewUser" -type GetApiReferralIfNewUserRequest -responseType .ApiReferralIfNewUserResponse +type GetApiReferralIfNewUserRequest struct { + client requestgen.AuthenticatedAPIClient +} + +func (c *RestClient) NewGetApiReferralIfNewUserRequest() *GetApiReferralIfNewUserRequest { + return &GetApiReferralIfNewUserRequest{client: c} +} diff --git a/pkg/exchange/binance/binanceapi/get_api_referral_if_new_user_request_requestgen.go b/pkg/exchange/binance/binanceapi/get_api_referral_if_new_user_request_requestgen.go new file mode 100644 index 0000000000..feb42d93d1 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/get_api_referral_if_new_user_request_requestgen.go @@ -0,0 +1,135 @@ +// Code generated by "requestgen -method GET -url /sapi/v1/apiReferral/ifNewUser -type GetApiReferralIfNewUserRequest -responseType .ApiReferralIfNewUserResponse"; DO NOT EDIT. + +package binanceapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetApiReferralIfNewUserRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetApiReferralIfNewUserRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetApiReferralIfNewUserRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetApiReferralIfNewUserRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetApiReferralIfNewUserRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetApiReferralIfNewUserRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetApiReferralIfNewUserRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetApiReferralIfNewUserRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetApiReferralIfNewUserRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetApiReferralIfNewUserRequest) Do(ctx context.Context) (*ApiReferralIfNewUserResponse, error) { + + // no body params + var params interface{} + query := url.Values{} + + apiURL := "/sapi/v1/apiReferral/ifNewUser" + + req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse ApiReferralIfNewUserResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return &apiResponse, nil +} diff --git a/pkg/exchange/binance/binanceapi/get_deposit_address_request.go b/pkg/exchange/binance/binanceapi/get_deposit_address_request.go new file mode 100644 index 0000000000..17b0005e31 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/get_deposit_address_request.go @@ -0,0 +1,25 @@ +package binanceapi + +import ( + "github.com/c9s/requestgen" +) + +type DepositAddress struct { + Address string `json:"address"` + Coin string `json:"coin"` + Tag string `json:"tag"` + Url string `json:"url"` +} + +//go:generate requestgen -method GET -url "/sapi/v1/capital/deposit/address" -type GetDepositAddressRequest -responseType .DepositAddress +type GetDepositAddressRequest struct { + client requestgen.AuthenticatedAPIClient + + coin string `param:"coin"` + + network *string `param:"network"` +} + +func (c *RestClient) NewGetDepositAddressRequest() *GetDepositAddressRequest { + return &GetDepositAddressRequest{client: c} +} diff --git a/pkg/exchange/binance/binanceapi/get_deposit_address_request_requestgen.go b/pkg/exchange/binance/binanceapi/get_deposit_address_request_requestgen.go new file mode 100644 index 0000000000..6406dcf711 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/get_deposit_address_request_requestgen.go @@ -0,0 +1,161 @@ +// Code generated by "requestgen -method GET -url /sapi/v1/capital/deposit/address -type GetDepositAddressRequest -responseType .DepositAddress"; DO NOT EDIT. + +package binanceapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +func (g *GetDepositAddressRequest) Coin(coin string) *GetDepositAddressRequest { + g.coin = coin + return g +} + +func (g *GetDepositAddressRequest) Network(network string) *GetDepositAddressRequest { + g.network = &network + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetDepositAddressRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetDepositAddressRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check coin field -> json key coin + coin := g.coin + + // assign parameter of coin + params["coin"] = coin + // check network field -> json key network + if g.network != nil { + network := *g.network + + // assign parameter of network + params["network"] = network + } else { + } + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetDepositAddressRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetDepositAddressRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetDepositAddressRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetDepositAddressRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetDepositAddressRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetDepositAddressRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetDepositAddressRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetDepositAddressRequest) Do(ctx context.Context) (*DepositAddress, error) { + + // empty params for GET operation + var params interface{} + query, err := g.GetParametersQuery() + if err != nil { + return nil, err + } + + apiURL := "/sapi/v1/capital/deposit/address" + + req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse DepositAddress + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return &apiResponse, nil +} diff --git a/pkg/exchange/binance/binanceapi/get_deposit_history_request.go b/pkg/exchange/binance/binanceapi/get_deposit_history_request.go new file mode 100644 index 0000000000..e000de98ce --- /dev/null +++ b/pkg/exchange/binance/binanceapi/get_deposit_history_request.go @@ -0,0 +1,38 @@ +package binanceapi + +import ( + "time" + + "github.com/c9s/requestgen" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +type DepositHistory struct { + Amount fixedpoint.Value `json:"amount"` + Coin string `json:"coin"` + Network string `json:"network"` + Status int `json:"status"` + Address string `json:"address"` + AddressTag string `json:"addressTag"` + TxId string `json:"txId"` + InsertTime types.MillisecondTimestamp `json:"insertTime"` + TransferType int `json:"transferType"` + UnlockConfirm int `json:"unlockConfirm"` + ConfirmTimes string `json:"confirmTimes"` +} + +//go:generate requestgen -method GET -url "/sapi/v1/capital/deposit/hisrec" -type GetDepositHistoryRequest -responseType []DepositHistory +type GetDepositHistoryRequest struct { + client requestgen.AuthenticatedAPIClient + + coin *string `param:"coin"` + + startTime *time.Time `param:"startTime,milliseconds"` + endTime *time.Time `param:"endTime,milliseconds"` +} + +func (c *RestClient) NewGetDepositHistoryRequest() *GetDepositHistoryRequest { + return &GetDepositHistoryRequest{client: c} +} diff --git a/pkg/exchange/binance/binanceapi/get_deposit_history_request_requestgen.go b/pkg/exchange/binance/binanceapi/get_deposit_history_request_requestgen.go new file mode 100644 index 0000000000..dce6cb8b7b --- /dev/null +++ b/pkg/exchange/binance/binanceapi/get_deposit_history_request_requestgen.go @@ -0,0 +1,181 @@ +// Code generated by "requestgen -method GET -url /sapi/v1/capital/deposit/hisrec -type GetDepositHistoryRequest -responseType []DepositHistory"; DO NOT EDIT. + +package binanceapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" + "strconv" + "time" +) + +func (g *GetDepositHistoryRequest) Coin(coin string) *GetDepositHistoryRequest { + g.coin = &coin + return g +} + +func (g *GetDepositHistoryRequest) StartTime(startTime time.Time) *GetDepositHistoryRequest { + g.startTime = &startTime + return g +} + +func (g *GetDepositHistoryRequest) EndTime(endTime time.Time) *GetDepositHistoryRequest { + g.endTime = &endTime + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetDepositHistoryRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetDepositHistoryRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check coin field -> json key coin + if g.coin != nil { + coin := *g.coin + + // assign parameter of coin + params["coin"] = coin + } else { + } + // check startTime field -> json key startTime + if g.startTime != nil { + startTime := *g.startTime + + // assign parameter of startTime + // convert time.Time to milliseconds time stamp + params["startTime"] = strconv.FormatInt(startTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check endTime field -> json key endTime + if g.endTime != nil { + endTime := *g.endTime + + // assign parameter of endTime + // convert time.Time to milliseconds time stamp + params["endTime"] = strconv.FormatInt(endTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetDepositHistoryRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetDepositHistoryRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetDepositHistoryRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetDepositHistoryRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetDepositHistoryRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetDepositHistoryRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetDepositHistoryRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetDepositHistoryRequest) Do(ctx context.Context) ([]DepositHistory, error) { + + // empty params for GET operation + var params interface{} + query, err := g.GetParametersQuery() + if err != nil { + return nil, err + } + + apiURL := "/sapi/v1/capital/deposit/hisrec" + + req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse []DepositHistory + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return apiResponse, nil +} diff --git a/pkg/exchange/binance/binanceapi/get_margin_interest_history_request.go b/pkg/exchange/binance/binanceapi/get_margin_interest_history_request.go new file mode 100644 index 0000000000..59d241d1b2 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/get_margin_interest_history_request.go @@ -0,0 +1,52 @@ +package binanceapi + +import ( + "time" + + "github.com/c9s/requestgen" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +// interest type in response has 4 enums: +// PERIODIC interest charged per hour +// ON_BORROW first interest charged on borrow +// PERIODIC_CONVERTED interest charged per hour converted into BNB +// ON_BORROW_CONVERTED first interest charged on borrow converted into BNB +type InterestType string + +const ( + InterestTypePeriodic InterestType = "PERIODIC" + InterestTypeOnBorrow InterestType = "ON_BORROW" + InterestTypePeriodicConverted InterestType = "PERIODIC_CONVERTED" + InterestTypeOnBorrowConverted InterestType = "ON_BORROW_CONVERTED" +) + +// MarginInterest is the user margin interest record +type MarginInterest struct { + IsolatedSymbol string `json:"isolatedSymbol"` + Asset string `json:"asset"` + Interest fixedpoint.Value `json:"interest"` + InterestAccuredTime types.MillisecondTimestamp `json:"interestAccuredTime"` + InterestRate fixedpoint.Value `json:"interestRate"` + Principal fixedpoint.Value `json:"principal"` + Type InterestType `json:"type"` +} + +//go:generate requestgen -method GET -url "/sapi/v1/margin/interestHistory" -type GetMarginInterestHistoryRequest -responseType .RowsResponse -responseDataField Rows -responseDataType []MarginInterest +type GetMarginInterestHistoryRequest struct { + client requestgen.AuthenticatedAPIClient + + asset string `param:"asset"` + startTime *time.Time `param:"startTime,milliseconds"` + endTime *time.Time `param:"endTime,milliseconds"` + isolatedSymbol *string `param:"isolatedSymbol"` + archived *bool `param:"archived"` + size *int `param:"size"` + current *int `param:"current"` +} + +func (c *RestClient) NewGetMarginInterestHistoryRequest() *GetMarginInterestHistoryRequest { + return &GetMarginInterestHistoryRequest{client: c} +} diff --git a/pkg/exchange/binance/binanceapi/get_margin_interest_history_request_requestgen.go b/pkg/exchange/binance/binanceapi/get_margin_interest_history_request_requestgen.go new file mode 100644 index 0000000000..b73d167f81 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/get_margin_interest_history_request_requestgen.go @@ -0,0 +1,234 @@ +// Code generated by "requestgen -method GET -url /sapi/v1/margin/interestHistory -type GetMarginInterestHistoryRequest -responseType .RowsResponse -responseDataField Rows -responseDataType []MarginInterest"; DO NOT EDIT. + +package binanceapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" + "strconv" + "time" +) + +func (g *GetMarginInterestHistoryRequest) Asset(asset string) *GetMarginInterestHistoryRequest { + g.asset = asset + return g +} + +func (g *GetMarginInterestHistoryRequest) StartTime(startTime time.Time) *GetMarginInterestHistoryRequest { + g.startTime = &startTime + return g +} + +func (g *GetMarginInterestHistoryRequest) EndTime(endTime time.Time) *GetMarginInterestHistoryRequest { + g.endTime = &endTime + return g +} + +func (g *GetMarginInterestHistoryRequest) IsolatedSymbol(isolatedSymbol string) *GetMarginInterestHistoryRequest { + g.isolatedSymbol = &isolatedSymbol + return g +} + +func (g *GetMarginInterestHistoryRequest) Archived(archived bool) *GetMarginInterestHistoryRequest { + g.archived = &archived + return g +} + +func (g *GetMarginInterestHistoryRequest) Size(size int) *GetMarginInterestHistoryRequest { + g.size = &size + return g +} + +func (g *GetMarginInterestHistoryRequest) Current(current int) *GetMarginInterestHistoryRequest { + g.current = ¤t + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetMarginInterestHistoryRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetMarginInterestHistoryRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check asset field -> json key asset + asset := g.asset + + // assign parameter of asset + params["asset"] = asset + // check startTime field -> json key startTime + if g.startTime != nil { + startTime := *g.startTime + + // assign parameter of startTime + // convert time.Time to milliseconds time stamp + params["startTime"] = strconv.FormatInt(startTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check endTime field -> json key endTime + if g.endTime != nil { + endTime := *g.endTime + + // assign parameter of endTime + // convert time.Time to milliseconds time stamp + params["endTime"] = strconv.FormatInt(endTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check isolatedSymbol field -> json key isolatedSymbol + if g.isolatedSymbol != nil { + isolatedSymbol := *g.isolatedSymbol + + // assign parameter of isolatedSymbol + params["isolatedSymbol"] = isolatedSymbol + } else { + } + // check archived field -> json key archived + if g.archived != nil { + archived := *g.archived + + // assign parameter of archived + params["archived"] = archived + } else { + } + // check size field -> json key size + if g.size != nil { + size := *g.size + + // assign parameter of size + params["size"] = size + } else { + } + // check current field -> json key current + if g.current != nil { + current := *g.current + + // assign parameter of current + params["current"] = current + } else { + } + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetMarginInterestHistoryRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetMarginInterestHistoryRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetMarginInterestHistoryRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetMarginInterestHistoryRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetMarginInterestHistoryRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetMarginInterestHistoryRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetMarginInterestHistoryRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetMarginInterestHistoryRequest) Do(ctx context.Context) ([]MarginInterest, error) { + + // empty params for GET operation + var params interface{} + query, err := g.GetParametersQuery() + if err != nil { + return nil, err + } + + apiURL := "/sapi/v1/margin/interestHistory" + + req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse RowsResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + var data []MarginInterest + if err := json.Unmarshal(apiResponse.Rows, &data); err != nil { + return nil, err + } + return data, nil +} diff --git a/pkg/exchange/binance/binanceapi/get_margin_interest_history_request_test.go b/pkg/exchange/binance/binanceapi/get_margin_interest_history_request_test.go new file mode 100644 index 0000000000..60540c35c8 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/get_margin_interest_history_request_test.go @@ -0,0 +1,29 @@ +package binanceapi + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func Test_GetMarginInterestHistoryRequest(t *testing.T) { + client := getTestClientOrSkip(t) + ctx := context.Background() + + err := client.SetTimeOffsetFromServer(ctx) + assert.NoError(t, err) + + req := client.NewGetMarginInterestHistoryRequest() + req.Asset("USDT") + req.IsolatedSymbol("DOTUSDT") + req.StartTime(time.Date(2022, time.February, 1, 0, 0, 0, 0, time.UTC)) + req.EndTime(time.Date(2022, time.March, 1, 0, 0, 0, 0, time.UTC)) + req.Size(100) + + records, err := req.Do(ctx) + assert.NoError(t, err) + assert.NotEmpty(t, records) + t.Logf("interest: %+v", records) +} diff --git a/pkg/exchange/binance/binanceapi/get_margin_interest_rate_history_request.go b/pkg/exchange/binance/binanceapi/get_margin_interest_rate_history_request.go new file mode 100644 index 0000000000..86d05dd72f --- /dev/null +++ b/pkg/exchange/binance/binanceapi/get_margin_interest_rate_history_request.go @@ -0,0 +1,30 @@ +package binanceapi + +import ( + "time" + + "github.com/c9s/requestgen" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +type MarginInterestRate struct { + Asset string `json:"asset"` + DailyInterestRate fixedpoint.Value `json:"dailyInterestRate"` + Timestamp types.MillisecondTimestamp `json:"timestamp"` + VipLevel int `json:"vipLevel"` +} + +//go:generate requestgen -method GET -url "/sapi/v1/margin/interestRateHistory" -type GetMarginInterestRateHistoryRequest -responseType []MarginInterestRate +type GetMarginInterestRateHistoryRequest struct { + client requestgen.APIClient + + asset string `param:"asset"` + startTime *time.Time `param:"startTime,milliseconds"` + endTime *time.Time `param:"endTime,milliseconds"` +} + +func (c *RestClient) NewGetMarginInterestRateHistoryRequest() *GetMarginInterestRateHistoryRequest { + return &GetMarginInterestRateHistoryRequest{client: c} +} diff --git a/pkg/exchange/binance/binanceapi/get_margin_interest_rate_history_request_requestgen.go b/pkg/exchange/binance/binanceapi/get_margin_interest_rate_history_request_requestgen.go new file mode 100644 index 0000000000..1f80665cc3 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/get_margin_interest_rate_history_request_requestgen.go @@ -0,0 +1,178 @@ +// Code generated by "requestgen -method GET -url /sapi/v1/margin/interestRateHistory -type GetMarginInterestRateHistoryRequest -responseType []MarginInterestRate"; DO NOT EDIT. + +package binanceapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" + "strconv" + "time" +) + +func (g *GetMarginInterestRateHistoryRequest) Asset(asset string) *GetMarginInterestRateHistoryRequest { + g.asset = asset + return g +} + +func (g *GetMarginInterestRateHistoryRequest) StartTime(startTime time.Time) *GetMarginInterestRateHistoryRequest { + g.startTime = &startTime + return g +} + +func (g *GetMarginInterestRateHistoryRequest) EndTime(endTime time.Time) *GetMarginInterestRateHistoryRequest { + g.endTime = &endTime + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetMarginInterestRateHistoryRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetMarginInterestRateHistoryRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check asset field -> json key asset + asset := g.asset + + // assign parameter of asset + params["asset"] = asset + // check startTime field -> json key startTime + if g.startTime != nil { + startTime := *g.startTime + + // assign parameter of startTime + // convert time.Time to milliseconds time stamp + params["startTime"] = strconv.FormatInt(startTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check endTime field -> json key endTime + if g.endTime != nil { + endTime := *g.endTime + + // assign parameter of endTime + // convert time.Time to milliseconds time stamp + params["endTime"] = strconv.FormatInt(endTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetMarginInterestRateHistoryRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetMarginInterestRateHistoryRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetMarginInterestRateHistoryRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetMarginInterestRateHistoryRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetMarginInterestRateHistoryRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetMarginInterestRateHistoryRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetMarginInterestRateHistoryRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetMarginInterestRateHistoryRequest) Do(ctx context.Context) ([]MarginInterestRate, error) { + + // empty params for GET operation + var params interface{} + query, err := g.GetParametersQuery() + if err != nil { + return nil, err + } + + apiURL := "/sapi/v1/margin/interestRateHistory" + + req, err := g.client.NewRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse []MarginInterestRate + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return apiResponse, nil +} diff --git a/pkg/exchange/binance/binanceapi/get_margin_liquidation_history_request.go b/pkg/exchange/binance/binanceapi/get_margin_liquidation_history_request.go new file mode 100644 index 0000000000..31ce6c73d7 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/get_margin_liquidation_history_request.go @@ -0,0 +1,38 @@ +package binanceapi + +import ( + "time" + + "github.com/c9s/requestgen" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +type MarginLiquidationRecord struct { + AveragePrice fixedpoint.Value `json:"avgPrice"` + ExecutedQuantity fixedpoint.Value `json:"executedQty"` + OrderId uint64 `json:"orderId"` + Price fixedpoint.Value `json:"price"` + Quantity fixedpoint.Value `json:"qty"` + Side SideType `json:"side"` + Symbol string `json:"symbol"` + TimeInForce string `json:"timeInForce"` + IsIsolated bool `json:"isIsolated"` + UpdatedTime types.MillisecondTimestamp `json:"updatedTime"` +} + +//go:generate requestgen -method GET -url "/sapi/v1/margin/forceLiquidationRec" -type GetMarginLiquidationHistoryRequest -responseType .RowsResponse -responseDataField Rows -responseDataType []MarginLiquidationRecord +type GetMarginLiquidationHistoryRequest struct { + client requestgen.AuthenticatedAPIClient + + isolatedSymbol *string `param:"isolatedSymbol"` + startTime *time.Time `param:"startTime,milliseconds"` + endTime *time.Time `param:"endTime,milliseconds"` + size *int `param:"size"` + current *int `param:"current"` +} + +func (c *RestClient) NewGetMarginLiquidationHistoryRequest() *GetMarginLiquidationHistoryRequest { + return &GetMarginLiquidationHistoryRequest{client: c} +} diff --git a/pkg/exchange/binance/binanceapi/get_margin_liquidation_history_request_requestgen.go b/pkg/exchange/binance/binanceapi/get_margin_liquidation_history_request_requestgen.go new file mode 100644 index 0000000000..942491998a --- /dev/null +++ b/pkg/exchange/binance/binanceapi/get_margin_liquidation_history_request_requestgen.go @@ -0,0 +1,211 @@ +// Code generated by "requestgen -method GET -url /sapi/v1/margin/forceLiquidationRec -type GetMarginLiquidationHistoryRequest -responseType .RowsResponse -responseDataField Rows -responseDataType []MarginLiquidationRecord"; DO NOT EDIT. + +package binanceapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" + "strconv" + "time" +) + +func (g *GetMarginLiquidationHistoryRequest) IsolatedSymbol(isolatedSymbol string) *GetMarginLiquidationHistoryRequest { + g.isolatedSymbol = &isolatedSymbol + return g +} + +func (g *GetMarginLiquidationHistoryRequest) StartTime(startTime time.Time) *GetMarginLiquidationHistoryRequest { + g.startTime = &startTime + return g +} + +func (g *GetMarginLiquidationHistoryRequest) EndTime(endTime time.Time) *GetMarginLiquidationHistoryRequest { + g.endTime = &endTime + return g +} + +func (g *GetMarginLiquidationHistoryRequest) Size(size int) *GetMarginLiquidationHistoryRequest { + g.size = &size + return g +} + +func (g *GetMarginLiquidationHistoryRequest) Current(current int) *GetMarginLiquidationHistoryRequest { + g.current = ¤t + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetMarginLiquidationHistoryRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetMarginLiquidationHistoryRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check isolatedSymbol field -> json key isolatedSymbol + if g.isolatedSymbol != nil { + isolatedSymbol := *g.isolatedSymbol + + // assign parameter of isolatedSymbol + params["isolatedSymbol"] = isolatedSymbol + } else { + } + // check startTime field -> json key startTime + if g.startTime != nil { + startTime := *g.startTime + + // assign parameter of startTime + // convert time.Time to milliseconds time stamp + params["startTime"] = strconv.FormatInt(startTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check endTime field -> json key endTime + if g.endTime != nil { + endTime := *g.endTime + + // assign parameter of endTime + // convert time.Time to milliseconds time stamp + params["endTime"] = strconv.FormatInt(endTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check size field -> json key size + if g.size != nil { + size := *g.size + + // assign parameter of size + params["size"] = size + } else { + } + // check current field -> json key current + if g.current != nil { + current := *g.current + + // assign parameter of current + params["current"] = current + } else { + } + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetMarginLiquidationHistoryRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetMarginLiquidationHistoryRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetMarginLiquidationHistoryRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetMarginLiquidationHistoryRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetMarginLiquidationHistoryRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetMarginLiquidationHistoryRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetMarginLiquidationHistoryRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetMarginLiquidationHistoryRequest) Do(ctx context.Context) ([]MarginLiquidationRecord, error) { + + // empty params for GET operation + var params interface{} + query, err := g.GetParametersQuery() + if err != nil { + return nil, err + } + + apiURL := "/sapi/v1/margin/forceLiquidationRec" + + req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse RowsResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + var data []MarginLiquidationRecord + if err := json.Unmarshal(apiResponse.Rows, &data); err != nil { + return nil, err + } + return data, nil +} diff --git a/pkg/exchange/binance/binanceapi/get_margin_loan_history_request.go b/pkg/exchange/binance/binanceapi/get_margin_loan_history_request.go new file mode 100644 index 0000000000..e7a801a9dd --- /dev/null +++ b/pkg/exchange/binance/binanceapi/get_margin_loan_history_request.go @@ -0,0 +1,54 @@ +package binanceapi + +import ( + "time" + + "github.com/c9s/requestgen" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +// one of PENDING (pending execution), CONFIRMED (successfully loaned), FAILED (execution failed, nothing happened to your account); +type LoanStatus string + +const ( + LoanStatusPending LoanStatus = "PENDING" + LoanStatusConfirmed LoanStatus = "CONFIRMED" + LoanStatusFailed LoanStatus = "FAILED" +) + +type MarginLoanRecord struct { + IsolatedSymbol string `json:"isolatedSymbol"` + TxId int64 `json:"txId"` + Asset string `json:"asset"` + Principal fixedpoint.Value `json:"principal"` + Timestamp types.MillisecondTimestamp `json:"timestamp"` + Status LoanStatus `json:"status"` +} + +// GetMarginLoanHistoryRequest +// +// txId or startTime must be sent. txId takes precedence. +// Response in descending order +// If isolatedSymbol is not sent, crossed margin data will be returned +// The max interval between startTime and endTime is 30 days. +// If startTime and endTime not sent, return records of the last 7 days by default +// Set archived to true to query data from 6 months ago +// +//go:generate requestgen -method GET -url "/sapi/v1/margin/loan" -type GetMarginLoanHistoryRequest -responseType .RowsResponse -responseDataField Rows -responseDataType []MarginLoanRecord +type GetMarginLoanHistoryRequest struct { + client requestgen.AuthenticatedAPIClient + + asset string `param:"asset"` + startTime *time.Time `param:"startTime,milliseconds"` + endTime *time.Time `param:"endTime,milliseconds"` + isolatedSymbol *string `param:"isolatedSymbol"` + archived *bool `param:"archived"` + size *int `param:"size"` + current *int `param:"current"` +} + +func (c *RestClient) NewGetMarginLoanHistoryRequest() *GetMarginLoanHistoryRequest { + return &GetMarginLoanHistoryRequest{client: c} +} diff --git a/pkg/exchange/binance/binanceapi/get_margin_loan_history_request_requestgen.go b/pkg/exchange/binance/binanceapi/get_margin_loan_history_request_requestgen.go new file mode 100644 index 0000000000..d893d55f57 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/get_margin_loan_history_request_requestgen.go @@ -0,0 +1,234 @@ +// Code generated by "requestgen -method GET -url /sapi/v1/margin/loan -type GetMarginLoanHistoryRequest -responseType .RowsResponse -responseDataField Rows -responseDataType []MarginLoanRecord"; DO NOT EDIT. + +package binanceapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" + "strconv" + "time" +) + +func (g *GetMarginLoanHistoryRequest) Asset(asset string) *GetMarginLoanHistoryRequest { + g.asset = asset + return g +} + +func (g *GetMarginLoanHistoryRequest) StartTime(startTime time.Time) *GetMarginLoanHistoryRequest { + g.startTime = &startTime + return g +} + +func (g *GetMarginLoanHistoryRequest) EndTime(endTime time.Time) *GetMarginLoanHistoryRequest { + g.endTime = &endTime + return g +} + +func (g *GetMarginLoanHistoryRequest) IsolatedSymbol(isolatedSymbol string) *GetMarginLoanHistoryRequest { + g.isolatedSymbol = &isolatedSymbol + return g +} + +func (g *GetMarginLoanHistoryRequest) Archived(archived bool) *GetMarginLoanHistoryRequest { + g.archived = &archived + return g +} + +func (g *GetMarginLoanHistoryRequest) Size(size int) *GetMarginLoanHistoryRequest { + g.size = &size + return g +} + +func (g *GetMarginLoanHistoryRequest) Current(current int) *GetMarginLoanHistoryRequest { + g.current = ¤t + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetMarginLoanHistoryRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetMarginLoanHistoryRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check asset field -> json key asset + asset := g.asset + + // assign parameter of asset + params["asset"] = asset + // check startTime field -> json key startTime + if g.startTime != nil { + startTime := *g.startTime + + // assign parameter of startTime + // convert time.Time to milliseconds time stamp + params["startTime"] = strconv.FormatInt(startTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check endTime field -> json key endTime + if g.endTime != nil { + endTime := *g.endTime + + // assign parameter of endTime + // convert time.Time to milliseconds time stamp + params["endTime"] = strconv.FormatInt(endTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check isolatedSymbol field -> json key isolatedSymbol + if g.isolatedSymbol != nil { + isolatedSymbol := *g.isolatedSymbol + + // assign parameter of isolatedSymbol + params["isolatedSymbol"] = isolatedSymbol + } else { + } + // check archived field -> json key archived + if g.archived != nil { + archived := *g.archived + + // assign parameter of archived + params["archived"] = archived + } else { + } + // check size field -> json key size + if g.size != nil { + size := *g.size + + // assign parameter of size + params["size"] = size + } else { + } + // check current field -> json key current + if g.current != nil { + current := *g.current + + // assign parameter of current + params["current"] = current + } else { + } + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetMarginLoanHistoryRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetMarginLoanHistoryRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetMarginLoanHistoryRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetMarginLoanHistoryRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetMarginLoanHistoryRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetMarginLoanHistoryRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetMarginLoanHistoryRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetMarginLoanHistoryRequest) Do(ctx context.Context) ([]MarginLoanRecord, error) { + + // empty params for GET operation + var params interface{} + query, err := g.GetParametersQuery() + if err != nil { + return nil, err + } + + apiURL := "/sapi/v1/margin/loan" + + req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse RowsResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + var data []MarginLoanRecord + if err := json.Unmarshal(apiResponse.Rows, &data); err != nil { + return nil, err + } + return data, nil +} diff --git a/pkg/exchange/binance/binanceapi/get_margin_loan_history_request_test.go b/pkg/exchange/binance/binanceapi/get_margin_loan_history_request_test.go new file mode 100644 index 0000000000..c9daa028f1 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/get_margin_loan_history_request_test.go @@ -0,0 +1,29 @@ +package binanceapi + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func Test_GetMarginLoanHistoryRequest(t *testing.T) { + client := getTestClientOrSkip(t) + ctx := context.Background() + + err := client.SetTimeOffsetFromServer(ctx) + assert.NoError(t, err) + + req := client.NewGetMarginLoanHistoryRequest() + req.Asset("USDT") + req.IsolatedSymbol("DOTUSDT") + req.StartTime(time.Date(2022, time.February, 1, 0, 0, 0, 0, time.UTC)) + req.EndTime(time.Date(2022, time.March, 1, 0, 0, 0, 0, time.UTC)) + req.Size(100) + + records, err := req.Do(ctx) + assert.NoError(t, err) + assert.NotEmpty(t, records) + t.Logf("loans: %+v", records) +} diff --git a/pkg/exchange/binance/binanceapi/get_margin_max_borrowable_request.go b/pkg/exchange/binance/binanceapi/get_margin_max_borrowable_request.go new file mode 100644 index 0000000000..31e307ac53 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/get_margin_max_borrowable_request.go @@ -0,0 +1,25 @@ +package binanceapi + +import ( + "github.com/c9s/requestgen" + + "github.com/c9s/bbgo/pkg/fixedpoint" +) + +// MarginMaxBorrowable is the user margin interest record +type MarginMaxBorrowable struct { + Amount fixedpoint.Value `json:"amount"` + BorrowLimit fixedpoint.Value `json:"borrowLimit"` +} + +//go:generate requestgen -method GET -url "/sapi/v1/margin/maxBorrowable" -type GetMarginMaxBorrowableRequest -responseType .MarginMaxBorrowable +type GetMarginMaxBorrowableRequest struct { + client requestgen.AuthenticatedAPIClient + + asset string `param:"asset"` + isolatedSymbol *string `param:"isolatedSymbol"` +} + +func (c *RestClient) NewGetMarginMaxBorrowableRequest() *GetMarginMaxBorrowableRequest { + return &GetMarginMaxBorrowableRequest{client: c} +} diff --git a/pkg/exchange/binance/binanceapi/get_margin_max_borrowable_request_requestgen.go b/pkg/exchange/binance/binanceapi/get_margin_max_borrowable_request_requestgen.go new file mode 100644 index 0000000000..a4b3298643 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/get_margin_max_borrowable_request_requestgen.go @@ -0,0 +1,161 @@ +// Code generated by "requestgen -method GET -url /sapi/v1/margin/maxBorrowable -type GetMarginMaxBorrowableRequest -responseType .MarginMaxBorrowable"; DO NOT EDIT. + +package binanceapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +func (g *GetMarginMaxBorrowableRequest) Asset(asset string) *GetMarginMaxBorrowableRequest { + g.asset = asset + return g +} + +func (g *GetMarginMaxBorrowableRequest) IsolatedSymbol(isolatedSymbol string) *GetMarginMaxBorrowableRequest { + g.isolatedSymbol = &isolatedSymbol + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetMarginMaxBorrowableRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetMarginMaxBorrowableRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check asset field -> json key asset + asset := g.asset + + // assign parameter of asset + params["asset"] = asset + // check isolatedSymbol field -> json key isolatedSymbol + if g.isolatedSymbol != nil { + isolatedSymbol := *g.isolatedSymbol + + // assign parameter of isolatedSymbol + params["isolatedSymbol"] = isolatedSymbol + } else { + } + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetMarginMaxBorrowableRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetMarginMaxBorrowableRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetMarginMaxBorrowableRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetMarginMaxBorrowableRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetMarginMaxBorrowableRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetMarginMaxBorrowableRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetMarginMaxBorrowableRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetMarginMaxBorrowableRequest) Do(ctx context.Context) (*MarginMaxBorrowable, error) { + + // empty params for GET operation + var params interface{} + query, err := g.GetParametersQuery() + if err != nil { + return nil, err + } + + apiURL := "/sapi/v1/margin/maxBorrowable" + + req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse MarginMaxBorrowable + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return &apiResponse, nil +} diff --git a/pkg/exchange/binance/binanceapi/get_margin_repay_history_request.go b/pkg/exchange/binance/binanceapi/get_margin_repay_history_request.go new file mode 100644 index 0000000000..6d9a13448b --- /dev/null +++ b/pkg/exchange/binance/binanceapi/get_margin_repay_history_request.go @@ -0,0 +1,47 @@ +package binanceapi + +import ( + "time" + + "github.com/c9s/requestgen" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +// RepayStatus one of PENDING (pending execution), CONFIRMED (successfully loaned), FAILED (execution failed, nothing happened to your account); +type RepayStatus string + +const ( + RepayStatusPending LoanStatus = "PENDING" + RepayStatusConfirmed LoanStatus = "CONFIRMED" + RepayStatusFailed LoanStatus = "FAILED" +) + +type MarginRepayRecord struct { + IsolatedSymbol string `json:"isolatedSymbol"` + Amount fixedpoint.Value `json:"amount"` + Asset string `json:"asset"` + Interest fixedpoint.Value `json:"interest"` + Principal fixedpoint.Value `json:"principal"` + Status string `json:"status"` + Timestamp types.MillisecondTimestamp `json:"timestamp"` + TxId uint64 `json:"txId"` +} + +//go:generate requestgen -method GET -url "/sapi/v1/margin/repay" -type GetMarginRepayHistoryRequest -responseType .RowsResponse -responseDataField Rows -responseDataType []MarginRepayRecord +type GetMarginRepayHistoryRequest struct { + client requestgen.AuthenticatedAPIClient + + asset string `param:"asset"` + startTime *time.Time `param:"startTime,milliseconds"` + endTime *time.Time `param:"endTime,milliseconds"` + isolatedSymbol *string `param:"isolatedSymbol"` + archived *bool `param:"archived"` + size *int `param:"size"` + current *int `param:"current"` +} + +func (c *RestClient) NewGetMarginRepayHistoryRequest() *GetMarginRepayHistoryRequest { + return &GetMarginRepayHistoryRequest{client: c} +} diff --git a/pkg/exchange/binance/binanceapi/get_margin_repay_history_request_requestgen.go b/pkg/exchange/binance/binanceapi/get_margin_repay_history_request_requestgen.go new file mode 100644 index 0000000000..17e5364155 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/get_margin_repay_history_request_requestgen.go @@ -0,0 +1,234 @@ +// Code generated by "requestgen -method GET -url /sapi/v1/margin/repay -type GetMarginRepayHistoryRequest -responseType .RowsResponse -responseDataField Rows -responseDataType []MarginRepayRecord"; DO NOT EDIT. + +package binanceapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" + "strconv" + "time" +) + +func (g *GetMarginRepayHistoryRequest) Asset(asset string) *GetMarginRepayHistoryRequest { + g.asset = asset + return g +} + +func (g *GetMarginRepayHistoryRequest) StartTime(startTime time.Time) *GetMarginRepayHistoryRequest { + g.startTime = &startTime + return g +} + +func (g *GetMarginRepayHistoryRequest) EndTime(endTime time.Time) *GetMarginRepayHistoryRequest { + g.endTime = &endTime + return g +} + +func (g *GetMarginRepayHistoryRequest) IsolatedSymbol(isolatedSymbol string) *GetMarginRepayHistoryRequest { + g.isolatedSymbol = &isolatedSymbol + return g +} + +func (g *GetMarginRepayHistoryRequest) Archived(archived bool) *GetMarginRepayHistoryRequest { + g.archived = &archived + return g +} + +func (g *GetMarginRepayHistoryRequest) Size(size int) *GetMarginRepayHistoryRequest { + g.size = &size + return g +} + +func (g *GetMarginRepayHistoryRequest) Current(current int) *GetMarginRepayHistoryRequest { + g.current = ¤t + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetMarginRepayHistoryRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetMarginRepayHistoryRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check asset field -> json key asset + asset := g.asset + + // assign parameter of asset + params["asset"] = asset + // check startTime field -> json key startTime + if g.startTime != nil { + startTime := *g.startTime + + // assign parameter of startTime + // convert time.Time to milliseconds time stamp + params["startTime"] = strconv.FormatInt(startTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check endTime field -> json key endTime + if g.endTime != nil { + endTime := *g.endTime + + // assign parameter of endTime + // convert time.Time to milliseconds time stamp + params["endTime"] = strconv.FormatInt(endTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check isolatedSymbol field -> json key isolatedSymbol + if g.isolatedSymbol != nil { + isolatedSymbol := *g.isolatedSymbol + + // assign parameter of isolatedSymbol + params["isolatedSymbol"] = isolatedSymbol + } else { + } + // check archived field -> json key archived + if g.archived != nil { + archived := *g.archived + + // assign parameter of archived + params["archived"] = archived + } else { + } + // check size field -> json key size + if g.size != nil { + size := *g.size + + // assign parameter of size + params["size"] = size + } else { + } + // check current field -> json key current + if g.current != nil { + current := *g.current + + // assign parameter of current + params["current"] = current + } else { + } + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetMarginRepayHistoryRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetMarginRepayHistoryRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetMarginRepayHistoryRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetMarginRepayHistoryRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetMarginRepayHistoryRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetMarginRepayHistoryRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetMarginRepayHistoryRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetMarginRepayHistoryRequest) Do(ctx context.Context) ([]MarginRepayRecord, error) { + + // empty params for GET operation + var params interface{} + query, err := g.GetParametersQuery() + if err != nil { + return nil, err + } + + apiURL := "/sapi/v1/margin/repay" + + req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse RowsResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + var data []MarginRepayRecord + if err := json.Unmarshal(apiResponse.Rows, &data); err != nil { + return nil, err + } + return data, nil +} diff --git a/pkg/exchange/binance/binanceapi/get_margin_repay_history_request_test.go b/pkg/exchange/binance/binanceapi/get_margin_repay_history_request_test.go new file mode 100644 index 0000000000..5161d32ff1 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/get_margin_repay_history_request_test.go @@ -0,0 +1,29 @@ +package binanceapi + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func Test_GetMarginRepayHistoryRequest(t *testing.T) { + client := getTestClientOrSkip(t) + ctx := context.Background() + + err := client.SetTimeOffsetFromServer(ctx) + assert.NoError(t, err) + + req := client.NewGetMarginRepayHistoryRequest() + req.Asset("USDT") + req.IsolatedSymbol("DOTUSDT") + req.StartTime(time.Date(2022, time.February, 1, 0, 0, 0, 0, time.UTC)) + req.EndTime(time.Date(2022, time.March, 1, 0, 0, 0, 0, time.UTC)) + req.Size(100) + + records, err := req.Do(ctx) + assert.NoError(t, err) + assert.NotEmpty(t, records) + t.Logf("loans: %+v", records) +} diff --git a/pkg/exchange/binance/binanceapi/get_spot_rebate_history_request.go b/pkg/exchange/binance/binanceapi/get_spot_rebate_history_request.go new file mode 100644 index 0000000000..7fff74ffbe --- /dev/null +++ b/pkg/exchange/binance/binanceapi/get_spot_rebate_history_request.go @@ -0,0 +1,41 @@ +package binanceapi + +import ( + "time" + + "github.com/c9s/requestgen" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +// rebate type1 is commission rebate2 is referral kickback +type RebateType int + +const ( + RebateTypeCommission = 1 + RebateTypeReferralKickback = 2 +) + +type SpotRebate struct { + Asset string `json:"asset"` + Type RebateType `json:"type"` + Amount fixedpoint.Value `json:"amount"` + UpdateTime types.MillisecondTimestamp `json:"updateTime"` +} + +// GetSpotRebateHistoryRequest +// The max interval between startTime and endTime is 30 days. +// If startTime and endTime are not sent, the recent 7 days' data will be returned. +// The earliest startTime is supported on June 10, 2020 +//go:generate requestgen -method GET -url "/sapi/v1/rebate/taxQuery" -type GetSpotRebateHistoryRequest -responseType PagedDataResponse -responseDataField Data.Data -responseDataType []SpotRebate +type GetSpotRebateHistoryRequest struct { + client requestgen.AuthenticatedAPIClient + + startTime *time.Time `param:"startTime,milliseconds"` + endTime *time.Time `param:"endTime,milliseconds"` +} + +func (c *RestClient) NewGetSpotRebateHistoryRequest() *GetSpotRebateHistoryRequest { + return &GetSpotRebateHistoryRequest{client: c} +} diff --git a/pkg/exchange/binance/binanceapi/get_spot_rebate_history_request_requestgen.go b/pkg/exchange/binance/binanceapi/get_spot_rebate_history_request_requestgen.go new file mode 100644 index 0000000000..05cc5b67e6 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/get_spot_rebate_history_request_requestgen.go @@ -0,0 +1,172 @@ +// Code generated by "requestgen -method GET -url /sapi/v1/rebate/taxQuery -type GetSpotRebateHistoryRequest -responseType PagedDataResponse -responseDataField Data.Data -responseDataType []SpotRebate"; DO NOT EDIT. + +package binanceapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" + "strconv" + "time" +) + +func (g *GetSpotRebateHistoryRequest) StartTime(startTime time.Time) *GetSpotRebateHistoryRequest { + g.startTime = &startTime + return g +} + +func (g *GetSpotRebateHistoryRequest) EndTime(endTime time.Time) *GetSpotRebateHistoryRequest { + g.endTime = &endTime + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetSpotRebateHistoryRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetSpotRebateHistoryRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check startTime field -> json key startTime + if g.startTime != nil { + startTime := *g.startTime + + // assign parameter of startTime + // convert time.Time to milliseconds time stamp + params["startTime"] = strconv.FormatInt(startTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check endTime field -> json key endTime + if g.endTime != nil { + endTime := *g.endTime + + // assign parameter of endTime + // convert time.Time to milliseconds time stamp + params["endTime"] = strconv.FormatInt(endTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetSpotRebateHistoryRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetSpotRebateHistoryRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetSpotRebateHistoryRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetSpotRebateHistoryRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetSpotRebateHistoryRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetSpotRebateHistoryRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetSpotRebateHistoryRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetSpotRebateHistoryRequest) Do(ctx context.Context) ([]SpotRebate, error) { + + // empty params for GET operation + var params interface{} + query, err := g.GetParametersQuery() + if err != nil { + return nil, err + } + + apiURL := "/sapi/v1/rebate/taxQuery" + + req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse PagedDataResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + var data []SpotRebate + if err := json.Unmarshal(apiResponse.Data.Data, &data); err != nil { + return nil, err + } + return data, nil +} diff --git a/pkg/exchange/binance/binanceapi/get_trade_fee_request.go b/pkg/exchange/binance/binanceapi/get_trade_fee_request.go new file mode 100644 index 0000000000..0b6c544062 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/get_trade_fee_request.go @@ -0,0 +1,22 @@ +package binanceapi + +import ( + "github.com/c9s/requestgen" + + "github.com/c9s/bbgo/pkg/fixedpoint" +) + +type TradeFee struct { + Symbol string `json:"symbol"` + MakerCommission fixedpoint.Value `json:"makerCommission"` + TakerCommission fixedpoint.Value `json:"takerCommission"` +} + +//go:generate requestgen -method GET -url "/sapi/v1/asset/tradeFee" -type GetTradeFeeRequest -responseType []TradeFee +type GetTradeFeeRequest struct { + client requestgen.AuthenticatedAPIClient +} + +func (c *RestClient) NewGetTradeFeeRequest() *GetTradeFeeRequest { + return &GetTradeFeeRequest{client: c} +} diff --git a/pkg/exchange/binance/binanceapi/get_trade_fee_request_requestgen.go b/pkg/exchange/binance/binanceapi/get_trade_fee_request_requestgen.go new file mode 100644 index 0000000000..77aac0c9e0 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/get_trade_fee_request_requestgen.go @@ -0,0 +1,135 @@ +// Code generated by "requestgen -method GET -url /sapi/v1/asset/tradeFee -type GetTradeFeeRequest -responseType []TradeFee"; DO NOT EDIT. + +package binanceapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetTradeFeeRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetTradeFeeRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetTradeFeeRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetTradeFeeRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetTradeFeeRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetTradeFeeRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetTradeFeeRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetTradeFeeRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetTradeFeeRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetTradeFeeRequest) Do(ctx context.Context) ([]TradeFee, error) { + + // no body params + var params interface{} + query := url.Values{} + + apiURL := "/sapi/v1/asset/tradeFee" + + req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse []TradeFee + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return apiResponse, nil +} diff --git a/pkg/exchange/binance/binanceapi/get_withdraw_history_request.go b/pkg/exchange/binance/binanceapi/get_withdraw_history_request.go new file mode 100644 index 0000000000..4e84dbc249 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/get_withdraw_history_request.go @@ -0,0 +1,67 @@ +package binanceapi + +import ( + "time" + + "github.com/c9s/requestgen" + + "github.com/c9s/bbgo/pkg/fixedpoint" +) + +//go:generate stringer -type=TransferType +// 1 for internal transfer, 0 for external transfer +type TransferType int + +const ( + TransferTypeInternal TransferType = 0 + TransferTypeExternal TransferType = 0 +) + +type WithdrawRecord struct { + Id string `json:"id"` + Address string `json:"address"` + Amount fixedpoint.Value `json:"amount"` + ApplyTime string `json:"applyTime"` + Coin string `json:"coin"` + WithdrawOrderID string `json:"withdrawOrderId"` + Network string `json:"network"` + TransferType TransferType `json:"transferType"` + Status WithdrawStatus `json:"status"` + TransactionFee fixedpoint.Value `json:"transactionFee"` + ConfirmNo int `json:"confirmNo"` + Info string `json:"info"` + TxID string `json:"txId"` +} + +//go:generate stringer -type=WithdrawStatus +type WithdrawStatus int + +// WithdrawStatus: 0(0:Email Sent,1:Cancelled 2:Awaiting Approval 3:Rejected 4:Processing 5:Failure 6:Completed) +const ( + WithdrawStatusEmailSent WithdrawStatus = iota + WithdrawStatusCancelled + WithdrawStatusAwaitingApproval + WithdrawStatusRejected + WithdrawStatusProcessing + WithdrawStatusFailure + WithdrawStatusCompleted +) + +//go:generate requestgen -method GET -url "/sapi/v1/capital/withdraw/history" -type GetWithdrawHistoryRequest -responseType []WithdrawRecord +type GetWithdrawHistoryRequest struct { + client requestgen.AuthenticatedAPIClient + coin string `param:"coin"` + + withdrawOrderId *string `param:"withdrawOrderId"` + + status *WithdrawStatus `param:"status"` + + startTime *time.Time `param:"startTime,milliseconds"` + endTime *time.Time `param:"endTime,milliseconds"` + limit *uint64 `param:"limit"` + offset *uint64 `param:"offset"` +} + +func (c *RestClient) NewGetWithdrawHistoryRequest() *GetWithdrawHistoryRequest { + return &GetWithdrawHistoryRequest{client: c} +} diff --git a/pkg/exchange/binance/binanceapi/get_withdraw_history_request_requestgen.go b/pkg/exchange/binance/binanceapi/get_withdraw_history_request_requestgen.go new file mode 100644 index 0000000000..74717d3c44 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/get_withdraw_history_request_requestgen.go @@ -0,0 +1,241 @@ +// Code generated by "requestgen -method GET -url /sapi/v1/capital/withdraw/history -type GetWithdrawHistoryRequest -responseType []WithdrawRecord"; DO NOT EDIT. + +package binanceapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" + "strconv" + "time" +) + +func (g *GetWithdrawHistoryRequest) Coin(coin string) *GetWithdrawHistoryRequest { + g.coin = coin + return g +} + +func (g *GetWithdrawHistoryRequest) WithdrawOrderId(withdrawOrderId string) *GetWithdrawHistoryRequest { + g.withdrawOrderId = &withdrawOrderId + return g +} + +func (g *GetWithdrawHistoryRequest) Status(status WithdrawStatus) *GetWithdrawHistoryRequest { + g.status = &status + return g +} + +func (g *GetWithdrawHistoryRequest) StartTime(startTime time.Time) *GetWithdrawHistoryRequest { + g.startTime = &startTime + return g +} + +func (g *GetWithdrawHistoryRequest) EndTime(endTime time.Time) *GetWithdrawHistoryRequest { + g.endTime = &endTime + return g +} + +func (g *GetWithdrawHistoryRequest) Limit(limit uint64) *GetWithdrawHistoryRequest { + g.limit = &limit + return g +} + +func (g *GetWithdrawHistoryRequest) Offset(offset uint64) *GetWithdrawHistoryRequest { + g.offset = &offset + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetWithdrawHistoryRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetWithdrawHistoryRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check coin field -> json key coin + coin := g.coin + + // assign parameter of coin + params["coin"] = coin + // check withdrawOrderId field -> json key withdrawOrderId + if g.withdrawOrderId != nil { + withdrawOrderId := *g.withdrawOrderId + + // assign parameter of withdrawOrderId + params["withdrawOrderId"] = withdrawOrderId + } else { + } + // check status field -> json key status + if g.status != nil { + status := *g.status + + // TEMPLATE check-valid-values + switch status { + case WithdrawStatusEmailSent: + params["status"] = status + + default: + return nil, fmt.Errorf("status value %v is invalid", status) + + } + // END TEMPLATE check-valid-values + + // assign parameter of status + params["status"] = status + } else { + } + // check startTime field -> json key startTime + if g.startTime != nil { + startTime := *g.startTime + + // assign parameter of startTime + // convert time.Time to milliseconds time stamp + params["startTime"] = strconv.FormatInt(startTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check endTime field -> json key endTime + if g.endTime != nil { + endTime := *g.endTime + + // assign parameter of endTime + // convert time.Time to milliseconds time stamp + params["endTime"] = strconv.FormatInt(endTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check limit field -> json key limit + if g.limit != nil { + limit := *g.limit + + // assign parameter of limit + params["limit"] = limit + } else { + } + // check offset field -> json key offset + if g.offset != nil { + offset := *g.offset + + // assign parameter of offset + params["offset"] = offset + } else { + } + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetWithdrawHistoryRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetWithdrawHistoryRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetWithdrawHistoryRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetWithdrawHistoryRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetWithdrawHistoryRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetWithdrawHistoryRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetWithdrawHistoryRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetWithdrawHistoryRequest) Do(ctx context.Context) ([]WithdrawRecord, error) { + + // empty params for GET operation + var params interface{} + query, err := g.GetParametersQuery() + if err != nil { + return nil, err + } + + apiURL := "/sapi/v1/capital/withdraw/history" + + req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse []WithdrawRecord + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return apiResponse, nil +} diff --git a/pkg/exchange/binance/binanceapi/page.go b/pkg/exchange/binance/binanceapi/page.go new file mode 100644 index 0000000000..1daec6472e --- /dev/null +++ b/pkg/exchange/binance/binanceapi/page.go @@ -0,0 +1,15 @@ +package binanceapi + +import "encoding/json" + +type PagedDataResponse struct { + Status string `json:"status"` + Type string `json:"type"` + Code string `json:"code"` + Data struct { + Page int `json:"page"` + TotalRecords int `json:"totalRecords"` + TotalPageNum int `json:"totalPageNum"` + Data json.RawMessage `json:"data"` + } `json:"data"` +} diff --git a/pkg/exchange/binance/binanceapi/rows.go b/pkg/exchange/binance/binanceapi/rows.go new file mode 100644 index 0000000000..60398419a7 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/rows.go @@ -0,0 +1,8 @@ +package binanceapi + +import "encoding/json" + +type RowsResponse struct { + Rows json.RawMessage `json:"rows"` + Total int `json:"total"` +} diff --git a/pkg/exchange/binance/binanceapi/transfertype_string.go b/pkg/exchange/binance/binanceapi/transfertype_string.go new file mode 100644 index 0000000000..8fad40b79b --- /dev/null +++ b/pkg/exchange/binance/binanceapi/transfertype_string.go @@ -0,0 +1,24 @@ +// Code generated by "stringer -type=TransferType"; DO NOT EDIT. + +package binanceapi + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[TransferTypeInternal-0] + _ = x[TransferTypeExternal-0] +} + +const _TransferType_name = "TransferTypeInternal" + +var _TransferType_index = [...]uint8{0, 20} + +func (i TransferType) String() string { + if i < 0 || i >= TransferType(len(_TransferType_index)-1) { + return "TransferType(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _TransferType_name[_TransferType_index[i]:_TransferType_index[i+1]] +} diff --git a/pkg/exchange/binance/binanceapi/withdraw_request.go b/pkg/exchange/binance/binanceapi/withdraw_request.go new file mode 100644 index 0000000000..50388292b8 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/withdraw_request.go @@ -0,0 +1,41 @@ +package binanceapi + +import "github.com/c9s/requestgen" + +type WalletType int + +const ( + WalletTypeSpot WalletType = 0 + WalletTypeFunding WalletType = 1 +) + +type WithdrawResponse struct { + ID string `json:"id"` +} + +//go:generate requestgen -method POST -url "/sapi/v1/capital/withdraw/apply" -type WithdrawRequest -responseType .WithdrawResponse +type WithdrawRequest struct { + client requestgen.AuthenticatedAPIClient + coin string `param:"coin"` + network *string `param:"network"` + + address string `param:"address"` + addressTag *string `param:"addressTag"` + + // amount is a decimal in string format + amount string `param:"amount"` + + withdrawOrderId *string `param:"withdrawOrderId"` + + transactionFeeFlag *bool `param:"transactionFeeFlag"` + + // name is the address name + name *string `param:"name"` + + // The wallet type for withdraw: 0-spot wallet 1-funding wallet.Default spot wallet + walletType *WalletType `param:"walletType"` +} + +func (c *RestClient) NewWithdrawRequest() *WithdrawRequest { + return &WithdrawRequest{client: c} +} diff --git a/pkg/exchange/binance/binanceapi/withdraw_request_requestgen.go b/pkg/exchange/binance/binanceapi/withdraw_request_requestgen.go new file mode 100644 index 0000000000..557041c2e7 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/withdraw_request_requestgen.go @@ -0,0 +1,256 @@ +// Code generated by "requestgen -method POST -url /sapi/v1/capital/withdraw/apply -type WithdrawRequest -responseType .WithdrawResponse"; DO NOT EDIT. + +package binanceapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +func (w *WithdrawRequest) Coin(coin string) *WithdrawRequest { + w.coin = coin + return w +} + +func (w *WithdrawRequest) Network(network string) *WithdrawRequest { + w.network = &network + return w +} + +func (w *WithdrawRequest) Address(address string) *WithdrawRequest { + w.address = address + return w +} + +func (w *WithdrawRequest) AddressTag(addressTag string) *WithdrawRequest { + w.addressTag = &addressTag + return w +} + +func (w *WithdrawRequest) Amount(amount string) *WithdrawRequest { + w.amount = amount + return w +} + +func (w *WithdrawRequest) WithdrawOrderId(withdrawOrderId string) *WithdrawRequest { + w.withdrawOrderId = &withdrawOrderId + return w +} + +func (w *WithdrawRequest) TransactionFeeFlag(transactionFeeFlag bool) *WithdrawRequest { + w.transactionFeeFlag = &transactionFeeFlag + return w +} + +func (w *WithdrawRequest) Name(name string) *WithdrawRequest { + w.name = &name + return w +} + +func (w *WithdrawRequest) WalletType(walletType WalletType) *WithdrawRequest { + w.walletType = &walletType + return w +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (w *WithdrawRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (w *WithdrawRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check coin field -> json key coin + coin := w.coin + + // assign parameter of coin + params["coin"] = coin + // check network field -> json key network + if w.network != nil { + network := *w.network + + // assign parameter of network + params["network"] = network + } else { + } + // check address field -> json key address + address := w.address + + // assign parameter of address + params["address"] = address + // check addressTag field -> json key addressTag + if w.addressTag != nil { + addressTag := *w.addressTag + + // assign parameter of addressTag + params["addressTag"] = addressTag + } else { + } + // check amount field -> json key amount + amount := w.amount + + // assign parameter of amount + params["amount"] = amount + // check withdrawOrderId field -> json key withdrawOrderId + if w.withdrawOrderId != nil { + withdrawOrderId := *w.withdrawOrderId + + // assign parameter of withdrawOrderId + params["withdrawOrderId"] = withdrawOrderId + } else { + } + // check transactionFeeFlag field -> json key transactionFeeFlag + if w.transactionFeeFlag != nil { + transactionFeeFlag := *w.transactionFeeFlag + + // assign parameter of transactionFeeFlag + params["transactionFeeFlag"] = transactionFeeFlag + } else { + } + // check name field -> json key name + if w.name != nil { + name := *w.name + + // assign parameter of name + params["name"] = name + } else { + } + // check walletType field -> json key walletType + if w.walletType != nil { + walletType := *w.walletType + + // TEMPLATE check-valid-values + switch walletType { + case WalletTypeSpot, WalletTypeFunding: + params["walletType"] = walletType + + default: + return nil, fmt.Errorf("walletType value %v is invalid", walletType) + + } + // END TEMPLATE check-valid-values + + // assign parameter of walletType + params["walletType"] = walletType + } else { + } + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (w *WithdrawRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := w.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if w.isVarSlice(_v) { + w.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (w *WithdrawRequest) GetParametersJSON() ([]byte, error) { + params, err := w.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (w *WithdrawRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (w *WithdrawRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (w *WithdrawRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (w *WithdrawRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (w *WithdrawRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := w.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (w *WithdrawRequest) Do(ctx context.Context) (*WithdrawResponse, error) { + + params, err := w.GetParameters() + if err != nil { + return nil, err + } + query := url.Values{} + + apiURL := "/sapi/v1/capital/withdraw/apply" + + req, err := w.client.NewAuthenticatedRequest(ctx, "POST", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := w.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse WithdrawResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return &apiResponse, nil +} diff --git a/pkg/exchange/binance/binanceapi/withdrawstatus_string.go b/pkg/exchange/binance/binanceapi/withdrawstatus_string.go new file mode 100644 index 0000000000..7c972b7fd5 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/withdrawstatus_string.go @@ -0,0 +1,29 @@ +// Code generated by "stringer -type=WithdrawStatus"; DO NOT EDIT. + +package binanceapi + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[WithdrawStatusEmailSent-0] + _ = x[WithdrawStatusCancelled-1] + _ = x[WithdrawStatusAwaitingApproval-2] + _ = x[WithdrawStatusRejected-3] + _ = x[WithdrawStatusProcessing-4] + _ = x[WithdrawStatusFailure-5] + _ = x[WithdrawStatusCompleted-6] +} + +const _WithdrawStatus_name = "WithdrawStatusEmailSentWithdrawStatusCancelledWithdrawStatusAwaitingApprovalWithdrawStatusRejectedWithdrawStatusProcessingWithdrawStatusFailureWithdrawStatusCompleted" + +var _WithdrawStatus_index = [...]uint8{0, 23, 46, 76, 98, 122, 143, 166} + +func (i WithdrawStatus) String() string { + if i < 0 || i >= WithdrawStatus(len(_WithdrawStatus_index)-1) { + return "WithdrawStatus(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _WithdrawStatus_name[_WithdrawStatus_index[i]:_WithdrawStatus_index[i+1]] +} diff --git a/pkg/exchange/binance/convert.go b/pkg/exchange/binance/convert.go index 1805db8589..d160e4f96b 100644 --- a/pkg/exchange/binance/convert.go +++ b/pkg/exchange/binance/convert.go @@ -84,153 +84,14 @@ func toGlobalFuturesMarket(symbol futures.Symbol) types.Market { return market } -func toGlobalIsolatedUserAsset(userAsset binance.IsolatedUserAsset) types.IsolatedUserAsset { - return types.IsolatedUserAsset{ - Asset: userAsset.Asset, - Borrowed: fixedpoint.MustNewFromString(userAsset.Borrowed), - Free: fixedpoint.MustNewFromString(userAsset.Free), - Interest: fixedpoint.MustNewFromString(userAsset.Interest), - Locked: fixedpoint.MustNewFromString(userAsset.Locked), - NetAsset: fixedpoint.MustNewFromString(userAsset.NetAsset), - NetAssetOfBtc: fixedpoint.MustNewFromString(userAsset.NetAssetOfBtc), - BorrowEnabled: userAsset.BorrowEnabled, - RepayEnabled: userAsset.RepayEnabled, - TotalAsset: fixedpoint.MustNewFromString(userAsset.TotalAsset), - } -} - -func toGlobalIsolatedMarginAsset(asset binance.IsolatedMarginAsset) types.IsolatedMarginAsset { - return types.IsolatedMarginAsset{ - Symbol: asset.Symbol, - QuoteAsset: toGlobalIsolatedUserAsset(asset.QuoteAsset), - BaseAsset: toGlobalIsolatedUserAsset(asset.BaseAsset), - IsolatedCreated: asset.IsolatedCreated, - MarginLevel: fixedpoint.MustNewFromString(asset.MarginLevel), - MarginLevelStatus: asset.MarginLevelStatus, - MarginRatio: fixedpoint.MustNewFromString(asset.MarginRatio), - IndexPrice: fixedpoint.MustNewFromString(asset.IndexPrice), - LiquidatePrice: fixedpoint.MustNewFromString(asset.LiquidatePrice), - LiquidateRate: fixedpoint.MustNewFromString(asset.LiquidateRate), - TradeEnabled: false, - } -} - -func toGlobalIsolatedMarginAssets(assets []binance.IsolatedMarginAsset) (retAssets types.IsolatedMarginAssetMap) { - retMarginAssets := make(types.IsolatedMarginAssetMap) - for _, marginAsset := range assets { - retMarginAssets[marginAsset.Symbol] = toGlobalIsolatedMarginAsset(marginAsset) - } - - return retMarginAssets -} - -//func toGlobalIsolatedMarginAccount(account *binance.IsolatedMarginAccount) *types.IsolatedMarginAccount { +// func toGlobalIsolatedMarginAccount(account *binance.IsolatedMarginAccount) *types.IsolatedMarginAccount { // return &types.IsolatedMarginAccount{ // TotalAssetOfBTC: fixedpoint.MustNewFromString(account.TotalNetAssetOfBTC), // TotalLiabilityOfBTC: fixedpoint.MustNewFromString(account.TotalLiabilityOfBTC), // TotalNetAssetOfBTC: fixedpoint.MustNewFromString(account.TotalNetAssetOfBTC), // Assets: toGlobalIsolatedMarginAssets(account.Assets), // } -//} - -func toGlobalMarginUserAssets(assets []binance.UserAsset) types.MarginAssetMap { - retMarginAssets := make(types.MarginAssetMap) - for _, marginAsset := range assets { - retMarginAssets[marginAsset.Asset] = types.MarginUserAsset{ - Asset: marginAsset.Asset, - Borrowed: fixedpoint.MustNewFromString(marginAsset.Borrowed), - Free: fixedpoint.MustNewFromString(marginAsset.Free), - Interest: fixedpoint.MustNewFromString(marginAsset.Interest), - Locked: fixedpoint.MustNewFromString(marginAsset.Locked), - NetAsset: fixedpoint.MustNewFromString(marginAsset.NetAsset), - } - } - - return retMarginAssets -} - -func toGlobalMarginAccountInfo(account *binance.MarginAccount) *types.MarginAccountInfo { - return &types.MarginAccountInfo{ - BorrowEnabled: account.BorrowEnabled, - MarginLevel: fixedpoint.MustNewFromString(account.MarginLevel), - TotalAssetOfBTC: fixedpoint.MustNewFromString(account.TotalAssetOfBTC), - TotalLiabilityOfBTC: fixedpoint.MustNewFromString(account.TotalLiabilityOfBTC), - TotalNetAssetOfBTC: fixedpoint.MustNewFromString(account.TotalNetAssetOfBTC), - TradeEnabled: account.TradeEnabled, - TransferEnabled: account.TransferEnabled, - Assets: toGlobalMarginUserAssets(account.UserAssets), - } -} - -func toGlobalIsolatedMarginAccountInfo(account *binance.IsolatedMarginAccount) *types.IsolatedMarginAccountInfo { - return &types.IsolatedMarginAccountInfo{ - TotalAssetOfBTC: fixedpoint.MustNewFromString(account.TotalAssetOfBTC), - TotalLiabilityOfBTC: fixedpoint.MustNewFromString(account.TotalLiabilityOfBTC), - TotalNetAssetOfBTC: fixedpoint.MustNewFromString(account.TotalNetAssetOfBTC), - Assets: toGlobalIsolatedMarginAssets(account.Assets), - } -} - -func toGlobalFuturesAccountInfo(account *futures.Account) *types.FuturesAccountInfo { - return &types.FuturesAccountInfo{ - Assets: toGlobalFuturesUserAssets(account.Assets), - Positions: toGlobalFuturesPositions(account.Positions), - TotalInitialMargin: fixedpoint.MustNewFromString(account.TotalInitialMargin), - TotalMaintMargin: fixedpoint.MustNewFromString(account.TotalMaintMargin), - TotalMarginBalance: fixedpoint.MustNewFromString(account.TotalMarginBalance), - TotalOpenOrderInitialMargin: fixedpoint.MustNewFromString(account.TotalOpenOrderInitialMargin), - TotalPositionInitialMargin: fixedpoint.MustNewFromString(account.TotalPositionInitialMargin), - TotalUnrealizedProfit: fixedpoint.MustNewFromString(account.TotalUnrealizedProfit), - TotalWalletBalance: fixedpoint.MustNewFromString(account.TotalWalletBalance), - UpdateTime: account.UpdateTime, - } -} - -func toGlobalFuturesBalance(balances []*futures.Balance) types.BalanceMap { - retBalances := make(types.BalanceMap) - for _, balance := range balances { - retBalances[balance.Asset] = types.Balance{ - Currency: balance.Asset, - Available: fixedpoint.MustNewFromString(balance.AvailableBalance), - } - } - return retBalances -} - -func toGlobalFuturesPositions(futuresPositions []*futures.AccountPosition) types.FuturesPositionMap { - retFuturesPositions := make(types.FuturesPositionMap) - for _, futuresPosition := range futuresPositions { - retFuturesPositions[futuresPosition.Symbol] = types.FuturesPosition{ // TODO: types.FuturesPosition - Isolated: futuresPosition.Isolated, - PositionRisk: &types.PositionRisk{ - Leverage: fixedpoint.MustNewFromString(futuresPosition.Leverage), - }, - Symbol: futuresPosition.Symbol, - UpdateTime: futuresPosition.UpdateTime, - } - } - - return retFuturesPositions -} - -func toGlobalFuturesUserAssets(assets []*futures.AccountAsset) (retAssets types.FuturesAssetMap) { - retFuturesAssets := make(types.FuturesAssetMap) - for _, futuresAsset := range assets { - retFuturesAssets[futuresAsset.Asset] = types.FuturesUserAsset{ - Asset: futuresAsset.Asset, - InitialMargin: fixedpoint.MustNewFromString(futuresAsset.InitialMargin), - MaintMargin: fixedpoint.MustNewFromString(futuresAsset.MaintMargin), - MarginBalance: fixedpoint.MustNewFromString(futuresAsset.MarginBalance), - MaxWithdrawAmount: fixedpoint.MustNewFromString(futuresAsset.MaxWithdrawAmount), - OpenOrderInitialMargin: fixedpoint.MustNewFromString(futuresAsset.OpenOrderInitialMargin), - PositionInitialMargin: fixedpoint.MustNewFromString(futuresAsset.PositionInitialMargin), - UnrealizedProfit: fixedpoint.MustNewFromString(futuresAsset.UnrealizedProfit), - WalletBalance: fixedpoint.MustNewFromString(futuresAsset.WalletBalance), - } - } - - return retFuturesAssets -} +// } func toGlobalTicker(stats *binance.PriceChangeStats) (*types.Ticker, error) { return &types.Ticker{ @@ -245,6 +106,19 @@ func toGlobalTicker(stats *binance.PriceChangeStats) (*types.Ticker, error) { }, nil } +func toGlobalFuturesTicker(stats *futures.PriceChangeStats) (*types.Ticker, error) { + return &types.Ticker{ + Volume: fixedpoint.MustNewFromString(stats.Volume), + Last: fixedpoint.MustNewFromString(stats.LastPrice), + Open: fixedpoint.MustNewFromString(stats.OpenPrice), + High: fixedpoint.MustNewFromString(stats.HighPrice), + Low: fixedpoint.MustNewFromString(stats.LowPrice), + Buy: fixedpoint.MustNewFromString(stats.LastPrice), + Sell: fixedpoint.MustNewFromString(stats.LastPrice), + Time: time.Unix(0, stats.CloseTime*int64(time.Millisecond)), + }, nil +} + func toLocalOrderType(orderType types.OrderType) (binance.OrderType, error) { switch orderType { @@ -267,44 +141,9 @@ func toLocalOrderType(orderType types.OrderType) (binance.OrderType, error) { return "", fmt.Errorf("can not convert to local order, order type %s not supported", orderType) } -func toLocalFuturesOrderType(orderType types.OrderType) (futures.OrderType, error) { - switch orderType { - - // case types.OrderTypeLimitMaker: - // return futures.OrderTypeLimitMaker, nil //TODO - - case types.OrderTypeLimit, types.OrderTypeLimitMaker: - return futures.OrderTypeLimit, nil - - // case types.OrderTypeStopLimit: - // return futures.OrderTypeStopLossLimit, nil //TODO - - // case types.OrderTypeStopMarket: - // return futures.OrderTypeStopLoss, nil //TODO - - case types.OrderTypeMarket: - return futures.OrderTypeMarket, nil - } - - return "", fmt.Errorf("can not convert to local order, order type %s not supported", orderType) -} - -func toGlobalOrders(binanceOrders []*binance.Order) (orders []types.Order, err error) { +func toGlobalOrders(binanceOrders []*binance.Order, isMargin bool) (orders []types.Order, err error) { for _, binanceOrder := range binanceOrders { - order, err := toGlobalOrder(binanceOrder, false) - if err != nil { - return orders, err - } - - orders = append(orders, *order) - } - - return orders, err -} - -func toGlobalFuturesOrders(futuresOrders []*futures.Order) (orders []types.Order, err error) { - for _, futuresOrder := range futuresOrders { - order, err := toGlobalFuturesOrder(futuresOrder, false) + order, err := toGlobalOrder(binanceOrder, isMargin) if err != nil { return orders, err } @@ -338,29 +177,6 @@ func toGlobalOrder(binanceOrder *binance.Order, isMargin bool) (*types.Order, er }, nil } -func toGlobalFuturesOrder(futuresOrder *futures.Order, isMargin bool) (*types.Order, error) { - return &types.Order{ - SubmitOrder: types.SubmitOrder{ - ClientOrderID: futuresOrder.ClientOrderID, - Symbol: futuresOrder.Symbol, - Side: toGlobalFuturesSideType(futuresOrder.Side), - Type: toGlobalFuturesOrderType(futuresOrder.Type), - ReduceOnly: futuresOrder.ReduceOnly, - ClosePosition: futuresOrder.ClosePosition, - Quantity: fixedpoint.MustNewFromString(futuresOrder.OrigQuantity), - Price: fixedpoint.MustNewFromString(futuresOrder.Price), - TimeInForce: types.TimeInForce(futuresOrder.TimeInForce), - }, - Exchange: types.ExchangeBinance, - OrderID: uint64(futuresOrder.OrderID), - Status: toGlobalFuturesOrderStatus(futuresOrder.Status), - ExecutedQuantity: fixedpoint.MustNewFromString(futuresOrder.ExecutedQuantity), - CreationTime: types.Time(millisecondTime(futuresOrder.Time)), - UpdateTime: types.Time(millisecondTime(futuresOrder.UpdateTime)), - IsMargin: isMargin, - }, nil -} - func millisecondTime(t int64) time.Time { return time.Unix(0, t*int64(time.Millisecond)) } @@ -418,58 +234,6 @@ func toGlobalTrade(t binance.TradeV3, isMargin bool) (*types.Trade, error) { }, nil } -func toGlobalFuturesTrade(t futures.AccountTrade) (*types.Trade, error) { - // skip trade ID that is the same. however this should not happen - var side types.SideType - if t.Buyer { - side = types.SideTypeBuy - } else { - side = types.SideTypeSell - } - - price, err := fixedpoint.NewFromString(t.Price) - if err != nil { - return nil, errors.Wrapf(err, "price parse error, price: %+v", t.Price) - } - - quantity, err := fixedpoint.NewFromString(t.Quantity) - if err != nil { - return nil, errors.Wrapf(err, "quantity parse error, quantity: %+v", t.Quantity) - } - - var quoteQuantity fixedpoint.Value - if len(t.QuoteQuantity) > 0 { - quoteQuantity, err = fixedpoint.NewFromString(t.QuoteQuantity) - if err != nil { - return nil, errors.Wrapf(err, "quote quantity parse error, quoteQuantity: %+v", t.QuoteQuantity) - } - } else { - quoteQuantity = price.Mul(quantity) - } - - fee, err := fixedpoint.NewFromString(t.Commission) - if err != nil { - return nil, errors.Wrapf(err, "commission parse error, commission: %+v", t.Commission) - } - - return &types.Trade{ - ID: uint64(t.ID), - OrderID: uint64(t.OrderID), - Price: price, - Symbol: t.Symbol, - Exchange: "binance", - Quantity: quantity, - QuoteQuantity: quoteQuantity, - Side: side, - IsBuyer: t.Buyer, - IsMaker: t.Maker, - Fee: fee, - FeeCurrency: t.CommissionAsset, - Time: types.Time(millisecondTime(t.Time)), - IsFutures: true, - }, nil -} - func toGlobalSideType(side binance.SideType) types.SideType { switch side { case binance.SideTypeBuy: @@ -484,20 +248,6 @@ func toGlobalSideType(side binance.SideType) types.SideType { } } -func toGlobalFuturesSideType(side futures.SideType) types.SideType { - switch side { - case futures.SideTypeBuy: - return types.SideTypeBuy - - case futures.SideTypeSell: - return types.SideTypeSell - - default: - log.Errorf("can not convert futures side type, unknown side type: %q", side) - return "" - } -} - func toGlobalOrderType(orderType binance.OrderType) types.OrderType { switch orderType { @@ -520,27 +270,6 @@ func toGlobalOrderType(orderType binance.OrderType) types.OrderType { } } -func toGlobalFuturesOrderType(orderType futures.OrderType) types.OrderType { - switch orderType { - // TODO - case futures.OrderTypeLimit: // , futures.OrderTypeLimitMaker, futures.OrderTypeTakeProfitLimit: - return types.OrderTypeLimit - - case futures.OrderTypeMarket: - return types.OrderTypeMarket - // TODO - // case futures.OrderTypeStopLossLimit: - // return types.OrderTypeStopLimit - // TODO - // case futures.OrderTypeStopLoss: - // return types.OrderTypeStopMarket - - default: - log.Errorf("unsupported order type: %v", orderType) - return "" - } -} - func toGlobalOrderStatus(orderStatus binance.OrderStatusType) types.OrderStatus { switch orderStatus { case binance.OrderStatusTypeNew: @@ -549,7 +278,7 @@ func toGlobalOrderStatus(orderStatus binance.OrderStatusType) types.OrderStatus case binance.OrderStatusTypeRejected: return types.OrderStatusRejected - case binance.OrderStatusTypeCanceled: + case binance.OrderStatusTypeCanceled, binance.OrderStatusTypeExpired, binance.OrderStatusTypePendingCancel: return types.OrderStatusCanceled case binance.OrderStatusTypePartiallyFilled: @@ -562,31 +291,12 @@ func toGlobalOrderStatus(orderStatus binance.OrderStatusType) types.OrderStatus return types.OrderStatus(orderStatus) } -func toGlobalFuturesOrderStatus(orderStatus futures.OrderStatusType) types.OrderStatus { - switch orderStatus { - case futures.OrderStatusTypeNew: - return types.OrderStatusNew - - case futures.OrderStatusTypeRejected: - return types.OrderStatusRejected - - case futures.OrderStatusTypeCanceled: - return types.OrderStatusCanceled - - case futures.OrderStatusTypePartiallyFilled: - return types.OrderStatusPartiallyFilled - - case futures.OrderStatusTypeFilled: - return types.OrderStatusFilled - } - - return types.OrderStatus(orderStatus) -} - func convertSubscription(s types.Subscription) string { // binance uses lower case symbol name, // for kline, it's "@kline_" // for depth, it's "@depth OR @depth@100ms" + // for trade, it's "@trade" + // for aggregated trade, it's "@aggTrade" switch s.Channel { case types.KLineChannel: return fmt.Sprintf("%s@%s_%s", strings.ToLower(s.Symbol), s.Channel, s.Options.String()) @@ -618,47 +328,11 @@ func convertSubscription(s types.Subscription) string { return n case types.BookTickerChannel: return fmt.Sprintf("%s@bookTicker", strings.ToLower(s.Symbol)) + case types.MarketTradeChannel: + return fmt.Sprintf("%s@trade", strings.ToLower(s.Symbol)) + case types.AggTradeChannel: + return fmt.Sprintf("%s@aggTrade", strings.ToLower(s.Symbol)) } return fmt.Sprintf("%s@%s", strings.ToLower(s.Symbol), s.Channel) } - -func convertPremiumIndex(index *futures.PremiumIndex) (*types.PremiumIndex, error) { - markPrice, err := fixedpoint.NewFromString(index.MarkPrice) - if err != nil { - return nil, err - } - - lastFundingRate, err := fixedpoint.NewFromString(index.LastFundingRate) - if err != nil { - return nil, err - } - - nextFundingTime := time.Unix(0, index.NextFundingTime*int64(time.Millisecond)) - t := time.Unix(0, index.Time*int64(time.Millisecond)) - - return &types.PremiumIndex{ - Symbol: index.Symbol, - MarkPrice: markPrice, - NextFundingTime: nextFundingTime, - LastFundingRate: lastFundingRate, - Time: t, - }, nil -} - -func convertPositionRisk(risk *futures.PositionRisk) (*types.PositionRisk, error) { - leverage, err := fixedpoint.NewFromString(risk.Leverage) - if err != nil { - return nil, err - } - - liquidationPrice, err := fixedpoint.NewFromString(risk.LiquidationPrice) - if err != nil { - return nil, err - } - - return &types.PositionRisk{ - Leverage: leverage, - LiquidationPrice: liquidationPrice, - }, nil -} diff --git a/pkg/exchange/binance/convert_futures.go b/pkg/exchange/binance/convert_futures.go new file mode 100644 index 0000000000..30187c422a --- /dev/null +++ b/pkg/exchange/binance/convert_futures.go @@ -0,0 +1,289 @@ +package binance + +import ( + "fmt" + "time" + + "github.com/adshao/go-binance/v2/futures" + "github.com/pkg/errors" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +func toGlobalFuturesAccountInfo(account *futures.Account) *types.FuturesAccountInfo { + return &types.FuturesAccountInfo{ + Assets: toGlobalFuturesUserAssets(account.Assets), + Positions: toGlobalFuturesPositions(account.Positions), + TotalInitialMargin: fixedpoint.MustNewFromString(account.TotalInitialMargin), + TotalMaintMargin: fixedpoint.MustNewFromString(account.TotalMaintMargin), + TotalMarginBalance: fixedpoint.MustNewFromString(account.TotalMarginBalance), + TotalOpenOrderInitialMargin: fixedpoint.MustNewFromString(account.TotalOpenOrderInitialMargin), + TotalPositionInitialMargin: fixedpoint.MustNewFromString(account.TotalPositionInitialMargin), + TotalUnrealizedProfit: fixedpoint.MustNewFromString(account.TotalUnrealizedProfit), + TotalWalletBalance: fixedpoint.MustNewFromString(account.TotalWalletBalance), + UpdateTime: account.UpdateTime, + } +} + +func toGlobalFuturesBalance(balances []*futures.Balance) types.BalanceMap { + retBalances := make(types.BalanceMap) + for _, balance := range balances { + retBalances[balance.Asset] = types.Balance{ + Currency: balance.Asset, + Available: fixedpoint.MustNewFromString(balance.AvailableBalance), + } + } + return retBalances +} + +func toGlobalFuturesPositions(futuresPositions []*futures.AccountPosition) types.FuturesPositionMap { + retFuturesPositions := make(types.FuturesPositionMap) + for _, futuresPosition := range futuresPositions { + retFuturesPositions[futuresPosition.Symbol] = types.FuturesPosition{ // TODO: types.FuturesPosition + Isolated: futuresPosition.Isolated, + AverageCost: fixedpoint.MustNewFromString(futuresPosition.EntryPrice), + ApproximateAverageCost: fixedpoint.MustNewFromString(futuresPosition.EntryPrice), + Base: fixedpoint.MustNewFromString(futuresPosition.PositionAmt), + Quote: fixedpoint.MustNewFromString(futuresPosition.Notional), + + PositionRisk: &types.PositionRisk{ + Leverage: fixedpoint.MustNewFromString(futuresPosition.Leverage), + }, + Symbol: futuresPosition.Symbol, + UpdateTime: futuresPosition.UpdateTime, + } + } + + return retFuturesPositions +} + +func toGlobalFuturesUserAssets(assets []*futures.AccountAsset) (retAssets types.FuturesAssetMap) { + retFuturesAssets := make(types.FuturesAssetMap) + for _, futuresAsset := range assets { + retFuturesAssets[futuresAsset.Asset] = types.FuturesUserAsset{ + Asset: futuresAsset.Asset, + InitialMargin: fixedpoint.MustNewFromString(futuresAsset.InitialMargin), + MaintMargin: fixedpoint.MustNewFromString(futuresAsset.MaintMargin), + MarginBalance: fixedpoint.MustNewFromString(futuresAsset.MarginBalance), + MaxWithdrawAmount: fixedpoint.MustNewFromString(futuresAsset.MaxWithdrawAmount), + OpenOrderInitialMargin: fixedpoint.MustNewFromString(futuresAsset.OpenOrderInitialMargin), + PositionInitialMargin: fixedpoint.MustNewFromString(futuresAsset.PositionInitialMargin), + UnrealizedProfit: fixedpoint.MustNewFromString(futuresAsset.UnrealizedProfit), + WalletBalance: fixedpoint.MustNewFromString(futuresAsset.WalletBalance), + } + } + + return retFuturesAssets +} + +func toLocalFuturesOrderType(orderType types.OrderType) (futures.OrderType, error) { + switch orderType { + + // case types.OrderTypeLimitMaker: + // return futures.OrderTypeLimitMaker, nil //TODO + + case types.OrderTypeLimit, types.OrderTypeLimitMaker: + return futures.OrderTypeLimit, nil + + // case types.OrderTypeStopLimit: + // return futures.OrderTypeStopLossLimit, nil //TODO + + // case types.OrderTypeStopMarket: + // return futures.OrderTypeStopLoss, nil //TODO + + case types.OrderTypeMarket: + return futures.OrderTypeMarket, nil + } + + return "", fmt.Errorf("can not convert to local order, order type %s not supported", orderType) +} + +func toGlobalFuturesOrders(futuresOrders []*futures.Order, isIsolated bool) (orders []types.Order, err error) { + for _, futuresOrder := range futuresOrders { + order, err := toGlobalFuturesOrder(futuresOrder, isIsolated) + if err != nil { + return orders, err + } + + orders = append(orders, *order) + } + + return orders, err +} + +func toGlobalFuturesOrder(futuresOrder *futures.Order, isIsolated bool) (*types.Order, error) { + return &types.Order{ + SubmitOrder: types.SubmitOrder{ + ClientOrderID: futuresOrder.ClientOrderID, + Symbol: futuresOrder.Symbol, + Side: toGlobalFuturesSideType(futuresOrder.Side), + Type: toGlobalFuturesOrderType(futuresOrder.Type), + ReduceOnly: futuresOrder.ReduceOnly, + ClosePosition: futuresOrder.ClosePosition, + Quantity: fixedpoint.MustNewFromString(futuresOrder.OrigQuantity), + Price: fixedpoint.MustNewFromString(futuresOrder.Price), + TimeInForce: types.TimeInForce(futuresOrder.TimeInForce), + }, + Exchange: types.ExchangeBinance, + OrderID: uint64(futuresOrder.OrderID), + Status: toGlobalFuturesOrderStatus(futuresOrder.Status), + ExecutedQuantity: fixedpoint.MustNewFromString(futuresOrder.ExecutedQuantity), + CreationTime: types.Time(millisecondTime(futuresOrder.Time)), + UpdateTime: types.Time(millisecondTime(futuresOrder.UpdateTime)), + IsFutures: true, + }, nil +} + +func toGlobalFuturesTrade(t futures.AccountTrade) (*types.Trade, error) { + // skip trade ID that is the same. however this should not happen + var side types.SideType + if t.Buyer { + side = types.SideTypeBuy + } else { + side = types.SideTypeSell + } + + price, err := fixedpoint.NewFromString(t.Price) + if err != nil { + return nil, errors.Wrapf(err, "price parse error, price: %+v", t.Price) + } + + quantity, err := fixedpoint.NewFromString(t.Quantity) + if err != nil { + return nil, errors.Wrapf(err, "quantity parse error, quantity: %+v", t.Quantity) + } + + var quoteQuantity fixedpoint.Value + if len(t.QuoteQuantity) > 0 { + quoteQuantity, err = fixedpoint.NewFromString(t.QuoteQuantity) + if err != nil { + return nil, errors.Wrapf(err, "quote quantity parse error, quoteQuantity: %+v", t.QuoteQuantity) + } + } else { + quoteQuantity = price.Mul(quantity) + } + + fee, err := fixedpoint.NewFromString(t.Commission) + if err != nil { + return nil, errors.Wrapf(err, "commission parse error, commission: %+v", t.Commission) + } + + return &types.Trade{ + ID: uint64(t.ID), + OrderID: uint64(t.OrderID), + Price: price, + Symbol: t.Symbol, + Exchange: "binance", + Quantity: quantity, + QuoteQuantity: quoteQuantity, + Side: side, + IsBuyer: t.Buyer, + IsMaker: t.Maker, + Fee: fee, + FeeCurrency: t.CommissionAsset, + Time: types.Time(millisecondTime(t.Time)), + IsFutures: true, + }, nil +} + +func toGlobalFuturesSideType(side futures.SideType) types.SideType { + switch side { + case futures.SideTypeBuy: + return types.SideTypeBuy + + case futures.SideTypeSell: + return types.SideTypeSell + + default: + log.Errorf("can not convert futures side type, unknown side type: %q", side) + return "" + } +} + +func toGlobalFuturesOrderType(orderType futures.OrderType) types.OrderType { + switch orderType { + // FIXME: handle this order type + // case futures.OrderTypeTrailingStopMarket: + + case futures.OrderTypeTakeProfit: + return types.OrderTypeStopLimit + + case futures.OrderTypeTakeProfitMarket: + return types.OrderTypeStopMarket + + case futures.OrderTypeStopMarket: + return types.OrderTypeStopMarket + + case futures.OrderTypeLimit: + return types.OrderTypeLimit + + case futures.OrderTypeMarket: + return types.OrderTypeMarket + + default: + log.Errorf("unsupported binance futures order type: %s", orderType) + return "" + } +} + +func toGlobalFuturesOrderStatus(orderStatus futures.OrderStatusType) types.OrderStatus { + switch orderStatus { + case futures.OrderStatusTypeNew: + return types.OrderStatusNew + + case futures.OrderStatusTypeRejected: + return types.OrderStatusRejected + + case futures.OrderStatusTypeCanceled: + return types.OrderStatusCanceled + + case futures.OrderStatusTypePartiallyFilled: + return types.OrderStatusPartiallyFilled + + case futures.OrderStatusTypeFilled: + return types.OrderStatusFilled + } + + return types.OrderStatus(orderStatus) +} + +func convertPremiumIndex(index *futures.PremiumIndex) (*types.PremiumIndex, error) { + markPrice, err := fixedpoint.NewFromString(index.MarkPrice) + if err != nil { + return nil, err + } + + lastFundingRate, err := fixedpoint.NewFromString(index.LastFundingRate) + if err != nil { + return nil, err + } + + nextFundingTime := time.Unix(0, index.NextFundingTime*int64(time.Millisecond)) + t := time.Unix(0, index.Time*int64(time.Millisecond)) + + return &types.PremiumIndex{ + Symbol: index.Symbol, + MarkPrice: markPrice, + NextFundingTime: nextFundingTime, + LastFundingRate: lastFundingRate, + Time: t, + }, nil +} + +func convertPositionRisk(risk *futures.PositionRisk) (*types.PositionRisk, error) { + leverage, err := fixedpoint.NewFromString(risk.Leverage) + if err != nil { + return nil, err + } + + liquidationPrice, err := fixedpoint.NewFromString(risk.LiquidationPrice) + if err != nil { + return nil, err + } + + return &types.PositionRisk{ + Leverage: leverage, + LiquidationPrice: liquidationPrice, + }, nil +} diff --git a/pkg/exchange/binance/convert_margin.go b/pkg/exchange/binance/convert_margin.go new file mode 100644 index 0000000000..e04bad07e1 --- /dev/null +++ b/pkg/exchange/binance/convert_margin.go @@ -0,0 +1,137 @@ +package binance + +import ( + "github.com/adshao/go-binance/v2" + + "github.com/c9s/bbgo/pkg/exchange/binance/binanceapi" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +func toGlobalLoan(record binanceapi.MarginLoanRecord) types.MarginLoan { + return types.MarginLoan{ + Exchange: types.ExchangeBinance, + TransactionID: uint64(record.TxId), + Asset: record.Asset, + Principle: record.Principal, + Time: types.Time(record.Timestamp), + IsolatedSymbol: record.IsolatedSymbol, + } +} + +func toGlobalRepay(record binanceapi.MarginRepayRecord) types.MarginRepay { + return types.MarginRepay{ + Exchange: types.ExchangeBinance, + TransactionID: record.TxId, + Asset: record.Asset, + Principle: record.Principal, + Time: types.Time(record.Timestamp), + IsolatedSymbol: record.IsolatedSymbol, + } +} + +func toGlobalInterest(record binanceapi.MarginInterest) types.MarginInterest { + return types.MarginInterest{ + Exchange: types.ExchangeBinance, + Asset: record.Asset, + Principle: record.Principal, + Interest: record.Interest, + InterestRate: record.InterestRate, + IsolatedSymbol: record.IsolatedSymbol, + Time: types.Time(record.InterestAccuredTime), + } +} + +func toGlobalLiquidation(record binanceapi.MarginLiquidationRecord) types.MarginLiquidation { + return types.MarginLiquidation{ + Exchange: types.ExchangeBinance, + AveragePrice: record.AveragePrice, + ExecutedQuantity: record.ExecutedQuantity, + OrderID: record.OrderId, + Price: record.Price, + Quantity: record.Quantity, + Side: toGlobalSideType(record.Side), + Symbol: record.Symbol, + TimeInForce: types.TimeInForce(record.TimeInForce), + IsIsolated: record.IsIsolated, + UpdatedTime: types.Time(record.UpdatedTime), + } +} + +func toGlobalIsolatedUserAsset(userAsset binance.IsolatedUserAsset) types.IsolatedUserAsset { + return types.IsolatedUserAsset{ + Asset: userAsset.Asset, + Borrowed: fixedpoint.MustNewFromString(userAsset.Borrowed), + Free: fixedpoint.MustNewFromString(userAsset.Free), + Interest: fixedpoint.MustNewFromString(userAsset.Interest), + Locked: fixedpoint.MustNewFromString(userAsset.Locked), + NetAsset: fixedpoint.MustNewFromString(userAsset.NetAsset), + NetAssetOfBtc: fixedpoint.MustNewFromString(userAsset.NetAssetOfBtc), + BorrowEnabled: userAsset.BorrowEnabled, + RepayEnabled: userAsset.RepayEnabled, + TotalAsset: fixedpoint.MustNewFromString(userAsset.TotalAsset), + } +} + +func toGlobalIsolatedMarginAsset(asset binance.IsolatedMarginAsset) types.IsolatedMarginAsset { + return types.IsolatedMarginAsset{ + Symbol: asset.Symbol, + QuoteAsset: toGlobalIsolatedUserAsset(asset.QuoteAsset), + BaseAsset: toGlobalIsolatedUserAsset(asset.BaseAsset), + IsolatedCreated: asset.IsolatedCreated, + MarginLevel: fixedpoint.MustNewFromString(asset.MarginLevel), + MarginLevelStatus: asset.MarginLevelStatus, + MarginRatio: fixedpoint.MustNewFromString(asset.MarginRatio), + IndexPrice: fixedpoint.MustNewFromString(asset.IndexPrice), + LiquidatePrice: fixedpoint.MustNewFromString(asset.LiquidatePrice), + LiquidateRate: fixedpoint.MustNewFromString(asset.LiquidateRate), + TradeEnabled: false, + } +} + +func toGlobalIsolatedMarginAssets(assets []binance.IsolatedMarginAsset) (retAssets types.IsolatedMarginAssetMap) { + retMarginAssets := make(types.IsolatedMarginAssetMap) + for _, marginAsset := range assets { + retMarginAssets[marginAsset.Symbol] = toGlobalIsolatedMarginAsset(marginAsset) + } + + return retMarginAssets +} + +func toGlobalMarginUserAssets(assets []binance.UserAsset) types.MarginAssetMap { + retMarginAssets := make(types.MarginAssetMap) + for _, marginAsset := range assets { + retMarginAssets[marginAsset.Asset] = types.MarginUserAsset{ + Asset: marginAsset.Asset, + Borrowed: fixedpoint.MustNewFromString(marginAsset.Borrowed), + Free: fixedpoint.MustNewFromString(marginAsset.Free), + Interest: fixedpoint.MustNewFromString(marginAsset.Interest), + Locked: fixedpoint.MustNewFromString(marginAsset.Locked), + NetAsset: fixedpoint.MustNewFromString(marginAsset.NetAsset), + } + } + + return retMarginAssets +} + +func toGlobalMarginAccountInfo(account *binance.MarginAccount) *types.MarginAccountInfo { + return &types.MarginAccountInfo{ + BorrowEnabled: account.BorrowEnabled, + MarginLevel: fixedpoint.MustNewFromString(account.MarginLevel), + TotalAssetOfBTC: fixedpoint.MustNewFromString(account.TotalAssetOfBTC), + TotalLiabilityOfBTC: fixedpoint.MustNewFromString(account.TotalLiabilityOfBTC), + TotalNetAssetOfBTC: fixedpoint.MustNewFromString(account.TotalNetAssetOfBTC), + TradeEnabled: account.TradeEnabled, + TransferEnabled: account.TransferEnabled, + Assets: toGlobalMarginUserAssets(account.UserAssets), + } +} + +func toGlobalIsolatedMarginAccountInfo(account *binance.IsolatedMarginAccount) *types.IsolatedMarginAccountInfo { + return &types.IsolatedMarginAccountInfo{ + TotalAssetOfBTC: fixedpoint.MustNewFromString(account.TotalAssetOfBTC), + TotalLiabilityOfBTC: fixedpoint.MustNewFromString(account.TotalLiabilityOfBTC), + TotalNetAssetOfBTC: fixedpoint.MustNewFromString(account.TotalNetAssetOfBTC), + Assets: toGlobalIsolatedMarginAssets(account.Assets), + } +} diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index 8a7d327171..03a7a0a8e8 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -3,13 +3,14 @@ package binance import ( "context" "fmt" - "net/http" "os" "strconv" "strings" "sync" "time" + "github.com/adshao/go-binance/v2" + "github.com/adshao/go-binance/v2/futures" "github.com/spf13/viper" @@ -17,11 +18,11 @@ import ( "golang.org/x/time/rate" - "github.com/adshao/go-binance/v2" "github.com/google/uuid" "github.com/pkg/errors" "github.com/sirupsen/logrus" + "github.com/c9s/bbgo/pkg/exchange/binance/binanceapi" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/util" @@ -38,8 +39,12 @@ const FutureTestBaseURL = "https://testnet.binancefuture.com" const FuturesWebSocketURL = "wss://fstream.binance.com" const FuturesWebSocketTestURL = "wss://stream.binancefuture.com" -// 5 per second and a 2 initial bucket +// orderLimiter - the default order limiter apply 5 requests per second and a 2 initial bucket +// this includes SubmitOrder, CancelOrder and QueryClosedOrders +// +// Limit defines the maximum frequency of some events. Limit is represented as number of events per second. A zero Limit allows no events. var orderLimiter = rate.NewLimiter(5, 2) +var queryTradeLimiter = rate.NewLimiter(1, 2) var log = logrus.WithFields(logrus.Fields{ "exchange": "binance", @@ -50,9 +55,12 @@ func init() { _ = types.MarginExchange(&Exchange{}) _ = types.FuturesExchange(&Exchange{}) - // FIXME: this is not effected since dotenv is loaded in the rootCmd, not in the init function - if ok, _ := strconv.ParseBool(os.Getenv("DEBUG_BINANCE_STREAM")); ok { - log.Level = logrus.DebugLevel + if n, ok := util.GetEnvVarInt("BINANCE_ORDER_RATE_LIMITER"); ok { + orderLimiter = rate.NewLimiter(rate.Limit(n), 2) + } + + if n, ok := util.GetEnvVarInt("BINANCE_QUERY_TRADES_RATE_LIMITER"); ok { + queryTradeLimiter = rate.NewLimiter(rate.Limit(n), 2) } } @@ -77,17 +85,21 @@ type Exchange struct { // futuresClient is used for usdt-m futures futuresClient *futures.Client // USDT-M Futures // deliveryClient *delivery.Client // Coin-M Futures + + // client2 is a newer version of the binance api client implemented by ourselves. + client2 *binanceapi.RestClient } -var timeSetter sync.Once +var timeSetterOnce sync.Once func New(key, secret string) *Exchange { var client = binance.NewClient(key, secret) - client.HTTPClient = &http.Client{Timeout: 15 * time.Second} + client.HTTPClient = binanceapi.DefaultHttpClient client.Debug = viper.GetBool("debug-binance-client") var futuresClient = binance.NewFuturesClient(key, secret) - futuresClient.HTTPClient = &http.Client{Timeout: 15 * time.Second} + futuresClient.HTTPClient = binanceapi.DefaultHttpClient + futuresClient.Debug = viper.GetBool("debug-binance-futures-client") if isBinanceUs() { client.BaseURL = BinanceUSBaseURL @@ -98,27 +110,53 @@ func New(key, secret string) *Exchange { futuresClient.BaseURL = FutureTestBaseURL } - var err error - if len(key) > 0 && len(secret) > 0 { - timeSetter.Do(func() { - _, err = client.NewSetServerTimeService().Do(context.Background()) - if err != nil { - log.WithError(err).Error("can not set server time") - } + client2 := binanceapi.NewClient(client.BaseURL) - _, err = futuresClient.NewSetServerTimeService().Do(context.Background()) - if err != nil { - log.WithError(err).Error("can not set server time") - } - }) - } - - return &Exchange{ + ex := &Exchange{ key: key, secret: secret, client: client, futuresClient: futuresClient, - // deliveryClient: deliveryClient, + client2: client2, + } + + if len(key) > 0 && len(secret) > 0 { + client2.Auth(key, secret) + + ctx := context.Background() + go timeSetterOnce.Do(func() { + ex.setServerTimeOffset(ctx) + + ticker := time.NewTicker(time.Hour) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + + case <-ticker.C: + ex.setServerTimeOffset(ctx) + } + } + }) + } + + return ex +} + +func (e *Exchange) setServerTimeOffset(ctx context.Context) { + _, err := e.client.NewSetServerTimeService().Do(ctx) + if err != nil { + log.WithError(err).Error("can not set server time") + } + + _, err = e.futuresClient.NewSetServerTimeService().Do(ctx) + if err != nil { + log.WithError(err).Error("can not set server time") + } + + if err = e.client2.SetTimeOffsetFromServer(ctx); err != nil { + log.WithError(err).Error("can not set server time") } } @@ -127,6 +165,16 @@ func (e *Exchange) Name() types.ExchangeName { } func (e *Exchange) QueryTicker(ctx context.Context, symbol string) (*types.Ticker, error) { + if e.IsFutures { + req := e.futuresClient.NewListPriceChangeStatsService() + req.Symbol(strings.ToUpper(symbol)) + stats, err := req.Do(ctx) + if err != nil { + return nil, err + } + + return toGlobalFuturesTicker(stats[0]) + } req := e.client.NewListPriceChangeStatsService() req.Symbol(strings.ToUpper(symbol)) stats, err := req.Do(ctx) @@ -150,12 +198,6 @@ func (e *Exchange) QueryTickers(ctx context.Context, symbol ...string) (map[stri return tickers, nil } - var req = e.client.NewListPriceChangeStatsService() - changeStats, err := req.Do(ctx) - if err != nil { - return nil, err - } - m := make(map[string]struct{}) exists := struct{}{} @@ -163,6 +205,40 @@ func (e *Exchange) QueryTickers(ctx context.Context, symbol ...string) (map[stri m[s] = exists } + if e.IsFutures { + var req = e.futuresClient.NewListPriceChangeStatsService() + changeStats, err := req.Do(ctx) + if err != nil { + return nil, err + } + for _, stats := range changeStats { + if _, ok := m[stats.Symbol]; len(symbol) != 0 && !ok { + continue + } + + tick := types.Ticker{ + Volume: fixedpoint.MustNewFromString(stats.Volume), + Last: fixedpoint.MustNewFromString(stats.LastPrice), + Open: fixedpoint.MustNewFromString(stats.OpenPrice), + High: fixedpoint.MustNewFromString(stats.HighPrice), + Low: fixedpoint.MustNewFromString(stats.LowPrice), + Buy: fixedpoint.MustNewFromString(stats.LastPrice), + Sell: fixedpoint.MustNewFromString(stats.LastPrice), + Time: time.Unix(0, stats.CloseTime*int64(time.Millisecond)), + } + + tickers[stats.Symbol] = tick + } + + return tickers, nil + } + + var req = e.client.NewListPriceChangeStatsService() + changeStats, err := req.Do(ctx) + if err != nil { + return nil, err + } + for _, stats := range changeStats { if _, ok := m[stats.Symbol]; len(symbol) != 0 && !ok { continue @@ -231,17 +307,18 @@ func (e *Exchange) NewStream() types.Stream { } func (e *Exchange) QueryMarginAssetMaxBorrowable(ctx context.Context, asset string) (amount fixedpoint.Value, err error) { - req := e.client.NewGetMaxBorrowableService() + req := e.client2.NewGetMarginMaxBorrowableRequest() req.Asset(asset) if e.IsIsolatedMargin { req.IsolatedSymbol(e.IsolatedMarginSymbol) } + resp, err := req.Do(ctx) if err != nil { return fixedpoint.Zero, err } - return fixedpoint.NewFromString(resp.Amount) + return resp.Amount, nil } func (e *Exchange) RepayMarginAsset(ctx context.Context, asset string, amount fixedpoint.Value) error { @@ -249,7 +326,7 @@ func (e *Exchange) RepayMarginAsset(ctx context.Context, asset string, amount fi req.Asset(asset) req.Amount(amount.String()) if e.IsIsolatedMargin { - req.IsolatedSymbol(e.IsolatedMarginSymbol) + req.Symbol(e.IsolatedMarginSymbol) } log.Infof("repaying margin asset %s amount %f", asset, amount.Float64()) @@ -267,7 +344,7 @@ func (e *Exchange) BorrowMarginAsset(ctx context.Context, asset string, amount f req.Asset(asset) req.Amount(amount.String()) if e.IsIsolatedMargin { - req.IsolatedSymbol(e.IsolatedMarginSymbol) + req.Symbol(e.IsolatedMarginSymbol) } log.Infof("borrowing margin asset %s amount %f", asset, amount.Float64()) @@ -279,6 +356,17 @@ func (e *Exchange) BorrowMarginAsset(ctx context.Context, asset string, amount f return err } +func (e *Exchange) QueryMarginBorrowHistory(ctx context.Context, asset string) error { + req := e.client.NewListMarginLoansService() + req.Asset(asset) + history, err := req.Do(ctx) + if err != nil { + return err + } + _ = history + return nil +} + // transferCrossMarginAccountAsset transfer asset to the cross margin account or to the main account func (e *Exchange) transferCrossMarginAccountAsset(ctx context.Context, asset string, amount fixedpoint.Value, io int) error { req := e.client.NewMarginTransferService() @@ -299,7 +387,7 @@ func (e *Exchange) transferCrossMarginAccountAsset(ctx context.Context, asset st return err } -func (e *Exchange) queryCrossMarginAccount(ctx context.Context) (*types.Account, error) { +func (e *Exchange) QueryCrossMarginAccount(ctx context.Context) (*types.Account, error) { marginAccount, err := e.client.NewGetMarginAccountService().Do(ctx) if err != nil { return nil, err @@ -331,7 +419,7 @@ func (e *Exchange) queryCrossMarginAccount(ctx context.Context) (*types.Account, return a, nil } -func (e *Exchange) queryIsolatedMarginAccount(ctx context.Context) (*types.Account, error) { +func (e *Exchange) QueryIsolatedMarginAccount(ctx context.Context) (*types.Account, error) { req := e.client.NewGetIsolatedMarginAccountService() req.Symbols(e.IsolatedMarginSymbol) @@ -345,6 +433,10 @@ func (e *Exchange) queryIsolatedMarginAccount(ctx context.Context) (*types.Accou IsolatedMarginInfo: toGlobalIsolatedMarginAccountInfo(marginAccount), // In binance GO api, Account define marginAccount info which mantain []*AccountAsset and []*AccountPosition. } + if len(marginAccount.Assets) == 0 { + return nil, fmt.Errorf("empty margin account assets, please check your isolatedMarginSymbol is correctly set: %+v", marginAccount) + } + // for isolated margin account, we will only have one asset in the Assets array. if len(marginAccount.Assets) > 1 { return nil, fmt.Errorf("unexpected number of user assets returned, got %d user assets", len(marginAccount.Assets)) @@ -383,8 +475,8 @@ func (e *Exchange) queryIsolatedMarginAccount(ctx context.Context) (*types.Accou return a, nil } -func (e *Exchange) Withdrawal(ctx context.Context, asset string, amount fixedpoint.Value, address string, options *types.WithdrawalOptions) error { - req := e.client.NewCreateWithdrawService() +func (e *Exchange) Withdraw(ctx context.Context, asset string, amount fixedpoint.Value, address string, options *types.WithdrawalOptions) error { + req := e.client2.NewWithdrawRequest() req.Coin(asset) req.Address(address) req.Amount(fmt.Sprintf("%f", amount.Float64())) @@ -407,159 +499,112 @@ func (e *Exchange) Withdrawal(ctx context.Context, asset string, amount fixedpoi return nil } -func (e *Exchange) QueryWithdrawHistory(ctx context.Context, asset string, since, until time.Time) (allWithdraws []types.Withdraw, err error) { - startTime := since - +func (e *Exchange) QueryWithdrawHistory(ctx context.Context, asset string, since, until time.Time) (withdraws []types.Withdraw, err error) { var emptyTime = time.Time{} - if startTime == emptyTime { - startTime, err = getLaunchDate() + if since == emptyTime { + since, err = getLaunchDate() if err != nil { - return nil, err + return withdraws, err } } - txIDs := map[string]struct{}{} + // startTime ~ endTime must be in 90 days + historyDayRangeLimit := time.Hour * 24 * 89 + if until.Sub(since) >= historyDayRangeLimit { + until = since.Add(historyDayRangeLimit) + } - for startTime.Before(until) { - // startTime ~ endTime must be in 90 days - endTime := startTime.AddDate(0, 0, 60) - if endTime.After(until) { - endTime = until - } + req := e.client2.NewGetWithdrawHistoryRequest() + if len(asset) > 0 { + req.Coin(asset) + } - req := e.client.NewListWithdrawsService() - if len(asset) > 0 { - req.Coin(asset) - } + records, err := req. + StartTime(since). + EndTime(until). + Limit(1000). + Do(ctx) - withdraws, err := req. - StartTime(startTime.UnixNano() / int64(time.Millisecond)). - EndTime(endTime.UnixNano() / int64(time.Millisecond)). - Do(ctx) + if err != nil { + return withdraws, err + } + for _, d := range records { + // time format: 2006-01-02 15:04:05 + applyTime, err := time.Parse("2006-01-02 15:04:05", d.ApplyTime) if err != nil { - return allWithdraws, err - } - - for _, d := range withdraws { - if _, ok := txIDs[d.TxID]; ok { - continue - } - - status := "" - switch d.Status { - case 0: - status = "email_sent" - case 1: - status = "cancelled" - case 2: - status = "awaiting_approval" - case 3: - status = "rejected" - case 4: - status = "processing" - case 5: - status = "failure" - case 6: - status = "completed" - - default: - status = fmt.Sprintf("unsupported code: %d", d.Status) - } - - txIDs[d.TxID] = struct{}{} - - // 2006-01-02 15:04:05 - applyTime, err := time.Parse("2006-01-02 15:04:05", d.ApplyTime) - if err != nil { - return nil, err - } - - allWithdraws = append(allWithdraws, types.Withdraw{ - Exchange: types.ExchangeBinance, - ApplyTime: types.Time(applyTime), - Asset: d.Coin, - Amount: fixedpoint.MustNewFromString(d.Amount), - Address: d.Address, - TransactionID: d.TxID, - TransactionFee: fixedpoint.MustNewFromString(d.TransactionFee), - WithdrawOrderID: d.WithdrawOrderID, - Network: d.Network, - Status: status, - }) + return nil, err } - startTime = endTime + withdraws = append(withdraws, types.Withdraw{ + Exchange: types.ExchangeBinance, + ApplyTime: types.Time(applyTime), + Asset: d.Coin, + Amount: d.Amount, + Address: d.Address, + TransactionID: d.TxID, + TransactionFee: d.TransactionFee, + WithdrawOrderID: d.WithdrawOrderID, + Network: d.Network, + Status: d.Status.String(), + }) } - return allWithdraws, nil + return withdraws, nil } func (e *Exchange) QueryDepositHistory(ctx context.Context, asset string, since, until time.Time) (allDeposits []types.Deposit, err error) { - startTime := since - var emptyTime = time.Time{} - if startTime == emptyTime { - startTime, err = getLaunchDate() + if since == emptyTime { + since, err = getLaunchDate() if err != nil { return nil, err } } - txIDs := map[string]struct{}{} - for startTime.Before(until) { - - // startTime ~ endTime must be in 90 days - endTime := startTime.AddDate(0, 0, 60) - if endTime.After(until) { - endTime = until - } - - req := e.client.NewListDepositsService() - if len(asset) > 0 { - req.Coin(asset) - } - - deposits, err := req. - StartTime(startTime.UnixNano() / int64(time.Millisecond)). - EndTime(endTime.UnixNano() / int64(time.Millisecond)). - Do(ctx) - if err != nil { - return nil, err - } + // startTime ~ endTime must be in 90 days + historyDayRangeLimit := time.Hour * 24 * 89 + if until.Sub(since) >= historyDayRangeLimit { + until = since.Add(historyDayRangeLimit) + } - for _, d := range deposits { - if _, ok := txIDs[d.TxID]; ok { - continue - } + req := e.client2.NewGetDepositHistoryRequest() + if len(asset) > 0 { + req.Coin(asset) + } - // 0(0:pending,6: credited but cannot withdraw, 1:success) - status := types.DepositStatus(fmt.Sprintf("code: %d", d.Status)) - - switch d.Status { - case 0: - status = types.DepositPending - case 6: - // https://www.binance.com/en/support/faq/115003736451 - status = types.DepositCredited - case 1: - status = types.DepositSuccess - } + req.StartTime(since). + EndTime(until) - txIDs[d.TxID] = struct{}{} - allDeposits = append(allDeposits, types.Deposit{ - Exchange: types.ExchangeBinance, - Time: types.Time(time.Unix(0, d.InsertTime*int64(time.Millisecond))), - Asset: d.Coin, - Amount: fixedpoint.MustNewFromString(d.Amount), - Address: d.Address, - AddressTag: d.AddressTag, - TransactionID: d.TxID, - Status: status, - }) - } + records, err := req.Do(ctx) + if err != nil { + return nil, err + } - startTime = endTime + for _, d := range records { + // 0(0:pending,6: credited but cannot withdraw, 1:success) + // set the default status + status := types.DepositStatus(fmt.Sprintf("code: %d", d.Status)) + switch d.Status { + case 0: + status = types.DepositPending + case 6: + // https://www.binance.com/en/support/faq/115003736451 + status = types.DepositCredited + case 1: + status = types.DepositSuccess + } + + allDeposits = append(allDeposits, types.Deposit{ + Exchange: types.ExchangeBinance, + Time: types.Time(d.InsertTime.Time()), + Asset: d.Coin, + Amount: d.Amount, + Address: d.Address, + AddressTag: d.AddressTag, + TransactionID: d.TxId, + Status: status, + }) } return allDeposits, nil @@ -603,6 +648,9 @@ func (e *Exchange) QuerySpotAccount(ctx context.Context) (*types.Account, error) return a, nil } +// QueryFuturesAccount gets the futures account balances from Binance +// Balance.Available = Wallet Balance(in Binance UI) - Used Margin +// Balance.Locked = Used Margin func (e *Exchange) QueryFuturesAccount(ctx context.Context) (*types.Account, error) { account, err := e.futuresClient.NewGetAccountService().Do(ctx) if err != nil { @@ -615,9 +663,13 @@ func (e *Exchange) QueryFuturesAccount(ctx context.Context) (*types.Account, err var balances = map[string]types.Balance{} for _, b := range accountBalances { + balanceAvailable := fixedpoint.Must(fixedpoint.NewFromString(b.AvailableBalance)) + balanceTotal := fixedpoint.Must(fixedpoint.NewFromString(b.Balance)) + unrealizedPnl := fixedpoint.Must(fixedpoint.NewFromString(b.CrossUnPnl)) balances[b.Asset] = types.Balance{ Currency: b.Asset, - Available: fixedpoint.Must(fixedpoint.NewFromString(b.AvailableBalance)), + Available: balanceAvailable, + Locked: balanceTotal.Sub(balanceAvailable.Sub(unrealizedPnl)), } } @@ -638,9 +690,9 @@ func (e *Exchange) QueryAccount(ctx context.Context) (*types.Account, error) { if e.IsFutures { account, err = e.QueryFuturesAccount(ctx) } else if e.IsIsolatedMargin { - account, err = e.queryIsolatedMarginAccount(ctx) + account, err = e.QueryIsolatedMarginAccount(ctx) } else if e.IsMargin { - account, err = e.queryCrossMarginAccount(ctx) + account, err = e.QueryCrossMarginAccount(ctx) } else { account, err = e.QuerySpotAccount(ctx) } @@ -658,7 +710,7 @@ func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders [ return orders, err } - return toGlobalOrders(binanceOrders) + return toGlobalOrders(binanceOrders, false) } if e.IsFutures { @@ -669,7 +721,7 @@ func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders [ return orders, err } - return toGlobalFuturesOrders(binanceOrders) + return toGlobalFuturesOrders(binanceOrders, false) } binanceOrders, err := e.client.NewListOpenOrdersService().Symbol(symbol).Do(ctx) @@ -677,7 +729,37 @@ func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders [ return orders, err } - return toGlobalOrders(binanceOrders) + return toGlobalOrders(binanceOrders, false) +} + +func (e *Exchange) QueryOrderTrades(ctx context.Context, q types.OrderQuery) ([]types.Trade, error) { + orderID, err := strconv.ParseInt(q.OrderID, 10, 64) + if err != nil { + return nil, err + } + + if len(q.Symbol) == 0 { + return nil, errors.New("binance: symbol parameter is a mandatory parameter for querying order trades") + } + + remoteTrades, err := e.client.NewListTradesService().Symbol(q.Symbol).OrderId(orderID).Do(ctx) + if err != nil { + return nil, err + } + + var trades []types.Trade + for _, t := range remoteTrades { + localTrade, err := toGlobalTrade(*t, e.IsMargin) + if err != nil { + log.WithError(err).Errorf("binance: can not convert trade: %+v", t) + continue + } + + trades = append(trades, *localTrade) + } + + trades = types.SortTradesAscending(trades) + return trades, nil } func (e *Exchange) QueryOrder(ctx context.Context, q types.OrderQuery) (*types.Order, error) { @@ -734,7 +816,7 @@ func (e *Exchange) QueryClosedOrders(ctx context.Context, symbol string, since, return orders, err } - return toGlobalOrders(binanceOrders) + return toGlobalOrders(binanceOrders, e.IsMargin) } if e.IsFutures { @@ -753,7 +835,7 @@ func (e *Exchange) QueryClosedOrders(ctx context.Context, symbol string, since, if err != nil { return orders, err } - return toGlobalFuturesOrders(binanceOrders) + return toGlobalFuturesOrders(binanceOrders, false) } // If orderId is set, it will get orders >= that orderId. Otherwise most recent orders are returned. @@ -779,7 +861,7 @@ func (e *Exchange) QueryClosedOrders(ctx context.Context, symbol string, since, return orders, err } - return toGlobalOrders(binanceOrders) + return toGlobalOrders(binanceOrders, e.IsMargin) } func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) (err error) { @@ -1010,7 +1092,7 @@ func (e *Exchange) submitFuturesOrder(ctx context.Context, order types.SubmitOrd req.TimeInForce(futures.TimeInForceType(order.TimeInForce)) } else { switch order.Type { - case types.OrderTypeLimit, types.OrderTypeStopLimit: + case types.OrderTypeLimit, types.OrderTypeLimitMaker, types.OrderTypeStopLimit: req.TimeInForce(futures.TimeInForceTypeGTC) } } @@ -1034,7 +1116,7 @@ func (e *Exchange) submitFuturesOrder(ctx context.Context, order types.SubmitOrd Type: response.Type, Side: response.Side, ReduceOnly: response.ReduceOnly, - }, true) + }, false) return createdOrder, err } @@ -1182,33 +1264,20 @@ func (e *Exchange) submitSpotOrder(ctx context.Context, order types.SubmitOrder) return createdOrder, err } -func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) (createdOrders types.OrderSlice, err error) { - for _, order := range orders { - if err := orderLimiter.Wait(ctx); err != nil { - log.WithError(err).Errorf("order rate limiter wait error") - } - - var createdOrder *types.Order - if e.IsMargin { - createdOrder, err = e.submitMarginOrder(ctx, order) - } else if e.IsFutures { - createdOrder, err = e.submitFuturesOrder(ctx, order) - } else { - createdOrder, err = e.submitSpotOrder(ctx, order) - } - - if err != nil { - return createdOrders, err - } - - if createdOrder == nil { - return createdOrders, errors.New("nil converted order") - } +func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (createdOrder *types.Order, err error) { + if err := orderLimiter.Wait(ctx); err != nil { + log.WithError(err).Errorf("order rate limiter wait error") + } - createdOrders = append(createdOrders, *createdOrder) + if e.IsMargin { + createdOrder, err = e.submitMarginOrder(ctx, order) + } else if e.IsFutures { + createdOrder, err = e.submitFuturesOrder(ctx, order) + } else { + createdOrder, err = e.submitSpotOrder(ctx, order) } - return createdOrders, err + return createdOrder, err } // QueryKLines queries the Kline/candlestick bars for a symbol. Klines are uniquely identified by their open time. @@ -1222,6 +1291,9 @@ func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder // the endTime of a binance kline, is the (startTime + interval time - 1 millisecond), e.g., // millisecond unix timestamp: 1620172860000 and 1620172919999 func (e *Exchange) QueryKLines(ctx context.Context, symbol string, interval types.Interval, options types.KLineQueryOptions) ([]types.KLine, error) { + if e.IsFutures { + return e.QueryFuturesKLines(ctx, symbol, interval, options) + } var limit = 1000 if options.Limit > 0 { @@ -1237,11 +1309,11 @@ func (e *Exchange) QueryKLines(ctx context.Context, symbol string, interval type Limit(limit) if options.StartTime != nil { - req.StartTime(options.StartTime.UnixNano() / int64(time.Millisecond)) + req.StartTime(options.StartTime.UnixMilli()) } if options.EndTime != nil { - req.EndTime(options.EndTime.UnixNano() / int64(time.Millisecond)) + req.EndTime(options.EndTime.UnixMilli()) } resp, err := req.Do(ctx) @@ -1270,72 +1342,132 @@ func (e *Exchange) QueryKLines(ctx context.Context, symbol string, interval type Closed: true, }) } + + kLines = types.SortKLinesAscending(kLines) return kLines, nil } -func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *types.TradeQueryOptions) (trades []types.Trade, err error) { - if e.IsMargin { - var remoteTrades []*binance.TradeV3 - req := e.client.NewListMarginTradesService(). - IsIsolated(e.IsIsolatedMargin). - Symbol(symbol) +func (e *Exchange) QueryFuturesKLines(ctx context.Context, symbol string, interval types.Interval, options types.KLineQueryOptions) ([]types.KLine, error) { - if options.Limit > 0 { - req.Limit(int(options.Limit)) - } else { - req.Limit(1000) - } + var limit = 1000 + if options.Limit > 0 { + // default limit == 1000 + limit = options.Limit + } - // BINANCE uses inclusive last trade ID - if options.LastTradeID > 0 { - req.FromID(int64(options.LastTradeID)) - } + log.Infof("querying kline %s %s %v", symbol, interval, options) - if options.StartTime != nil && options.EndTime != nil { - if options.EndTime.Sub(*options.StartTime) < 24*time.Hour { - req.StartTime(options.StartTime.UnixMilli()) - req.EndTime(options.EndTime.UnixMilli()) - } else { - req.StartTime(options.StartTime.UnixMilli()) - } - } else if options.StartTime != nil { + req := e.futuresClient.NewKlinesService(). + Symbol(symbol). + Interval(string(interval)). + Limit(limit) + + if options.StartTime != nil { + req.StartTime(options.StartTime.UnixMilli()) + } + + if options.EndTime != nil { + req.EndTime(options.EndTime.UnixMilli()) + } + + resp, err := req.Do(ctx) + if err != nil { + return nil, err + } + + var kLines []types.KLine + for _, k := range resp { + kLines = append(kLines, types.KLine{ + Exchange: types.ExchangeBinance, + Symbol: symbol, + Interval: interval, + StartTime: types.NewTimeFromUnix(0, k.OpenTime*int64(time.Millisecond)), + EndTime: types.NewTimeFromUnix(0, k.CloseTime*int64(time.Millisecond)), + Open: fixedpoint.MustNewFromString(k.Open), + Close: fixedpoint.MustNewFromString(k.Close), + High: fixedpoint.MustNewFromString(k.High), + Low: fixedpoint.MustNewFromString(k.Low), + Volume: fixedpoint.MustNewFromString(k.Volume), + QuoteVolume: fixedpoint.MustNewFromString(k.QuoteAssetVolume), + TakerBuyBaseAssetVolume: fixedpoint.MustNewFromString(k.TakerBuyBaseAssetVolume), + TakerBuyQuoteAssetVolume: fixedpoint.MustNewFromString(k.TakerBuyQuoteAssetVolume), + LastTradeID: 0, + NumberOfTrades: uint64(k.TradeNum), + Closed: true, + }) + } + + kLines = types.SortKLinesAscending(kLines) + return kLines, nil +} + +func (e *Exchange) queryMarginTrades(ctx context.Context, symbol string, options *types.TradeQueryOptions) (trades []types.Trade, err error) { + var remoteTrades []*binance.TradeV3 + req := e.client.NewListMarginTradesService(). + IsIsolated(e.IsIsolatedMargin). + Symbol(symbol) + + if options.Limit > 0 { + req.Limit(int(options.Limit)) + } else { + req.Limit(1000) + } + + // BINANCE uses inclusive last trade ID + if options.LastTradeID > 0 { + req.FromID(int64(options.LastTradeID)) + } + + if options.StartTime != nil && options.EndTime != nil { + if options.EndTime.Sub(*options.StartTime) < 24*time.Hour { req.StartTime(options.StartTime.UnixMilli()) - } else if options.EndTime != nil { req.EndTime(options.EndTime.UnixMilli()) + } else { + req.StartTime(options.StartTime.UnixMilli()) } + } else if options.StartTime != nil { + req.StartTime(options.StartTime.UnixMilli()) + } else if options.EndTime != nil { + req.EndTime(options.EndTime.UnixMilli()) + } - remoteTrades, err = req.Do(ctx) + remoteTrades, err = req.Do(ctx) + if err != nil { + return nil, err + } + for _, t := range remoteTrades { + localTrade, err := toGlobalTrade(*t, e.IsMargin) if err != nil { - return nil, err + log.WithError(err).Errorf("can not convert binance trade: %+v", t) + continue } - for _, t := range remoteTrades { - localTrade, err := toGlobalTrade(*t, e.IsMargin) - if err != nil { - log.WithError(err).Errorf("can not convert binance trade: %+v", t) - continue - } - trades = append(trades, *localTrade) - } + trades = append(trades, *localTrade) + } - trades = types.SortTradesAscending(trades) + trades = types.SortTradesAscending(trades) + return trades, nil +} - return trades, nil - } else if e.IsFutures { - var remoteTrades []*futures.AccountTrade - req := e.futuresClient.NewListAccountTradeService(). - Symbol(symbol) - if options.Limit > 0 { - req.Limit(int(options.Limit)) - } else { - req.Limit(1000) - } +func (e *Exchange) queryFuturesTrades(ctx context.Context, symbol string, options *types.TradeQueryOptions) (trades []types.Trade, err error) { - // BINANCE uses inclusive last trade ID - if options.LastTradeID > 0 { - req.FromID(int64(options.LastTradeID)) - } + var remoteTrades []*futures.AccountTrade + req := e.futuresClient.NewListAccountTradeService(). + Symbol(symbol) + if options.Limit > 0 { + req.Limit(int(options.Limit)) + } else { + req.Limit(1000) + } + + // BINANCE uses inclusive last trade ID + if options.LastTradeID > 0 { + req.FromID(int64(options.LastTradeID)) + } + // The parameter fromId cannot be sent with startTime or endTime. + // Mentioned in binance futures docs + if options.LastTradeID <= 0 { if options.StartTime != nil && options.EndTime != nil { if options.EndTime.Sub(*options.StartTime) < 24*time.Hour { req.StartTime(options.StartTime.UnixMilli()) @@ -1343,81 +1475,116 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type } else { req.StartTime(options.StartTime.UnixMilli()) } - } else if options.StartTime != nil { - req.StartTime(options.StartTime.UnixMilli()) } else if options.EndTime != nil { req.EndTime(options.EndTime.UnixMilli()) } + } - remoteTrades, err = req.Do(ctx) + remoteTrades, err = req.Do(ctx) + if err != nil { + return nil, err + } + for _, t := range remoteTrades { + localTrade, err := toGlobalFuturesTrade(*t) if err != nil { - return nil, err + log.WithError(err).Errorf("can not convert binance futures trade: %+v", t) + continue } - for _, t := range remoteTrades { - localTrade, err := toGlobalFuturesTrade(*t) - if err != nil { - log.WithError(err).Errorf("can not convert binance futures trade: %+v", t) - continue - } - trades = append(trades, *localTrade) - } + trades = append(trades, *localTrade) + } - trades = types.SortTradesAscending(trades) - return trades, nil - } else { - var remoteTrades []*binance.TradeV3 - req := e.client.NewListTradesService(). - Symbol(symbol) + trades = types.SortTradesAscending(trades) + return trades, nil +} - if options.Limit > 0 { - req.Limit(int(options.Limit)) - } else { - req.Limit(1000) - } +func (e *Exchange) querySpotTrades(ctx context.Context, symbol string, options *types.TradeQueryOptions) (trades []types.Trade, err error) { + var remoteTrades []*binance.TradeV3 + req := e.client.NewListTradesService(). + Symbol(symbol) - // BINANCE uses inclusive last trade ID - if options.LastTradeID > 0 { - req.FromID(int64(options.LastTradeID)) - } + if options.Limit > 0 { + req.Limit(int(options.Limit)) + } else { + req.Limit(1000) + } - if options.StartTime != nil && options.EndTime != nil { - if options.EndTime.Sub(*options.StartTime) < 24*time.Hour { - req.StartTime(options.StartTime.UnixMilli()) - req.EndTime(options.EndTime.UnixMilli()) - } else { - req.StartTime(options.StartTime.UnixMilli()) - } - } else if options.StartTime != nil { + // BINANCE uses inclusive last trade ID + if options.LastTradeID > 0 { + req.FromID(int64(options.LastTradeID)) + } + + if options.StartTime != nil && options.EndTime != nil { + if options.EndTime.Sub(*options.StartTime) < 24*time.Hour { req.StartTime(options.StartTime.UnixMilli()) - } else if options.EndTime != nil { req.EndTime(options.EndTime.UnixMilli()) + } else { + req.StartTime(options.StartTime.UnixMilli()) } + } else if options.StartTime != nil { + req.StartTime(options.StartTime.UnixMilli()) + } else if options.EndTime != nil { + req.EndTime(options.EndTime.UnixMilli()) + } - remoteTrades, err = req.Do(ctx) + remoteTrades, err = req.Do(ctx) + if err != nil { + return nil, err + } + for _, t := range remoteTrades { + localTrade, err := toGlobalTrade(*t, e.IsMargin) if err != nil { - return nil, err + log.WithError(err).Errorf("can not convert binance trade: %+v", t) + continue } - for _, t := range remoteTrades { - localTrade, err := toGlobalTrade(*t, e.IsMargin) - if err != nil { - log.WithError(err).Errorf("can not convert binance trade: %+v", t) - continue - } - trades = append(trades, *localTrade) - } + trades = append(trades, *localTrade) + } + + trades = types.SortTradesAscending(trades) + return trades, nil +} + +func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *types.TradeQueryOptions) ([]types.Trade, error) { + if err := queryTradeLimiter.Wait(ctx); err != nil { + return nil, err + } - trades = types.SortTradesAscending(trades) - return trades, nil + if e.IsMargin { + return e.queryMarginTrades(ctx, symbol, options) + } else if e.IsFutures { + return e.queryFuturesTrades(ctx, symbol, options) + } + return e.querySpotTrades(ctx, symbol, options) +} + +// DefaultFeeRates returns the Binance VIP 0 fee schedule +// See also https://www.binance.com/en/fee/schedule +func (e *Exchange) DefaultFeeRates() types.ExchangeFee { + return types.ExchangeFee{ + MakerFeeRate: fixedpoint.NewFromFloat(0.01 * 0.075), // 0.075% + TakerFeeRate: fixedpoint.NewFromFloat(0.01 * 0.075), // 0.075% } } // QueryDepth query the order book depth of a symbol func (e *Exchange) QueryDepth(ctx context.Context, symbol string) (snapshot types.SliceOrderBook, finalUpdateID int64, err error) { - response, err := e.client.NewDepthService().Symbol(symbol).Do(ctx) - if err != nil { - return snapshot, finalUpdateID, err + var response *binance.DepthResponse + if e.IsFutures { + res, err := e.futuresClient.NewDepthService().Symbol(symbol).Do(ctx) + if err != nil { + return snapshot, finalUpdateID, err + } + response = &binance.DepthResponse{ + LastUpdateID: res.LastUpdateID, + Bids: res.Bids, + Asks: res.Asks, + } + } else { + response, err = e.client.NewDepthService().Symbol(symbol).Do(ctx) + if err != nil { + return snapshot, finalUpdateID, err + } } snapshot.Symbol = symbol @@ -1454,37 +1621,10 @@ func (e *Exchange) QueryDepth(ctx context.Context, symbol string) (snapshot type return snapshot, finalUpdateID, nil } -func (e *Exchange) BatchQueryKLines(ctx context.Context, symbol string, interval types.Interval, startTime, endTime time.Time) ([]types.KLine, error) { - var allKLines []types.KLine - - for startTime.Before(endTime) { - klines, err := e.QueryKLines(ctx, symbol, interval, types.KLineQueryOptions{ - StartTime: &startTime, - Limit: 1000, - }) - - if err != nil { - return nil, err - } - - for _, kline := range klines { - if kline.EndTime.After(endTime) { - return allKLines, nil - } - - allKLines = append(allKLines, kline) - startTime = kline.EndTime.Time() - } - } - - return allKLines, nil -} - +// QueryPremiumIndex is only for futures func (e *Exchange) QueryPremiumIndex(ctx context.Context, symbol string) (*types.PremiumIndex, error) { - futuresClient := binance.NewFuturesClient(e.key, e.secret) - // when symbol is set, only one index will be returned. - indexes, err := futuresClient.NewPremiumIndexService().Symbol(symbol).Do(ctx) + indexes, err := e.futuresClient.NewPremiumIndexService().Symbol(symbol).Do(ctx) if err != nil { return nil, err } @@ -1493,8 +1633,7 @@ func (e *Exchange) QueryPremiumIndex(ctx context.Context, symbol string) (*types } func (e *Exchange) QueryFundingRateHistory(ctx context.Context, symbol string) (*types.FundingRate, error) { - futuresClient := binance.NewFuturesClient(e.key, e.secret) - rates, err := futuresClient.NewFundingRateService(). + rates, err := e.futuresClient.NewFundingRateService(). Symbol(symbol). Limit(1). Do(ctx) @@ -1520,10 +1659,8 @@ func (e *Exchange) QueryFundingRateHistory(ctx context.Context, symbol string) ( } func (e *Exchange) QueryPositionRisk(ctx context.Context, symbol string) (*types.PositionRisk, error) { - futuresClient := binance.NewFuturesClient(e.key, e.secret) - // when symbol is set, only one position risk will be returned. - risks, err := futuresClient.NewGetPositionRiskService().Symbol(symbol).Do(ctx) + risks, err := e.futuresClient.NewGetPositionRiskService().Symbol(symbol).Do(ctx) if err != nil { return nil, err } @@ -1531,6 +1668,32 @@ func (e *Exchange) QueryPositionRisk(ctx context.Context, symbol string) (*types return convertPositionRisk(risks[0]) } +// in seconds +var SupportedIntervals = map[types.Interval]int{ + types.Interval1s: 1, + types.Interval1m: 1 * 60, + types.Interval5m: 5 * 60, + types.Interval15m: 15 * 60, + types.Interval30m: 30 * 60, + types.Interval1h: 60 * 60, + types.Interval2h: 60 * 60 * 2, + types.Interval4h: 60 * 60 * 4, + types.Interval6h: 60 * 60 * 6, + types.Interval12h: 60 * 60 * 12, + types.Interval1d: 60 * 60 * 24, + types.Interval3d: 60 * 60 * 24 * 3, + types.Interval1w: 60 * 60 * 24 * 7, +} + +func (e *Exchange) SupportedInterval() map[types.Interval]int { + return SupportedIntervals +} + +func (e *Exchange) IsSupportedInterval(interval types.Interval) bool { + _, ok := SupportedIntervals[interval] + return ok +} + func getLaunchDate() (time.Time, error) { // binance launch date 12:00 July 14th, 2017 loc, err := time.LoadLocation("Asia/Shanghai") diff --git a/pkg/exchange/binance/margin_history.go b/pkg/exchange/binance/margin_history.go new file mode 100644 index 0000000000..5408e04ba5 --- /dev/null +++ b/pkg/exchange/binance/margin_history.go @@ -0,0 +1,167 @@ +package binance + +import ( + "context" + "time" + + "github.com/c9s/bbgo/pkg/types" +) + +func (e *Exchange) QueryLoanHistory(ctx context.Context, asset string, startTime, endTime *time.Time) ([]types.MarginLoan, error) { + req := e.client2.NewGetMarginLoanHistoryRequest() + req.Asset(asset) + req.Size(100) + + if startTime != nil { + req.StartTime(*startTime) + + // 6 months + if time.Since(*startTime) > time.Hour*24*30*6 { + req.Archived(true) + } + } + + if startTime != nil && endTime != nil { + duration := endTime.Sub(*startTime) + if duration > time.Hour*24*30 { + t := startTime.Add(time.Hour * 24 * 30) + endTime = &t + } + } + + if endTime != nil { + req.EndTime(*endTime) + } + + if e.MarginSettings.IsIsolatedMargin { + req.IsolatedSymbol(e.MarginSettings.IsolatedMarginSymbol) + } + + records, err := req.Do(ctx) + if err != nil { + return nil, err + } + + var loans []types.MarginLoan + for _, record := range records { + loans = append(loans, toGlobalLoan(record)) + } + + return loans, err +} + +func (e *Exchange) QueryRepayHistory(ctx context.Context, asset string, startTime, endTime *time.Time) ([]types.MarginRepay, error) { + req := e.client2.NewGetMarginRepayHistoryRequest() + req.Asset(asset) + req.Size(100) + + if startTime != nil { + req.StartTime(*startTime) + + // 6 months + if time.Since(*startTime) > time.Hour*24*30*6 { + req.Archived(true) + } + } + + if startTime != nil && endTime != nil { + duration := endTime.Sub(*startTime) + if duration > time.Hour*24*30 { + t := startTime.Add(time.Hour * 24 * 30) + endTime = &t + } + } + + if endTime != nil { + req.EndTime(*endTime) + } + + if e.MarginSettings.IsIsolatedMargin { + req.IsolatedSymbol(e.MarginSettings.IsolatedMarginSymbol) + } + + records, err := req.Do(ctx) + + var repays []types.MarginRepay + for _, record := range records { + repays = append(repays, toGlobalRepay(record)) + } + + return repays, err +} + +func (e *Exchange) QueryLiquidationHistory(ctx context.Context, startTime, endTime *time.Time) ([]types.MarginLiquidation, error) { + req := e.client2.NewGetMarginLiquidationHistoryRequest() + req.Size(100) + + if startTime != nil { + req.StartTime(*startTime) + } + + if startTime != nil && endTime != nil { + duration := endTime.Sub(*startTime) + if duration > time.Hour*24*30 { + t := startTime.Add(time.Hour * 24 * 30) + endTime = &t + } + } + + if endTime != nil { + req.EndTime(*endTime) + } + + if e.MarginSettings.IsIsolatedMargin { + req.IsolatedSymbol(e.MarginSettings.IsolatedMarginSymbol) + } + + records, err := req.Do(ctx) + var liquidations []types.MarginLiquidation + for _, record := range records { + liquidations = append(liquidations, toGlobalLiquidation(record)) + } + + return liquidations, err +} + +func (e *Exchange) QueryInterestHistory(ctx context.Context, asset string, startTime, endTime *time.Time) ([]types.MarginInterest, error) { + req := e.client2.NewGetMarginInterestHistoryRequest() + req.Asset(asset) + req.Size(100) + + if startTime != nil { + req.StartTime(*startTime) + + // 6 months + if time.Since(*startTime) > time.Hour*24*30*6 { + req.Archived(true) + } + } + + if startTime != nil && endTime != nil { + duration := endTime.Sub(*startTime) + if duration > time.Hour*24*30 { + t := startTime.Add(time.Hour * 24 * 30) + endTime = &t + } + } + + if endTime != nil { + req.EndTime(*endTime) + } + + if e.MarginSettings.IsIsolatedMargin { + req.IsolatedSymbol(e.MarginSettings.IsolatedMarginSymbol) + } + + records, err := req.Do(ctx) + if err != nil { + return nil, err + } + + var interests []types.MarginInterest + for _, record := range records { + interests = append(interests, toGlobalInterest(record)) + } + + return interests, err +} diff --git a/pkg/exchange/binance/parse.go b/pkg/exchange/binance/parse.go index 14ca17cce8..efbe780945 100644 --- a/pkg/exchange/binance/parse.go +++ b/pkg/exchange/binance/parse.go @@ -51,7 +51,7 @@ executionReport "O": 1499405658657, // Order creation time "Z": "0.00000000", // Cumulative quote asset transacted quantity "Y": "0.00000000", // Last quote asset transacted quantity (i.e. lastPrice * lastQty) - "Q": "0.00000000" // Quote Order Qty + "Q": "0.00000000" // Quote Order Quantity } */ type ExecutionReportEvent struct { @@ -113,17 +113,16 @@ func (e *ExecutionReportEvent) Order() (*types.Order, error) { orderCreationTime := time.Unix(0, e.OrderCreationTime*int64(time.Millisecond)) return &types.Order{ SubmitOrder: types.SubmitOrder{ - ClientOrderID: e.ClientOrderID, - Symbol: e.Symbol, - Side: toGlobalSideType(binance.SideType(e.Side)), - Type: toGlobalOrderType(binance.OrderType(e.OrderType)), - Quantity: e.OrderQuantity, - Price: e.OrderPrice, - StopPrice: e.StopPrice, - TimeInForce: types.TimeInForce(e.TimeInForce), - IsFutures: false, - ReduceOnly: false, - ClosePosition: false, + ClientOrderID: e.ClientOrderID, + Symbol: e.Symbol, + Side: toGlobalSideType(binance.SideType(e.Side)), + Type: toGlobalOrderType(binance.OrderType(e.OrderType)), + Quantity: e.OrderQuantity, + Price: e.OrderPrice, + StopPrice: e.StopPrice, + TimeInForce: types.TimeInForce(e.TimeInForce), + ReduceOnly: false, + ClosePosition: false, }, Exchange: types.ExchangeBinance, IsWorking: e.IsOnBook, @@ -276,7 +275,7 @@ func parseWebSocketEvent(message []byte) (interface{}, error) { // fmt.Println(str) eventType := string(val.GetStringBytes("e")) if eventType == "" && IsBookTicker(val) { - eventType = "bookticker" + eventType = "bookTicker" } switch eventType { @@ -284,7 +283,7 @@ func parseWebSocketEvent(message []byte) (interface{}, error) { var event KLineEvent err := json.Unmarshal([]byte(message), &event) return &event, err - case "bookticker": + case "bookTicker": var event BookTickerEvent err := json.Unmarshal([]byte(message), &event) event.Event = eventType @@ -292,22 +291,22 @@ func parseWebSocketEvent(message []byte) (interface{}, error) { case "outboundAccountPosition": var event OutboundAccountPositionEvent - err := json.Unmarshal([]byte(message), &event) + err = json.Unmarshal([]byte(message), &event) return &event, err case "outboundAccountInfo": var event OutboundAccountInfoEvent - err := json.Unmarshal([]byte(message), &event) + err = json.Unmarshal([]byte(message), &event) return &event, err case "balanceUpdate": var event BalanceUpdateEvent - err := json.Unmarshal([]byte(message), &event) + err = json.Unmarshal([]byte(message), &event) return &event, err case "executionReport": var event ExecutionReportEvent - err := json.Unmarshal([]byte(message), &event) + err = json.Unmarshal([]byte(message), &event) return &event, err case "depthUpdate": @@ -315,35 +314,45 @@ func parseWebSocketEvent(message []byte) (interface{}, error) { case "markPriceUpdate": var event MarkPriceUpdateEvent - err := json.Unmarshal([]byte(message), &event) + err = json.Unmarshal([]byte(message), &event) + return &event, err + + case "listenKeyExpired": + var event ListenKeyExpired + err = json.Unmarshal([]byte(message), &event) return &event, err // Binance futures data -------------- case "continuousKline": var event ContinuousKLineEvent - err := json.Unmarshal([]byte(message), &event) + err = json.Unmarshal([]byte(message), &event) return &event, err case "ORDER_TRADE_UPDATE": var event OrderTradeUpdateEvent - err := json.Unmarshal([]byte(message), &event) + err = json.Unmarshal([]byte(message), &event) return &event, err // Event: Balance and Position Update case "ACCOUNT_UPDATE": var event AccountUpdateEvent - err := json.Unmarshal([]byte(message), &event) + err = json.Unmarshal([]byte(message), &event) return &event, err // Event: Order Update case "ACCOUNT_CONFIG_UPDATE": var event AccountConfigUpdateEvent - err := json.Unmarshal([]byte(message), &event) + err = json.Unmarshal([]byte(message), &event) return &event, err case "trade": var event MarketTradeEvent - err := json.Unmarshal([]byte(message), &event) + err = json.Unmarshal([]byte(message), &event) + return &event, err + + case "aggTrade": + var event AggTradeEvent + err = json.Unmarshal([]byte(message), &event) return &event, err default: @@ -538,6 +547,63 @@ func (e *MarketTradeEvent) Trade() types.Trade { } } +type AggTradeEvent struct { + EventBase + Symbol string `json:"s"` + Quantity fixedpoint.Value `json:"q"` + Price fixedpoint.Value `json:"p"` + FirstTradeId int64 `json:"f"` + LastTradeId int64 `json:"l"` + OrderTradeTime int64 `json:"T"` + IsMaker bool `json:"m"` + Dummy bool `json:"M"` +} + +/* +aggregate trade +{ + "e": "aggTrade", // Event type + "E": 123456789, // Event time + "s": "BNBBTC", // Symbol + "a": 12345, // Aggregate trade ID + "p": "0.001", // Price + "q": "100", // Quantity + "f": 100, // First trade ID + "l": 105, // Last trade ID + "T": 123456785, // Trade time + "m": true, // Is the buyer the market maker? + "M": true // Ignore +} +*/ + +func (e *AggTradeEvent) Trade() types.Trade { + tt := time.Unix(0, e.OrderTradeTime*int64(time.Millisecond)) + var side types.SideType + var isBuyer bool + if e.IsMaker { + side = types.SideTypeSell + isBuyer = false + } else { + side = types.SideTypeBuy + isBuyer = true + } + return types.Trade{ + ID: uint64(e.LastTradeId), + Exchange: types.ExchangeBinance, + Symbol: e.Symbol, + OrderID: 0, + Side: side, + Price: e.Price, + Quantity: e.Quantity, + QuoteQuantity: e.Quantity, + IsBuyer: isBuyer, + IsMaker: e.IsMaker, + Time: types.Time(tt), + Fee: fixedpoint.Zero, + FeeCurrency: "", + } +} + type KLine struct { StartTime int64 `json:"t"` EndTime int64 `json:"T"` @@ -619,6 +685,10 @@ func (k *KLine) KLine() types.KLine { } } +type ListenKeyExpired struct { + EventBase +} + type MarkPriceUpdateEvent struct { EventBase @@ -810,7 +880,7 @@ func (e *OrderTradeUpdateEvent) TradeFutures() (*types.Trade, error) { Side: toGlobalSideType(binance.SideType(e.OrderTrade.Side)), Price: e.OrderTrade.LastFilledPrice, Quantity: e.OrderTrade.OrderLastFilledQuantity, - QuoteQuantity: e.OrderTrade.OrderFilledAccumulatedQuantity, + QuoteQuantity: e.OrderTrade.LastFilledPrice.Mul(e.OrderTrade.OrderLastFilledQuantity), IsBuyer: e.OrderTrade.Side == "BUY", IsMaker: e.OrderTrade.IsMaker, Time: types.Time(tt), diff --git a/pkg/exchange/binance/parse_test.go b/pkg/exchange/binance/parse_test.go index ad9d36e37a..0d83664ce1 100644 --- a/pkg/exchange/binance/parse_test.go +++ b/pkg/exchange/binance/parse_test.go @@ -168,7 +168,7 @@ func TestParseOrderUpdate(t *testing.T) { "O": 1499405658657, // Order creation time "Z": "0.1", // Cumulative quote asset transacted quantity "Y": "0.00000000", // Last quote asset transacted quantity (i.e. lastPrice * lastQty) - "Q": "2.0" // Quote Order Qty + "Q": "2.0" // Quote Order Quantity }` payload = jsCommentTrimmer.ReplaceAllLiteralString(payload, "") diff --git a/pkg/exchange/binance/reward.go b/pkg/exchange/binance/reward.go new file mode 100644 index 0000000000..8bf3dfdcf2 --- /dev/null +++ b/pkg/exchange/binance/reward.go @@ -0,0 +1,45 @@ +package binance + +import ( + "context" + "strconv" + "time" + + "github.com/c9s/bbgo/pkg/exchange/binance/binanceapi" + "github.com/c9s/bbgo/pkg/types" +) + +func (e *Exchange) QueryRewards(ctx context.Context, startTime time.Time) ([]types.Reward, error) { + req := e.client2.NewGetSpotRebateHistoryRequest() + req.StartTime(startTime) + history, err := req.Do(ctx) + if err != nil { + return nil, err + } + + var rewards []types.Reward + + for _, entry := range history { + t := types.RewardCommission + switch entry.Type { + case binanceapi.RebateTypeReferralKickback: + t = types.RewardReferralKickback + case binanceapi.RebateTypeCommission: + // use the default type + } + + rewards = append(rewards, types.Reward{ + UUID: strconv.FormatInt(entry.UpdateTime.Time().UnixMilli(), 10), + Exchange: types.ExchangeBinance, + Type: t, + Currency: entry.Asset, + Quantity: entry.Amount, + State: "done", + Note: "", + Spent: false, + CreatedAt: types.Time(entry.UpdateTime), + }) + } + + return rewards, nil +} diff --git a/pkg/exchange/binance/stream.go b/pkg/exchange/binance/stream.go index 54e3754a21..2a1b39dfcb 100644 --- a/pkg/exchange/binance/stream.go +++ b/pkg/exchange/binance/stream.go @@ -48,6 +48,7 @@ type Stream struct { markPriceUpdateEventCallbacks []func(e *MarkPriceUpdateEvent) marketTradeEventCallbacks []func(e *MarketTradeEvent) + aggTradeEventCallbacks []func(e *AggTradeEvent) continuousKLineEventCallbacks []func(e *ContinuousKLineEvent) continuousKLineClosedEventCallbacks []func(e *ContinuousKLineEvent) @@ -62,6 +63,8 @@ type Stream struct { accountUpdateEventCallbacks []func(e *AccountUpdateEvent) accountConfigUpdateEventCallbacks []func(e *AccountConfigUpdateEvent) + listenKeyExpiredCallbacks []func(e *ListenKeyExpired) + depthBuffers map[string]*depth.Buffer } @@ -118,6 +121,7 @@ func NewStream(ex *Exchange, client *binance.Client, futuresClient *futures.Clie stream.OnExecutionReportEvent(stream.handleExecutionReportEvent) stream.OnContinuousKLineEvent(stream.handleContinuousKLineEvent) stream.OnMarketTradeEvent(stream.handleMarketTradeEvent) + stream.OnAggTradeEvent(stream.handleAggTradeEvent) // Event type ACCOUNT_UPDATE from user data stream updates Balance and FuturesPosition. stream.OnAccountUpdateEvent(stream.handleAccountUpdateEvent) @@ -125,6 +129,9 @@ func NewStream(ex *Exchange, client *binance.Client, futuresClient *futures.Clie stream.OnOrderTradeUpdateEvent(stream.handleOrderTradeUpdateEvent) stream.OnDisconnect(stream.handleDisconnect) stream.OnConnect(stream.handleConnect) + stream.OnListenKeyExpired(func(e *ListenKeyExpired) { + stream.Reconnect() + }) return stream } @@ -213,6 +220,10 @@ func (s *Stream) handleMarketTradeEvent(e *MarketTradeEvent) { s.EmitMarketTrade(e.Trade()) } +func (s *Stream) handleAggTradeEvent(e *AggTradeEvent) { + s.EmitAggTrade(e.Trade()) +} + func (s *Stream) handleKLineEvent(e *KLineEvent) { kline := e.KLine.KLine() if e.KLine.Closed { @@ -268,6 +279,17 @@ func (s *Stream) handleOrderTradeUpdateEvent(e *OrderTradeUpdateEvent) { s.EmitTradeUpdate(*trade) + order, err := e.OrderFutures() + if err != nil { + log.WithError(err).Error("futures order convert error") + return + } + + // Update Order with FILLED event + if order.Status == types.OrderStatusFilled { + s.EmitOrderUpdate(*order) + } + case "CALCULATED - Liquidation Execution": log.Infof("CALCULATED - Liquidation Execution not support yet.") } @@ -326,6 +348,9 @@ func (s *Stream) dispatchEvent(e interface{}) { case *MarketTradeEvent: s.EmitMarketTradeEvent(e) + case *AggTradeEvent: + s.EmitAggTradeEvent(e) + case *KLineEvent: s.EmitKLineEvent(e) @@ -352,6 +377,10 @@ func (s *Stream) dispatchEvent(e interface{}) { case *AccountConfigUpdateEvent: s.EmitAccountConfigUpdateEvent(e) + + case *ListenKeyExpired: + s.EmitListenKeyExpired(e) + } } diff --git a/pkg/exchange/binance/stream_callbacks.go b/pkg/exchange/binance/stream_callbacks.go index 9f9d3cea9f..d335ccff99 100644 --- a/pkg/exchange/binance/stream_callbacks.go +++ b/pkg/exchange/binance/stream_callbacks.go @@ -54,6 +54,16 @@ func (s *Stream) EmitMarketTradeEvent(e *MarketTradeEvent) { } } +func (s *Stream) OnAggTradeEvent(cb func(e *AggTradeEvent)) { + s.aggTradeEventCallbacks = append(s.aggTradeEventCallbacks, cb) +} + +func (s *Stream) EmitAggTradeEvent(e *AggTradeEvent) { + for _, cb := range s.aggTradeEventCallbacks { + cb(e) + } +} + func (s *Stream) OnContinuousKLineEvent(cb func(e *ContinuousKLineEvent)) { s.continuousKLineEventCallbacks = append(s.continuousKLineEventCallbacks, cb) } @@ -154,6 +164,16 @@ func (s *Stream) EmitAccountConfigUpdateEvent(e *AccountConfigUpdateEvent) { } } +func (s *Stream) OnListenKeyExpired(cb func(e *ListenKeyExpired)) { + s.listenKeyExpiredCallbacks = append(s.listenKeyExpiredCallbacks, cb) +} + +func (s *Stream) EmitListenKeyExpired(e *ListenKeyExpired) { + for _, cb := range s.listenKeyExpiredCallbacks { + cb(e) + } +} + type StreamEventHub interface { OnDepthEvent(cb func(e *DepthEvent)) @@ -165,6 +185,8 @@ type StreamEventHub interface { OnMarketTradeEvent(cb func(e *MarketTradeEvent)) + OnAggTradeEvent(cb func(e *AggTradeEvent)) + OnContinuousKLineEvent(cb func(e *ContinuousKLineEvent)) OnContinuousKLineClosedEvent(cb func(e *ContinuousKLineEvent)) @@ -184,4 +206,6 @@ type StreamEventHub interface { OnAccountUpdateEvent(cb func(e *AccountUpdateEvent)) OnAccountConfigUpdateEvent(cb func(e *AccountConfigUpdateEvent)) + + OnListenKeyExpired(cb func(e *ListenKeyExpired)) } diff --git a/pkg/exchange/binance/stream_test.go b/pkg/exchange/binance/stream_test.go deleted file mode 100644 index eedef1a9cc..0000000000 --- a/pkg/exchange/binance/stream_test.go +++ /dev/null @@ -1,58 +0,0 @@ -package binance - -import ( - "context" - batch2 "github.com/c9s/bbgo/pkg/exchange/batch" - "github.com/c9s/bbgo/pkg/types" - "github.com/stretchr/testify/assert" - "os" - "testing" - "time" -) - -func Test_Batch(t *testing.T) { - key := os.Getenv("BINANCE_API_KEY") - secret := os.Getenv("BINANCE_API_SECRET") - if len(key) == 0 && len(secret) == 0 { - t.Skip("api key/secret are not configured") - } - - e := New(key, secret) - //stream := NewStream(key, secret, subAccount, e) - - batch := &batch2.KLineBatchQuery{Exchange: e} - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - // should use channel here - - starttime, _ := time.Parse("2006-1-2 15:04", "2021-08-01 00:00") - endtime, _ := time.Parse("2006-1-2 15:04", "2021-12-14 00:19") - klineC, _ := batch.Query(ctx, "XRPUSDT", types.Interval1m, starttime, endtime) - - var lastmintime time.Time - var lastmaxtime time.Time - for klines := range klineC { - assert.NotEmpty(t, klines) - - var nowMinTime = klines[0].StartTime - var nowMaxTime = klines[0].StartTime - for _, item := range klines { - if nowMaxTime.Unix() < item.StartTime.Unix() { - nowMaxTime = item.StartTime - } - if nowMinTime.Unix() > item.StartTime.Unix() { - nowMinTime = item.StartTime - } - } - assert.True(t, nowMinTime.Unix() <= nowMaxTime.Unix()) - assert.True(t, nowMinTime.Unix() > lastmaxtime.Unix()) - assert.True(t, nowMaxTime.Unix() > lastmaxtime.Unix()) - - lastmintime = nowMinTime.Time() - lastmaxtime = nowMaxTime.Time() - assert.True(t, lastmintime.Unix() <= lastmaxtime.Unix()) - - } - -} diff --git a/pkg/exchange/factory.go b/pkg/exchange/factory.go new file mode 100644 index 0000000000..d03f8654e9 --- /dev/null +++ b/pkg/exchange/factory.go @@ -0,0 +1,65 @@ +package exchange + +import ( + "fmt" + "os" + "strings" + + "github.com/c9s/bbgo/pkg/exchange/binance" + "github.com/c9s/bbgo/pkg/exchange/ftx" + "github.com/c9s/bbgo/pkg/exchange/kucoin" + "github.com/c9s/bbgo/pkg/exchange/max" + "github.com/c9s/bbgo/pkg/exchange/okex" + "github.com/c9s/bbgo/pkg/types" +) + +func NewPublic(exchangeName types.ExchangeName) (types.Exchange, error) { + return NewStandard(exchangeName, "", "", "", "") +} + +func NewStandard(n types.ExchangeName, key, secret, passphrase, subAccount string) (types.Exchange, error) { + switch n { + + case types.ExchangeFTX: + return ftx.NewExchange(key, secret, subAccount), nil + + case types.ExchangeBinance: + return binance.New(key, secret), nil + + case types.ExchangeMax: + return max.New(key, secret), nil + + case types.ExchangeOKEx: + return okex.New(key, secret, passphrase), nil + + case types.ExchangeKucoin: + return kucoin.New(key, secret, passphrase), nil + + default: + return nil, fmt.Errorf("unsupported exchange: %v", n) + + } +} + +func NewWithEnvVarPrefix(n types.ExchangeName, varPrefix string) (types.Exchange, error) { + if len(varPrefix) == 0 { + varPrefix = n.String() + } + + varPrefix = strings.ToUpper(varPrefix) + + key := os.Getenv(varPrefix + "_API_KEY") + secret := os.Getenv(varPrefix + "_API_SECRET") + if len(key) == 0 || len(secret) == 0 { + return nil, fmt.Errorf("can not initialize exchange %s: empty key or secret, env var prefix: %s", n, varPrefix) + } + + passphrase := os.Getenv(varPrefix + "_API_PASSPHRASE") + subAccount := os.Getenv(varPrefix + "_SUBACCOUNT") + return NewStandard(n, key, secret, passphrase, subAccount) +} + +// New constructor exchange object from viper config. +func New(n types.ExchangeName) (types.Exchange, error) { + return NewWithEnvVarPrefix(n, "") +} diff --git a/pkg/exchange/ftx/convert_test.go b/pkg/exchange/ftx/convert_test.go index 3eee61a6af..3a1ea7f1e7 100644 --- a/pkg/exchange/ftx/convert_test.go +++ b/pkg/exchange/ftx/convert_test.go @@ -119,4 +119,3 @@ func Test_toLocalOrderTypeWithMarket(t *testing.T) { assert.NoError(t, err) assert.Equal(t, ftxapi.OrderTypeMarket, orderType) } - diff --git a/pkg/exchange/ftx/exchange.go b/pkg/exchange/ftx/exchange.go index bed81a992a..d5b3294918 100644 --- a/pkg/exchange/ftx/exchange.go +++ b/pkg/exchange/ftx/exchange.go @@ -30,14 +30,17 @@ var logger = logrus.WithField("exchange", "ftx") // POST https://ftx.com/api/orders 429, Success: false, err: Do not send more than 2 orders on this market per 200ms var requestLimit = rate.NewLimiter(rate.Every(220*time.Millisecond), 2) +var marketDataLimiter = rate.NewLimiter(rate.Every(500*time.Millisecond), 2) + //go:generate go run generate_symbol_map.go type Exchange struct { client *ftxapi.RestClient - key, secret string - subAccount string - restEndpoint *url.URL + key, secret string + subAccount string + restEndpoint *url.URL + orderAmountReduceFactor fixedpoint.Value } type MarketTicker struct { @@ -88,8 +91,10 @@ func NewExchange(key, secret string, subAccount string) *Exchange { client: client, restEndpoint: u, key: key, - secret: secret, - subAccount: subAccount, + // pragma: allowlist nextline secret + secret: secret, + subAccount: subAccount, + orderAmountReduceFactor: fixedpoint.One, } } @@ -209,15 +214,32 @@ func (e *Exchange) QueryAccountBalances(ctx context.Context) (types.BalanceMap, return balances, nil } +// DefaultFeeRates returns the FTX Tier 1 fee +// See also https://help.ftx.com/hc/en-us/articles/360024479432-Fees +func (e *Exchange) DefaultFeeRates() types.ExchangeFee { + return types.ExchangeFee{ + MakerFeeRate: fixedpoint.NewFromFloat(0.01 * 0.020), // 0.020% + TakerFeeRate: fixedpoint.NewFromFloat(0.01 * 0.070), // 0.070% + } +} + +// SetModifyOrderAmountForFee protects the limit buy orders by reducing amount with taker fee. +// The amount is recalculated before submit: submit_amount = original_amount / (1 + taker_fee_rate) . +// This prevents balance exceeding error while closing position without spot margin enabled. +func (e *Exchange) SetModifyOrderAmountForFee(feeRate types.ExchangeFee) { + e.orderAmountReduceFactor = fixedpoint.One.Add(feeRate.TakerFeeRate) +} + // resolution field in api // window length in seconds. options: 15, 60, 300, 900, 3600, 14400, 86400, or any multiple of 86400 up to 30*86400 var supportedIntervals = map[types.Interval]int{ - types.Interval1m: 1, - types.Interval5m: 5, - types.Interval15m: 15, - types.Interval1h: 60, - types.Interval1d: 60 * 24, - types.Interval3d: 60 * 24 * 3, + types.Interval1m: 1 * 60, + types.Interval5m: 5 * 60, + types.Interval15m: 15 * 60, + types.Interval1h: 60 * 60, + types.Interval4h: 60 * 60 * 4, + types.Interval1d: 60 * 60 * 24, + types.Interval3d: 60 * 60 * 24 * 3, } func (e *Exchange) SupportedInterval() map[types.Interval]int { @@ -230,94 +252,45 @@ func (e *Exchange) IsSupportedInterval(interval types.Interval) bool { func (e *Exchange) QueryKLines(ctx context.Context, symbol string, interval types.Interval, options types.KLineQueryOptions) ([]types.KLine, error) { var klines []types.KLine - var since, until, currentEnd time.Time - if options.StartTime != nil { - since = *options.StartTime - } - if options.EndTime != nil { - until = *options.EndTime - } else { - until = time.Now() - } - - currentEnd = until - - for { - - // the fetch result is from newest to oldest - endTime := currentEnd.Add(interval.Duration()) - options.EndTime = &endTime - lines, err := e._queryKLines(ctx, symbol, interval, types.KLineQueryOptions{ - StartTime: &since, - EndTime: ¤tEnd, - }) - - if err != nil { - return nil, err - } - - if len(lines) == 0 { - break - } - for _, line := range lines { - - if line.StartTime.Unix() < currentEnd.Unix() { - currentEnd = line.StartTime.Time() - } - - if line.StartTime.Unix() > since.Unix() { - klines = append(klines, line) - } - } - - if len(lines) == 1 && lines[0].StartTime.Unix() == currentEnd.Unix() { - break - } - - outBound := currentEnd.Add(interval.Duration()*-1).Unix() <= since.Unix() - if since.IsZero() || currentEnd.Unix() == since.Unix() || outBound { - break - } - - if options.Limit != 0 && options.Limit <= len(lines) { - break - } - } - sort.Slice(klines, func(i, j int) bool { return klines[i].StartTime.Unix() < klines[j].StartTime.Unix() }) - - if options.Limit != 0 { - limitedItems := len(klines) - options.Limit - if limitedItems > 0 { - return klines[limitedItems:], nil - } + // the fetch result is from newest to oldest + // currentEnd = until + // endTime := currentEnd.Add(interval.Duration()) + klines, err := e._queryKLines(ctx, symbol, interval, options) + if err != nil { + return nil, err } + klines = types.SortKLinesAscending(klines) return klines, nil } func (e *Exchange) _queryKLines(ctx context.Context, symbol string, interval types.Interval, options types.KLineQueryOptions) ([]types.KLine, error) { - var since, until time.Time - if options.StartTime != nil { - since = *options.StartTime - } - if options.EndTime != nil { - until = *options.EndTime - } else { - until = time.Now() - } - if since.After(until) { - return nil, fmt.Errorf("invalid query klines time range, since: %+v, until: %+v", since, until) - } if !isIntervalSupportedInKLine(interval) { return nil, fmt.Errorf("interval %s is not supported", interval.String()) } - if err := requestLimit.Wait(ctx); err != nil { + if err := marketDataLimiter.Wait(ctx); err != nil { return nil, err } - resp, err := e.newRest().HistoricalPrices(ctx, toLocalSymbol(symbol), interval, 0, since, until) + // assign limit to a default value since ftx has the limit + if options.Limit == 0 { + options.Limit = 500 + } + + // if the time range exceed the ftx valid time range, we need to adjust the endTime + if options.StartTime != nil && options.EndTime != nil { + rangeDuration := options.EndTime.Sub(*options.StartTime) + estimatedCount := rangeDuration / interval.Duration() + + if options.Limit != 0 && uint64(estimatedCount) > uint64(options.Limit) { + endTime := options.StartTime.Add(interval.Duration() * time.Duration(options.Limit)) + options.EndTime = &endTime + } + } + + resp, err := e.newRest().marketRequest.HistoricalPrices(ctx, toLocalSymbol(symbol), interval, int64(options.Limit), options.StartTime, options.EndTime) if err != nil { return nil, err } @@ -333,6 +306,7 @@ func (e *Exchange) _queryKLines(ctx context.Context, symbol string, interval typ } klines = append(klines, globalKline) } + return klines, nil } @@ -427,55 +401,54 @@ func (e *Exchange) QueryDepositHistory(ctx context.Context, asset string, since, return } -func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) (types.OrderSlice, error) { - var createdOrders types.OrderSlice +func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (*types.Order, error) { // TODO: currently only support limit and market order // TODO: support time in force - for _, so := range orders { - if err := requestLimit.Wait(ctx); err != nil { - logrus.WithError(err).Error("rate limit error") - } - - orderType, err := toLocalOrderType(so.Type) - if err != nil { - logrus.WithError(err).Error("type error") - } + so := order + if err := requestLimit.Wait(ctx); err != nil { + logrus.WithError(err).Error("rate limit error") + } - req := e.client.NewPlaceOrderRequest() - req.Market(toLocalSymbol(TrimUpperString(so.Symbol))) - req.OrderType(orderType) - req.Side(ftxapi.Side(TrimLowerString(string(so.Side)))) - req.Size(so.Quantity) + orderType, err := toLocalOrderType(so.Type) + if err != nil { + logrus.WithError(err).Error("type error") + } - switch so.Type { - case types.OrderTypeLimit, types.OrderTypeLimitMaker: - req.Price(so.Price) + submitQuantity := so.Quantity + switch orderType { + case ftxapi.OrderTypeLimit, ftxapi.OrderTypeStopLimit: + submitQuantity = so.Quantity.Div(e.orderAmountReduceFactor) + } - } + req := e.client.NewPlaceOrderRequest() + req.Market(toLocalSymbol(TrimUpperString(so.Symbol))) + req.OrderType(orderType) + req.Side(ftxapi.Side(TrimLowerString(string(so.Side)))) + req.Size(submitQuantity) - if so.Type == types.OrderTypeLimitMaker { - req.PostOnly(true) - } + switch so.Type { + case types.OrderTypeLimit, types.OrderTypeLimitMaker: + req.Price(so.Price) - if so.TimeInForce == types.TimeInForceIOC { - req.Ioc(true) - } + } - req.ClientID(newSpotClientOrderID(so.ClientOrderID)) + if so.Type == types.OrderTypeLimitMaker { + req.PostOnly(true) + } - or, err := req.Do(ctx) - if err != nil { - return createdOrders, fmt.Errorf("failed to place order %+v: %w", so, err) - } + if so.TimeInForce == types.TimeInForceIOC { + req.Ioc(true) + } - globalOrder, err := toGlobalOrderNew(*or) - if err != nil { - return createdOrders, fmt.Errorf("failed to convert response to global order") - } + req.ClientID(newSpotClientOrderID(so.ClientOrderID)) - createdOrders = append(createdOrders, globalOrder) + or, err := req.Do(ctx) + if err != nil { + return nil, fmt.Errorf("failed to place order %+v: %w", so, err) } - return createdOrders, nil + + globalOrder, err := toGlobalOrderNew(*or) + return &globalOrder, err } func (e *Exchange) QueryOrder(ctx context.Context, q types.OrderQuery) (*types.Order, error) { @@ -490,8 +463,12 @@ func (e *Exchange) QueryOrder(ctx context.Context, q types.OrderQuery) (*types.O return nil, err } - order, err := toGlobalOrderNew(*ftxOrder) - return &order, err + o, err := toGlobalOrderNew(*ftxOrder) + if err != nil { + return nil, err + } + + return &o, err } func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders []types.Order, err error) { @@ -592,7 +569,6 @@ func (e *Exchange) QueryTicker(ctx context.Context, symbol string) (*types.Ticke } func (e *Exchange) QueryTickers(ctx context.Context, symbol ...string) (map[string]types.Ticker, error) { - var tickers = make(map[string]types.Ticker) markets, err := e._queryMarkets(ctx) @@ -606,7 +582,6 @@ func (e *Exchange) QueryTickers(ctx context.Context, symbol ...string) (map[stri } rest := e.newRest() - for k, v := range markets { // if we provide symbol as condition then we only query the gieven symbol , @@ -620,7 +595,10 @@ func (e *Exchange) QueryTickers(ctx context.Context, symbol ...string) (map[stri } // ctx context.Context, market string, interval types.Interval, limit int64, start, end time.Time - prices, err := rest.HistoricalPrices(ctx, v.Market.LocalSymbol, types.Interval1h, 1, time.Now().Add(time.Duration(-1)*time.Hour), time.Now()) + now := time.Now() + since := now.Add(time.Duration(-1) * time.Hour) + until := now + prices, err := rest.marketRequest.HistoricalPrices(ctx, v.Market.LocalSymbol, types.Interval1h, 1, &since, &until) if err != nil || !prices.Success || len(prices.Result) == 0 { continue } diff --git a/pkg/exchange/ftx/exchange_test.go b/pkg/exchange/ftx/exchange_test.go index eb7cc731a1..1f4f46ebd2 100644 --- a/pkg/exchange/ftx/exchange_test.go +++ b/pkg/exchange/ftx/exchange_test.go @@ -34,7 +34,7 @@ func TestExchange_IOCOrder(t *testing.T) { } ex := NewExchange(key, secret, "") - createdOrder, err := ex.SubmitOrders(context.Background(), types.SubmitOrder{ + createdOrder, err := ex.SubmitOrder(context.Background(), types.SubmitOrder{ Symbol: "LTCUSDT", Side: types.SideTypeBuy, Type: types.OrderTypeLimitMaker, diff --git a/pkg/exchange/ftx/ftxapi/account.go b/pkg/exchange/ftx/ftxapi/account.go index 345ce78648..f6309f272c 100644 --- a/pkg/exchange/ftx/ftxapi/account.go +++ b/pkg/exchange/ftx/ftxapi/account.go @@ -66,7 +66,6 @@ func (c *RestClient) NewGetPositionsRequest() *GetPositionsRequest { } } - type Balance struct { Coin string `json:"coin"` Free fixedpoint.Value `json:"free"` diff --git a/pkg/exchange/ftx/ftxapi/client.go b/pkg/exchange/ftx/ftxapi/client.go index 0da9ee5167..2437bd48f9 100644 --- a/pkg/exchange/ftx/ftxapi/client.go +++ b/pkg/exchange/ftx/ftxapi/client.go @@ -68,6 +68,7 @@ func NewClient() *RestClient { func (c *RestClient) Auth(key, secret, subAccount string) { c.Key = key + // pragma: allowlist nextline secret c.Secret = secret c.subAccount = subAccount } @@ -200,5 +201,3 @@ func castPayload(payload interface{}) ([]byte, error) { return nil, nil } - - diff --git a/pkg/exchange/ftx/ftxapi/client_test.go b/pkg/exchange/ftx/ftxapi/client_test.go index 41101521dd..a73f595663 100644 --- a/pkg/exchange/ftx/ftxapi/client_test.go +++ b/pkg/exchange/ftx/ftxapi/client_test.go @@ -37,7 +37,7 @@ func TestClient_Requests(t *testing.T) { return } - ctx, cancel := context.WithTimeout(context.TODO(), 15 * time.Second) + ctx, cancel := context.WithTimeout(context.TODO(), 15*time.Second) defer cancel() client := NewClient() @@ -45,13 +45,23 @@ func TestClient_Requests(t *testing.T) { testCases := []struct { name string - tt func(t *testing.T) - } { + tt func(t *testing.T) + }{ + { + name: "GetMarketsRequest", + tt: func(t *testing.T) { + req := client.NewGetMarketsRequest() + markets, err := req.Do(ctx) + assert.NoError(t, err) + assert.NotNil(t, markets) + t.Logf("markets: %+v", markets) + }, + }, { name: "GetAccountRequest", tt: func(t *testing.T) { req := client.NewGetAccountRequest() - account ,err := req.Do(ctx) + account, err := req.Do(ctx) assert.NoError(t, err) assert.NotNil(t, account) t.Logf("account: %+v", account) @@ -68,7 +78,7 @@ func TestClient_Requests(t *testing.T) { Side(SideBuy). Market("LTC/USDT") - createdOrder,err := req.Do(ctx) + createdOrder, err := req.Do(ctx) if assert.NoError(t, err) { assert.NotNil(t, createdOrder) t.Logf("createdOrder: %+v", createdOrder) @@ -85,7 +95,7 @@ func TestClient_Requests(t *testing.T) { name: "GetFillsRequest", tt: func(t *testing.T) { req := client.NewGetFillsRequest() - req.Market("CRO/USDT") + req.Market("CRO/USD") fills, err := req.Do(ctx) assert.NoError(t, err) assert.NotNil(t, fills) diff --git a/pkg/exchange/ftx/ftxapi/get_market_request_requestgen.go b/pkg/exchange/ftx/ftxapi/get_market_request_requestgen.go index 3c8dc3a896..72825a4c29 100644 --- a/pkg/exchange/ftx/ftxapi/get_market_request_requestgen.go +++ b/pkg/exchange/ftx/ftxapi/get_market_request_requestgen.go @@ -7,6 +7,7 @@ import ( "encoding/json" "fmt" "net/url" + "reflect" "regexp" ) @@ -20,8 +21,8 @@ func (g *GetMarketRequest) GetQueryParameters() (url.Values, error) { var params = map[string]interface{}{} query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) } return query, nil @@ -43,8 +44,14 @@ func (g *GetMarketRequest) GetParametersQuery() (url.Values, error) { return query, err } - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } } return query, nil @@ -73,14 +80,31 @@ func (g *GetMarketRequest) GetSlugParameters() (map[string]interface{}, error) { } func (g *GetMarketRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) } return url } +func (g *GetMarketRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetMarketRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + func (g *GetMarketRequest) GetSlugsMap() (map[string]string, error) { slugs := map[string]string{} params, err := g.GetSlugParameters() @@ -88,8 +112,8 @@ func (g *GetMarketRequest) GetSlugsMap() (map[string]string, error) { return slugs, nil } - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) } return slugs, nil diff --git a/pkg/exchange/ftx/ftxapi/get_markets_request_requestgen.go b/pkg/exchange/ftx/ftxapi/get_markets_request_requestgen.go index d7592854e5..db8e591bc8 100644 --- a/pkg/exchange/ftx/ftxapi/get_markets_request_requestgen.go +++ b/pkg/exchange/ftx/ftxapi/get_markets_request_requestgen.go @@ -7,6 +7,7 @@ import ( "encoding/json" "fmt" "net/url" + "reflect" "regexp" ) @@ -15,8 +16,8 @@ func (g *GetMarketsRequest) GetQueryParameters() (url.Values, error) { var params = map[string]interface{}{} query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) } return query, nil @@ -38,8 +39,14 @@ func (g *GetMarketsRequest) GetParametersQuery() (url.Values, error) { return query, err } - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } } return query, nil @@ -63,14 +70,31 @@ func (g *GetMarketsRequest) GetSlugParameters() (map[string]interface{}, error) } func (g *GetMarketsRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) } return url } +func (g *GetMarketsRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetMarketsRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + func (g *GetMarketsRequest) GetSlugsMap() (map[string]string, error) { slugs := map[string]string{} params, err := g.GetSlugParameters() @@ -78,8 +102,8 @@ func (g *GetMarketsRequest) GetSlugsMap() (map[string]string, error) { return slugs, nil } - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) } return slugs, nil @@ -93,7 +117,7 @@ func (g *GetMarketsRequest) Do(ctx context.Context) ([]Market, error) { apiURL := "api/markets" - req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + req, err := g.client.NewRequest(ctx, "GET", apiURL, query, params) if err != nil { return nil, err } diff --git a/pkg/exchange/ftx/ftxapi/market.go b/pkg/exchange/ftx/ftxapi/market.go index 5da448e5e1..4cac2fee9e 100644 --- a/pkg/exchange/ftx/ftxapi/market.go +++ b/pkg/exchange/ftx/ftxapi/market.go @@ -25,7 +25,7 @@ type Market struct { Underlying string `json:"underlying"` Enabled bool `json:"enabled"` Ask fixedpoint.Value `json:"ask"` - Bid fixedpoint.Value `json:"bid"` + Bid fixedpoint.Value `json:"bid"` Last fixedpoint.Value `json:"last"` PostOnly bool `json:"postOnly"` Price fixedpoint.Value `json:"price"` @@ -33,9 +33,10 @@ type Market struct { SizeIncrement fixedpoint.Value `json:"sizeIncrement"` Restricted bool `json:"restricted"` } + //go:generate GetRequest -url "api/markets" -type GetMarketsRequest -responseDataType []Market type GetMarketsRequest struct { - client requestgen.AuthenticatedAPIClient + client requestgen.APIClient } func (c *RestClient) NewGetMarketsRequest() *GetMarketsRequest { diff --git a/pkg/exchange/ftx/ftxapi/trade.go b/pkg/exchange/ftx/ftxapi/trade.go index ccc390c398..323481d20d 100644 --- a/pkg/exchange/ftx/ftxapi/trade.go +++ b/pkg/exchange/ftx/ftxapi/trade.go @@ -128,7 +128,7 @@ type Fill struct { BaseCurrency string `json:"baseCurrency"` QuoteCurrency string `json:"quoteCurrency"` OrderId uint64 `json:"orderId"` - TradeId uint64 `json:"tradeId"` + TradeId uint64 `json:"tradeId"` Price fixedpoint.Value `json:"price"` Side Side `json:"side"` Size fixedpoint.Value `json:"size"` diff --git a/pkg/exchange/ftx/generate_symbol_map.go b/pkg/exchange/ftx/generate_symbol_map.go index 1096494b0a..b2c68072ea 100644 --- a/pkg/exchange/ftx/generate_symbol_map.go +++ b/pkg/exchange/ftx/generate_symbol_map.go @@ -1,3 +1,4 @@ +//go:build ignore // +build ignore package main diff --git a/pkg/exchange/ftx/rest.go b/pkg/exchange/ftx/rest.go index 9d9cc96b1c..18282551ca 100644 --- a/pkg/exchange/ftx/rest.go +++ b/pkg/exchange/ftx/rest.go @@ -92,6 +92,7 @@ func newRestRequest(c *http.Client, baseURL *url.URL) *restRequest { func (r *restRequest) Auth(key, secret string) *restRequest { r.key = key + // pragma: allowlist nextline secret r.secret = secret return r } diff --git a/pkg/exchange/ftx/rest_market_request.go b/pkg/exchange/ftx/rest_market_request.go index 039bdaf519..aeb41e17a5 100644 --- a/pkg/exchange/ftx/rest_market_request.go +++ b/pkg/exchange/ftx/rest_market_request.go @@ -18,7 +18,7 @@ type marketRequest struct { supported resolutions: window length in seconds. options: 15, 60, 300, 900, 3600, 14400, 86400 doc: https://docs.ftx.com/?javascript#get-historical-prices */ -func (r *marketRequest) HistoricalPrices(ctx context.Context, market string, interval types.Interval, limit int64, start, end time.Time) (HistoricalPricesResponse, error) { +func (r *marketRequest) HistoricalPrices(ctx context.Context, market string, interval types.Interval, limit int64, start, end *time.Time) (HistoricalPricesResponse, error) { q := map[string]string{ "resolution": strconv.FormatInt(int64(interval.Minutes())*60, 10), } @@ -27,11 +27,11 @@ func (r *marketRequest) HistoricalPrices(ctx context.Context, market string, int q["limit"] = strconv.FormatInt(limit, 10) } - if start != (time.Time{}) { + if start != nil { q["start_time"] = strconv.FormatInt(start.Unix(), 10) } - if end != (time.Time{}) { + if end != nil { q["end_time"] = strconv.FormatInt(end.Unix(), 10) } diff --git a/pkg/exchange/ftx/rest_responses.go b/pkg/exchange/ftx/rest_responses.go index cb6c318afd..15da5e606d 100644 --- a/pkg/exchange/ftx/rest_responses.go +++ b/pkg/exchange/ftx/rest_responses.go @@ -5,8 +5,8 @@ import ( "strings" "time" - "github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" ) // ex: 2019-03-05T09:56:55.728933+00:00 @@ -82,7 +82,7 @@ func (d *datetime) UnmarshalJSON(b []byte) error { } } */ -type accountResponse struct { +type accountResponse struct { // nolint:golint,deadcode Success bool `json:"success"` Result account `json:"result"` } @@ -93,7 +93,7 @@ type account struct { TotalAccountValue fixedpoint.Value `json:"totalAccountValue"` } -type positionsResponse struct { +type positionsResponse struct { // nolint:golint,deadcode Success bool `json:"success"` Result []position `json:"result"` } @@ -121,7 +121,7 @@ type position struct { Cost fixedpoint.Value `json:"cost"` EntryPrice fixedpoint.Value `json:"entryPrice"` EstimatedLiquidationPrice fixedpoint.Value `json:"estimatedLiquidationPrice"` - Future string `json:"future"` + Future string `json:"future"` InitialMarginRequirement fixedpoint.Value `json:"initialMarginRequirement"` LongOrderSize fixedpoint.Value `json:"longOrderSize"` MaintenanceMarginRequirement fixedpoint.Value `json:"maintenanceMarginRequirement"` @@ -129,17 +129,17 @@ type position struct { OpenSize fixedpoint.Value `json:"openSize"` RealizedPnl fixedpoint.Value `json:"realizedPnl"` ShortOrderSize fixedpoint.Value `json:"shortOrderSize"` - Side string `json:"Side"` + Side string `json:"Side"` Size fixedpoint.Value `json:"size"` UnrealizedPnl fixedpoint.Value `json:"unrealizedPnl"` CollateralUsed fixedpoint.Value `json:"collateralUsed"` } -type balances struct { +type balances struct { // nolint:golint,deadcode Success bool `json:"success"` Result []struct { - Coin string `json:"coin"` + Coin string `json:"coin"` Free fixedpoint.Value `json:"free"` Total fixedpoint.Value `json:"total"` } `json:"result"` @@ -172,15 +172,15 @@ type balances struct { } ] */ -type marketsResponse struct { +type marketsResponse struct { // nolint:golint,deadcode Success bool `json:"success"` Result []market `json:"result"` } type market struct { - Name string `json:"name"` - Enabled bool `json:"enabled"` - PostOnly bool `json:"postOnly"` + Name string `json:"name"` + Enabled bool `json:"enabled"` + PostOnly bool `json:"postOnly"` PriceIncrement fixedpoint.Value `json:"priceIncrement"` SizeIncrement fixedpoint.Value `json:"sizeIncrement"` MinProvideSize fixedpoint.Value `json:"minProvideSize"` @@ -188,12 +188,12 @@ type market struct { Bid fixedpoint.Value `json:"bid"` Ask fixedpoint.Value `json:"ask"` Price fixedpoint.Value `json:"price"` - Type string `json:"type"` - BaseCurrency string `json:"baseCurrency"` - QuoteCurrency string `json:"quoteCurrency"` - Underlying string `json:"underlying"` - Restricted bool `json:"restricted"` - HighLeverageFeeExempt bool `json:"highLeverageFeeExempt"` + Type string `json:"type"` + BaseCurrency string `json:"baseCurrency"` + QuoteCurrency string `json:"quoteCurrency"` + Underlying string `json:"underlying"` + Restricted bool `json:"restricted"` + HighLeverageFeeExempt bool `json:"highLeverageFeeExempt"` Change1h fixedpoint.Value `json:"change1h"` Change24h fixedpoint.Value `json:"change24h"` ChangeBod fixedpoint.Value `json:"changeBod"` @@ -222,50 +222,50 @@ type HistoricalPricesResponse struct { } type Candle struct { - Close fixedpoint.Value `json:"close"` - High fixedpoint.Value `json:"high"` - Low fixedpoint.Value `json:"low"` - Open fixedpoint.Value `json:"open"` - StartTime datetime `json:"startTime"` - Volume fixedpoint.Value `json:"volume"` + Close fixedpoint.Value `json:"close"` + High fixedpoint.Value `json:"high"` + Low fixedpoint.Value `json:"low"` + Open fixedpoint.Value `json:"open"` + StartTime datetime `json:"startTime"` + Volume fixedpoint.Value `json:"volume"` } -type ordersHistoryResponse struct { +type ordersHistoryResponse struct { // nolint:golint,deadcode Success bool `json:"success"` Result []order `json:"result"` HasMoreData bool `json:"hasMoreData"` } -type ordersResponse struct { +type ordersResponse struct { // nolint:golint,deadcode Success bool `json:"success"` Result []order `json:"result"` } -type cancelOrderResponse struct { +type cancelOrderResponse struct { // nolint:golint,deadcode Success bool `json:"success"` Result string `json:"result"` } type order struct { - CreatedAt datetime `json:"createdAt"` - FilledSize fixedpoint.Value `json:"filledSize"` + CreatedAt datetime `json:"createdAt"` + FilledSize fixedpoint.Value `json:"filledSize"` // Future field is not defined in the response format table but in the response example. - Future string `json:"future"` - ID int64 `json:"id"` - Market string `json:"market"` + Future string `json:"future"` + ID int64 `json:"id"` + Market string `json:"market"` Price fixedpoint.Value `json:"price"` AvgFillPrice fixedpoint.Value `json:"avgFillPrice"` RemainingSize fixedpoint.Value `json:"remainingSize"` - Side string `json:"side"` + Side string `json:"side"` Size fixedpoint.Value `json:"size"` - Status string `json:"status"` - Type string `json:"type"` - ReduceOnly bool `json:"reduceOnly"` - Ioc bool `json:"ioc"` - PostOnly bool `json:"postOnly"` - ClientId string `json:"clientId"` - Liquidation bool `json:"liquidation"` + Status string `json:"status"` + Type string `json:"type"` + ReduceOnly bool `json:"reduceOnly"` + Ioc bool `json:"ioc"` + PostOnly bool `json:"postOnly"` + ClientId string `json:"clientId"` + Liquidation bool `json:"liquidation"` } type orderResponse struct { @@ -299,18 +299,18 @@ type depositHistoryResponse struct { } type depositHistory struct { - ID int64 `json:"id"` - Coin string `json:"coin"` - TxID string `json:"txid"` - Address address `json:"address"` - Confirmations int64 `json:"confirmations"` - ConfirmedTime datetime `json:"confirmedTime"` - Fee fixedpoint.Value `json:"fee"` - SentTime datetime `json:"sentTime"` - Size fixedpoint.Value `json:"size"` - Status string `json:"status"` - Time datetime `json:"time"` - Notes string `json:"notes"` + ID int64 `json:"id"` + Coin string `json:"coin"` + TxID string `json:"txid"` + Address address `json:"address"` + Confirmations int64 `json:"confirmations"` + ConfirmedTime datetime `json:"confirmedTime"` + Fee fixedpoint.Value `json:"fee"` + SentTime datetime `json:"sentTime"` + Size fixedpoint.Value `json:"size"` + Status string `json:"status"` + Time datetime `json:"time"` + Notes string `json:"notes"` } /** @@ -354,22 +354,22 @@ type fillsResponse struct { } */ type fill struct { - ID int64 `json:"id"` - Market string `json:"market"` - Future string `json:"future"` - BaseCurrency string `json:"baseCurrency"` - QuoteCurrency string `json:"quoteCurrency"` - Type string `json:"type"` - Side types.SideType `json:"side"` - Price fixedpoint.Value `json:"price"` - Size fixedpoint.Value `json:"size"` - OrderId uint64 `json:"orderId"` - Time datetime `json:"time"` - TradeId uint64 `json:"tradeId"` - FeeRate fixedpoint.Value `json:"feeRate"` - Fee fixedpoint.Value `json:"fee"` - FeeCurrency string `json:"feeCurrency"` - Liquidity string `json:"liquidity"` + ID int64 `json:"id"` + Market string `json:"market"` + Future string `json:"future"` + BaseCurrency string `json:"baseCurrency"` + QuoteCurrency string `json:"quoteCurrency"` + Type string `json:"type"` + Side types.SideType `json:"side"` + Price fixedpoint.Value `json:"price"` + Size fixedpoint.Value `json:"size"` + OrderId uint64 `json:"orderId"` + Time datetime `json:"time"` + TradeId uint64 `json:"tradeId"` + FeeRate fixedpoint.Value `json:"feeRate"` + Fee fixedpoint.Value `json:"fee"` + FeeCurrency string `json:"feeCurrency"` + Liquidity string `json:"liquidity"` } type transferResponse struct { @@ -378,12 +378,12 @@ type transferResponse struct { } type transfer struct { - Id uint `json:"id"` - Coin string `json:"coin"` + Id uint `json:"id"` + Coin string `json:"coin"` Size fixedpoint.Value `json:"size"` - Time string `json:"time"` - Notes string `json:"notes"` - Status string `json:"status"` + Time string `json:"time"` + Notes string `json:"notes"` + Status string `json:"status"` } func (t *transfer) String() string { diff --git a/pkg/exchange/ftx/rest_wallet_request.go b/pkg/exchange/ftx/rest_wallet_request.go index addbab521a..039a325530 100644 --- a/pkg/exchange/ftx/rest_wallet_request.go +++ b/pkg/exchange/ftx/rest_wallet_request.go @@ -42,4 +42,3 @@ func (r *walletRequest) DepositHistory(ctx context.Context, since time.Time, unt return d, nil } - diff --git a/pkg/exchange/ftx/stream.go b/pkg/exchange/ftx/stream.go index 309ae41b38..6a70a249db 100644 --- a/pkg/exchange/ftx/stream.go +++ b/pkg/exchange/ftx/stream.go @@ -7,8 +7,9 @@ import ( "github.com/gorilla/websocket" "github.com/pkg/errors" + log "github.com/sirupsen/logrus" - "github.com/c9s/bbgo/pkg/service" + "github.com/c9s/bbgo/pkg/net/websocketbase" "github.com/c9s/bbgo/pkg/types" ) @@ -17,7 +18,7 @@ const endpoint = "wss://ftx.com/ws/" type Stream struct { *types.StandardStream - ws *service.WebsocketClientBase + ws *websocketbase.WebsocketClientBase exchange *Exchange key string @@ -36,12 +37,13 @@ type klineSubscription struct { func NewStream(key, secret string, subAccount string, e *Exchange) *Stream { s := &Stream{ - exchange: e, - key: key, + exchange: e, + key: key, + // pragma: allowlist nextline secret secret: secret, subAccount: subAccount, StandardStream: &types.StandardStream{}, - ws: service.NewWebsocketClientBase(endpoint, 3*time.Second), + ws: websocketbase.NewWebsocketClientBase(endpoint, 3*time.Second), } s.ws.OnMessage((&messageHandler{StandardStream: s.StandardStream}).handleMessage) @@ -70,7 +72,9 @@ func (s *Stream) Connect(ctx context.Context) error { return err } s.EmitStart() + go s.pollKLines(ctx) + go s.pollBalances(ctx) go func() { // https://docs.ftx.com/?javascript#request-process @@ -144,6 +148,26 @@ func (s *Stream) Subscribe(channel types.Channel, symbol string, option types.Su } } +func (s *Stream) pollBalances(ctx context.Context) { + ticker := time.NewTicker(15 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + + case <-ticker.C: + balances, err := s.exchange.QueryAccountBalances(ctx) + if err != nil { + log.WithError(err).Errorf("query balance error") + continue + } + s.EmitBalanceSnapshot(balances) + } + } +} + func (s *Stream) pollKLines(ctx context.Context) { lastClosed := make(map[string]map[types.Interval]time.Time, 0) // get current kline candle diff --git a/pkg/exchange/ftx/stream_test.go b/pkg/exchange/ftx/stream_test.go deleted file mode 100644 index 843cb4ce20..0000000000 --- a/pkg/exchange/ftx/stream_test.go +++ /dev/null @@ -1,89 +0,0 @@ -package ftx - -import ( - "context" - batch2 "github.com/c9s/bbgo/pkg/exchange/batch" - "github.com/c9s/bbgo/pkg/types" - "github.com/stretchr/testify/assert" - "os" - "testing" - "time" -) - -func TestLastKline(t *testing.T) { - key := os.Getenv("FTX_API_KEY") - secret := os.Getenv("FTX_API_SECRET") - subAccount := os.Getenv("FTX_SUBACCOUNT") - if len(key) == 0 && len(secret) == 0 { - t.Skip("api key/secret are not configured") - } - - e := NewExchange(key, secret, subAccount) - //stream := NewStream(key, secret, subAccount, e) - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - klines := getLastClosedKLine(e, ctx, "XRPUSD", types.Interval1m) - assert.Equal(t, 1, len(klines)) - -} - -func Test_Batch(t *testing.T) { - key := os.Getenv("FTX_API_KEY") - secret := os.Getenv("FTX_API_SECRET") - subAccount := os.Getenv("FTX_SUBACCOUNT") - if len(key) == 0 && len(secret) == 0 { - t.Skip("api key/secret are not configured") - } - - e := NewExchange(key, secret, subAccount) - //stream := NewStream(key, secret, subAccount, e) - - batch := &batch2.KLineBatchQuery{Exchange: e} - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - // should use channel here - - starttime, err := time.Parse("2006-1-2 15:04", "2021-08-01 00:00") - assert.NoError(t, err) - endtime, err := time.Parse("2006-1-2 15:04", "2021-08-04 00:19") - assert.NoError(t, err) - - klineC, errC := batch.Query(ctx, "XRPUSDT", types.Interval1d, starttime, endtime) - - if err := <-errC; err != nil { - assert.NoError(t, err) - } - - var lastmintime time.Time - var lastmaxtime time.Time - - for klines := range klineC { - assert.NotEmpty(t, klines) - - var nowMinTime = klines[0].StartTime - var nowMaxTime = klines[0].StartTime - for _, item := range klines { - - if nowMaxTime.Unix() < item.StartTime.Unix() { - nowMaxTime = item.StartTime - } - if nowMinTime.Unix() > item.StartTime.Unix() { - nowMinTime = item.StartTime - } - - } - - if !lastmintime.IsZero() { - assert.True(t, nowMinTime.Unix() <= nowMaxTime.Unix()) - assert.True(t, nowMinTime.Unix() > lastmaxtime.Unix()) - assert.True(t, nowMaxTime.Unix() > lastmaxtime.Unix()) - } - lastmintime = nowMinTime.Time() - lastmaxtime = nowMaxTime.Time() - assert.True(t, lastmintime.Unix() <= lastmaxtime.Unix()) - - } - -} diff --git a/pkg/exchange/ftx/symbols.go b/pkg/exchange/ftx/symbols.go index 77907297c7..33cb022965 100644 --- a/pkg/exchange/ftx/symbols.go +++ b/pkg/exchange/ftx/symbols.go @@ -1,818 +1,819 @@ // Code generated by go generate; DO NOT EDIT. package ftx + var symbolMap = map[string]string{ - "1INCH-0325": "1INCH-0325", - "1INCH-PERP": "1INCH-PERP", - "1INCHUSD": "1INCH/USD", - "AAPL-0325": "AAPL-0325", - "AAPLUSD": "AAPL/USD", - "AAVE-0325": "AAVE-0325", - "AAVE-PERP": "AAVE-PERP", - "AAVEUSD": "AAVE/USD", - "AAVEUSDT": "AAVE/USDT", - "ABNB-0325": "ABNB-0325", - "ABNBUSD": "ABNB/USD", - "ACB-0325": "ACB-0325", - "ACBUSD": "ACB/USD", - "ADA-0325": "ADA-0325", - "ADA-PERP": "ADA-PERP", - "ADABEARUSD": "ADABEAR/USD", - "ADABULLUSD": "ADABULL/USD", - "ADAHALFUSD": "ADAHALF/USD", - "ADAHEDGEUSD": "ADAHEDGE/USD", - "AGLD-PERP": "AGLD-PERP", - "AGLDUSD": "AGLD/USD", - "AKROUSD": "AKRO/USD", - "AKROUSDT": "AKRO/USDT", - "ALCX-PERP": "ALCX-PERP", - "ALCXUSD": "ALCX/USD", - "ALEPHUSD": "ALEPH/USD", - "ALGO-0325": "ALGO-0325", - "ALGO-PERP": "ALGO-PERP", - "ALGOBEARUSD": "ALGOBEAR/USD", - "ALGOBULLUSD": "ALGOBULL/USD", - "ALGOHALFUSD": "ALGOHALF/USD", - "ALGOHEDGEUSD": "ALGOHEDGE/USD", - "ALICE-PERP": "ALICE-PERP", - "ALICEUSD": "ALICE/USD", - "ALPHA-PERP": "ALPHA-PERP", - "ALPHAUSD": "ALPHA/USD", - "ALT-0325": "ALT-0325", - "ALT-PERP": "ALT-PERP", - "ALTBEARUSD": "ALTBEAR/USD", - "ALTBULLUSD": "ALTBULL/USD", - "ALTHALFUSD": "ALTHALF/USD", - "ALTHEDGEUSD": "ALTHEDGE/USD", - "AMC-0325": "AMC-0325", - "AMCUSD": "AMC/USD", - "AMD-0325": "AMD-0325", - "AMDUSD": "AMD/USD", - "AMPL-PERP": "AMPL-PERP", - "AMPLUSD": "AMPL/USD", - "AMPLUSDT": "AMPL/USDT", - "AMZN-0325": "AMZN-0325", - "AMZNUSD": "AMZN/USD", - "APHAUSD": "APHA/USD", - "AR-PERP": "AR-PERP", - "ARKK-0325": "ARKK-0325", - "ARKKUSD": "ARKK/USD", - "ASD-PERP": "ASD-PERP", - "ASDBEARUSD": "ASDBEAR/USD", - "ASDBEARUSDT": "ASDBEAR/USDT", - "ASDBULLUSD": "ASDBULL/USD", - "ASDBULLUSDT": "ASDBULL/USDT", - "ASDHALFUSD": "ASDHALF/USD", - "ASDHEDGEUSD": "ASDHEDGE/USD", - "ASDUSD": "ASD/USD", - "ATLAS-PERP": "ATLAS-PERP", - "ATLASUSD": "ATLAS/USD", - "ATOM-0325": "ATOM-0325", - "ATOM-PERP": "ATOM-PERP", - "ATOMBEARUSD": "ATOMBEAR/USD", - "ATOMBULLUSD": "ATOMBULL/USD", - "ATOMHALFUSD": "ATOMHALF/USD", - "ATOMHEDGEUSD": "ATOMHEDGE/USD", - "ATOMUSD": "ATOM/USD", - "ATOMUSDT": "ATOM/USDT", - "AUDIO-PERP": "AUDIO-PERP", - "AUDIOUSD": "AUDIO/USD", - "AUDIOUSDT": "AUDIO/USDT", - "AURYUSD": "AURY/USD", - "AVAX-0325": "AVAX-0325", - "AVAX-PERP": "AVAX-PERP", - "AVAXBTC": "AVAX/BTC", - "AVAXUSD": "AVAX/USD", - "AVAXUSDT": "AVAX/USDT", - "AXS-PERP": "AXS-PERP", - "AXSUSD": "AXS/USD", - "BABA-0325": "BABA-0325", - "BABAUSD": "BABA/USD", - "BADGER-PERP": "BADGER-PERP", - "BADGERUSD": "BADGER/USD", - "BAL-0325": "BAL-0325", - "BAL-PERP": "BAL-PERP", - "BALBEARUSD": "BALBEAR/USD", - "BALBEARUSDT": "BALBEAR/USDT", - "BALBULLUSD": "BALBULL/USD", - "BALBULLUSDT": "BALBULL/USDT", - "BALHALFUSD": "BALHALF/USD", - "BALHEDGEUSD": "BALHEDGE/USD", - "BALUSD": "BAL/USD", - "BALUSDT": "BAL/USDT", - "BAND-PERP": "BAND-PERP", - "BANDUSD": "BAND/USD", - "BAO-PERP": "BAO-PERP", - "BAOUSD": "BAO/USD", - "BARUSD": "BAR/USD", - "BAT-PERP": "BAT-PERP", - "BATUSD": "BAT/USD", - "BB-0325": "BB-0325", - "BBUSD": "BB/USD", - "BCH-0325": "BCH-0325", - "BCH-PERP": "BCH-PERP", - "BCHBEARUSD": "BCHBEAR/USD", - "BCHBEARUSDT": "BCHBEAR/USDT", - "BCHBTC": "BCH/BTC", - "BCHBULLUSD": "BCHBULL/USD", - "BCHBULLUSDT": "BCHBULL/USDT", - "BCHHALFUSD": "BCHHALF/USD", - "BCHHEDGEUSD": "BCHHEDGE/USD", - "BCHUSD": "BCH/USD", - "BCHUSDT": "BCH/USDT", - "BEARSHITUSD": "BEARSHIT/USD", - "BEARUSD": "BEAR/USD", - "BEARUSDT": "BEAR/USDT", - "BICOUSD": "BICO/USD", - "BILI-0325": "BILI-0325", - "BILIUSD": "BILI/USD", - "BIT-PERP": "BIT-PERP", - "BITO-0325": "BITO-0325", - "BITOUSD": "BITO/USD", - "BITUSD": "BIT/USD", - "BITW-0325": "BITW-0325", - "BITWUSD": "BITW/USD", - "BLTUSD": "BLT/USD", - "BNB-0325": "BNB-0325", - "BNB-PERP": "BNB-PERP", - "BNBBEARUSD": "BNBBEAR/USD", - "BNBBEARUSDT": "BNBBEAR/USDT", - "BNBBTC": "BNB/BTC", - "BNBBULLUSD": "BNBBULL/USD", - "BNBBULLUSDT": "BNBBULL/USDT", - "BNBHALFUSD": "BNBHALF/USD", - "BNBHEDGEUSD": "BNBHEDGE/USD", - "BNBUSD": "BNB/USD", - "BNBUSDT": "BNB/USDT", - "BNT-PERP": "BNT-PERP", - "BNTUSD": "BNT/USD", - "BNTX-0325": "BNTX-0325", - "BNTXUSD": "BNTX/USD", - "BOBA-PERP": "BOBA-PERP", - "BOBAUSD": "BOBA/USD", - "BOLSONARO2022": "BOLSONARO2022", - "BRZ-PERP": "BRZ-PERP", - "BRZUSD": "BRZ/USD", - "BRZUSDT": "BRZ/USDT", - "BSV-0325": "BSV-0325", - "BSV-PERP": "BSV-PERP", - "BSVBEARUSD": "BSVBEAR/USD", - "BSVBEARUSDT": "BSVBEAR/USDT", - "BSVBULLUSD": "BSVBULL/USD", - "BSVBULLUSDT": "BSVBULL/USDT", - "BSVHALFUSD": "BSVHALF/USD", - "BSVHEDGEUSD": "BSVHEDGE/USD", - "BTC-0325": "BTC-0325", - "BTC-0624": "BTC-0624", - "BTC-MOVE-0303": "BTC-MOVE-0303", - "BTC-MOVE-0304": "BTC-MOVE-0304", - "BTC-MOVE-2022Q1": "BTC-MOVE-2022Q1", - "BTC-MOVE-2022Q2": "BTC-MOVE-2022Q2", - "BTC-MOVE-2022Q3": "BTC-MOVE-2022Q3", + "1INCH-0325": "1INCH-0325", + "1INCH-PERP": "1INCH-PERP", + "1INCHUSD": "1INCH/USD", + "AAPL-0325": "AAPL-0325", + "AAPLUSD": "AAPL/USD", + "AAVE-0325": "AAVE-0325", + "AAVE-PERP": "AAVE-PERP", + "AAVEUSD": "AAVE/USD", + "AAVEUSDT": "AAVE/USDT", + "ABNB-0325": "ABNB-0325", + "ABNBUSD": "ABNB/USD", + "ACB-0325": "ACB-0325", + "ACBUSD": "ACB/USD", + "ADA-0325": "ADA-0325", + "ADA-PERP": "ADA-PERP", + "ADABEARUSD": "ADABEAR/USD", + "ADABULLUSD": "ADABULL/USD", + "ADAHALFUSD": "ADAHALF/USD", + "ADAHEDGEUSD": "ADAHEDGE/USD", + "AGLD-PERP": "AGLD-PERP", + "AGLDUSD": "AGLD/USD", + "AKROUSD": "AKRO/USD", + "AKROUSDT": "AKRO/USDT", + "ALCX-PERP": "ALCX-PERP", + "ALCXUSD": "ALCX/USD", + "ALEPHUSD": "ALEPH/USD", + "ALGO-0325": "ALGO-0325", + "ALGO-PERP": "ALGO-PERP", + "ALGOBEARUSD": "ALGOBEAR/USD", + "ALGOBULLUSD": "ALGOBULL/USD", + "ALGOHALFUSD": "ALGOHALF/USD", + "ALGOHEDGEUSD": "ALGOHEDGE/USD", + "ALICE-PERP": "ALICE-PERP", + "ALICEUSD": "ALICE/USD", + "ALPHA-PERP": "ALPHA-PERP", + "ALPHAUSD": "ALPHA/USD", + "ALT-0325": "ALT-0325", + "ALT-PERP": "ALT-PERP", + "ALTBEARUSD": "ALTBEAR/USD", + "ALTBULLUSD": "ALTBULL/USD", + "ALTHALFUSD": "ALTHALF/USD", + "ALTHEDGEUSD": "ALTHEDGE/USD", + "AMC-0325": "AMC-0325", + "AMCUSD": "AMC/USD", + "AMD-0325": "AMD-0325", + "AMDUSD": "AMD/USD", + "AMPL-PERP": "AMPL-PERP", + "AMPLUSD": "AMPL/USD", + "AMPLUSDT": "AMPL/USDT", + "AMZN-0325": "AMZN-0325", + "AMZNUSD": "AMZN/USD", + "APHAUSD": "APHA/USD", + "AR-PERP": "AR-PERP", + "ARKK-0325": "ARKK-0325", + "ARKKUSD": "ARKK/USD", + "ASD-PERP": "ASD-PERP", + "ASDBEARUSD": "ASDBEAR/USD", + "ASDBEARUSDT": "ASDBEAR/USDT", + "ASDBULLUSD": "ASDBULL/USD", + "ASDBULLUSDT": "ASDBULL/USDT", + "ASDHALFUSD": "ASDHALF/USD", + "ASDHEDGEUSD": "ASDHEDGE/USD", + "ASDUSD": "ASD/USD", + "ATLAS-PERP": "ATLAS-PERP", + "ATLASUSD": "ATLAS/USD", + "ATOM-0325": "ATOM-0325", + "ATOM-PERP": "ATOM-PERP", + "ATOMBEARUSD": "ATOMBEAR/USD", + "ATOMBULLUSD": "ATOMBULL/USD", + "ATOMHALFUSD": "ATOMHALF/USD", + "ATOMHEDGEUSD": "ATOMHEDGE/USD", + "ATOMUSD": "ATOM/USD", + "ATOMUSDT": "ATOM/USDT", + "AUDIO-PERP": "AUDIO-PERP", + "AUDIOUSD": "AUDIO/USD", + "AUDIOUSDT": "AUDIO/USDT", + "AURYUSD": "AURY/USD", + "AVAX-0325": "AVAX-0325", + "AVAX-PERP": "AVAX-PERP", + "AVAXBTC": "AVAX/BTC", + "AVAXUSD": "AVAX/USD", + "AVAXUSDT": "AVAX/USDT", + "AXS-PERP": "AXS-PERP", + "AXSUSD": "AXS/USD", + "BABA-0325": "BABA-0325", + "BABAUSD": "BABA/USD", + "BADGER-PERP": "BADGER-PERP", + "BADGERUSD": "BADGER/USD", + "BAL-0325": "BAL-0325", + "BAL-PERP": "BAL-PERP", + "BALBEARUSD": "BALBEAR/USD", + "BALBEARUSDT": "BALBEAR/USDT", + "BALBULLUSD": "BALBULL/USD", + "BALBULLUSDT": "BALBULL/USDT", + "BALHALFUSD": "BALHALF/USD", + "BALHEDGEUSD": "BALHEDGE/USD", + "BALUSD": "BAL/USD", + "BALUSDT": "BAL/USDT", + "BAND-PERP": "BAND-PERP", + "BANDUSD": "BAND/USD", + "BAO-PERP": "BAO-PERP", + "BAOUSD": "BAO/USD", + "BARUSD": "BAR/USD", + "BAT-PERP": "BAT-PERP", + "BATUSD": "BAT/USD", + "BB-0325": "BB-0325", + "BBUSD": "BB/USD", + "BCH-0325": "BCH-0325", + "BCH-PERP": "BCH-PERP", + "BCHBEARUSD": "BCHBEAR/USD", + "BCHBEARUSDT": "BCHBEAR/USDT", + "BCHBTC": "BCH/BTC", + "BCHBULLUSD": "BCHBULL/USD", + "BCHBULLUSDT": "BCHBULL/USDT", + "BCHHALFUSD": "BCHHALF/USD", + "BCHHEDGEUSD": "BCHHEDGE/USD", + "BCHUSD": "BCH/USD", + "BCHUSDT": "BCH/USDT", + "BEARSHITUSD": "BEARSHIT/USD", + "BEARUSD": "BEAR/USD", + "BEARUSDT": "BEAR/USDT", + "BICOUSD": "BICO/USD", + "BILI-0325": "BILI-0325", + "BILIUSD": "BILI/USD", + "BIT-PERP": "BIT-PERP", + "BITO-0325": "BITO-0325", + "BITOUSD": "BITO/USD", + "BITUSD": "BIT/USD", + "BITW-0325": "BITW-0325", + "BITWUSD": "BITW/USD", + "BLTUSD": "BLT/USD", + "BNB-0325": "BNB-0325", + "BNB-PERP": "BNB-PERP", + "BNBBEARUSD": "BNBBEAR/USD", + "BNBBEARUSDT": "BNBBEAR/USDT", + "BNBBTC": "BNB/BTC", + "BNBBULLUSD": "BNBBULL/USD", + "BNBBULLUSDT": "BNBBULL/USDT", + "BNBHALFUSD": "BNBHALF/USD", + "BNBHEDGEUSD": "BNBHEDGE/USD", + "BNBUSD": "BNB/USD", + "BNBUSDT": "BNB/USDT", + "BNT-PERP": "BNT-PERP", + "BNTUSD": "BNT/USD", + "BNTX-0325": "BNTX-0325", + "BNTXUSD": "BNTX/USD", + "BOBA-PERP": "BOBA-PERP", + "BOBAUSD": "BOBA/USD", + "BOLSONARO2022": "BOLSONARO2022", + "BRZ-PERP": "BRZ-PERP", + "BRZUSD": "BRZ/USD", + "BRZUSDT": "BRZ/USDT", + "BSV-0325": "BSV-0325", + "BSV-PERP": "BSV-PERP", + "BSVBEARUSD": "BSVBEAR/USD", + "BSVBEARUSDT": "BSVBEAR/USDT", + "BSVBULLUSD": "BSVBULL/USD", + "BSVBULLUSDT": "BSVBULL/USDT", + "BSVHALFUSD": "BSVHALF/USD", + "BSVHEDGEUSD": "BSVHEDGE/USD", + "BTC-0325": "BTC-0325", + "BTC-0624": "BTC-0624", + "BTC-MOVE-0303": "BTC-MOVE-0303", + "BTC-MOVE-0304": "BTC-MOVE-0304", + "BTC-MOVE-2022Q1": "BTC-MOVE-2022Q1", + "BTC-MOVE-2022Q2": "BTC-MOVE-2022Q2", + "BTC-MOVE-2022Q3": "BTC-MOVE-2022Q3", "BTC-MOVE-WK-0304": "BTC-MOVE-WK-0304", "BTC-MOVE-WK-0311": "BTC-MOVE-WK-0311", "BTC-MOVE-WK-0318": "BTC-MOVE-WK-0318", "BTC-MOVE-WK-0325": "BTC-MOVE-WK-0325", - "BTC-PERP": "BTC-PERP", - "BTCBRZ": "BTC/BRZ", - "BTCEUR": "BTC/EUR", - "BTCTRYB": "BTC/TRYB", - "BTCUSD": "BTC/USD", - "BTCUSDT": "BTC/USDT", - "BTT-PERP": "BTT-PERP", - "BTTUSD": "BTT/USD", - "BULLSHITUSD": "BULLSHIT/USD", - "BULLUSD": "BULL/USD", - "BULLUSDT": "BULL/USDT", - "BVOLBTC": "BVOL/BTC", - "BVOLUSD": "BVOL/USD", - "BVOLUSDT": "BVOL/USDT", - "BYND-0325": "BYND-0325", - "BYNDUSD": "BYND/USD", - "C98-PERP": "C98-PERP", - "C98USD": "C98/USD", - "CADUSD": "CAD/USD", - "CAKE-PERP": "CAKE-PERP", - "CEL-0325": "CEL-0325", - "CEL-PERP": "CEL-PERP", - "CELBTC": "CEL/BTC", - "CELO-PERP": "CELO-PERP", - "CELUSD": "CEL/USD", - "CGC-0325": "CGC-0325", - "CGCUSD": "CGC/USD", - "CHR-PERP": "CHR-PERP", - "CHRUSD": "CHR/USD", - "CHZ-0325": "CHZ-0325", - "CHZ-PERP": "CHZ-PERP", - "CHZUSD": "CHZ/USD", - "CHZUSDT": "CHZ/USDT", - "CITYUSD": "CITY/USD", - "CLV-PERP": "CLV-PERP", - "CLVUSD": "CLV/USD", - "COINUSD": "COIN/USD", - "COMP-0325": "COMP-0325", - "COMP-PERP": "COMP-PERP", - "COMPBEARUSD": "COMPBEAR/USD", - "COMPBEARUSDT": "COMPBEAR/USDT", - "COMPBULLUSD": "COMPBULL/USD", - "COMPBULLUSDT": "COMPBULL/USDT", - "COMPHALFUSD": "COMPHALF/USD", - "COMPHEDGEUSD": "COMPHEDGE/USD", - "COMPUSD": "COMP/USD", - "COMPUSDT": "COMP/USDT", - "CONV-PERP": "CONV-PERP", - "CONVUSD": "CONV/USD", - "COPEUSD": "COPE/USD", - "CQTUSD": "CQT/USD", - "CREAM-PERP": "CREAM-PERP", - "CREAMUSD": "CREAM/USD", - "CREAMUSDT": "CREAM/USDT", - "CRO-PERP": "CRO-PERP", - "CRON-0325": "CRON-0325", - "CRONUSD": "CRON/USD", - "CROUSD": "CRO/USD", - "CRV-PERP": "CRV-PERP", - "CRVUSD": "CRV/USD", - "CUSDT-PERP": "CUSDT-PERP", - "CUSDTBEARUSD": "CUSDTBEAR/USD", - "CUSDTBEARUSDT": "CUSDTBEAR/USDT", - "CUSDTBULLUSD": "CUSDTBULL/USD", - "CUSDTBULLUSDT": "CUSDTBULL/USDT", - "CUSDTHALFUSD": "CUSDTHALF/USD", - "CUSDTHEDGEUSD": "CUSDTHEDGE/USD", - "CUSDTUSD": "CUSDT/USD", - "CUSDTUSDT": "CUSDT/USDT", - "CVC-PERP": "CVC-PERP", - "CVCUSD": "CVC/USD", - "DAIUSD": "DAI/USD", - "DAIUSDT": "DAI/USDT", - "DASH-PERP": "DASH-PERP", - "DAWN-PERP": "DAWN-PERP", - "DAWNUSD": "DAWN/USD", - "DEFI-0325": "DEFI-0325", - "DEFI-PERP": "DEFI-PERP", - "DEFIBEARUSD": "DEFIBEAR/USD", - "DEFIBEARUSDT": "DEFIBEAR/USDT", - "DEFIBULLUSD": "DEFIBULL/USD", - "DEFIBULLUSDT": "DEFIBULL/USDT", - "DEFIHALFUSD": "DEFIHALF/USD", - "DEFIHEDGEUSD": "DEFIHEDGE/USD", - "DENT-PERP": "DENT-PERP", - "DENTUSD": "DENT/USD", - "DFLUSD": "DFL/USD", - "DKNG-0325": "DKNG-0325", - "DKNGUSD": "DKNG/USD", - "DMGUSD": "DMG/USD", - "DMGUSDT": "DMG/USDT", - "DODO-PERP": "DODO-PERP", - "DODOUSD": "DODO/USD", - "DOGE-0325": "DOGE-0325", - "DOGE-PERP": "DOGE-PERP", - "DOGEBEAR2021USD": "DOGEBEAR2021/USD", - "DOGEBTC": "DOGE/BTC", - "DOGEBULLUSD": "DOGEBULL/USD", - "DOGEHALFUSD": "DOGEHALF/USD", - "DOGEHEDGEUSD": "DOGEHEDGE/USD", - "DOGEUSD": "DOGE/USD", - "DOGEUSDT": "DOGE/USDT", - "DOT-0325": "DOT-0325", - "DOT-PERP": "DOT-PERP", - "DOTBTC": "DOT/BTC", - "DOTUSD": "DOT/USD", - "DOTUSDT": "DOT/USDT", - "DRGN-0325": "DRGN-0325", - "DRGN-PERP": "DRGN-PERP", - "DRGNBEARUSD": "DRGNBEAR/USD", - "DRGNBULLUSD": "DRGNBULL/USD", - "DRGNHALFUSD": "DRGNHALF/USD", - "DRGNHEDGEUSD": "DRGNHEDGE/USD", - "DYDX-PERP": "DYDX-PERP", - "DYDXUSD": "DYDX/USD", - "EDEN-0325": "EDEN-0325", - "EDEN-PERP": "EDEN-PERP", - "EDENUSD": "EDEN/USD", - "EGLD-PERP": "EGLD-PERP", - "EMBUSD": "EMB/USD", - "ENJ-PERP": "ENJ-PERP", - "ENJUSD": "ENJ/USD", - "ENS-PERP": "ENS-PERP", - "ENSUSD": "ENS/USD", - "EOS-0325": "EOS-0325", - "EOS-PERP": "EOS-PERP", - "EOSBEARUSD": "EOSBEAR/USD", - "EOSBEARUSDT": "EOSBEAR/USDT", - "EOSBULLUSD": "EOSBULL/USD", - "EOSBULLUSDT": "EOSBULL/USDT", - "EOSHALFUSD": "EOSHALF/USD", - "EOSHEDGEUSD": "EOSHEDGE/USD", - "ETC-PERP": "ETC-PERP", - "ETCBEARUSD": "ETCBEAR/USD", - "ETCBULLUSD": "ETCBULL/USD", - "ETCHALFUSD": "ETCHALF/USD", - "ETCHEDGEUSD": "ETCHEDGE/USD", - "ETH-0325": "ETH-0325", - "ETH-0624": "ETH-0624", - "ETH-PERP": "ETH-PERP", - "ETHBEARUSD": "ETHBEAR/USD", - "ETHBEARUSDT": "ETHBEAR/USDT", - "ETHBRZ": "ETH/BRZ", - "ETHBTC": "ETH/BTC", - "ETHBULLUSD": "ETHBULL/USD", - "ETHBULLUSDT": "ETHBULL/USDT", - "ETHE-0325": "ETHE-0325", - "ETHEUR": "ETH/EUR", - "ETHEUSD": "ETHE/USD", - "ETHHALFUSD": "ETHHALF/USD", - "ETHHEDGEUSD": "ETHHEDGE/USD", - "ETHUSD": "ETH/USD", - "ETHUSDT": "ETH/USDT", - "EURTEUR": "EURT/EUR", - "EURTUSD": "EURT/USD", - "EURTUSDT": "EURT/USDT", - "EURUSD": "EUR/USD", - "EXCH-0325": "EXCH-0325", - "EXCH-PERP": "EXCH-PERP", - "EXCHBEARUSD": "EXCHBEAR/USD", - "EXCHBULLUSD": "EXCHBULL/USD", - "EXCHHALFUSD": "EXCHHALF/USD", - "EXCHHEDGEUSD": "EXCHHEDGE/USD", - "FB-0325": "FB-0325", - "FBUSD": "FB/USD", - "FIDA-PERP": "FIDA-PERP", - "FIDAUSD": "FIDA/USD", - "FIDAUSDT": "FIDA/USDT", - "FIL-0325": "FIL-0325", - "FIL-PERP": "FIL-PERP", - "FLM-PERP": "FLM-PERP", - "FLOW-PERP": "FLOW-PERP", - "FRONTUSD": "FRONT/USD", - "FRONTUSDT": "FRONT/USDT", - "FTM-PERP": "FTM-PERP", - "FTMUSD": "FTM/USD", - "FTT-PERP": "FTT-PERP", - "FTTBTC": "FTT/BTC", - "FTTUSD": "FTT/USD", - "FTTUSDT": "FTT/USDT", - "GALA-PERP": "GALA-PERP", - "GALAUSD": "GALA/USD", - "GALUSD": "GAL/USD", - "GARIUSD": "GARI/USD", - "GBPUSD": "GBP/USD", - "GBTC-0325": "GBTC-0325", - "GBTCUSD": "GBTC/USD", - "GDX-0325": "GDX-0325", - "GDXJ-0325": "GDXJ-0325", - "GDXJUSD": "GDXJ/USD", - "GDXUSD": "GDX/USD", - "GENEUSD": "GENE/USD", - "GLD-0325": "GLD-0325", - "GLDUSD": "GLD/USD", - "GLXYUSD": "GLXY/USD", - "GME-0325": "GME-0325", - "GMEUSD": "GME/USD", - "GODSUSD": "GODS/USD", - "GOGUSD": "GOG/USD", - "GOOGL-0325": "GOOGL-0325", - "GOOGLUSD": "GOOGL/USD", - "GRT-0325": "GRT-0325", - "GRT-PERP": "GRT-PERP", - "GRTBEARUSD": "GRTBEAR/USD", - "GRTBULLUSD": "GRTBULL/USD", - "GRTUSD": "GRT/USD", - "GTUSD": "GT/USD", - "HALFSHITUSD": "HALFSHIT/USD", - "HALFUSD": "HALF/USD", - "HBAR-PERP": "HBAR-PERP", - "HEDGESHITUSD": "HEDGESHIT/USD", - "HEDGEUSD": "HEDGE/USD", - "HGETUSD": "HGET/USD", - "HGETUSDT": "HGET/USDT", - "HMTUSD": "HMT/USD", - "HNT-PERP": "HNT-PERP", - "HNTUSD": "HNT/USD", - "HNTUSDT": "HNT/USDT", - "HOLY-PERP": "HOLY-PERP", - "HOLYUSD": "HOLY/USD", - "HOODUSD": "HOOD/USD", - "HOT-PERP": "HOT-PERP", - "HT-PERP": "HT-PERP", - "HTBEARUSD": "HTBEAR/USD", - "HTBULLUSD": "HTBULL/USD", - "HTHALFUSD": "HTHALF/USD", - "HTHEDGEUSD": "HTHEDGE/USD", - "HTUSD": "HT/USD", - "HUM-PERP": "HUM-PERP", - "HUMUSD": "HUM/USD", - "HXROUSD": "HXRO/USD", - "HXROUSDT": "HXRO/USDT", - "IBVOLBTC": "IBVOL/BTC", - "IBVOLUSD": "IBVOL/USD", - "IBVOLUSDT": "IBVOL/USDT", - "ICP-PERP": "ICP-PERP", - "ICX-PERP": "ICX-PERP", - "IMX-PERP": "IMX-PERP", - "IMXUSD": "IMX/USD", - "INTERUSD": "INTER/USD", - "IOTA-PERP": "IOTA-PERP", - "JETUSD": "JET/USD", - "JOEUSD": "JOE/USD", - "JSTUSD": "JST/USD", - "KAVA-PERP": "KAVA-PERP", - "KBTT-PERP": "KBTT-PERP", - "KBTTUSD": "KBTT/USD", - "KIN-PERP": "KIN-PERP", - "KINUSD": "KIN/USD", - "KNC-PERP": "KNC-PERP", - "KNCBEARUSD": "KNCBEAR/USD", - "KNCBEARUSDT": "KNCBEAR/USDT", - "KNCBULLUSD": "KNCBULL/USD", - "KNCBULLUSDT": "KNCBULL/USDT", - "KNCHALFUSD": "KNCHALF/USD", - "KNCHEDGEUSD": "KNCHEDGE/USD", - "KNCUSD": "KNC/USD", - "KNCUSDT": "KNC/USDT", - "KSHIB-PERP": "KSHIB-PERP", - "KSHIBUSD": "KSHIB/USD", - "KSM-PERP": "KSM-PERP", - "KSOS-PERP": "KSOS-PERP", - "KSOSUSD": "KSOS/USD", - "LEO-PERP": "LEO-PERP", - "LEOBEARUSD": "LEOBEAR/USD", - "LEOBULLUSD": "LEOBULL/USD", - "LEOHALFUSD": "LEOHALF/USD", - "LEOHEDGEUSD": "LEOHEDGE/USD", - "LEOUSD": "LEO/USD", - "LINA-PERP": "LINA-PERP", - "LINAUSD": "LINA/USD", - "LINK-0325": "LINK-0325", - "LINK-PERP": "LINK-PERP", - "LINKBEARUSD": "LINKBEAR/USD", - "LINKBEARUSDT": "LINKBEAR/USDT", - "LINKBTC": "LINK/BTC", - "LINKBULLUSD": "LINKBULL/USD", - "LINKBULLUSDT": "LINKBULL/USDT", - "LINKHALFUSD": "LINKHALF/USD", - "LINKHEDGEUSD": "LINKHEDGE/USD", - "LINKUSD": "LINK/USD", - "LINKUSDT": "LINK/USDT", - "LOOKS-PERP": "LOOKS-PERP", - "LOOKSUSD": "LOOKS/USD", - "LRC-PERP": "LRC-PERP", - "LRCUSD": "LRC/USD", - "LTC-0325": "LTC-0325", - "LTC-PERP": "LTC-PERP", - "LTCBEARUSD": "LTCBEAR/USD", - "LTCBEARUSDT": "LTCBEAR/USDT", - "LTCBTC": "LTC/BTC", - "LTCBULLUSD": "LTCBULL/USD", - "LTCBULLUSDT": "LTCBULL/USDT", - "LTCHALFUSD": "LTCHALF/USD", - "LTCHEDGEUSD": "LTCHEDGE/USD", - "LTCUSD": "LTC/USD", - "LTCUSDT": "LTC/USDT", - "LUAUSD": "LUA/USD", - "LUAUSDT": "LUA/USDT", - "LUNA-PERP": "LUNA-PERP", - "LUNAUSD": "LUNA/USD", - "LUNAUSDT": "LUNA/USDT", - "MANA-PERP": "MANA-PERP", - "MANAUSD": "MANA/USD", - "MAPS-PERP": "MAPS-PERP", - "MAPSUSD": "MAPS/USD", - "MAPSUSDT": "MAPS/USDT", - "MATHUSD": "MATH/USD", - "MATHUSDT": "MATH/USDT", - "MATIC-PERP": "MATIC-PERP", + "BTC-PERP": "BTC-PERP", + "BTCBRZ": "BTC/BRZ", + "BTCEUR": "BTC/EUR", + "BTCTRYB": "BTC/TRYB", + "BTCUSD": "BTC/USD", + "BTCUSDT": "BTC/USDT", + "BTT-PERP": "BTT-PERP", + "BTTUSD": "BTT/USD", + "BULLSHITUSD": "BULLSHIT/USD", + "BULLUSD": "BULL/USD", + "BULLUSDT": "BULL/USDT", + "BVOLBTC": "BVOL/BTC", + "BVOLUSD": "BVOL/USD", + "BVOLUSDT": "BVOL/USDT", + "BYND-0325": "BYND-0325", + "BYNDUSD": "BYND/USD", + "C98-PERP": "C98-PERP", + "C98USD": "C98/USD", + "CADUSD": "CAD/USD", + "CAKE-PERP": "CAKE-PERP", + "CEL-0325": "CEL-0325", + "CEL-PERP": "CEL-PERP", + "CELBTC": "CEL/BTC", + "CELO-PERP": "CELO-PERP", + "CELUSD": "CEL/USD", + "CGC-0325": "CGC-0325", + "CGCUSD": "CGC/USD", + "CHR-PERP": "CHR-PERP", + "CHRUSD": "CHR/USD", + "CHZ-0325": "CHZ-0325", + "CHZ-PERP": "CHZ-PERP", + "CHZUSD": "CHZ/USD", + "CHZUSDT": "CHZ/USDT", + "CITYUSD": "CITY/USD", + "CLV-PERP": "CLV-PERP", + "CLVUSD": "CLV/USD", + "COINUSD": "COIN/USD", + "COMP-0325": "COMP-0325", + "COMP-PERP": "COMP-PERP", + "COMPBEARUSD": "COMPBEAR/USD", + "COMPBEARUSDT": "COMPBEAR/USDT", + "COMPBULLUSD": "COMPBULL/USD", + "COMPBULLUSDT": "COMPBULL/USDT", + "COMPHALFUSD": "COMPHALF/USD", + "COMPHEDGEUSD": "COMPHEDGE/USD", + "COMPUSD": "COMP/USD", + "COMPUSDT": "COMP/USDT", + "CONV-PERP": "CONV-PERP", + "CONVUSD": "CONV/USD", + "COPEUSD": "COPE/USD", + "CQTUSD": "CQT/USD", + "CREAM-PERP": "CREAM-PERP", + "CREAMUSD": "CREAM/USD", + "CREAMUSDT": "CREAM/USDT", + "CRO-PERP": "CRO-PERP", + "CRON-0325": "CRON-0325", + "CRONUSD": "CRON/USD", + "CROUSD": "CRO/USD", + "CRV-PERP": "CRV-PERP", + "CRVUSD": "CRV/USD", + "CUSDT-PERP": "CUSDT-PERP", + "CUSDTBEARUSD": "CUSDTBEAR/USD", + "CUSDTBEARUSDT": "CUSDTBEAR/USDT", + "CUSDTBULLUSD": "CUSDTBULL/USD", + "CUSDTBULLUSDT": "CUSDTBULL/USDT", + "CUSDTHALFUSD": "CUSDTHALF/USD", + "CUSDTHEDGEUSD": "CUSDTHEDGE/USD", + "CUSDTUSD": "CUSDT/USD", + "CUSDTUSDT": "CUSDT/USDT", + "CVC-PERP": "CVC-PERP", + "CVCUSD": "CVC/USD", + "DAIUSD": "DAI/USD", + "DAIUSDT": "DAI/USDT", + "DASH-PERP": "DASH-PERP", + "DAWN-PERP": "DAWN-PERP", + "DAWNUSD": "DAWN/USD", + "DEFI-0325": "DEFI-0325", + "DEFI-PERP": "DEFI-PERP", + "DEFIBEARUSD": "DEFIBEAR/USD", + "DEFIBEARUSDT": "DEFIBEAR/USDT", + "DEFIBULLUSD": "DEFIBULL/USD", + "DEFIBULLUSDT": "DEFIBULL/USDT", + "DEFIHALFUSD": "DEFIHALF/USD", + "DEFIHEDGEUSD": "DEFIHEDGE/USD", + "DENT-PERP": "DENT-PERP", + "DENTUSD": "DENT/USD", + "DFLUSD": "DFL/USD", + "DKNG-0325": "DKNG-0325", + "DKNGUSD": "DKNG/USD", + "DMGUSD": "DMG/USD", + "DMGUSDT": "DMG/USDT", + "DODO-PERP": "DODO-PERP", + "DODOUSD": "DODO/USD", + "DOGE-0325": "DOGE-0325", + "DOGE-PERP": "DOGE-PERP", + "DOGEBEAR2021USD": "DOGEBEAR2021/USD", + "DOGEBTC": "DOGE/BTC", + "DOGEBULLUSD": "DOGEBULL/USD", + "DOGEHALFUSD": "DOGEHALF/USD", + "DOGEHEDGEUSD": "DOGEHEDGE/USD", + "DOGEUSD": "DOGE/USD", + "DOGEUSDT": "DOGE/USDT", + "DOT-0325": "DOT-0325", + "DOT-PERP": "DOT-PERP", + "DOTBTC": "DOT/BTC", + "DOTUSD": "DOT/USD", + "DOTUSDT": "DOT/USDT", + "DRGN-0325": "DRGN-0325", + "DRGN-PERP": "DRGN-PERP", + "DRGNBEARUSD": "DRGNBEAR/USD", + "DRGNBULLUSD": "DRGNBULL/USD", + "DRGNHALFUSD": "DRGNHALF/USD", + "DRGNHEDGEUSD": "DRGNHEDGE/USD", + "DYDX-PERP": "DYDX-PERP", + "DYDXUSD": "DYDX/USD", + "EDEN-0325": "EDEN-0325", + "EDEN-PERP": "EDEN-PERP", + "EDENUSD": "EDEN/USD", + "EGLD-PERP": "EGLD-PERP", + "EMBUSD": "EMB/USD", + "ENJ-PERP": "ENJ-PERP", + "ENJUSD": "ENJ/USD", + "ENS-PERP": "ENS-PERP", + "ENSUSD": "ENS/USD", + "EOS-0325": "EOS-0325", + "EOS-PERP": "EOS-PERP", + "EOSBEARUSD": "EOSBEAR/USD", + "EOSBEARUSDT": "EOSBEAR/USDT", + "EOSBULLUSD": "EOSBULL/USD", + "EOSBULLUSDT": "EOSBULL/USDT", + "EOSHALFUSD": "EOSHALF/USD", + "EOSHEDGEUSD": "EOSHEDGE/USD", + "ETC-PERP": "ETC-PERP", + "ETCBEARUSD": "ETCBEAR/USD", + "ETCBULLUSD": "ETCBULL/USD", + "ETCHALFUSD": "ETCHALF/USD", + "ETCHEDGEUSD": "ETCHEDGE/USD", + "ETH-0325": "ETH-0325", + "ETH-0624": "ETH-0624", + "ETH-PERP": "ETH-PERP", + "ETHBEARUSD": "ETHBEAR/USD", + "ETHBEARUSDT": "ETHBEAR/USDT", + "ETHBRZ": "ETH/BRZ", + "ETHBTC": "ETH/BTC", + "ETHBULLUSD": "ETHBULL/USD", + "ETHBULLUSDT": "ETHBULL/USDT", + "ETHE-0325": "ETHE-0325", + "ETHEUR": "ETH/EUR", + "ETHEUSD": "ETHE/USD", + "ETHHALFUSD": "ETHHALF/USD", + "ETHHEDGEUSD": "ETHHEDGE/USD", + "ETHUSD": "ETH/USD", + "ETHUSDT": "ETH/USDT", + "EURTEUR": "EURT/EUR", + "EURTUSD": "EURT/USD", + "EURTUSDT": "EURT/USDT", + "EURUSD": "EUR/USD", + "EXCH-0325": "EXCH-0325", + "EXCH-PERP": "EXCH-PERP", + "EXCHBEARUSD": "EXCHBEAR/USD", + "EXCHBULLUSD": "EXCHBULL/USD", + "EXCHHALFUSD": "EXCHHALF/USD", + "EXCHHEDGEUSD": "EXCHHEDGE/USD", + "FB-0325": "FB-0325", + "FBUSD": "FB/USD", + "FIDA-PERP": "FIDA-PERP", + "FIDAUSD": "FIDA/USD", + "FIDAUSDT": "FIDA/USDT", + "FIL-0325": "FIL-0325", + "FIL-PERP": "FIL-PERP", + "FLM-PERP": "FLM-PERP", + "FLOW-PERP": "FLOW-PERP", + "FRONTUSD": "FRONT/USD", + "FRONTUSDT": "FRONT/USDT", + "FTM-PERP": "FTM-PERP", + "FTMUSD": "FTM/USD", + "FTT-PERP": "FTT-PERP", + "FTTBTC": "FTT/BTC", + "FTTUSD": "FTT/USD", + "FTTUSDT": "FTT/USDT", + "GALA-PERP": "GALA-PERP", + "GALAUSD": "GALA/USD", + "GALUSD": "GAL/USD", + "GARIUSD": "GARI/USD", + "GBPUSD": "GBP/USD", + "GBTC-0325": "GBTC-0325", + "GBTCUSD": "GBTC/USD", + "GDX-0325": "GDX-0325", + "GDXJ-0325": "GDXJ-0325", + "GDXJUSD": "GDXJ/USD", + "GDXUSD": "GDX/USD", + "GENEUSD": "GENE/USD", + "GLD-0325": "GLD-0325", + "GLDUSD": "GLD/USD", + "GLXYUSD": "GLXY/USD", + "GME-0325": "GME-0325", + "GMEUSD": "GME/USD", + "GODSUSD": "GODS/USD", + "GOGUSD": "GOG/USD", + "GOOGL-0325": "GOOGL-0325", + "GOOGLUSD": "GOOGL/USD", + "GRT-0325": "GRT-0325", + "GRT-PERP": "GRT-PERP", + "GRTBEARUSD": "GRTBEAR/USD", + "GRTBULLUSD": "GRTBULL/USD", + "GRTUSD": "GRT/USD", + "GTUSD": "GT/USD", + "HALFSHITUSD": "HALFSHIT/USD", + "HALFUSD": "HALF/USD", + "HBAR-PERP": "HBAR-PERP", + "HEDGESHITUSD": "HEDGESHIT/USD", + "HEDGEUSD": "HEDGE/USD", + "HGETUSD": "HGET/USD", + "HGETUSDT": "HGET/USDT", + "HMTUSD": "HMT/USD", + "HNT-PERP": "HNT-PERP", + "HNTUSD": "HNT/USD", + "HNTUSDT": "HNT/USDT", + "HOLY-PERP": "HOLY-PERP", + "HOLYUSD": "HOLY/USD", + "HOODUSD": "HOOD/USD", + "HOT-PERP": "HOT-PERP", + "HT-PERP": "HT-PERP", + "HTBEARUSD": "HTBEAR/USD", + "HTBULLUSD": "HTBULL/USD", + "HTHALFUSD": "HTHALF/USD", + "HTHEDGEUSD": "HTHEDGE/USD", + "HTUSD": "HT/USD", + "HUM-PERP": "HUM-PERP", + "HUMUSD": "HUM/USD", + "HXROUSD": "HXRO/USD", + "HXROUSDT": "HXRO/USDT", + "IBVOLBTC": "IBVOL/BTC", + "IBVOLUSD": "IBVOL/USD", + "IBVOLUSDT": "IBVOL/USDT", + "ICP-PERP": "ICP-PERP", + "ICX-PERP": "ICX-PERP", + "IMX-PERP": "IMX-PERP", + "IMXUSD": "IMX/USD", + "INTERUSD": "INTER/USD", + "IOTA-PERP": "IOTA-PERP", + "JETUSD": "JET/USD", + "JOEUSD": "JOE/USD", + "JSTUSD": "JST/USD", + "KAVA-PERP": "KAVA-PERP", + "KBTT-PERP": "KBTT-PERP", + "KBTTUSD": "KBTT/USD", + "KIN-PERP": "KIN-PERP", + "KINUSD": "KIN/USD", + "KNC-PERP": "KNC-PERP", + "KNCBEARUSD": "KNCBEAR/USD", + "KNCBEARUSDT": "KNCBEAR/USDT", + "KNCBULLUSD": "KNCBULL/USD", + "KNCBULLUSDT": "KNCBULL/USDT", + "KNCHALFUSD": "KNCHALF/USD", + "KNCHEDGEUSD": "KNCHEDGE/USD", + "KNCUSD": "KNC/USD", + "KNCUSDT": "KNC/USDT", + "KSHIB-PERP": "KSHIB-PERP", + "KSHIBUSD": "KSHIB/USD", + "KSM-PERP": "KSM-PERP", + "KSOS-PERP": "KSOS-PERP", + "KSOSUSD": "KSOS/USD", + "LEO-PERP": "LEO-PERP", + "LEOBEARUSD": "LEOBEAR/USD", + "LEOBULLUSD": "LEOBULL/USD", + "LEOHALFUSD": "LEOHALF/USD", + "LEOHEDGEUSD": "LEOHEDGE/USD", + "LEOUSD": "LEO/USD", + "LINA-PERP": "LINA-PERP", + "LINAUSD": "LINA/USD", + "LINK-0325": "LINK-0325", + "LINK-PERP": "LINK-PERP", + "LINKBEARUSD": "LINKBEAR/USD", + "LINKBEARUSDT": "LINKBEAR/USDT", + "LINKBTC": "LINK/BTC", + "LINKBULLUSD": "LINKBULL/USD", + "LINKBULLUSDT": "LINKBULL/USDT", + "LINKHALFUSD": "LINKHALF/USD", + "LINKHEDGEUSD": "LINKHEDGE/USD", + "LINKUSD": "LINK/USD", + "LINKUSDT": "LINK/USDT", + "LOOKS-PERP": "LOOKS-PERP", + "LOOKSUSD": "LOOKS/USD", + "LRC-PERP": "LRC-PERP", + "LRCUSD": "LRC/USD", + "LTC-0325": "LTC-0325", + "LTC-PERP": "LTC-PERP", + "LTCBEARUSD": "LTCBEAR/USD", + "LTCBEARUSDT": "LTCBEAR/USDT", + "LTCBTC": "LTC/BTC", + "LTCBULLUSD": "LTCBULL/USD", + "LTCBULLUSDT": "LTCBULL/USDT", + "LTCHALFUSD": "LTCHALF/USD", + "LTCHEDGEUSD": "LTCHEDGE/USD", + "LTCUSD": "LTC/USD", + "LTCUSDT": "LTC/USDT", + "LUAUSD": "LUA/USD", + "LUAUSDT": "LUA/USDT", + "LUNA-PERP": "LUNA-PERP", + "LUNAUSD": "LUNA/USD", + "LUNAUSDT": "LUNA/USDT", + "MANA-PERP": "MANA-PERP", + "MANAUSD": "MANA/USD", + "MAPS-PERP": "MAPS-PERP", + "MAPSUSD": "MAPS/USD", + "MAPSUSDT": "MAPS/USDT", + "MATHUSD": "MATH/USD", + "MATHUSDT": "MATH/USDT", + "MATIC-PERP": "MATIC-PERP", "MATICBEAR2021USD": "MATICBEAR2021/USD", - "MATICBTC": "MATIC/BTC", - "MATICBULLUSD": "MATICBULL/USD", - "MATICHALFUSD": "MATICHALF/USD", - "MATICHEDGEUSD": "MATICHEDGE/USD", - "MATICUSD": "MATIC/USD", - "MBSUSD": "MBS/USD", - "MCB-PERP": "MCB-PERP", - "MCBUSD": "MCB/USD", - "MEDIA-PERP": "MEDIA-PERP", - "MEDIAUSD": "MEDIA/USD", - "MER-PERP": "MER-PERP", - "MERUSD": "MER/USD", - "MID-0325": "MID-0325", - "MID-PERP": "MID-PERP", - "MIDBEARUSD": "MIDBEAR/USD", - "MIDBULLUSD": "MIDBULL/USD", - "MIDHALFUSD": "MIDHALF/USD", - "MIDHEDGEUSD": "MIDHEDGE/USD", - "MINA-PERP": "MINA-PERP", - "MKR-PERP": "MKR-PERP", - "MKRBEARUSD": "MKRBEAR/USD", - "MKRBULLUSD": "MKRBULL/USD", - "MKRUSD": "MKR/USD", - "MKRUSDT": "MKR/USDT", - "MNGO-PERP": "MNGO-PERP", - "MNGOUSD": "MNGO/USD", - "MOBUSD": "MOB/USD", - "MOBUSDT": "MOB/USDT", - "MRNA-0325": "MRNA-0325", - "MRNAUSD": "MRNA/USD", - "MSOLUSD": "MSOL/USD", - "MSTR-0325": "MSTR-0325", - "MSTRUSD": "MSTR/USD", - "MTA-PERP": "MTA-PERP", - "MTAUSD": "MTA/USD", - "MTAUSDT": "MTA/USDT", - "MTL-PERP": "MTL-PERP", - "MTLUSD": "MTL/USD", - "MVDA10-PERP": "MVDA10-PERP", - "MVDA25-PERP": "MVDA25-PERP", - "NEAR-PERP": "NEAR-PERP", - "NEO-PERP": "NEO-PERP", - "NEXOUSD": "NEXO/USD", - "NFLX-0325": "NFLX-0325", - "NFLXUSD": "NFLX/USD", - "NIO-0325": "NIO-0325", - "NIOUSD": "NIO/USD", - "NOK-0325": "NOK-0325", - "NOKUSD": "NOK/USD", - "NVDA-0325": "NVDA-0325", - "NVDAUSD": "NVDA/USD", - "OKB-0325": "OKB-0325", - "OKB-PERP": "OKB-PERP", - "OKBBEARUSD": "OKBBEAR/USD", - "OKBBULLUSD": "OKBBULL/USD", - "OKBHALFUSD": "OKBHALF/USD", - "OKBHEDGEUSD": "OKBHEDGE/USD", - "OKBUSD": "OKB/USD", - "OMG-0325": "OMG-0325", - "OMG-PERP": "OMG-PERP", - "OMGUSD": "OMG/USD", - "ONE-PERP": "ONE-PERP", - "ONT-PERP": "ONT-PERP", - "ORBS-PERP": "ORBS-PERP", - "ORBSUSD": "ORBS/USD", - "OXY-PERP": "OXY-PERP", - "OXYUSD": "OXY/USD", - "OXYUSDT": "OXY/USDT", - "PAXG-PERP": "PAXG-PERP", - "PAXGBEARUSD": "PAXGBEAR/USD", - "PAXGBULLUSD": "PAXGBULL/USD", - "PAXGHALFUSD": "PAXGHALF/USD", - "PAXGHEDGEUSD": "PAXGHEDGE/USD", - "PAXGUSD": "PAXG/USD", - "PAXGUSDT": "PAXG/USDT", - "PENN-0325": "PENN-0325", - "PENNUSD": "PENN/USD", - "PEOPLE-PERP": "PEOPLE-PERP", - "PEOPLEUSD": "PEOPLE/USD", - "PERP-PERP": "PERP-PERP", - "PERPUSD": "PERP/USD", - "PFE-0325": "PFE-0325", - "PFEUSD": "PFE/USD", - "POLIS-PERP": "POLIS-PERP", - "POLISUSD": "POLIS/USD", - "PORTUSD": "PORT/USD", - "PRISMUSD": "PRISM/USD", - "PRIV-0325": "PRIV-0325", - "PRIV-PERP": "PRIV-PERP", - "PRIVBEARUSD": "PRIVBEAR/USD", - "PRIVBULLUSD": "PRIVBULL/USD", - "PRIVHALFUSD": "PRIVHALF/USD", - "PRIVHEDGEUSD": "PRIVHEDGE/USD", - "PROM-PERP": "PROM-PERP", - "PROMUSD": "PROM/USD", - "PSGUSD": "PSG/USD", - "PSYUSD": "PSY/USD", - "PTUUSD": "PTU/USD", - "PUNDIX-PERP": "PUNDIX-PERP", - "PUNDIXUSD": "PUNDIX/USD", - "PYPL-0325": "PYPL-0325", - "PYPLUSD": "PYPL/USD", - "QIUSD": "QI/USD", - "QTUM-PERP": "QTUM-PERP", - "RAMP-PERP": "RAMP-PERP", - "RAMPUSD": "RAMP/USD", - "RAY-PERP": "RAY-PERP", - "RAYUSD": "RAY/USD", - "REALUSD": "REAL/USD", - "REEF-0325": "REEF-0325", - "REEF-PERP": "REEF-PERP", - "REEFUSD": "REEF/USD", - "REN-PERP": "REN-PERP", - "RENUSD": "REN/USD", - "RNDR-PERP": "RNDR-PERP", - "RNDRUSD": "RNDR/USD", - "RON-PERP": "RON-PERP", - "ROOK-PERP": "ROOK-PERP", - "ROOKUSD": "ROOK/USD", - "ROOKUSDT": "ROOK/USDT", - "ROSE-PERP": "ROSE-PERP", - "RSR-PERP": "RSR-PERP", - "RSRUSD": "RSR/USD", - "RUNE-PERP": "RUNE-PERP", - "RUNEUSD": "RUNE/USD", - "RUNEUSDT": "RUNE/USDT", - "SAND-PERP": "SAND-PERP", - "SANDUSD": "SAND/USD", - "SC-PERP": "SC-PERP", - "SCRT-PERP": "SCRT-PERP", - "SECO-PERP": "SECO-PERP", - "SECOUSD": "SECO/USD", - "SHIB-PERP": "SHIB-PERP", - "SHIBUSD": "SHIB/USD", - "SHIT-0325": "SHIT-0325", - "SHIT-PERP": "SHIT-PERP", - "SKL-PERP": "SKL-PERP", - "SKLUSD": "SKL/USD", - "SLNDUSD": "SLND/USD", - "SLP-PERP": "SLP-PERP", - "SLPUSD": "SLP/USD", - "SLRSUSD": "SLRS/USD", - "SLV-0325": "SLV-0325", - "SLVUSD": "SLV/USD", - "SNX-PERP": "SNX-PERP", - "SNXUSD": "SNX/USD", - "SNYUSD": "SNY/USD", - "SOL-0325": "SOL-0325", - "SOL-PERP": "SOL-PERP", - "SOLBTC": "SOL/BTC", - "SOLUSD": "SOL/USD", - "SOLUSDT": "SOL/USDT", - "SOS-PERP": "SOS-PERP", - "SOSUSD": "SOS/USD", - "SPELL-PERP": "SPELL-PERP", - "SPELLUSD": "SPELL/USD", - "SPY-0325": "SPY-0325", - "SPYUSD": "SPY/USD", - "SQ-0325": "SQ-0325", - "SQUSD": "SQ/USD", - "SRM-PERP": "SRM-PERP", - "SRMUSD": "SRM/USD", - "SRMUSDT": "SRM/USDT", - "SRN-PERP": "SRN-PERP", - "STARSUSD": "STARS/USD", - "STEP-PERP": "STEP-PERP", - "STEPUSD": "STEP/USD", - "STETHUSD": "STETH/USD", - "STMX-PERP": "STMX-PERP", - "STMXUSD": "STMX/USD", - "STORJ-PERP": "STORJ-PERP", - "STORJUSD": "STORJ/USD", - "STSOLUSD": "STSOL/USD", - "STX-PERP": "STX-PERP", - "SUNUSD": "SUN/USD", - "SUSHI-0325": "SUSHI-0325", - "SUSHI-PERP": "SUSHI-PERP", - "SUSHIBEARUSD": "SUSHIBEAR/USD", - "SUSHIBTC": "SUSHI/BTC", - "SUSHIBULLUSD": "SUSHIBULL/USD", - "SUSHIUSD": "SUSHI/USD", - "SUSHIUSDT": "SUSHI/USDT", - "SXP-0325": "SXP-0325", - "SXP-PERP": "SXP-PERP", - "SXPBEARUSD": "SXPBEAR/USD", - "SXPBTC": "SXP/BTC", - "SXPBULLUSD": "SXPBULL/USD", - "SXPHALFUSD": "SXPHALF/USD", - "SXPHALFUSDT": "SXPHALF/USDT", - "SXPHEDGEUSD": "SXPHEDGE/USD", - "SXPUSD": "SXP/USD", - "SXPUSDT": "SXP/USDT", - "THETA-0325": "THETA-0325", - "THETA-PERP": "THETA-PERP", - "THETABEARUSD": "THETABEAR/USD", - "THETABULLUSD": "THETABULL/USD", - "THETAHALFUSD": "THETAHALF/USD", - "THETAHEDGEUSD": "THETAHEDGE/USD", - "TLM-PERP": "TLM-PERP", - "TLMUSD": "TLM/USD", - "TLRY-0325": "TLRY-0325", - "TLRYUSD": "TLRY/USD", - "TOMO-PERP": "TOMO-PERP", - "TOMOBEAR2021USD": "TOMOBEAR2021/USD", - "TOMOBULLUSD": "TOMOBULL/USD", - "TOMOHALFUSD": "TOMOHALF/USD", - "TOMOHEDGEUSD": "TOMOHEDGE/USD", - "TOMOUSD": "TOMO/USD", - "TOMOUSDT": "TOMO/USDT", - "TONCOIN-PERP": "TONCOIN-PERP", - "TONCOINUSD": "TONCOIN/USD", - "TRU-PERP": "TRU-PERP", - "TRUMP2024": "TRUMP2024", - "TRUUSD": "TRU/USD", - "TRUUSDT": "TRU/USDT", - "TRX-0325": "TRX-0325", - "TRX-PERP": "TRX-PERP", - "TRXBEARUSD": "TRXBEAR/USD", - "TRXBTC": "TRX/BTC", - "TRXBULLUSD": "TRXBULL/USD", - "TRXHALFUSD": "TRXHALF/USD", - "TRXHEDGEUSD": "TRXHEDGE/USD", - "TRXUSD": "TRX/USD", - "TRXUSDT": "TRX/USDT", - "TRYB-PERP": "TRYB-PERP", - "TRYBBEARUSD": "TRYBBEAR/USD", - "TRYBBULLUSD": "TRYBBULL/USD", - "TRYBHALFUSD": "TRYBHALF/USD", - "TRYBHEDGEUSD": "TRYBHEDGE/USD", - "TRYBUSD": "TRYB/USD", - "TSLA-0325": "TSLA-0325", - "TSLABTC": "TSLA/BTC", - "TSLADOGE": "TSLA/DOGE", - "TSLAUSD": "TSLA/USD", - "TSM-0325": "TSM-0325", - "TSMUSD": "TSM/USD", - "TULIP-PERP": "TULIP-PERP", - "TULIPUSD": "TULIP/USD", - "TWTR-0325": "TWTR-0325", - "TWTRUSD": "TWTR/USD", - "UBER-0325": "UBER-0325", - "UBERUSD": "UBER/USD", - "UBXTUSD": "UBXT/USD", - "UBXTUSDT": "UBXT/USDT", - "UMEEUSD": "UMEE/USD", - "UNI-0325": "UNI-0325", - "UNI-PERP": "UNI-PERP", - "UNIBTC": "UNI/BTC", - "UNISWAP-0325": "UNISWAP-0325", - "UNISWAP-PERP": "UNISWAP-PERP", - "UNISWAPBEARUSD": "UNISWAPBEAR/USD", - "UNISWAPBULLUSD": "UNISWAPBULL/USD", - "UNIUSD": "UNI/USD", - "UNIUSDT": "UNI/USDT", - "USDT-0325": "USDT-0325", - "USDT-PERP": "USDT-PERP", - "USDTBEARUSD": "USDTBEAR/USD", - "USDTBULLUSD": "USDTBULL/USD", - "USDTHALFUSD": "USDTHALF/USD", - "USDTHEDGEUSD": "USDTHEDGE/USD", - "USDTUSD": "USDT/USD", - "USO-0325": "USO-0325", - "USOUSD": "USO/USD", - "UST-PERP": "UST-PERP", - "USTUSD": "UST/USD", - "USTUSDT": "UST/USDT", - "VET-PERP": "VET-PERP", - "VETBEARUSD": "VETBEAR/USD", - "VETBEARUSDT": "VETBEAR/USDT", - "VETBULLUSD": "VETBULL/USD", - "VETBULLUSDT": "VETBULL/USDT", - "VETHEDGEUSD": "VETHEDGE/USD", - "VGXUSD": "VGX/USD", - "WAVES-0325": "WAVES-0325", - "WAVES-PERP": "WAVES-PERP", - "WAVESUSD": "WAVES/USD", - "WBTCBTC": "WBTC/BTC", - "WBTCUSD": "WBTC/USD", - "WNDRUSD": "WNDR/USD", - "WRXUSD": "WRX/USD", - "WRXUSDT": "WRX/USDT", - "WSB-0325": "WSB-0325", - "XAUT-0325": "XAUT-0325", - "XAUT-PERP": "XAUT-PERP", - "XAUTBEARUSD": "XAUTBEAR/USD", - "XAUTBULLUSD": "XAUTBULL/USD", - "XAUTHALFUSD": "XAUTHALF/USD", - "XAUTHEDGEUSD": "XAUTHEDGE/USD", - "XAUTUSD": "XAUT/USD", - "XAUTUSDT": "XAUT/USDT", - "XEM-PERP": "XEM-PERP", - "XLM-PERP": "XLM-PERP", - "XLMBEARUSD": "XLMBEAR/USD", - "XLMBULLUSD": "XLMBULL/USD", - "XMR-PERP": "XMR-PERP", - "XRP-0325": "XRP-0325", - "XRP-PERP": "XRP-PERP", - "XRPBEARUSD": "XRPBEAR/USD", - "XRPBEARUSDT": "XRPBEAR/USDT", - "XRPBTC": "XRP/BTC", - "XRPBULLUSD": "XRPBULL/USD", - "XRPBULLUSDT": "XRPBULL/USDT", - "XRPHALFUSD": "XRPHALF/USD", - "XRPHEDGEUSD": "XRPHEDGE/USD", - "XRPUSD": "XRP/USD", - "XRPUSDT": "XRP/USDT", - "XTZ-0325": "XTZ-0325", - "XTZ-PERP": "XTZ-PERP", - "XTZBEARUSD": "XTZBEAR/USD", - "XTZBEARUSDT": "XTZBEAR/USDT", - "XTZBULLUSD": "XTZBULL/USD", - "XTZBULLUSDT": "XTZBULL/USDT", - "XTZHALFUSD": "XTZHALF/USD", - "XTZHEDGEUSD": "XTZHEDGE/USD", - "YFI-0325": "YFI-0325", - "YFI-PERP": "YFI-PERP", - "YFIBTC": "YFI/BTC", - "YFII-PERP": "YFII-PERP", - "YFIIUSD": "YFII/USD", - "YFIUSD": "YFI/USD", - "YFIUSDT": "YFI/USDT", - "YGGUSD": "YGG/USD", - "ZEC-PERP": "ZEC-PERP", - "ZECBEARUSD": "ZECBEAR/USD", - "ZECBULLUSD": "ZECBULL/USD", - "ZIL-PERP": "ZIL-PERP", - "ZM-0325": "ZM-0325", - "ZMUSD": "ZM/USD", - "ZRX-PERP": "ZRX-PERP", - "ZRXUSD": "ZRX/USD", - "GMTUSD": "GMT/USD", - "GMT-PERP": "GMT-PERP", + "MATICBTC": "MATIC/BTC", + "MATICBULLUSD": "MATICBULL/USD", + "MATICHALFUSD": "MATICHALF/USD", + "MATICHEDGEUSD": "MATICHEDGE/USD", + "MATICUSD": "MATIC/USD", + "MBSUSD": "MBS/USD", + "MCB-PERP": "MCB-PERP", + "MCBUSD": "MCB/USD", + "MEDIA-PERP": "MEDIA-PERP", + "MEDIAUSD": "MEDIA/USD", + "MER-PERP": "MER-PERP", + "MERUSD": "MER/USD", + "MID-0325": "MID-0325", + "MID-PERP": "MID-PERP", + "MIDBEARUSD": "MIDBEAR/USD", + "MIDBULLUSD": "MIDBULL/USD", + "MIDHALFUSD": "MIDHALF/USD", + "MIDHEDGEUSD": "MIDHEDGE/USD", + "MINA-PERP": "MINA-PERP", + "MKR-PERP": "MKR-PERP", + "MKRBEARUSD": "MKRBEAR/USD", + "MKRBULLUSD": "MKRBULL/USD", + "MKRUSD": "MKR/USD", + "MKRUSDT": "MKR/USDT", + "MNGO-PERP": "MNGO-PERP", + "MNGOUSD": "MNGO/USD", + "MOBUSD": "MOB/USD", + "MOBUSDT": "MOB/USDT", + "MRNA-0325": "MRNA-0325", + "MRNAUSD": "MRNA/USD", + "MSOLUSD": "MSOL/USD", + "MSTR-0325": "MSTR-0325", + "MSTRUSD": "MSTR/USD", + "MTA-PERP": "MTA-PERP", + "MTAUSD": "MTA/USD", + "MTAUSDT": "MTA/USDT", + "MTL-PERP": "MTL-PERP", + "MTLUSD": "MTL/USD", + "MVDA10-PERP": "MVDA10-PERP", + "MVDA25-PERP": "MVDA25-PERP", + "NEAR-PERP": "NEAR-PERP", + "NEO-PERP": "NEO-PERP", + "NEXOUSD": "NEXO/USD", + "NFLX-0325": "NFLX-0325", + "NFLXUSD": "NFLX/USD", + "NIO-0325": "NIO-0325", + "NIOUSD": "NIO/USD", + "NOK-0325": "NOK-0325", + "NOKUSD": "NOK/USD", + "NVDA-0325": "NVDA-0325", + "NVDAUSD": "NVDA/USD", + "OKB-0325": "OKB-0325", + "OKB-PERP": "OKB-PERP", + "OKBBEARUSD": "OKBBEAR/USD", + "OKBBULLUSD": "OKBBULL/USD", + "OKBHALFUSD": "OKBHALF/USD", + "OKBHEDGEUSD": "OKBHEDGE/USD", + "OKBUSD": "OKB/USD", + "OMG-0325": "OMG-0325", + "OMG-PERP": "OMG-PERP", + "OMGUSD": "OMG/USD", + "ONE-PERP": "ONE-PERP", + "ONT-PERP": "ONT-PERP", + "ORBS-PERP": "ORBS-PERP", + "ORBSUSD": "ORBS/USD", + "OXY-PERP": "OXY-PERP", + "OXYUSD": "OXY/USD", + "OXYUSDT": "OXY/USDT", + "PAXG-PERP": "PAXG-PERP", + "PAXGBEARUSD": "PAXGBEAR/USD", + "PAXGBULLUSD": "PAXGBULL/USD", + "PAXGHALFUSD": "PAXGHALF/USD", + "PAXGHEDGEUSD": "PAXGHEDGE/USD", + "PAXGUSD": "PAXG/USD", + "PAXGUSDT": "PAXG/USDT", + "PENN-0325": "PENN-0325", + "PENNUSD": "PENN/USD", + "PEOPLE-PERP": "PEOPLE-PERP", + "PEOPLEUSD": "PEOPLE/USD", + "PERP-PERP": "PERP-PERP", + "PERPUSD": "PERP/USD", + "PFE-0325": "PFE-0325", + "PFEUSD": "PFE/USD", + "POLIS-PERP": "POLIS-PERP", + "POLISUSD": "POLIS/USD", + "PORTUSD": "PORT/USD", + "PRISMUSD": "PRISM/USD", + "PRIV-0325": "PRIV-0325", + "PRIV-PERP": "PRIV-PERP", + "PRIVBEARUSD": "PRIVBEAR/USD", + "PRIVBULLUSD": "PRIVBULL/USD", + "PRIVHALFUSD": "PRIVHALF/USD", + "PRIVHEDGEUSD": "PRIVHEDGE/USD", + "PROM-PERP": "PROM-PERP", + "PROMUSD": "PROM/USD", + "PSGUSD": "PSG/USD", + "PSYUSD": "PSY/USD", + "PTUUSD": "PTU/USD", + "PUNDIX-PERP": "PUNDIX-PERP", + "PUNDIXUSD": "PUNDIX/USD", + "PYPL-0325": "PYPL-0325", + "PYPLUSD": "PYPL/USD", + "QIUSD": "QI/USD", + "QTUM-PERP": "QTUM-PERP", + "RAMP-PERP": "RAMP-PERP", + "RAMPUSD": "RAMP/USD", + "RAY-PERP": "RAY-PERP", + "RAYUSD": "RAY/USD", + "REALUSD": "REAL/USD", + "REEF-0325": "REEF-0325", + "REEF-PERP": "REEF-PERP", + "REEFUSD": "REEF/USD", + "REN-PERP": "REN-PERP", + "RENUSD": "REN/USD", + "RNDR-PERP": "RNDR-PERP", + "RNDRUSD": "RNDR/USD", + "RON-PERP": "RON-PERP", + "ROOK-PERP": "ROOK-PERP", + "ROOKUSD": "ROOK/USD", + "ROOKUSDT": "ROOK/USDT", + "ROSE-PERP": "ROSE-PERP", + "RSR-PERP": "RSR-PERP", + "RSRUSD": "RSR/USD", + "RUNE-PERP": "RUNE-PERP", + "RUNEUSD": "RUNE/USD", + "RUNEUSDT": "RUNE/USDT", + "SAND-PERP": "SAND-PERP", + "SANDUSD": "SAND/USD", + "SC-PERP": "SC-PERP", + "SCRT-PERP": "SCRT-PERP", + "SECO-PERP": "SECO-PERP", + "SECOUSD": "SECO/USD", + "SHIB-PERP": "SHIB-PERP", + "SHIBUSD": "SHIB/USD", + "SHIT-0325": "SHIT-0325", + "SHIT-PERP": "SHIT-PERP", + "SKL-PERP": "SKL-PERP", + "SKLUSD": "SKL/USD", + "SLNDUSD": "SLND/USD", + "SLP-PERP": "SLP-PERP", + "SLPUSD": "SLP/USD", + "SLRSUSD": "SLRS/USD", + "SLV-0325": "SLV-0325", + "SLVUSD": "SLV/USD", + "SNX-PERP": "SNX-PERP", + "SNXUSD": "SNX/USD", + "SNYUSD": "SNY/USD", + "SOL-0325": "SOL-0325", + "SOL-PERP": "SOL-PERP", + "SOLBTC": "SOL/BTC", + "SOLUSD": "SOL/USD", + "SOLUSDT": "SOL/USDT", + "SOS-PERP": "SOS-PERP", + "SOSUSD": "SOS/USD", + "SPELL-PERP": "SPELL-PERP", + "SPELLUSD": "SPELL/USD", + "SPY-0325": "SPY-0325", + "SPYUSD": "SPY/USD", + "SQ-0325": "SQ-0325", + "SQUSD": "SQ/USD", + "SRM-PERP": "SRM-PERP", + "SRMUSD": "SRM/USD", + "SRMUSDT": "SRM/USDT", + "SRN-PERP": "SRN-PERP", + "STARSUSD": "STARS/USD", + "STEP-PERP": "STEP-PERP", + "STEPUSD": "STEP/USD", + "STETHUSD": "STETH/USD", + "STMX-PERP": "STMX-PERP", + "STMXUSD": "STMX/USD", + "STORJ-PERP": "STORJ-PERP", + "STORJUSD": "STORJ/USD", + "STSOLUSD": "STSOL/USD", + "STX-PERP": "STX-PERP", + "SUNUSD": "SUN/USD", + "SUSHI-0325": "SUSHI-0325", + "SUSHI-PERP": "SUSHI-PERP", + "SUSHIBEARUSD": "SUSHIBEAR/USD", + "SUSHIBTC": "SUSHI/BTC", + "SUSHIBULLUSD": "SUSHIBULL/USD", + "SUSHIUSD": "SUSHI/USD", + "SUSHIUSDT": "SUSHI/USDT", + "SXP-0325": "SXP-0325", + "SXP-PERP": "SXP-PERP", + "SXPBEARUSD": "SXPBEAR/USD", + "SXPBTC": "SXP/BTC", + "SXPBULLUSD": "SXPBULL/USD", + "SXPHALFUSD": "SXPHALF/USD", + "SXPHALFUSDT": "SXPHALF/USDT", + "SXPHEDGEUSD": "SXPHEDGE/USD", + "SXPUSD": "SXP/USD", + "SXPUSDT": "SXP/USDT", + "THETA-0325": "THETA-0325", + "THETA-PERP": "THETA-PERP", + "THETABEARUSD": "THETABEAR/USD", + "THETABULLUSD": "THETABULL/USD", + "THETAHALFUSD": "THETAHALF/USD", + "THETAHEDGEUSD": "THETAHEDGE/USD", + "TLM-PERP": "TLM-PERP", + "TLMUSD": "TLM/USD", + "TLRY-0325": "TLRY-0325", + "TLRYUSD": "TLRY/USD", + "TOMO-PERP": "TOMO-PERP", + "TOMOBEAR2021USD": "TOMOBEAR2021/USD", + "TOMOBULLUSD": "TOMOBULL/USD", + "TOMOHALFUSD": "TOMOHALF/USD", + "TOMOHEDGEUSD": "TOMOHEDGE/USD", + "TOMOUSD": "TOMO/USD", + "TOMOUSDT": "TOMO/USDT", + "TONCOIN-PERP": "TONCOIN-PERP", + "TONCOINUSD": "TONCOIN/USD", + "TRU-PERP": "TRU-PERP", + "TRUMP2024": "TRUMP2024", + "TRUUSD": "TRU/USD", + "TRUUSDT": "TRU/USDT", + "TRX-0325": "TRX-0325", + "TRX-PERP": "TRX-PERP", + "TRXBEARUSD": "TRXBEAR/USD", + "TRXBTC": "TRX/BTC", + "TRXBULLUSD": "TRXBULL/USD", + "TRXHALFUSD": "TRXHALF/USD", + "TRXHEDGEUSD": "TRXHEDGE/USD", + "TRXUSD": "TRX/USD", + "TRXUSDT": "TRX/USDT", + "TRYB-PERP": "TRYB-PERP", + "TRYBBEARUSD": "TRYBBEAR/USD", + "TRYBBULLUSD": "TRYBBULL/USD", + "TRYBHALFUSD": "TRYBHALF/USD", + "TRYBHEDGEUSD": "TRYBHEDGE/USD", + "TRYBUSD": "TRYB/USD", + "TSLA-0325": "TSLA-0325", + "TSLABTC": "TSLA/BTC", + "TSLADOGE": "TSLA/DOGE", + "TSLAUSD": "TSLA/USD", + "TSM-0325": "TSM-0325", + "TSMUSD": "TSM/USD", + "TULIP-PERP": "TULIP-PERP", + "TULIPUSD": "TULIP/USD", + "TWTR-0325": "TWTR-0325", + "TWTRUSD": "TWTR/USD", + "UBER-0325": "UBER-0325", + "UBERUSD": "UBER/USD", + "UBXTUSD": "UBXT/USD", + "UBXTUSDT": "UBXT/USDT", + "UMEEUSD": "UMEE/USD", + "UNI-0325": "UNI-0325", + "UNI-PERP": "UNI-PERP", + "UNIBTC": "UNI/BTC", + "UNISWAP-0325": "UNISWAP-0325", + "UNISWAP-PERP": "UNISWAP-PERP", + "UNISWAPBEARUSD": "UNISWAPBEAR/USD", + "UNISWAPBULLUSD": "UNISWAPBULL/USD", + "UNIUSD": "UNI/USD", + "UNIUSDT": "UNI/USDT", + "USDT-0325": "USDT-0325", + "USDT-PERP": "USDT-PERP", + "USDTBEARUSD": "USDTBEAR/USD", + "USDTBULLUSD": "USDTBULL/USD", + "USDTHALFUSD": "USDTHALF/USD", + "USDTHEDGEUSD": "USDTHEDGE/USD", + "USDTUSD": "USDT/USD", + "USO-0325": "USO-0325", + "USOUSD": "USO/USD", + "UST-PERP": "UST-PERP", + "USTUSD": "UST/USD", + "USTUSDT": "UST/USDT", + "VET-PERP": "VET-PERP", + "VETBEARUSD": "VETBEAR/USD", + "VETBEARUSDT": "VETBEAR/USDT", + "VETBULLUSD": "VETBULL/USD", + "VETBULLUSDT": "VETBULL/USDT", + "VETHEDGEUSD": "VETHEDGE/USD", + "VGXUSD": "VGX/USD", + "WAVES-0325": "WAVES-0325", + "WAVES-PERP": "WAVES-PERP", + "WAVESUSD": "WAVES/USD", + "WBTCBTC": "WBTC/BTC", + "WBTCUSD": "WBTC/USD", + "WNDRUSD": "WNDR/USD", + "WRXUSD": "WRX/USD", + "WRXUSDT": "WRX/USDT", + "WSB-0325": "WSB-0325", + "XAUT-0325": "XAUT-0325", + "XAUT-PERP": "XAUT-PERP", + "XAUTBEARUSD": "XAUTBEAR/USD", + "XAUTBULLUSD": "XAUTBULL/USD", + "XAUTHALFUSD": "XAUTHALF/USD", + "XAUTHEDGEUSD": "XAUTHEDGE/USD", + "XAUTUSD": "XAUT/USD", + "XAUTUSDT": "XAUT/USDT", + "XEM-PERP": "XEM-PERP", + "XLM-PERP": "XLM-PERP", + "XLMBEARUSD": "XLMBEAR/USD", + "XLMBULLUSD": "XLMBULL/USD", + "XMR-PERP": "XMR-PERP", + "XRP-0325": "XRP-0325", + "XRP-PERP": "XRP-PERP", + "XRPBEARUSD": "XRPBEAR/USD", + "XRPBEARUSDT": "XRPBEAR/USDT", + "XRPBTC": "XRP/BTC", + "XRPBULLUSD": "XRPBULL/USD", + "XRPBULLUSDT": "XRPBULL/USDT", + "XRPHALFUSD": "XRPHALF/USD", + "XRPHEDGEUSD": "XRPHEDGE/USD", + "XRPUSD": "XRP/USD", + "XRPUSDT": "XRP/USDT", + "XTZ-0325": "XTZ-0325", + "XTZ-PERP": "XTZ-PERP", + "XTZBEARUSD": "XTZBEAR/USD", + "XTZBEARUSDT": "XTZBEAR/USDT", + "XTZBULLUSD": "XTZBULL/USD", + "XTZBULLUSDT": "XTZBULL/USDT", + "XTZHALFUSD": "XTZHALF/USD", + "XTZHEDGEUSD": "XTZHEDGE/USD", + "YFI-0325": "YFI-0325", + "YFI-PERP": "YFI-PERP", + "YFIBTC": "YFI/BTC", + "YFII-PERP": "YFII-PERP", + "YFIIUSD": "YFII/USD", + "YFIUSD": "YFI/USD", + "YFIUSDT": "YFI/USDT", + "YGGUSD": "YGG/USD", + "ZEC-PERP": "ZEC-PERP", + "ZECBEARUSD": "ZECBEAR/USD", + "ZECBULLUSD": "ZECBULL/USD", + "ZIL-PERP": "ZIL-PERP", + "ZM-0325": "ZM-0325", + "ZMUSD": "ZM/USD", + "ZRX-PERP": "ZRX-PERP", + "ZRXUSD": "ZRX/USD", + "GMTUSD": "GMT/USD", + "GMT-PERP": "GMT-PERP", } diff --git a/pkg/exchange/ftx/websocket_messages_test.go b/pkg/exchange/ftx/websocket_messages_test.go index bbe0f6e6fb..5a9635e5a1 100644 --- a/pkg/exchange/ftx/websocket_messages_test.go +++ b/pkg/exchange/ftx/websocket_messages_test.go @@ -182,6 +182,7 @@ func Test_insertAt(t *testing.T) { func Test_newLoginRequest(t *testing.T) { // From API doc: https://docs.ftx.com/?javascript#authentication-2 r := newLoginRequest("", "Y2QTHI23f23f23jfjas23f23To0RfUwX3H42fvN-", time.Unix(0, 1557246346499*int64(time.Millisecond)), "") + // pragma: allowlist nextline secret expectedSignature := "d10b5a67a1a941ae9463a60b285ae845cdeac1b11edc7da9977bef0228b96de9" assert.Equal(t, expectedSignature, r.Login.Signature) jsonStr, err := json.Marshal(r) diff --git a/pkg/exchange/kucoin/convert.go b/pkg/exchange/kucoin/convert.go index 450c5b9fd7..d86f84db95 100644 --- a/pkg/exchange/kucoin/convert.go +++ b/pkg/exchange/kucoin/convert.go @@ -245,4 +245,3 @@ func toGlobalTrade(fill kucoinapi.Fill) types.Trade { } return trade } - diff --git a/pkg/exchange/kucoin/exchange.go b/pkg/exchange/kucoin/exchange.go index 0310affab0..47da3da98d 100644 --- a/pkg/exchange/kucoin/exchange.go +++ b/pkg/exchange/kucoin/exchange.go @@ -17,9 +17,9 @@ import ( "github.com/c9s/bbgo/pkg/types" ) -var marketDataLimiter = rate.NewLimiter(rate.Every(1*time.Second), 1) -var queryTradeLimiter = rate.NewLimiter(rate.Every(5*time.Second), 1) -var queryOrderLimiter = rate.NewLimiter(rate.Every(5*time.Second), 1) +var marketDataLimiter = rate.NewLimiter(rate.Every(6*time.Second), 1) +var queryTradeLimiter = rate.NewLimiter(rate.Every(6*time.Second), 1) +var queryOrderLimiter = rate.NewLimiter(rate.Every(6*time.Second), 1) var ErrMissingSequence = errors.New("sequence is missing") @@ -44,7 +44,8 @@ func New(key, secret, passphrase string) *Exchange { } return &Exchange{ - key: key, + key: key, + // pragma: allowlist nextline secret secret: secret, passphrase: passphrase, client: client, @@ -138,10 +139,10 @@ func (e *Exchange) QueryTickers(ctx context.Context, symbols ...string) (map[str // From the doc // Type of candlestick patterns: 1min, 3min, 5min, 15min, 30min, 1hour, 2hour, 4hour, 6hour, 8hour, 12hour, 1day, 1week var supportedIntervals = map[types.Interval]int{ - types.Interval1m: 60, - types.Interval5m: 60 * 5, - types.Interval15m: 60 * 15, - types.Interval30m: 60 * 30, + types.Interval1m: 1 * 60, + types.Interval5m: 5 * 60, + types.Interval15m: 15 * 60, + types.Interval30m: 30 * 60, types.Interval1h: 60 * 60, types.Interval2h: 60 * 60 * 2, types.Interval4h: 60 * 60 * 4, @@ -160,7 +161,9 @@ func (e *Exchange) IsSupportedInterval(interval types.Interval) bool { } func (e *Exchange) QueryKLines(ctx context.Context, symbol string, interval types.Interval, options types.KLineQueryOptions) ([]types.KLine, error) { - _ = marketDataLimiter.Wait(ctx) + if err := marketDataLimiter.Wait(ctx); err != nil { + return nil, err + } req := e.client.MarketDataService.NewGetKLinesRequest() req.Symbol(toLocalSymbol(symbol)) @@ -204,78 +207,74 @@ func (e *Exchange) QueryKLines(ctx context.Context, symbol string, interval type return klines, nil } -func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) (createdOrders types.OrderSlice, err error) { - for _, order := range orders { - req := e.client.TradeService.NewPlaceOrderRequest() - req.Symbol(toLocalSymbol(order.Symbol)) - req.Side(toLocalSide(order.Side)) +func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (createdOrder *types.Order, err error) { + req := e.client.TradeService.NewPlaceOrderRequest() + req.Symbol(toLocalSymbol(order.Symbol)) + req.Side(toLocalSide(order.Side)) - if order.ClientOrderID != "" { - req.ClientOrderID(order.ClientOrderID) - } + if order.ClientOrderID != "" { + req.ClientOrderID(order.ClientOrderID) + } + + if order.Market.Symbol != "" { + req.Size(order.Market.FormatQuantity(order.Quantity)) + } else { + // TODO: report error? + req.Size(order.Quantity.FormatString(8)) + } + // set price field for limit orders + switch order.Type { + case types.OrderTypeStopLimit, types.OrderTypeLimit, types.OrderTypeLimitMaker: if order.Market.Symbol != "" { - req.Size(order.Market.FormatQuantity(order.Quantity)) + req.Price(order.Market.FormatPrice(order.Price)) } else { // TODO: report error? - req.Size(order.Quantity.FormatString(8)) - } - - // set price field for limit orders - switch order.Type { - case types.OrderTypeStopLimit, types.OrderTypeLimit, types.OrderTypeLimitMaker: - if order.Market.Symbol != "" { - req.Price(order.Market.FormatPrice(order.Price)) - } else { - // TODO: report error? - req.Price(order.Price.FormatString(8)) - } - } - - if order.Type == types.OrderTypeLimitMaker { - req.PostOnly(true) + req.Price(order.Price.FormatString(8)) } + } - switch order.TimeInForce { - case "FOK": - req.TimeInForce(kucoinapi.TimeInForceFOK) - case "IOC": - req.TimeInForce(kucoinapi.TimeInForceIOC) - default: - // default to GTC - req.TimeInForce(kucoinapi.TimeInForceGTC) - } + if order.Type == types.OrderTypeLimitMaker { + req.PostOnly(true) + } - switch order.Type { - case types.OrderTypeStopLimit: - req.OrderType(kucoinapi.OrderTypeStopLimit) + switch order.TimeInForce { + case "FOK": + req.TimeInForce(kucoinapi.TimeInForceFOK) + case "IOC": + req.TimeInForce(kucoinapi.TimeInForceIOC) + default: + // default to GTC + req.TimeInForce(kucoinapi.TimeInForceGTC) + } - case types.OrderTypeLimit, types.OrderTypeLimitMaker: - req.OrderType(kucoinapi.OrderTypeLimit) + switch order.Type { + case types.OrderTypeStopLimit: + req.OrderType(kucoinapi.OrderTypeStopLimit) - case types.OrderTypeMarket: - req.OrderType(kucoinapi.OrderTypeMarket) - } + case types.OrderTypeLimit, types.OrderTypeLimitMaker: + req.OrderType(kucoinapi.OrderTypeLimit) - orderResponse, err := req.Do(ctx) - if err != nil { - return createdOrders, err - } + case types.OrderTypeMarket: + req.OrderType(kucoinapi.OrderTypeMarket) + } - createdOrders = append(createdOrders, types.Order{ - SubmitOrder: order, - Exchange: types.ExchangeKucoin, - OrderID: hashStringID(orderResponse.OrderID), - UUID: orderResponse.OrderID, - Status: types.OrderStatusNew, - ExecutedQuantity: fixedpoint.Zero, - IsWorking: true, - CreationTime: types.Time(time.Now()), - UpdateTime: types.Time(time.Now()), - }) + orderResponse, err := req.Do(ctx) + if err != nil { + return createdOrder, err } - return createdOrders, err + return &types.Order{ + SubmitOrder: order, + Exchange: types.ExchangeKucoin, + OrderID: hashStringID(orderResponse.OrderID), + UUID: orderResponse.OrderID, + Status: types.OrderStatusNew, + ExecutedQuantity: fixedpoint.Zero, + IsWorking: true, + CreationTime: types.Time(time.Now()), + UpdateTime: types.Time(time.Now()), + }, nil } // QueryOpenOrders diff --git a/pkg/exchange/kucoin/generate_symbol_map.go b/pkg/exchange/kucoin/generate_symbol_map.go index b67806641c..249a5ec17b 100644 --- a/pkg/exchange/kucoin/generate_symbol_map.go +++ b/pkg/exchange/kucoin/generate_symbol_map.go @@ -53,7 +53,7 @@ func main() { defer resp.Body.Close() r := &ApiResponse{} - if err := json.NewDecoder(resp.Body).Decode(r) ; err != nil { + if err := json.NewDecoder(resp.Body).Decode(r); err != nil { log.Fatal(err) } diff --git a/pkg/exchange/kucoin/kucoinapi/bullet.go b/pkg/exchange/kucoin/kucoinapi/bullet.go index 213ca8c950..0226fad9bc 100644 --- a/pkg/exchange/kucoin/kucoinapi/bullet.go +++ b/pkg/exchange/kucoin/kucoinapi/bullet.go @@ -61,7 +61,6 @@ type GetPublicBulletRequest struct { client requestgen.APIClient } - //go:generate requestgen -type GetPrivateBulletRequest -method "POST" -url "/api/v1/bullet-private" -responseType .APIResponse -responseDataField Data -responseDataType .Bullet type GetPrivateBulletRequest struct { client requestgen.AuthenticatedAPIClient diff --git a/pkg/exchange/kucoin/kucoinapi/client.go b/pkg/exchange/kucoin/kucoinapi/client.go index 4977ff58c0..6df47191c2 100644 --- a/pkg/exchange/kucoin/kucoinapi/client.go +++ b/pkg/exchange/kucoin/kucoinapi/client.go @@ -22,9 +22,7 @@ const RestBaseURL = "https://api.kucoin.com/api" const SandboxRestBaseURL = "https://openapi-sandbox.kucoin.com/api" type RestClient struct { - BaseURL *url.URL - - client *http.Client + requestgen.BaseAPIClient Key, Secret, Passphrase string KeyVersion string @@ -42,11 +40,13 @@ func NewClient() *RestClient { } client := &RestClient{ - BaseURL: u, - KeyVersion: "2", - client: &http.Client{ - Timeout: defaultHTTPTimeout, + BaseAPIClient: requestgen.BaseAPIClient{ + BaseURL: u, + HttpClient: &http.Client{ + Timeout: defaultHTTPTimeout, + }, }, + KeyVersion: "2", } client.AccountService = &AccountService{client: client} @@ -58,51 +58,11 @@ func NewClient() *RestClient { func (c *RestClient) Auth(key, secret, passphrase string) { c.Key = key + // pragma: allowlist nextline secret c.Secret = secret c.Passphrase = passphrase } -// NewRequest create new API request. Relative url can be provided in refURL. -func (c *RestClient) NewRequest(ctx context.Context, method, refURL string, params url.Values, payload interface{}) (*http.Request, error) { - rel, err := url.Parse(refURL) - if err != nil { - return nil, err - } - - if params != nil { - rel.RawQuery = params.Encode() - } - - body, err := castPayload(payload) - if err != nil { - return nil, err - } - - pathURL := c.BaseURL.ResolveReference(rel) - return http.NewRequestWithContext(ctx, method, pathURL.String(), bytes.NewReader(body)) -} - -// sendRequest sends the request to the API server and handle the response -func (c *RestClient) SendRequest(req *http.Request) (*requestgen.Response, error) { - resp, err := c.client.Do(req) - if err != nil { - return nil, err - } - - // newResponse reads the response body and return a new Response object - response, err := requestgen.NewResponse(resp) - if err != nil { - return response, err - } - - // Check error, if there is an error, return the ErrorResponse struct type - if response.IsError() { - return response, errors.New(string(response.Body)) - } - - return response, nil -} - // newAuthenticatedRequest creates new http request for authenticated routes. func (c *RestClient) NewAuthenticatedRequest(ctx context.Context, method, refURL string, params url.Values, payload interface{}) (*http.Request, error) { if len(c.Key) == 0 { @@ -173,21 +133,19 @@ func sign(secret, payload string) string { } func castPayload(payload interface{}) ([]byte, error) { - if payload != nil { - switch v := payload.(type) { - case string: - return []byte(v), nil - - case []byte: - return v, nil - - default: - body, err := json.Marshal(v) - return body, err - } + if payload == nil { + return nil, nil } - return nil, nil + switch v := payload.(type) { + case string: + return []byte(v), nil + + case []byte: + return v, nil + + } + return json.Marshal(payload) } type APIResponse struct { diff --git a/pkg/exchange/kucoin/stream.go b/pkg/exchange/kucoin/stream.go index 77e06a2d62..f6ad67f973 100644 --- a/pkg/exchange/kucoin/stream.go +++ b/pkg/exchange/kucoin/stream.go @@ -9,9 +9,9 @@ import ( "github.com/c9s/bbgo/pkg/depth" "github.com/c9s/bbgo/pkg/exchange/kucoin/kucoinapi" + "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/util" - "github.com/c9s/bbgo/pkg/fixedpoint" ) const readTimeout = 30 * time.Second @@ -20,8 +20,8 @@ const readTimeout = 30 * time.Second type Stream struct { types.StandardStream - client *kucoinapi.RestClient - exchange *Exchange + client *kucoinapi.RestClient + exchange *Exchange bullet *kucoinapi.Bullet candleEventCallbacks []func(candle *WebSocketCandleEvent, e *WebSocketEvent) @@ -125,8 +125,8 @@ func (s *Stream) handlePrivateOrderEvent(e *WebSocketPrivateOrderEvent) { IsBuyer: e.Side == "buy", IsMaker: e.Liquidity == "maker", Time: types.Time(e.Ts.Time()), - Fee: fixedpoint.Zero, // not supported - FeeCurrency: "", // not supported + Fee: fixedpoint.Zero, // not supported + FeeCurrency: "", // not supported }) } diff --git a/pkg/exchange/kucoin/symbols.go b/pkg/exchange/kucoin/symbols.go index dccc5ebe7e..ddd85b7fb7 100644 --- a/pkg/exchange/kucoin/symbols.go +++ b/pkg/exchange/kucoin/symbols.go @@ -2,1109 +2,1109 @@ package kucoin var symbolMap = map[string]string{ - "1EARTHUSDT": "1EARTH-USDT", - "1INCHUSDT": "1INCH-USDT", - "2CRZBTC": "2CRZ-BTC", - "2CRZUSDT": "2CRZ-USDT", - "AAVE3LUSDT": "AAVE3L-USDT", - "AAVE3SUSDT": "AAVE3S-USDT", - "AAVEBTC": "AAVE-BTC", - "AAVEKCS": "AAVE-KCS", - "AAVEUSDT": "AAVE-USDT", - "AAVEUST": "AAVE-UST", - "ABBCBTC": "ABBC-BTC", - "ABBCUSDT": "ABBC-USDT", - "ACEUSDT": "ACE-USDT", - "ACOINUSDT": "ACOIN-USDT", - "ACTBTC": "ACT-BTC", - "ACTETH": "ACT-ETH", - "ADA3LUSDT": "ADA3L-USDT", - "ADA3SUSDT": "ADA3S-USDT", - "ADABTC": "ADA-BTC", - "ADAKCS": "ADA-KCS", - "ADAUSDC": "ADA-USDC", - "ADAUSDT": "ADA-USDT", - "ADBBTC": "ADB-BTC", - "ADBETH": "ADB-ETH", - "ADXUSDT": "ADX-USDT", - "AERGOBTC": "AERGO-BTC", - "AERGOUSDT": "AERGO-USDT", - "AGIXBTC": "AGIX-BTC", - "AGIXETH": "AGIX-ETH", - "AGIXUSDT": "AGIX-USDT", - "AGLDUSDT": "AGLD-USDT", - "AIONBTC": "AION-BTC", - "AIONETH": "AION-ETH", - "AIOZUSDT": "AIOZ-USDT", - "AIUSDT": "AI-USDT", - "AKROBTC": "AKRO-BTC", - "AKROUSDT": "AKRO-USDT", - "ALBTETH": "ALBT-ETH", - "ALBTUSDT": "ALBT-USDT", - "ALEPHUSDT": "ALEPH-USDT", - "ALGOBTC": "ALGO-BTC", - "ALGOETH": "ALGO-ETH", - "ALGOKCS": "ALGO-KCS", - "ALGOUSDT": "ALGO-USDT", - "ALICEBTC": "ALICE-BTC", - "ALICEETH": "ALICE-ETH", - "ALICEUSDT": "ALICE-USDT", - "ALPACAUSDT": "ALPACA-USDT", - "ALPHABTC": "ALPHA-BTC", - "ALPHAUSDT": "ALPHA-USDT", - "AMBBTC": "AMB-BTC", - "AMBETH": "AMB-ETH", - "AMPLBTC": "AMPL-BTC", - "AMPLETH": "AMPL-ETH", - "AMPLUSDT": "AMPL-USDT", - "ANCUSDT": "ANC-USDT", - "ANCUST": "ANC-UST", - "ANKRBTC": "ANKR-BTC", - "ANKRUSDT": "ANKR-USDT", - "ANTBTC": "ANT-BTC", - "ANTUSDT": "ANT-USDT", - "AOABTC": "AOA-BTC", - "AOAUSDT": "AOA-USDT", - "API3USDT": "API3-USDT", - "APLBTC": "APL-BTC", - "APLUSDT": "APL-USDT", - "ARBTC": "AR-BTC", - "ARKERUSDT": "ARKER-USDT", - "ARPAUSDT": "ARPA-USDT", - "ARRRBTC": "ARRR-BTC", - "ARRRUSDT": "ARRR-USDT", - "ARUSDT": "AR-USDT", - "ARXUSDT": "ARX-USDT", - "ASDUSDT": "ASD-USDT", - "ATABTC": "ATA-BTC", - "ATAUSDT": "ATA-USDT", - "ATOM3LUSDT": "ATOM3L-USDT", - "ATOM3SUSDT": "ATOM3S-USDT", - "ATOMBTC": "ATOM-BTC", - "ATOMETH": "ATOM-ETH", - "ATOMKCS": "ATOM-KCS", - "ATOMUSDT": "ATOM-USDT", - "ATOMUST": "ATOM-UST", - "AUDIOBTC": "AUDIO-BTC", - "AUDIOUSDT": "AUDIO-USDT", - "AURYUSDT": "AURY-USDT", - "AVABTC": "AVA-BTC", - "AVAETH": "AVA-ETH", - "AVAUSDT": "AVA-USDT", - "AVAX3LUSDT": "AVAX3L-USDT", - "AVAX3SUSDT": "AVAX3S-USDT", - "AVAXBTC": "AVAX-BTC", - "AVAXUSDT": "AVAX-USDT", - "AXCUSDT": "AXC-USDT", - "AXPRBTC": "AXPR-BTC", - "AXPRETH": "AXPR-ETH", - "AXS3LUSDT": "AXS3L-USDT", - "AXS3SUSDT": "AXS3S-USDT", - "AXSUSDT": "AXS-USDT", - "BADGERBTC": "BADGER-BTC", - "BADGERUSDT": "BADGER-USDT", - "BAKEBTC": "BAKE-BTC", - "BAKEETH": "BAKE-ETH", - "BAKEUSDT": "BAKE-USDT", - "BALBTC": "BAL-BTC", - "BALETH": "BAL-ETH", - "BALUSDT": "BAL-USDT", - "BANDBTC": "BAND-BTC", - "BANDUSDT": "BAND-USDT", - "BASICUSDT": "BASIC-USDT", - "BATUSDT": "BAT-USDT", - "BAXBTC": "BAX-BTC", - "BAXETH": "BAX-ETH", - "BAXUSDT": "BAX-USDT", - "BCDBTC": "BCD-BTC", - "BCDETH": "BCD-ETH", - "BCH3LUSDT": "BCH3L-USDT", - "BCH3SUSDT": "BCH3S-USDT", - "BCHBTC": "BCH-BTC", - "BCHKCS": "BCH-KCS", - "BCHSVBTC": "BCHSV-BTC", - "BCHSVETH": "BCHSV-ETH", - "BCHSVKCS": "BCHSV-KCS", - "BCHSVUSDC": "BCHSV-USDC", - "BCHSVUSDT": "BCHSV-USDT", - "BCHUSDC": "BCH-USDC", - "BCHUSDT": "BCH-USDT", - "BEPROBTC": "BEPRO-BTC", - "BEPROUSDT": "BEPRO-USDT", - "BLOKUSDT": "BLOK-USDT", - "BMONUSDT": "BMON-USDT", - "BNB3LUSDT": "BNB3L-USDT", - "BNB3SUSDT": "BNB3S-USDT", - "BNBBTC": "BNB-BTC", - "BNBKCS": "BNB-KCS", - "BNBUSDT": "BNB-USDT", - "BNSBTC": "BNS-BTC", - "BNSUSDT": "BNS-USDT", - "BNTBTC": "BNT-BTC", - "BNTETH": "BNT-ETH", - "BNTUSDT": "BNT-USDT", - "BOAUSDT": "BOA-USDT", - "BOLTBTC": "BOLT-BTC", - "BOLTUSDT": "BOLT-USDT", - "BONDLYETH": "BONDLY-ETH", - "BONDLYUSDT": "BONDLY-USDT", - "BONDUSDT": "BOND-USDT", - "BOSONETH": "BOSON-ETH", - "BOSONUSDT": "BOSON-USDT", - "BTC3LUSDT": "BTC3L-USDT", - "BTC3SUSDT": "BTC3S-USDT", - "BTCDAI": "BTC-DAI", - "BTCPAX": "BTC-PAX", - "BTCTUSD": "BTC-TUSD", - "BTCUSDC": "BTC-USDC", - "BTCUSDT": "BTC-USDT", - "BTCUST": "BTC-UST", - "BTTBTC": "BTT-BTC", - "BTTETH": "BTT-ETH", - "BTTTRX": "BTT-TRX", - "BTTUSDT": "BTT-USDT", - "BURGERBTC": "BURGER-BTC", - "BURGERUSDT": "BURGER-USDT", - "BURPUSDT": "BURP-USDT", - "BUXBTC": "BUX-BTC", - "BUXUSDT": "BUX-USDT", - "BUYBTC": "BUY-BTC", - "BUYUSDT": "BUY-USDT", - "C98USDT": "C98-USDT", - "CAKEUSDT": "CAKE-USDT", - "CAPPBTC": "CAPP-BTC", - "CAPPETH": "CAPP-ETH", - "CARDUSDT": "CARD-USDT", - "CARRBTC": "CARR-BTC", - "CARRUSDT": "CARR-USDT", - "CASBTC": "CAS-BTC", - "CASUSDT": "CAS-USDT", - "CBCBTC": "CBC-BTC", - "CBCUSDT": "CBC-USDT", - "CELOBTC": "CELO-BTC", - "CELOUSDT": "CELO-USDT", - "CEREUSDT": "CERE-USDT", - "CEURBTC": "CEUR-BTC", - "CEURUSDT": "CEUR-USDT", - "CFGBTC": "CFG-BTC", - "CFGUSDT": "CFG-USDT", - "CGGUSDT": "CGG-USDT", - "CHMBUSDT": "CHMB-USDT", - "CHRBTC": "CHR-BTC", - "CHRUSDT": "CHR-USDT", - "CHSBBTC": "CHSB-BTC", - "CHSBETH": "CHSB-ETH", - "CHZBTC": "CHZ-BTC", - "CHZUSDT": "CHZ-USDT", - "CIRUSETH": "CIRUS-ETH", - "CIRUSUSDT": "CIRUS-USDT", - "CIX100USDT": "CIX100-USDT", - "CKBBTC": "CKB-BTC", - "CKBUSDT": "CKB-USDT", - "CLVUSDT": "CLV-USDT", - "COMBUSDT": "COMB-USDT", - "COMPUSDT": "COMP-USDT", - "COTIBTC": "COTI-BTC", - "COTIUSDT": "COTI-USDT", - "COVBTC": "COV-BTC", - "COVETH": "COV-ETH", - "COVUSDT": "COV-USDT", - "CPCBTC": "CPC-BTC", - "CPCETH": "CPC-ETH", - "CPOOLUSDT": "CPOOL-USDT", - "CQTUSDT": "CQT-USDT", - "CREAMBTC": "CREAM-BTC", - "CREAMUSDT": "CREAM-USDT", - "CREDIUSDT": "CREDI-USDT", - "CROBTC": "CRO-BTC", - "CROUSDT": "CRO-USDT", - "CRPTBTC": "CRPT-BTC", - "CRPTETH": "CRPT-ETH", - "CRPTUSDT": "CRPT-USDT", - "CRVUSDT": "CRV-USDT", - "CSBTC": "CS-BTC", - "CSETH": "CS-ETH", - "CSPBTC": "CSP-BTC", - "CSPETH": "CSP-ETH", - "CTIETH": "CTI-ETH", - "CTIUSDT": "CTI-USDT", - "CTSIBTC": "CTSI-BTC", - "CTSIUSDT": "CTSI-USDT", - "CUDOSBTC": "CUDOS-BTC", - "CUDOSUSDT": "CUDOS-USDT", - "CUSDBTC": "CUSD-BTC", - "CUSDUSDT": "CUSD-USDT", - "CVBTC": "CV-BTC", - "CVCBTC": "CVC-BTC", - "CVETH": "CV-ETH", - "CWARBTC": "CWAR-BTC", - "CWARUSDT": "CWAR-USDT", - "CWSUSDT": "CWS-USDT", - "CXOBTC": "CXO-BTC", - "CXOETH": "CXO-ETH", - "DACCBTC": "DACC-BTC", - "DACCETH": "DACC-ETH", - "DAGBTC": "DAG-BTC", - "DAGETH": "DAG-ETH", - "DAGUSDT": "DAG-USDT", - "DAOUSDT": "DAO-USDT", - "DAPPTBTC": "DAPPT-BTC", - "DAPPTUSDT": "DAPPT-USDT", - "DAPPXUSDT": "DAPPX-USDT", - "DASHBTC": "DASH-BTC", - "DASHETH": "DASH-ETH", - "DASHKCS": "DASH-KCS", - "DASHUSDT": "DASH-USDT", - "DATABTC": "DATA-BTC", - "DATAUSDT": "DATA-USDT", - "DATXBTC": "DATX-BTC", - "DATXETH": "DATX-ETH", - "DCRBTC": "DCR-BTC", - "DCRETH": "DCR-ETH", - "DEGOETH": "DEGO-ETH", - "DEGOUSDT": "DEGO-USDT", - "DENTBTC": "DENT-BTC", - "DENTETH": "DENT-ETH", - "DEROBTC": "DERO-BTC", - "DEROUSDT": "DERO-USDT", - "DEXEBTC": "DEXE-BTC", - "DEXEETH": "DEXE-ETH", - "DEXEUSDT": "DEXE-USDT", - "DFIBTC": "DFI-BTC", - "DFIUSDT": "DFI-USDT", - "DFYNUSDT": "DFYN-USDT", - "DGBBTC": "DGB-BTC", - "DGBETH": "DGB-ETH", - "DGBUSDT": "DGB-USDT", - "DGTXBTC": "DGTX-BTC", - "DGTXETH": "DGTX-ETH", - "DIABTC": "DIA-BTC", - "DIAUSDT": "DIA-USDT", - "DINOUSDT": "DINO-USDT", - "DIVIUSDT": "DIVI-USDT", - "DMGUSDT": "DMG-USDT", - "DMTRUSDT": "DMTR-USDT", - "DOCKBTC": "DOCK-BTC", - "DOCKETH": "DOCK-ETH", - "DODOUSDT": "DODO-USDT", - "DOGE3LUSDT": "DOGE3L-USDT", - "DOGE3SUSDT": "DOGE3S-USDT", - "DOGEBTC": "DOGE-BTC", - "DOGEKCS": "DOGE-KCS", - "DOGEUSDC": "DOGE-USDC", - "DOGEUSDT": "DOGE-USDT", - "DORABTC": "DORA-BTC", - "DORAUSDT": "DORA-USDT", - "DOT3LUSDT": "DOT3L-USDT", - "DOT3SUSDT": "DOT3S-USDT", - "DOTBTC": "DOT-BTC", - "DOTKCS": "DOT-KCS", - "DOTUSDT": "DOT-USDT", - "DOTUST": "DOT-UST", - "DPETUSDT": "DPET-USDT", - "DPIUSDT": "DPI-USDT", - "DPRUSDT": "DPR-USDT", - "DREAMSUSDT": "DREAMS-USDT", - "DRGNBTC": "DRGN-BTC", - "DRGNETH": "DRGN-ETH", - "DSLABTC": "DSLA-BTC", - "DSLAUSDT": "DSLA-USDT", - "DVPNUSDT": "DVPN-USDT", - "DYDXUSDT": "DYDX-USDT", - "DYPETH": "DYP-ETH", - "DYPUSDT": "DYP-USDT", - "EDGBTC": "EDG-BTC", - "EDGUSDT": "EDG-USDT", - "EFXBTC": "EFX-BTC", - "EFXUSDT": "EFX-USDT", - "EGLDBTC": "EGLD-BTC", - "EGLDUSDT": "EGLD-USDT", - "ELABTC": "ELA-BTC", - "ELAETH": "ELA-ETH", - "ELAUSDT": "ELA-USDT", - "ELFBTC": "ELF-BTC", - "ELFETH": "ELF-ETH", - "ELONUSDT": "ELON-USDT", - "ENJBTC": "ENJ-BTC", - "ENJETH": "ENJ-ETH", - "ENJUSDT": "ENJ-USDT", - "ENQBTC": "ENQ-BTC", - "ENQUSDT": "ENQ-USDT", - "ENSUSDT": "ENS-USDT", - "EOS3LUSDT": "EOS3L-USDT", - "EOS3SUSDT": "EOS3S-USDT", - "EOSBTC": "EOS-BTC", - "EOSCUSDT": "EOSC-USDT", - "EOSETH": "EOS-ETH", - "EOSKCS": "EOS-KCS", - "EOSUSDC": "EOS-USDC", - "EOSUSDT": "EOS-USDT", - "EPIKUSDT": "EPIK-USDT", - "EPSBTC": "EPS-BTC", - "EPSUSDT": "EPS-USDT", - "EQXBTC": "EQX-BTC", - "EQXUSDT": "EQX-USDT", - "EQZBTC": "EQZ-BTC", - "EQZUSDT": "EQZ-USDT", - "ERGBTC": "ERG-BTC", - "ERGUSDT": "ERG-USDT", - "ERNBTC": "ERN-BTC", - "ERNUSDT": "ERN-USDT", - "ERSDLUSDT": "ERSDL-USDT", - "ETCBTC": "ETC-BTC", - "ETCETH": "ETC-ETH", - "ETCUSDT": "ETC-USDT", - "ETH2ETH": "ETH2-ETH", - "ETH3LUSDT": "ETH3L-USDT", - "ETH3SUSDT": "ETH3S-USDT", - "ETHBTC": "ETH-BTC", - "ETHDAI": "ETH-DAI", - "ETHOBTC": "ETHO-BTC", - "ETHOUSDT": "ETHO-USDT", - "ETHPAX": "ETH-PAX", - "ETHTUSD": "ETH-TUSD", - "ETHUSDC": "ETH-USDC", - "ETHUSDT": "ETH-USDT", - "ETHUST": "ETH-UST", - "ETNBTC": "ETN-BTC", - "ETNETH": "ETN-ETH", - "ETNUSDT": "ETN-USDT", - "EWTBTC": "EWT-BTC", - "EWTKCS": "EWT-KCS", - "EWTUSDT": "EWT-USDT", - "EXRDUSDT": "EXRD-USDT", - "FALCONSUSDT": "FALCONS-USDT", - "FCLETH": "FCL-ETH", - "FCLUSDT": "FCL-USDT", - "FEARUSDT": "FEAR-USDT", - "FETBTC": "FET-BTC", - "FETETH": "FET-ETH", - "FILUSDT": "FIL-USDT", - "FKXBTC": "FKX-BTC", - "FKXETH": "FKX-ETH", - "FKXUSDT": "FKX-USDT", - "FLAMEUSDT": "FLAME-USDT", - "FLOWBTC": "FLOW-BTC", - "FLOWUSDT": "FLOW-USDT", - "FLUXBTC": "FLUX-BTC", - "FLUXUSDT": "FLUX-USDT", - "FLYUSDT": "FLY-USDT", - "FORESTPLUSBTC": "FORESTPLUS-BTC", + "1EARTHUSDT": "1EARTH-USDT", + "1INCHUSDT": "1INCH-USDT", + "2CRZBTC": "2CRZ-BTC", + "2CRZUSDT": "2CRZ-USDT", + "AAVE3LUSDT": "AAVE3L-USDT", + "AAVE3SUSDT": "AAVE3S-USDT", + "AAVEBTC": "AAVE-BTC", + "AAVEKCS": "AAVE-KCS", + "AAVEUSDT": "AAVE-USDT", + "AAVEUST": "AAVE-UST", + "ABBCBTC": "ABBC-BTC", + "ABBCUSDT": "ABBC-USDT", + "ACEUSDT": "ACE-USDT", + "ACOINUSDT": "ACOIN-USDT", + "ACTBTC": "ACT-BTC", + "ACTETH": "ACT-ETH", + "ADA3LUSDT": "ADA3L-USDT", + "ADA3SUSDT": "ADA3S-USDT", + "ADABTC": "ADA-BTC", + "ADAKCS": "ADA-KCS", + "ADAUSDC": "ADA-USDC", + "ADAUSDT": "ADA-USDT", + "ADBBTC": "ADB-BTC", + "ADBETH": "ADB-ETH", + "ADXUSDT": "ADX-USDT", + "AERGOBTC": "AERGO-BTC", + "AERGOUSDT": "AERGO-USDT", + "AGIXBTC": "AGIX-BTC", + "AGIXETH": "AGIX-ETH", + "AGIXUSDT": "AGIX-USDT", + "AGLDUSDT": "AGLD-USDT", + "AIONBTC": "AION-BTC", + "AIONETH": "AION-ETH", + "AIOZUSDT": "AIOZ-USDT", + "AIUSDT": "AI-USDT", + "AKROBTC": "AKRO-BTC", + "AKROUSDT": "AKRO-USDT", + "ALBTETH": "ALBT-ETH", + "ALBTUSDT": "ALBT-USDT", + "ALEPHUSDT": "ALEPH-USDT", + "ALGOBTC": "ALGO-BTC", + "ALGOETH": "ALGO-ETH", + "ALGOKCS": "ALGO-KCS", + "ALGOUSDT": "ALGO-USDT", + "ALICEBTC": "ALICE-BTC", + "ALICEETH": "ALICE-ETH", + "ALICEUSDT": "ALICE-USDT", + "ALPACAUSDT": "ALPACA-USDT", + "ALPHABTC": "ALPHA-BTC", + "ALPHAUSDT": "ALPHA-USDT", + "AMBBTC": "AMB-BTC", + "AMBETH": "AMB-ETH", + "AMPLBTC": "AMPL-BTC", + "AMPLETH": "AMPL-ETH", + "AMPLUSDT": "AMPL-USDT", + "ANCUSDT": "ANC-USDT", + "ANCUST": "ANC-UST", + "ANKRBTC": "ANKR-BTC", + "ANKRUSDT": "ANKR-USDT", + "ANTBTC": "ANT-BTC", + "ANTUSDT": "ANT-USDT", + "AOABTC": "AOA-BTC", + "AOAUSDT": "AOA-USDT", + "API3USDT": "API3-USDT", + "APLBTC": "APL-BTC", + "APLUSDT": "APL-USDT", + "ARBTC": "AR-BTC", + "ARKERUSDT": "ARKER-USDT", + "ARPAUSDT": "ARPA-USDT", + "ARRRBTC": "ARRR-BTC", + "ARRRUSDT": "ARRR-USDT", + "ARUSDT": "AR-USDT", + "ARXUSDT": "ARX-USDT", + "ASDUSDT": "ASD-USDT", + "ATABTC": "ATA-BTC", + "ATAUSDT": "ATA-USDT", + "ATOM3LUSDT": "ATOM3L-USDT", + "ATOM3SUSDT": "ATOM3S-USDT", + "ATOMBTC": "ATOM-BTC", + "ATOMETH": "ATOM-ETH", + "ATOMKCS": "ATOM-KCS", + "ATOMUSDT": "ATOM-USDT", + "ATOMUST": "ATOM-UST", + "AUDIOBTC": "AUDIO-BTC", + "AUDIOUSDT": "AUDIO-USDT", + "AURYUSDT": "AURY-USDT", + "AVABTC": "AVA-BTC", + "AVAETH": "AVA-ETH", + "AVAUSDT": "AVA-USDT", + "AVAX3LUSDT": "AVAX3L-USDT", + "AVAX3SUSDT": "AVAX3S-USDT", + "AVAXBTC": "AVAX-BTC", + "AVAXUSDT": "AVAX-USDT", + "AXCUSDT": "AXC-USDT", + "AXPRBTC": "AXPR-BTC", + "AXPRETH": "AXPR-ETH", + "AXS3LUSDT": "AXS3L-USDT", + "AXS3SUSDT": "AXS3S-USDT", + "AXSUSDT": "AXS-USDT", + "BADGERBTC": "BADGER-BTC", + "BADGERUSDT": "BADGER-USDT", + "BAKEBTC": "BAKE-BTC", + "BAKEETH": "BAKE-ETH", + "BAKEUSDT": "BAKE-USDT", + "BALBTC": "BAL-BTC", + "BALETH": "BAL-ETH", + "BALUSDT": "BAL-USDT", + "BANDBTC": "BAND-BTC", + "BANDUSDT": "BAND-USDT", + "BASICUSDT": "BASIC-USDT", + "BATUSDT": "BAT-USDT", + "BAXBTC": "BAX-BTC", + "BAXETH": "BAX-ETH", + "BAXUSDT": "BAX-USDT", + "BCDBTC": "BCD-BTC", + "BCDETH": "BCD-ETH", + "BCH3LUSDT": "BCH3L-USDT", + "BCH3SUSDT": "BCH3S-USDT", + "BCHBTC": "BCH-BTC", + "BCHKCS": "BCH-KCS", + "BCHSVBTC": "BCHSV-BTC", + "BCHSVETH": "BCHSV-ETH", + "BCHSVKCS": "BCHSV-KCS", + "BCHSVUSDC": "BCHSV-USDC", + "BCHSVUSDT": "BCHSV-USDT", + "BCHUSDC": "BCH-USDC", + "BCHUSDT": "BCH-USDT", + "BEPROBTC": "BEPRO-BTC", + "BEPROUSDT": "BEPRO-USDT", + "BLOKUSDT": "BLOK-USDT", + "BMONUSDT": "BMON-USDT", + "BNB3LUSDT": "BNB3L-USDT", + "BNB3SUSDT": "BNB3S-USDT", + "BNBBTC": "BNB-BTC", + "BNBKCS": "BNB-KCS", + "BNBUSDT": "BNB-USDT", + "BNSBTC": "BNS-BTC", + "BNSUSDT": "BNS-USDT", + "BNTBTC": "BNT-BTC", + "BNTETH": "BNT-ETH", + "BNTUSDT": "BNT-USDT", + "BOAUSDT": "BOA-USDT", + "BOLTBTC": "BOLT-BTC", + "BOLTUSDT": "BOLT-USDT", + "BONDLYETH": "BONDLY-ETH", + "BONDLYUSDT": "BONDLY-USDT", + "BONDUSDT": "BOND-USDT", + "BOSONETH": "BOSON-ETH", + "BOSONUSDT": "BOSON-USDT", + "BTC3LUSDT": "BTC3L-USDT", + "BTC3SUSDT": "BTC3S-USDT", + "BTCDAI": "BTC-DAI", + "BTCPAX": "BTC-PAX", + "BTCTUSD": "BTC-TUSD", + "BTCUSDC": "BTC-USDC", + "BTCUSDT": "BTC-USDT", + "BTCUST": "BTC-UST", + "BTTBTC": "BTT-BTC", + "BTTETH": "BTT-ETH", + "BTTTRX": "BTT-TRX", + "BTTUSDT": "BTT-USDT", + "BURGERBTC": "BURGER-BTC", + "BURGERUSDT": "BURGER-USDT", + "BURPUSDT": "BURP-USDT", + "BUXBTC": "BUX-BTC", + "BUXUSDT": "BUX-USDT", + "BUYBTC": "BUY-BTC", + "BUYUSDT": "BUY-USDT", + "C98USDT": "C98-USDT", + "CAKEUSDT": "CAKE-USDT", + "CAPPBTC": "CAPP-BTC", + "CAPPETH": "CAPP-ETH", + "CARDUSDT": "CARD-USDT", + "CARRBTC": "CARR-BTC", + "CARRUSDT": "CARR-USDT", + "CASBTC": "CAS-BTC", + "CASUSDT": "CAS-USDT", + "CBCBTC": "CBC-BTC", + "CBCUSDT": "CBC-USDT", + "CELOBTC": "CELO-BTC", + "CELOUSDT": "CELO-USDT", + "CEREUSDT": "CERE-USDT", + "CEURBTC": "CEUR-BTC", + "CEURUSDT": "CEUR-USDT", + "CFGBTC": "CFG-BTC", + "CFGUSDT": "CFG-USDT", + "CGGUSDT": "CGG-USDT", + "CHMBUSDT": "CHMB-USDT", + "CHRBTC": "CHR-BTC", + "CHRUSDT": "CHR-USDT", + "CHSBBTC": "CHSB-BTC", + "CHSBETH": "CHSB-ETH", + "CHZBTC": "CHZ-BTC", + "CHZUSDT": "CHZ-USDT", + "CIRUSETH": "CIRUS-ETH", + "CIRUSUSDT": "CIRUS-USDT", + "CIX100USDT": "CIX100-USDT", + "CKBBTC": "CKB-BTC", + "CKBUSDT": "CKB-USDT", + "CLVUSDT": "CLV-USDT", + "COMBUSDT": "COMB-USDT", + "COMPUSDT": "COMP-USDT", + "COTIBTC": "COTI-BTC", + "COTIUSDT": "COTI-USDT", + "COVBTC": "COV-BTC", + "COVETH": "COV-ETH", + "COVUSDT": "COV-USDT", + "CPCBTC": "CPC-BTC", + "CPCETH": "CPC-ETH", + "CPOOLUSDT": "CPOOL-USDT", + "CQTUSDT": "CQT-USDT", + "CREAMBTC": "CREAM-BTC", + "CREAMUSDT": "CREAM-USDT", + "CREDIUSDT": "CREDI-USDT", + "CROBTC": "CRO-BTC", + "CROUSDT": "CRO-USDT", + "CRPTBTC": "CRPT-BTC", + "CRPTETH": "CRPT-ETH", + "CRPTUSDT": "CRPT-USDT", + "CRVUSDT": "CRV-USDT", + "CSBTC": "CS-BTC", + "CSETH": "CS-ETH", + "CSPBTC": "CSP-BTC", + "CSPETH": "CSP-ETH", + "CTIETH": "CTI-ETH", + "CTIUSDT": "CTI-USDT", + "CTSIBTC": "CTSI-BTC", + "CTSIUSDT": "CTSI-USDT", + "CUDOSBTC": "CUDOS-BTC", + "CUDOSUSDT": "CUDOS-USDT", + "CUSDBTC": "CUSD-BTC", + "CUSDUSDT": "CUSD-USDT", + "CVBTC": "CV-BTC", + "CVCBTC": "CVC-BTC", + "CVETH": "CV-ETH", + "CWARBTC": "CWAR-BTC", + "CWARUSDT": "CWAR-USDT", + "CWSUSDT": "CWS-USDT", + "CXOBTC": "CXO-BTC", + "CXOETH": "CXO-ETH", + "DACCBTC": "DACC-BTC", + "DACCETH": "DACC-ETH", + "DAGBTC": "DAG-BTC", + "DAGETH": "DAG-ETH", + "DAGUSDT": "DAG-USDT", + "DAOUSDT": "DAO-USDT", + "DAPPTBTC": "DAPPT-BTC", + "DAPPTUSDT": "DAPPT-USDT", + "DAPPXUSDT": "DAPPX-USDT", + "DASHBTC": "DASH-BTC", + "DASHETH": "DASH-ETH", + "DASHKCS": "DASH-KCS", + "DASHUSDT": "DASH-USDT", + "DATABTC": "DATA-BTC", + "DATAUSDT": "DATA-USDT", + "DATXBTC": "DATX-BTC", + "DATXETH": "DATX-ETH", + "DCRBTC": "DCR-BTC", + "DCRETH": "DCR-ETH", + "DEGOETH": "DEGO-ETH", + "DEGOUSDT": "DEGO-USDT", + "DENTBTC": "DENT-BTC", + "DENTETH": "DENT-ETH", + "DEROBTC": "DERO-BTC", + "DEROUSDT": "DERO-USDT", + "DEXEBTC": "DEXE-BTC", + "DEXEETH": "DEXE-ETH", + "DEXEUSDT": "DEXE-USDT", + "DFIBTC": "DFI-BTC", + "DFIUSDT": "DFI-USDT", + "DFYNUSDT": "DFYN-USDT", + "DGBBTC": "DGB-BTC", + "DGBETH": "DGB-ETH", + "DGBUSDT": "DGB-USDT", + "DGTXBTC": "DGTX-BTC", + "DGTXETH": "DGTX-ETH", + "DIABTC": "DIA-BTC", + "DIAUSDT": "DIA-USDT", + "DINOUSDT": "DINO-USDT", + "DIVIUSDT": "DIVI-USDT", + "DMGUSDT": "DMG-USDT", + "DMTRUSDT": "DMTR-USDT", + "DOCKBTC": "DOCK-BTC", + "DOCKETH": "DOCK-ETH", + "DODOUSDT": "DODO-USDT", + "DOGE3LUSDT": "DOGE3L-USDT", + "DOGE3SUSDT": "DOGE3S-USDT", + "DOGEBTC": "DOGE-BTC", + "DOGEKCS": "DOGE-KCS", + "DOGEUSDC": "DOGE-USDC", + "DOGEUSDT": "DOGE-USDT", + "DORABTC": "DORA-BTC", + "DORAUSDT": "DORA-USDT", + "DOT3LUSDT": "DOT3L-USDT", + "DOT3SUSDT": "DOT3S-USDT", + "DOTBTC": "DOT-BTC", + "DOTKCS": "DOT-KCS", + "DOTUSDT": "DOT-USDT", + "DOTUST": "DOT-UST", + "DPETUSDT": "DPET-USDT", + "DPIUSDT": "DPI-USDT", + "DPRUSDT": "DPR-USDT", + "DREAMSUSDT": "DREAMS-USDT", + "DRGNBTC": "DRGN-BTC", + "DRGNETH": "DRGN-ETH", + "DSLABTC": "DSLA-BTC", + "DSLAUSDT": "DSLA-USDT", + "DVPNUSDT": "DVPN-USDT", + "DYDXUSDT": "DYDX-USDT", + "DYPETH": "DYP-ETH", + "DYPUSDT": "DYP-USDT", + "EDGBTC": "EDG-BTC", + "EDGUSDT": "EDG-USDT", + "EFXBTC": "EFX-BTC", + "EFXUSDT": "EFX-USDT", + "EGLDBTC": "EGLD-BTC", + "EGLDUSDT": "EGLD-USDT", + "ELABTC": "ELA-BTC", + "ELAETH": "ELA-ETH", + "ELAUSDT": "ELA-USDT", + "ELFBTC": "ELF-BTC", + "ELFETH": "ELF-ETH", + "ELONUSDT": "ELON-USDT", + "ENJBTC": "ENJ-BTC", + "ENJETH": "ENJ-ETH", + "ENJUSDT": "ENJ-USDT", + "ENQBTC": "ENQ-BTC", + "ENQUSDT": "ENQ-USDT", + "ENSUSDT": "ENS-USDT", + "EOS3LUSDT": "EOS3L-USDT", + "EOS3SUSDT": "EOS3S-USDT", + "EOSBTC": "EOS-BTC", + "EOSCUSDT": "EOSC-USDT", + "EOSETH": "EOS-ETH", + "EOSKCS": "EOS-KCS", + "EOSUSDC": "EOS-USDC", + "EOSUSDT": "EOS-USDT", + "EPIKUSDT": "EPIK-USDT", + "EPSBTC": "EPS-BTC", + "EPSUSDT": "EPS-USDT", + "EQXBTC": "EQX-BTC", + "EQXUSDT": "EQX-USDT", + "EQZBTC": "EQZ-BTC", + "EQZUSDT": "EQZ-USDT", + "ERGBTC": "ERG-BTC", + "ERGUSDT": "ERG-USDT", + "ERNBTC": "ERN-BTC", + "ERNUSDT": "ERN-USDT", + "ERSDLUSDT": "ERSDL-USDT", + "ETCBTC": "ETC-BTC", + "ETCETH": "ETC-ETH", + "ETCUSDT": "ETC-USDT", + "ETH2ETH": "ETH2-ETH", + "ETH3LUSDT": "ETH3L-USDT", + "ETH3SUSDT": "ETH3S-USDT", + "ETHBTC": "ETH-BTC", + "ETHDAI": "ETH-DAI", + "ETHOBTC": "ETHO-BTC", + "ETHOUSDT": "ETHO-USDT", + "ETHPAX": "ETH-PAX", + "ETHTUSD": "ETH-TUSD", + "ETHUSDC": "ETH-USDC", + "ETHUSDT": "ETH-USDT", + "ETHUST": "ETH-UST", + "ETNBTC": "ETN-BTC", + "ETNETH": "ETN-ETH", + "ETNUSDT": "ETN-USDT", + "EWTBTC": "EWT-BTC", + "EWTKCS": "EWT-KCS", + "EWTUSDT": "EWT-USDT", + "EXRDUSDT": "EXRD-USDT", + "FALCONSUSDT": "FALCONS-USDT", + "FCLETH": "FCL-ETH", + "FCLUSDT": "FCL-USDT", + "FEARUSDT": "FEAR-USDT", + "FETBTC": "FET-BTC", + "FETETH": "FET-ETH", + "FILUSDT": "FIL-USDT", + "FKXBTC": "FKX-BTC", + "FKXETH": "FKX-ETH", + "FKXUSDT": "FKX-USDT", + "FLAMEUSDT": "FLAME-USDT", + "FLOWBTC": "FLOW-BTC", + "FLOWUSDT": "FLOW-USDT", + "FLUXBTC": "FLUX-BTC", + "FLUXUSDT": "FLUX-USDT", + "FLYUSDT": "FLY-USDT", + "FORESTPLUSBTC": "FORESTPLUS-BTC", "FORESTPLUSUSDT": "FORESTPLUS-USDT", - "FORMETH": "FORM-ETH", - "FORMUSDT": "FORM-USDT", - "FORTHUSDT": "FORTH-USDT", - "FRMUSDT": "FRM-USDT", - "FRONTBTC": "FRONT-BTC", - "FRONTUSDT": "FRONT-USDT", - "FTGUSDT": "FTG-USDT", - "FTM3LUSDT": "FTM3L-USDT", - "FTM3SUSDT": "FTM3S-USDT", - "FTMBTC": "FTM-BTC", - "FTMETH": "FTM-ETH", - "FTMUSDT": "FTM-USDT", - "FTTBTC": "FTT-BTC", - "FTTUSDT": "FTT-USDT", - "FXBTC": "FX-BTC", - "FXETH": "FX-ETH", - "FXSBTC": "FXS-BTC", - "FXSUSDT": "FXS-USDT", - "GAFIUSDT": "GAFI-USDT", - "GALAX3LUSDT": "GALAX3L-USDT", - "GALAX3SUSDT": "GALAX3S-USDT", - "GALAXUSDT": "GALAX-USDT", - "GASBTC": "GAS-BTC", - "GASUSDT": "GAS-USDT", - "GEEQUSDT": "GEEQ-USDT", - "GENSUSDT": "GENS-USDT", - "GHSTBTC": "GHST-BTC", - "GHSTUSDT": "GHST-USDT", - "GHXUSDT": "GHX-USDT", - "GLCHUSDT": "GLCH-USDT", - "GLMBTC": "GLM-BTC", - "GLMUSDT": "GLM-USDT", - "GLQBTC": "GLQ-BTC", - "GLQUSDT": "GLQ-USDT", - "GMBBTC": "GMB-BTC", - "GMBETH": "GMB-ETH", - "GMBUSDT": "GMB-USDT", - "GMEEUSDT": "GMEE-USDT", - "GOBTC": "GO-BTC", - "GODSUSDT": "GODS-USDT", - "GOETH": "GO-ETH", - "GOM2BTC": "GOM2-BTC", - "GOM2USDT": "GOM2-USDT", - "GOUSDT": "GO-USDT", - "GOVIBTC": "GOVI-BTC", - "GOVIUSDT": "GOVI-USDT", - "GRINBTC": "GRIN-BTC", - "GRINETH": "GRIN-ETH", - "GRINUSDT": "GRIN-USDT", - "GRTKCS": "GRT-KCS", - "GRTUSDT": "GRT-USDT", - "GSPIUSDT": "GSPI-USDT", - "GTCBTC": "GTC-BTC", - "GTCUSDT": "GTC-USDT", - "H3RO3SUSDT": "H3RO3S-USDT", - "HAIBTC": "HAI-BTC", - "HAIUSDT": "HAI-USDT", - "HAKAUSDT": "HAKA-USDT", - "HAPIUSDT": "HAPI-USDT", - "HARDUSDT": "HARD-USDT", - "HBARBTC": "HBAR-BTC", - "HBARUSDT": "HBAR-USDT", - "HEARTBTC": "HEART-BTC", - "HEARTUSDT": "HEART-USDT", - "HEGICBTC": "HEGIC-BTC", - "HEGICUSDT": "HEGIC-USDT", - "HEROUSDT": "HERO-USDT", - "HORDUSDT": "HORD-USDT", - "HOTCROSSUSDT": "HOTCROSS-USDT", - "HPBBTC": "HPB-BTC", - "HPBETH": "HPB-ETH", - "HTRBTC": "HTR-BTC", - "HTRUSDT": "HTR-USDT", - "HTUSDT": "HT-USDT", - "HYDRAUSDT": "HYDRA-USDT", - "HYVEBTC": "HYVE-BTC", - "HYVEUSDT": "HYVE-USDT", - "ICPBTC": "ICP-BTC", - "ICPUSDT": "ICP-USDT", - "IDEAUSDT": "IDEA-USDT", - "ILAUSDT": "ILA-USDT", - "ILVUSDT": "ILV-USDT", - "IMXUSDT": "IMX-USDT", - "INJBTC": "INJ-BTC", - "INJUSDT": "INJ-USDT", - "IOIUSDT": "IOI-USDT", - "IOSTBTC": "IOST-BTC", - "IOSTETH": "IOST-ETH", - "IOSTUSDT": "IOST-USDT", - "IOTXBTC": "IOTX-BTC", - "IOTXETH": "IOTX-ETH", - "IOTXUSDT": "IOTX-USDT", - "ISPUSDT": "ISP-USDT", - "IXSUSDT": "IXS-USDT", - "JARBTC": "JAR-BTC", - "JARUSDT": "JAR-USDT", - "JASMYUSDT": "JASMY-USDT", - "JSTUSDT": "JST-USDT", - "JUPETH": "JUP-ETH", - "JUPUSDT": "JUP-USDT", - "KAIBTC": "KAI-BTC", - "KAIETH": "KAI-ETH", - "KAIUSDT": "KAI-USDT", - "KARUSDT": "KAR-USDT", - "KATBTC": "KAT-BTC", - "KATUSDT": "KAT-USDT", - "KAVAUSDT": "KAVA-USDT", - "KCSBTC": "KCS-BTC", - "KCSETH": "KCS-ETH", - "KCSUSDT": "KCS-USDT", - "KDABTC": "KDA-BTC", - "KDAUSDT": "KDA-USDT", - "KDONUSDT": "KDON-USDT", - "KEEPBTC": "KEEP-BTC", - "KEEPUSDT": "KEEP-USDT", - "KEYBTC": "KEY-BTC", - "KEYETH": "KEY-ETH", - "KINUSDT": "KIN-USDT", - "KLAYBTC": "KLAY-BTC", - "KLAYUSDT": "KLAY-USDT", - "KLVBTC": "KLV-BTC", - "KLVTRX": "KLV-TRX", - "KLVUSDT": "KLV-USDT", - "KMAUSDT": "KMA-USDT", - "KMDBTC": "KMD-BTC", - "KMDUSDT": "KMD-USDT", - "KNCBTC": "KNC-BTC", - "KNCETH": "KNC-ETH", - "KOKUSDT": "KOK-USDT", - "KOLETH": "KOL-ETH", - "KOLUSDT": "KOL-USDT", - "KONOUSDT": "KONO-USDT", - "KRLBTC": "KRL-BTC", - "KRLUSDT": "KRL-USDT", - "KSMBTC": "KSM-BTC", - "KSMUSDT": "KSM-USDT", - "LABSETH": "LABS-ETH", - "LABSUSDT": "LABS-USDT", - "LACEETH": "LACE-ETH", - "LACEUSDT": "LACE-USDT", - "LAYERBTC": "LAYER-BTC", - "LAYERUSDT": "LAYER-USDT", - "LIKEUSDT": "LIKE-USDT", - "LINABTC": "LINA-BTC", - "LINAUSDT": "LINA-USDT", - "LINK3LUSDT": "LINK3L-USDT", - "LINK3SUSDT": "LINK3S-USDT", - "LINKBTC": "LINK-BTC", - "LINKKCS": "LINK-KCS", - "LINKUSDC": "LINK-USDC", - "LINKUSDT": "LINK-USDT", - "LITBTC": "LIT-BTC", - "LITHETH": "LITH-ETH", - "LITHUSDT": "LITH-USDT", - "LITUSDT": "LIT-USDT", - "LNCHXUSDT": "LNCHX-USDT", - "LOCGUSDT": "LOCG-USDT", - "LOCUSDT": "LOC-USDT", - "LOKIBTC": "LOKI-BTC", - "LOKIETH": "LOKI-ETH", - "LOKIUSDT": "LOKI-USDT", - "LONUSDT": "LON-USDT", - "LOOMBTC": "LOOM-BTC", - "LOOMETH": "LOOM-ETH", - "LPOOLBTC": "LPOOL-BTC", - "LPOOLUSDT": "LPOOL-USDT", - "LPTUSDT": "LPT-USDT", - "LRCBTC": "LRC-BTC", - "LRCETH": "LRC-ETH", - "LRCUSDT": "LRC-USDT", - "LSKBTC": "LSK-BTC", - "LSKETH": "LSK-ETH", - "LSSUSDT": "LSS-USDT", - "LTC3LUSDT": "LTC3L-USDT", - "LTC3SUSDT": "LTC3S-USDT", - "LTCBTC": "LTC-BTC", - "LTCETH": "LTC-ETH", - "LTCKCS": "LTC-KCS", - "LTCUSDC": "LTC-USDC", - "LTCUSDT": "LTC-USDT", - "LTOBTC": "LTO-BTC", - "LTOUSDT": "LTO-USDT", - "LTXBTC": "LTX-BTC", - "LTXUSDT": "LTX-USDT", - "LUNA3LUSDT": "LUNA3L-USDT", - "LUNA3SUSDT": "LUNA3S-USDT", - "LUNABTC": "LUNA-BTC", - "LUNAETH": "LUNA-ETH", - "LUNAKCS": "LUNA-KCS", - "LUNAUSDT": "LUNA-USDT", - "LUNAUST": "LUNA-UST", - "LYMBTC": "LYM-BTC", - "LYMETH": "LYM-ETH", - "LYMUSDT": "LYM-USDT", - "LYXEETH": "LYXE-ETH", - "LYXEUSDT": "LYXE-USDT", - "MAHABTC": "MAHA-BTC", - "MAHAUSDT": "MAHA-USDT", - "MAKIBTC": "MAKI-BTC", - "MAKIUSDT": "MAKI-USDT", - "MANA3LUSDT": "MANA3L-USDT", - "MANA3SUSDT": "MANA3S-USDT", - "MANABTC": "MANA-BTC", - "MANAETH": "MANA-ETH", - "MANAUSDT": "MANA-USDT", - "MANBTC": "MAN-BTC", - "MANUSDT": "MAN-USDT", - "MAPBTC": "MAP-BTC", - "MAPUSDT": "MAP-USDT", - "MARSHUSDT": "MARSH-USDT", - "MASKUSDT": "MASK-USDT", - "MATIC3LUSDT": "MATIC3L-USDT", - "MATIC3SUSDT": "MATIC3S-USDT", - "MATICBTC": "MATIC-BTC", - "MATICUSDT": "MATIC-USDT", - "MATICUST": "MATIC-UST", - "MATTERUSDT": "MATTER-USDT", - "MEMUSDT": "MEM-USDT", - "MFTBTC": "MFT-BTC", - "MFTUSDT": "MFT-USDT", - "MHCBTC": "MHC-BTC", - "MHCETH": "MHC-ETH", - "MHCUSDT": "MHC-USDT", - "MIRKCS": "MIR-KCS", - "MIRUSDT": "MIR-USDT", - "MIRUST": "MIR-UST", - "MITXBTC": "MITX-BTC", - "MITXUSDT": "MITX-USDT", - "MKRBTC": "MKR-BTC", - "MKRDAI": "MKR-DAI", - "MKRETH": "MKR-ETH", - "MKRUSDT": "MKR-USDT", - "MLKBTC": "MLK-BTC", - "MLKUSDT": "MLK-USDT", - "MLNBTC": "MLN-BTC", - "MLNUSDT": "MLN-USDT", - "MNETUSDT": "MNET-USDT", - "MNSTUSDT": "MNST-USDT", - "MNWUSDT": "MNW-USDT", - "MODEFIBTC": "MODEFI-BTC", - "MODEFIUSDT": "MODEFI-USDT", - "MONIUSDT": "MONI-USDT", - "MOVRETH": "MOVR-ETH", - "MOVRUSDT": "MOVR-USDT", - "MSWAPBTC": "MSWAP-BTC", - "MSWAPUSDT": "MSWAP-USDT", - "MTLBTC": "MTL-BTC", - "MTLUSDT": "MTL-USDT", - "MTRGUSDT": "MTRG-USDT", - "MTVBTC": "MTV-BTC", - "MTVETH": "MTV-ETH", - "MTVUSDT": "MTV-USDT", - "MVPBTC": "MVP-BTC", - "MVPETH": "MVP-ETH", - "MXCUSDT": "MXC-USDT", - "MXWUSDT": "MXW-USDT", - "NAKAUSDT": "NAKA-USDT", - "NANOBTC": "NANO-BTC", - "NANOETH": "NANO-ETH", - "NANOKCS": "NANO-KCS", - "NANOUSDT": "NANO-USDT", - "NDAUUSDT": "NDAU-USDT", - "NEAR3LUSDT": "NEAR3L-USDT", - "NEAR3SUSDT": "NEAR3S-USDT", - "NEARBTC": "NEAR-BTC", - "NEARUSDT": "NEAR-USDT", - "NEOBTC": "NEO-BTC", - "NEOETH": "NEO-ETH", - "NEOKCS": "NEO-KCS", - "NEOUSDT": "NEO-USDT", - "NFTBUSDT": "NFTB-USDT", - "NFTTRX": "NFT-TRX", - "NFTUSDT": "NFT-USDT", - "NGCUSDT": "NGC-USDT", - "NGLBTC": "NGL-BTC", - "NGLUSDT": "NGL-USDT", - "NGMUSDT": "NGM-USDT", - "NIFUSDT": "NIF-USDT", - "NIMBTC": "NIM-BTC", - "NIMETH": "NIM-ETH", - "NIMUSDT": "NIM-USDT", - "NKNBTC": "NKN-BTC", - "NKNUSDT": "NKN-USDT", - "NMRBTC": "NMR-BTC", - "NMRUSDT": "NMR-USDT", - "NOIABTC": "NOIA-BTC", - "NOIAUSDT": "NOIA-USDT", - "NORDBTC": "NORD-BTC", - "NORDUSDT": "NORD-USDT", - "NRGBTC": "NRG-BTC", - "NRGETH": "NRG-ETH", - "NTVRKUSDC": "NTVRK-USDC", - "NTVRKUSDT": "NTVRK-USDT", - "NUBTC": "NU-BTC", - "NULSBTC": "NULS-BTC", - "NULSETH": "NULS-ETH", - "NUMUSDT": "NUM-USDT", - "NUUSDT": "NU-USDT", - "NWCBTC": "NWC-BTC", - "NWCUSDT": "NWC-USDT", - "OCEANBTC": "OCEAN-BTC", - "OCEANETH": "OCEAN-ETH", - "ODDZUSDT": "ODDZ-USDT", - "OGNBTC": "OGN-BTC", - "OGNUSDT": "OGN-USDT", - "OLTBTC": "OLT-BTC", - "OLTETH": "OLT-ETH", - "OMBTC": "OM-BTC", - "OMGBTC": "OMG-BTC", - "OMGETH": "OMG-ETH", - "OMGUSDT": "OMG-USDT", - "OMUSDT": "OM-USDT", - "ONEBTC": "ONE-BTC", - "ONEUSDT": "ONE-USDT", - "ONTBTC": "ONT-BTC", - "ONTETH": "ONT-ETH", - "ONTUSDT": "ONT-USDT", - "OOEUSDT": "OOE-USDT", - "OPCTBTC": "OPCT-BTC", - "OPCTETH": "OPCT-ETH", - "OPCTUSDT": "OPCT-USDT", - "OPULUSDT": "OPUL-USDT", - "ORAIUSDT": "ORAI-USDT", - "ORBSBTC": "ORBS-BTC", - "ORBSUSDT": "ORBS-USDT", - "ORNUSDT": "ORN-USDT", - "OUSDBTC": "OUSD-BTC", - "OUSDUSDT": "OUSD-USDT", - "OXTBTC": "OXT-BTC", - "OXTETH": "OXT-ETH", - "OXTUSDT": "OXT-USDT", - "PAXGBTC": "PAXG-BTC", - "PAXGUSDT": "PAXG-USDT", - "PBRUSDT": "PBR-USDT", - "PBXUSDT": "PBX-USDT", - "PCXBTC": "PCX-BTC", - "PCXUSDT": "PCX-USDT", - "PDEXBTC": "PDEX-BTC", - "PDEXUSDT": "PDEX-USDT", - "PELUSDT": "PEL-USDT", - "PERPBTC": "PERP-BTC", - "PERPUSDT": "PERP-USDT", - "PHAETH": "PHA-ETH", - "PHAUSDT": "PHA-USDT", - "PHNXBTC": "PHNX-BTC", - "PHNXUSDT": "PHNX-USDT", - "PIVXBTC": "PIVX-BTC", - "PIVXETH": "PIVX-ETH", - "PIVXUSDT": "PIVX-USDT", - "PLAYBTC": "PLAY-BTC", - "PLAYETH": "PLAY-ETH", - "PLUUSDT": "PLU-USDT", - "PMONUSDT": "PMON-USDT", - "PNTBTC": "PNT-BTC", - "PNTUSDT": "PNT-USDT", - "POLCUSDT": "POLC-USDT", - "POLKBTC": "POLK-BTC", - "POLKUSDT": "POLK-USDT", - "POLSBTC": "POLS-BTC", - "POLSUSDT": "POLS-USDT", - "POLUSDT": "POL-USDT", - "POLXUSDT": "POLX-USDT", - "PONDBTC": "POND-BTC", - "PONDUSDT": "POND-USDT", - "POWRBTC": "POWR-BTC", - "POWRETH": "POWR-ETH", - "PPTBTC": "PPT-BTC", - "PPTETH": "PPT-ETH", - "PREBTC": "PRE-BTC", - "PREUSDT": "PRE-USDT", - "PROMBTC": "PROM-BTC", - "PROMUSDT": "PROM-USDT", - "PRQUSDT": "PRQ-USDT", - "PUNDIXBTC": "PUNDIX-BTC", - "PUNDIXUSDT": "PUNDIX-USDT", - "PUSHBTC": "PUSH-BTC", - "PUSHUSDT": "PUSH-USDT", - "PYRBTC": "PYR-BTC", - "PYRUSDT": "PYR-USDT", - "QIBTC": "QI-BTC", - "QIUSDT": "QI-USDT", - "QKCBTC": "QKC-BTC", - "QKCETH": "QKC-ETH", - "QNTUSDT": "QNT-USDT", - "QRDOETH": "QRDO-ETH", - "QRDOUSDT": "QRDO-USDT", - "QTUMBTC": "QTUM-BTC", - "QUICKBTC": "QUICK-BTC", - "QUICKUSDT": "QUICK-USDT", - "RBTCBTC": "RBTC-BTC", - "REAPUSDT": "REAP-USDT", - "REEFBTC": "REEF-BTC", - "REEFUSDT": "REEF-USDT", - "RENUSDT": "REN-USDT", - "REPBTC": "REP-BTC", - "REPETH": "REP-ETH", - "REPUSDT": "REP-USDT", - "REQBTC": "REQ-BTC", - "REQETH": "REQ-ETH", - "REQUSDT": "REQ-USDT", - "REVVBTC": "REVV-BTC", - "REVVUSDT": "REVV-USDT", - "RFOXUSDT": "RFOX-USDT", - "RFUELUSDT": "RFUEL-USDT", - "RIFBTC": "RIF-BTC", - "RLCBTC": "RLC-BTC", - "RLCUSDT": "RLC-USDT", - "RLYUSDT": "RLY-USDT", - "RMRKUSDT": "RMRK-USDT", - "RNDRBTC": "RNDR-BTC", - "RNDRUSDT": "RNDR-USDT", - "ROOBEEBTC": "ROOBEE-BTC", - "ROSEUSDT": "ROSE-USDT", - "ROSNUSDT": "ROSN-USDT", - "ROUTEUSDT": "ROUTE-USDT", - "RSRBTC": "RSR-BTC", - "RSRUSDT": "RSR-USDT", - "RUNEBTC": "RUNE-BTC", - "RUNEUSDT": "RUNE-USDT", - "RUSDT": "R-USDT", - "SAND3LUSDT": "SAND3L-USDT", - "SAND3SUSDT": "SAND3S-USDT", - "SANDUSDT": "SAND-USDT", - "SCLPBTC": "SCLP-BTC", - "SCLPUSDT": "SCLP-USDT", - "SDAOETH": "SDAO-ETH", - "SDAOUSDT": "SDAO-USDT", - "SDNETH": "SDN-ETH", - "SDNUSDT": "SDN-USDT", - "SENSOBTC": "SENSO-BTC", - "SENSOUSDT": "SENSO-USDT", - "SFPBTC": "SFP-BTC", - "SFPUSDT": "SFP-USDT", - "SFUNDUSDT": "SFUND-USDT", - "SHABTC": "SHA-BTC", - "SHAUSDT": "SHA-USDT", - "SHFTBTC": "SHFT-BTC", - "SHFTUSDT": "SHFT-USDT", - "SHIBDOGE": "SHIB-DOGE", - "SHIBUSDT": "SHIB-USDT", - "SHILLUSDT": "SHILL-USDT", - "SHRBTC": "SHR-BTC", - "SHRUSDT": "SHR-USDT", - "SKEYUSDT": "SKEY-USDT", - "SKLBTC": "SKL-BTC", - "SKLUSDT": "SKL-USDT", - "SKUBTC": "SKU-BTC", - "SKUUSDT": "SKU-USDT", - "SLIMUSDT": "SLIM-USDT", - "SLPUSDT": "SLP-USDT", - "SNTBTC": "SNT-BTC", - "SNTETH": "SNT-ETH", - "SNTVTBTC": "SNTVT-BTC", - "SNTVTETH": "SNTVT-ETH", - "SNXBTC": "SNX-BTC", - "SNXETH": "SNX-ETH", - "SNXUSDT": "SNX-USDT", - "SNXUST": "SNX-UST", - "SOL3LUSDT": "SOL3L-USDT", - "SOL3SUSDT": "SOL3S-USDT", - "SOLRUSDT": "SOLR-USDT", - "SOLUSDT": "SOL-USDT", - "SOLUST": "SOL-UST", - "SOLVEBTC": "SOLVE-BTC", - "SOLVEUSDT": "SOLVE-USDT", - "SOULBTC": "SOUL-BTC", - "SOULETH": "SOUL-ETH", - "SOULUSDT": "SOUL-USDT", - "SOVUSDT": "SOV-USDT", - "SPIUSDT": "SPI-USDT", - "SRKBTC": "SRK-BTC", - "SRKUSDT": "SRK-USDT", - "SRMBTC": "SRM-BTC", - "SRMUSDT": "SRM-USDT", - "STCBTC": "STC-BTC", - "STCUSDT": "STC-USDT", - "STMXUSDT": "STMX-USDT", - "STNDETH": "STND-ETH", - "STNDUSDT": "STND-USDT", - "STORJBTC": "STORJ-BTC", - "STORJETH": "STORJ-ETH", - "STORJUSDT": "STORJ-USDT", - "STRKBTC": "STRK-BTC", - "STRKETH": "STRK-ETH", - "STRONGUSDT": "STRONG-USDT", - "STXBTC": "STX-BTC", - "STXUSDT": "STX-USDT", - "SUKUBTC": "SUKU-BTC", - "SUKUUSDT": "SUKU-USDT", - "SUNUSDT": "SUN-USDT", - "SUPERBTC": "SUPER-BTC", - "SUPERUSDT": "SUPER-USDT", - "SUSDBTC": "SUSD-BTC", - "SUSDETH": "SUSD-ETH", - "SUSDUSDT": "SUSD-USDT", - "SUSHI3LUSDT": "SUSHI3L-USDT", - "SUSHI3SUSDT": "SUSHI3S-USDT", - "SUSHIUSDT": "SUSHI-USDT", - "SUTERBTC": "SUTER-BTC", - "SUTERUSDT": "SUTER-USDT", - "SWASHUSDT": "SWASH-USDT", - "SWINGBYBTC": "SWINGBY-BTC", - "SWINGBYUSDT": "SWINGBY-USDT", - "SWPUSDT": "SWP-USDT", - "SXPBTC": "SXP-BTC", - "SXPUSDT": "SXP-USDT", - "SYLOUSDT": "SYLO-USDT", - "TARAETH": "TARA-ETH", - "TARAUSDT": "TARA-USDT", - "TCPUSDT": "TCP-USDT", - "TELBTC": "TEL-BTC", - "TELETH": "TEL-ETH", - "TELUSDT": "TEL-USDT", - "THETAUSDT": "THETA-USDT", - "TIDALUSDT": "TIDAL-USDT", - "TIMEBTC": "TIME-BTC", - "TIMEETH": "TIME-ETH", - "TKOBTC": "TKO-BTC", - "TKOUSDT": "TKO-USDT", - "TKYBTC": "TKY-BTC", - "TKYETH": "TKY-ETH", - "TKYUSDT": "TKY-USDT", - "TLMBTC": "TLM-BTC", - "TLMETH": "TLM-ETH", - "TLMUSDT": "TLM-USDT", - "TLOSBTC": "TLOS-BTC", - "TLOSUSDT": "TLOS-USDT", - "TOKOBTC": "TOKO-BTC", - "TOKOKCS": "TOKO-KCS", - "TOKOUSDT": "TOKO-USDT", - "TOMOBTC": "TOMO-BTC", - "TOMOETH": "TOMO-ETH", - "TOMOUSDT": "TOMO-USDT", - "TONEBTC": "TONE-BTC", - "TONEETH": "TONE-ETH", - "TONEUSDT": "TONE-USDT", - "TOWERBTC": "TOWER-BTC", - "TOWERUSDT": "TOWER-USDT", - "TRACBTC": "TRAC-BTC", - "TRACETH": "TRAC-ETH", - "TRADEBTC": "TRADE-BTC", - "TRADEUSDT": "TRADE-USDT", - "TRBBTC": "TRB-BTC", - "TRBUSDT": "TRB-USDT", - "TRIASBTC": "TRIAS-BTC", - "TRIASUSDT": "TRIAS-USDT", - "TRIBEUSDT": "TRIBE-USDT", - "TRUBTC": "TRU-BTC", - "TRUUSDT": "TRU-USDT", - "TRVLUSDT": "TRVL-USDT", - "TRXBTC": "TRX-BTC", - "TRXETH": "TRX-ETH", - "TRXKCS": "TRX-KCS", - "TRXUSDT": "TRX-USDT", - "TVKBTC": "TVK-BTC", - "TVKUSDT": "TVK-USDT", - "TWTBTC": "TWT-BTC", - "TWTUSDT": "TWT-USDT", - "TXAUSDC": "TXA-USDC", - "TXAUSDT": "TXA-USDT", - "UBXETH": "UBX-ETH", - "UBXTUSDT": "UBXT-USDT", - "UBXUSDT": "UBX-USDT", - "UDOOETH": "UDOO-ETH", - "UFOUSDT": "UFO-USDT", - "UMAUSDT": "UMA-USDT", - "UMBUSDT": "UMB-USDT", - "UNBUSDT": "UNB-USDT", - "UNFIUSDT": "UNFI-USDT", - "UNI3LUSDT": "UNI3L-USDT", - "UNI3SUSDT": "UNI3S-USDT", - "UNICUSDT": "UNIC-USDT", - "UNIKCS": "UNI-KCS", - "UNIUSDT": "UNI-USDT", - "UNOBTC": "UNO-BTC", - "UNOUSDT": "UNO-USDT", - "UOSBTC": "UOS-BTC", - "UOSUSDT": "UOS-USDT", - "UQCBTC": "UQC-BTC", - "UQCETH": "UQC-ETH", - "USDCUSDT": "USDC-USDT", - "USDCUST": "USDC-UST", - "USDJUSDT": "USDJ-USDT", - "USDNUSDT": "USDN-USDT", - "USDTDAI": "USDT-DAI", - "USDTPAX": "USDT-PAX", - "USDTTUSD": "USDT-TUSD", - "USDTUSDC": "USDT-USDC", - "USDTUST": "USDT-UST", - "UTKBTC": "UTK-BTC", - "UTKETH": "UTK-ETH", - "VAIUSDT": "VAI-USDT", - "VEEDBTC": "VEED-BTC", - "VEEDUSDT": "VEED-USDT", - "VEGAETH": "VEGA-ETH", - "VEGAUSDT": "VEGA-USDT", - "VELOUSDT": "VELO-USDT", - "VET3LUSDT": "VET3L-USDT", - "VET3SUSDT": "VET3S-USDT", - "VETBTC": "VET-BTC", - "VETETH": "VET-ETH", - "VETKCS": "VET-KCS", - "VETUSDT": "VET-USDT", - "VIDBTC": "VID-BTC", - "VIDTBTC": "VIDT-BTC", - "VIDTUSDT": "VIDT-USDT", - "VIDUSDT": "VID-USDT", - "VLXBTC": "VLX-BTC", - "VLXUSDT": "VLX-USDT", - "VRABTC": "VRA-BTC", - "VRAUSDT": "VRA-USDT", - "VRUSDT": "VR-USDT", - "VSYSBTC": "VSYS-BTC", - "VSYSUSDT": "VSYS-USDT", - "VXVUSDT": "VXV-USDT", - "WANBTC": "WAN-BTC", - "WANETH": "WAN-ETH", - "WAVESBTC": "WAVES-BTC", - "WAVESUSDT": "WAVES-USDT", - "WAXBTC": "WAX-BTC", - "WAXETH": "WAX-ETH", - "WAXUSDT": "WAX-USDT", - "WBTCBTC": "WBTC-BTC", - "WBTCETH": "WBTC-ETH", - "WESTBTC": "WEST-BTC", - "WESTUSDT": "WEST-USDT", - "WILDUSDT": "WILD-USDT", - "WINBTC": "WIN-BTC", - "WINTRX": "WIN-TRX", - "WINUSDT": "WIN-USDT", - "WNCGBTC": "WNCG-BTC", - "WNCGUSDT": "WNCG-USDT", - "WNXMBTC": "WNXM-BTC", - "WNXMUSDT": "WNXM-USDT", - "WOMUSDT": "WOM-USDT", - "WOOUSDT": "WOO-USDT", - "WRXBTC": "WRX-BTC", - "WRXUSDT": "WRX-USDT", - "WSIENNAUSDT": "WSIENNA-USDT", - "WTCBTC": "WTC-BTC", - "WXTBTC": "WXT-BTC", - "WXTUSDT": "WXT-USDT", - "XAVAUSDT": "XAVA-USDT", - "XCADUSDT": "XCAD-USDT", - "XCHUSDT": "XCH-USDT", - "XCURBTC": "XCUR-BTC", - "XCURUSDT": "XCUR-USDT", - "XDBBTC": "XDB-BTC", - "XDBUSDT": "XDB-USDT", - "XDCBTC": "XDC-BTC", - "XDCETH": "XDC-ETH", - "XDCUSDT": "XDC-USDT", - "XECUSDT": "XEC-USDT", - "XEDBTC": "XED-BTC", - "XEDUSDT": "XED-USDT", - "XEMBTC": "XEM-BTC", - "XEMUSDT": "XEM-USDT", - "XHVBTC": "XHV-BTC", - "XHVUSDT": "XHV-USDT", - "XLMBTC": "XLM-BTC", - "XLMETH": "XLM-ETH", - "XLMKCS": "XLM-KCS", - "XLMUSDT": "XLM-USDT", - "XMRBTC": "XMR-BTC", - "XMRETH": "XMR-ETH", - "XMRUSDT": "XMR-USDT", - "XNLUSDT": "XNL-USDT", - "XPRBTC": "XPR-BTC", - "XPRTUSDT": "XPRT-USDT", - "XPRUSDT": "XPR-USDT", - "XRP3LUSDT": "XRP3L-USDT", - "XRP3SUSDT": "XRP3S-USDT", - "XRPBTC": "XRP-BTC", - "XRPETH": "XRP-ETH", - "XRPKCS": "XRP-KCS", - "XRPPAX": "XRP-PAX", - "XRPTUSD": "XRP-TUSD", - "XRPUSDC": "XRP-USDC", - "XRPUSDT": "XRP-USDT", - "XSRUSDT": "XSR-USDT", - "XTAGUSDT": "XTAG-USDT", - "XTMUSDT": "XTM-USDT", - "XTZBTC": "XTZ-BTC", - "XTZKCS": "XTZ-KCS", - "XTZUSDT": "XTZ-USDT", - "XVSBTC": "XVS-BTC", - "XVSUSDT": "XVS-USDT", - "XYMBTC": "XYM-BTC", - "XYMUSDT": "XYM-USDT", - "XYOBTC": "XYO-BTC", - "XYOETH": "XYO-ETH", - "XYOUSDT": "XYO-USDT", - "YFDAIBTC": "YFDAI-BTC", - "YFDAIUSDT": "YFDAI-USDT", - "YFIUSDT": "YFI-USDT", - "YFIUST": "YFI-UST", - "YGGUSDT": "YGG-USDT", - "YLDUSDT": "YLD-USDT", - "YOPETH": "YOP-ETH", - "YOPUSDT": "YOP-USDT", - "ZCXBTC": "ZCX-BTC", - "ZCXUSDT": "ZCX-USDT", - "ZECBTC": "ZEC-BTC", - "ZECKCS": "ZEC-KCS", - "ZECUSDT": "ZEC-USDT", - "ZEEUSDT": "ZEE-USDT", - "ZENUSDT": "ZEN-USDT", - "ZILBTC": "ZIL-BTC", - "ZILETH": "ZIL-ETH", - "ZILUSDT": "ZIL-USDT", - "ZKTUSDT": "ZKT-USDT", - "ZORTUSDT": "ZORT-USDT", - "ZRXBTC": "ZRX-BTC", - "ZRXETH": "ZRX-ETH", + "FORMETH": "FORM-ETH", + "FORMUSDT": "FORM-USDT", + "FORTHUSDT": "FORTH-USDT", + "FRMUSDT": "FRM-USDT", + "FRONTBTC": "FRONT-BTC", + "FRONTUSDT": "FRONT-USDT", + "FTGUSDT": "FTG-USDT", + "FTM3LUSDT": "FTM3L-USDT", + "FTM3SUSDT": "FTM3S-USDT", + "FTMBTC": "FTM-BTC", + "FTMETH": "FTM-ETH", + "FTMUSDT": "FTM-USDT", + "FTTBTC": "FTT-BTC", + "FTTUSDT": "FTT-USDT", + "FXBTC": "FX-BTC", + "FXETH": "FX-ETH", + "FXSBTC": "FXS-BTC", + "FXSUSDT": "FXS-USDT", + "GAFIUSDT": "GAFI-USDT", + "GALAX3LUSDT": "GALAX3L-USDT", + "GALAX3SUSDT": "GALAX3S-USDT", + "GALAXUSDT": "GALAX-USDT", + "GASBTC": "GAS-BTC", + "GASUSDT": "GAS-USDT", + "GEEQUSDT": "GEEQ-USDT", + "GENSUSDT": "GENS-USDT", + "GHSTBTC": "GHST-BTC", + "GHSTUSDT": "GHST-USDT", + "GHXUSDT": "GHX-USDT", + "GLCHUSDT": "GLCH-USDT", + "GLMBTC": "GLM-BTC", + "GLMUSDT": "GLM-USDT", + "GLQBTC": "GLQ-BTC", + "GLQUSDT": "GLQ-USDT", + "GMBBTC": "GMB-BTC", + "GMBETH": "GMB-ETH", + "GMBUSDT": "GMB-USDT", + "GMEEUSDT": "GMEE-USDT", + "GOBTC": "GO-BTC", + "GODSUSDT": "GODS-USDT", + "GOETH": "GO-ETH", + "GOM2BTC": "GOM2-BTC", + "GOM2USDT": "GOM2-USDT", + "GOUSDT": "GO-USDT", + "GOVIBTC": "GOVI-BTC", + "GOVIUSDT": "GOVI-USDT", + "GRINBTC": "GRIN-BTC", + "GRINETH": "GRIN-ETH", + "GRINUSDT": "GRIN-USDT", + "GRTKCS": "GRT-KCS", + "GRTUSDT": "GRT-USDT", + "GSPIUSDT": "GSPI-USDT", + "GTCBTC": "GTC-BTC", + "GTCUSDT": "GTC-USDT", + "H3RO3SUSDT": "H3RO3S-USDT", + "HAIBTC": "HAI-BTC", + "HAIUSDT": "HAI-USDT", + "HAKAUSDT": "HAKA-USDT", + "HAPIUSDT": "HAPI-USDT", + "HARDUSDT": "HARD-USDT", + "HBARBTC": "HBAR-BTC", + "HBARUSDT": "HBAR-USDT", + "HEARTBTC": "HEART-BTC", + "HEARTUSDT": "HEART-USDT", + "HEGICBTC": "HEGIC-BTC", + "HEGICUSDT": "HEGIC-USDT", + "HEROUSDT": "HERO-USDT", + "HORDUSDT": "HORD-USDT", + "HOTCROSSUSDT": "HOTCROSS-USDT", + "HPBBTC": "HPB-BTC", + "HPBETH": "HPB-ETH", + "HTRBTC": "HTR-BTC", + "HTRUSDT": "HTR-USDT", + "HTUSDT": "HT-USDT", + "HYDRAUSDT": "HYDRA-USDT", + "HYVEBTC": "HYVE-BTC", + "HYVEUSDT": "HYVE-USDT", + "ICPBTC": "ICP-BTC", + "ICPUSDT": "ICP-USDT", + "IDEAUSDT": "IDEA-USDT", + "ILAUSDT": "ILA-USDT", + "ILVUSDT": "ILV-USDT", + "IMXUSDT": "IMX-USDT", + "INJBTC": "INJ-BTC", + "INJUSDT": "INJ-USDT", + "IOIUSDT": "IOI-USDT", + "IOSTBTC": "IOST-BTC", + "IOSTETH": "IOST-ETH", + "IOSTUSDT": "IOST-USDT", + "IOTXBTC": "IOTX-BTC", + "IOTXETH": "IOTX-ETH", + "IOTXUSDT": "IOTX-USDT", + "ISPUSDT": "ISP-USDT", + "IXSUSDT": "IXS-USDT", + "JARBTC": "JAR-BTC", + "JARUSDT": "JAR-USDT", + "JASMYUSDT": "JASMY-USDT", + "JSTUSDT": "JST-USDT", + "JUPETH": "JUP-ETH", + "JUPUSDT": "JUP-USDT", + "KAIBTC": "KAI-BTC", + "KAIETH": "KAI-ETH", + "KAIUSDT": "KAI-USDT", + "KARUSDT": "KAR-USDT", + "KATBTC": "KAT-BTC", + "KATUSDT": "KAT-USDT", + "KAVAUSDT": "KAVA-USDT", + "KCSBTC": "KCS-BTC", + "KCSETH": "KCS-ETH", + "KCSUSDT": "KCS-USDT", + "KDABTC": "KDA-BTC", + "KDAUSDT": "KDA-USDT", + "KDONUSDT": "KDON-USDT", + "KEEPBTC": "KEEP-BTC", + "KEEPUSDT": "KEEP-USDT", + "KEYBTC": "KEY-BTC", + "KEYETH": "KEY-ETH", + "KINUSDT": "KIN-USDT", + "KLAYBTC": "KLAY-BTC", + "KLAYUSDT": "KLAY-USDT", + "KLVBTC": "KLV-BTC", + "KLVTRX": "KLV-TRX", + "KLVUSDT": "KLV-USDT", + "KMAUSDT": "KMA-USDT", + "KMDBTC": "KMD-BTC", + "KMDUSDT": "KMD-USDT", + "KNCBTC": "KNC-BTC", + "KNCETH": "KNC-ETH", + "KOKUSDT": "KOK-USDT", + "KOLETH": "KOL-ETH", + "KOLUSDT": "KOL-USDT", + "KONOUSDT": "KONO-USDT", + "KRLBTC": "KRL-BTC", + "KRLUSDT": "KRL-USDT", + "KSMBTC": "KSM-BTC", + "KSMUSDT": "KSM-USDT", + "LABSETH": "LABS-ETH", + "LABSUSDT": "LABS-USDT", + "LACEETH": "LACE-ETH", + "LACEUSDT": "LACE-USDT", + "LAYERBTC": "LAYER-BTC", + "LAYERUSDT": "LAYER-USDT", + "LIKEUSDT": "LIKE-USDT", + "LINABTC": "LINA-BTC", + "LINAUSDT": "LINA-USDT", + "LINK3LUSDT": "LINK3L-USDT", + "LINK3SUSDT": "LINK3S-USDT", + "LINKBTC": "LINK-BTC", + "LINKKCS": "LINK-KCS", + "LINKUSDC": "LINK-USDC", + "LINKUSDT": "LINK-USDT", + "LITBTC": "LIT-BTC", + "LITHETH": "LITH-ETH", + "LITHUSDT": "LITH-USDT", + "LITUSDT": "LIT-USDT", + "LNCHXUSDT": "LNCHX-USDT", + "LOCGUSDT": "LOCG-USDT", + "LOCUSDT": "LOC-USDT", + "LOKIBTC": "LOKI-BTC", + "LOKIETH": "LOKI-ETH", + "LOKIUSDT": "LOKI-USDT", + "LONUSDT": "LON-USDT", + "LOOMBTC": "LOOM-BTC", + "LOOMETH": "LOOM-ETH", + "LPOOLBTC": "LPOOL-BTC", + "LPOOLUSDT": "LPOOL-USDT", + "LPTUSDT": "LPT-USDT", + "LRCBTC": "LRC-BTC", + "LRCETH": "LRC-ETH", + "LRCUSDT": "LRC-USDT", + "LSKBTC": "LSK-BTC", + "LSKETH": "LSK-ETH", + "LSSUSDT": "LSS-USDT", + "LTC3LUSDT": "LTC3L-USDT", + "LTC3SUSDT": "LTC3S-USDT", + "LTCBTC": "LTC-BTC", + "LTCETH": "LTC-ETH", + "LTCKCS": "LTC-KCS", + "LTCUSDC": "LTC-USDC", + "LTCUSDT": "LTC-USDT", + "LTOBTC": "LTO-BTC", + "LTOUSDT": "LTO-USDT", + "LTXBTC": "LTX-BTC", + "LTXUSDT": "LTX-USDT", + "LUNA3LUSDT": "LUNA3L-USDT", + "LUNA3SUSDT": "LUNA3S-USDT", + "LUNABTC": "LUNA-BTC", + "LUNAETH": "LUNA-ETH", + "LUNAKCS": "LUNA-KCS", + "LUNAUSDT": "LUNA-USDT", + "LUNAUST": "LUNA-UST", + "LYMBTC": "LYM-BTC", + "LYMETH": "LYM-ETH", + "LYMUSDT": "LYM-USDT", + "LYXEETH": "LYXE-ETH", + "LYXEUSDT": "LYXE-USDT", + "MAHABTC": "MAHA-BTC", + "MAHAUSDT": "MAHA-USDT", + "MAKIBTC": "MAKI-BTC", + "MAKIUSDT": "MAKI-USDT", + "MANA3LUSDT": "MANA3L-USDT", + "MANA3SUSDT": "MANA3S-USDT", + "MANABTC": "MANA-BTC", + "MANAETH": "MANA-ETH", + "MANAUSDT": "MANA-USDT", + "MANBTC": "MAN-BTC", + "MANUSDT": "MAN-USDT", + "MAPBTC": "MAP-BTC", + "MAPUSDT": "MAP-USDT", + "MARSHUSDT": "MARSH-USDT", + "MASKUSDT": "MASK-USDT", + "MATIC3LUSDT": "MATIC3L-USDT", + "MATIC3SUSDT": "MATIC3S-USDT", + "MATICBTC": "MATIC-BTC", + "MATICUSDT": "MATIC-USDT", + "MATICUST": "MATIC-UST", + "MATTERUSDT": "MATTER-USDT", + "MEMUSDT": "MEM-USDT", + "MFTBTC": "MFT-BTC", + "MFTUSDT": "MFT-USDT", + "MHCBTC": "MHC-BTC", + "MHCETH": "MHC-ETH", + "MHCUSDT": "MHC-USDT", + "MIRKCS": "MIR-KCS", + "MIRUSDT": "MIR-USDT", + "MIRUST": "MIR-UST", + "MITXBTC": "MITX-BTC", + "MITXUSDT": "MITX-USDT", + "MKRBTC": "MKR-BTC", + "MKRDAI": "MKR-DAI", + "MKRETH": "MKR-ETH", + "MKRUSDT": "MKR-USDT", + "MLKBTC": "MLK-BTC", + "MLKUSDT": "MLK-USDT", + "MLNBTC": "MLN-BTC", + "MLNUSDT": "MLN-USDT", + "MNETUSDT": "MNET-USDT", + "MNSTUSDT": "MNST-USDT", + "MNWUSDT": "MNW-USDT", + "MODEFIBTC": "MODEFI-BTC", + "MODEFIUSDT": "MODEFI-USDT", + "MONIUSDT": "MONI-USDT", + "MOVRETH": "MOVR-ETH", + "MOVRUSDT": "MOVR-USDT", + "MSWAPBTC": "MSWAP-BTC", + "MSWAPUSDT": "MSWAP-USDT", + "MTLBTC": "MTL-BTC", + "MTLUSDT": "MTL-USDT", + "MTRGUSDT": "MTRG-USDT", + "MTVBTC": "MTV-BTC", + "MTVETH": "MTV-ETH", + "MTVUSDT": "MTV-USDT", + "MVPBTC": "MVP-BTC", + "MVPETH": "MVP-ETH", + "MXCUSDT": "MXC-USDT", + "MXWUSDT": "MXW-USDT", + "NAKAUSDT": "NAKA-USDT", + "NANOBTC": "NANO-BTC", + "NANOETH": "NANO-ETH", + "NANOKCS": "NANO-KCS", + "NANOUSDT": "NANO-USDT", + "NDAUUSDT": "NDAU-USDT", + "NEAR3LUSDT": "NEAR3L-USDT", + "NEAR3SUSDT": "NEAR3S-USDT", + "NEARBTC": "NEAR-BTC", + "NEARUSDT": "NEAR-USDT", + "NEOBTC": "NEO-BTC", + "NEOETH": "NEO-ETH", + "NEOKCS": "NEO-KCS", + "NEOUSDT": "NEO-USDT", + "NFTBUSDT": "NFTB-USDT", + "NFTTRX": "NFT-TRX", + "NFTUSDT": "NFT-USDT", + "NGCUSDT": "NGC-USDT", + "NGLBTC": "NGL-BTC", + "NGLUSDT": "NGL-USDT", + "NGMUSDT": "NGM-USDT", + "NIFUSDT": "NIF-USDT", + "NIMBTC": "NIM-BTC", + "NIMETH": "NIM-ETH", + "NIMUSDT": "NIM-USDT", + "NKNBTC": "NKN-BTC", + "NKNUSDT": "NKN-USDT", + "NMRBTC": "NMR-BTC", + "NMRUSDT": "NMR-USDT", + "NOIABTC": "NOIA-BTC", + "NOIAUSDT": "NOIA-USDT", + "NORDBTC": "NORD-BTC", + "NORDUSDT": "NORD-USDT", + "NRGBTC": "NRG-BTC", + "NRGETH": "NRG-ETH", + "NTVRKUSDC": "NTVRK-USDC", + "NTVRKUSDT": "NTVRK-USDT", + "NUBTC": "NU-BTC", + "NULSBTC": "NULS-BTC", + "NULSETH": "NULS-ETH", + "NUMUSDT": "NUM-USDT", + "NUUSDT": "NU-USDT", + "NWCBTC": "NWC-BTC", + "NWCUSDT": "NWC-USDT", + "OCEANBTC": "OCEAN-BTC", + "OCEANETH": "OCEAN-ETH", + "ODDZUSDT": "ODDZ-USDT", + "OGNBTC": "OGN-BTC", + "OGNUSDT": "OGN-USDT", + "OLTBTC": "OLT-BTC", + "OLTETH": "OLT-ETH", + "OMBTC": "OM-BTC", + "OMGBTC": "OMG-BTC", + "OMGETH": "OMG-ETH", + "OMGUSDT": "OMG-USDT", + "OMUSDT": "OM-USDT", + "ONEBTC": "ONE-BTC", + "ONEUSDT": "ONE-USDT", + "ONTBTC": "ONT-BTC", + "ONTETH": "ONT-ETH", + "ONTUSDT": "ONT-USDT", + "OOEUSDT": "OOE-USDT", + "OPCTBTC": "OPCT-BTC", + "OPCTETH": "OPCT-ETH", + "OPCTUSDT": "OPCT-USDT", + "OPULUSDT": "OPUL-USDT", + "ORAIUSDT": "ORAI-USDT", + "ORBSBTC": "ORBS-BTC", + "ORBSUSDT": "ORBS-USDT", + "ORNUSDT": "ORN-USDT", + "OUSDBTC": "OUSD-BTC", + "OUSDUSDT": "OUSD-USDT", + "OXTBTC": "OXT-BTC", + "OXTETH": "OXT-ETH", + "OXTUSDT": "OXT-USDT", + "PAXGBTC": "PAXG-BTC", + "PAXGUSDT": "PAXG-USDT", + "PBRUSDT": "PBR-USDT", + "PBXUSDT": "PBX-USDT", + "PCXBTC": "PCX-BTC", + "PCXUSDT": "PCX-USDT", + "PDEXBTC": "PDEX-BTC", + "PDEXUSDT": "PDEX-USDT", + "PELUSDT": "PEL-USDT", + "PERPBTC": "PERP-BTC", + "PERPUSDT": "PERP-USDT", + "PHAETH": "PHA-ETH", + "PHAUSDT": "PHA-USDT", + "PHNXBTC": "PHNX-BTC", + "PHNXUSDT": "PHNX-USDT", + "PIVXBTC": "PIVX-BTC", + "PIVXETH": "PIVX-ETH", + "PIVXUSDT": "PIVX-USDT", + "PLAYBTC": "PLAY-BTC", + "PLAYETH": "PLAY-ETH", + "PLUUSDT": "PLU-USDT", + "PMONUSDT": "PMON-USDT", + "PNTBTC": "PNT-BTC", + "PNTUSDT": "PNT-USDT", + "POLCUSDT": "POLC-USDT", + "POLKBTC": "POLK-BTC", + "POLKUSDT": "POLK-USDT", + "POLSBTC": "POLS-BTC", + "POLSUSDT": "POLS-USDT", + "POLUSDT": "POL-USDT", + "POLXUSDT": "POLX-USDT", + "PONDBTC": "POND-BTC", + "PONDUSDT": "POND-USDT", + "POWRBTC": "POWR-BTC", + "POWRETH": "POWR-ETH", + "PPTBTC": "PPT-BTC", + "PPTETH": "PPT-ETH", + "PREBTC": "PRE-BTC", + "PREUSDT": "PRE-USDT", + "PROMBTC": "PROM-BTC", + "PROMUSDT": "PROM-USDT", + "PRQUSDT": "PRQ-USDT", + "PUNDIXBTC": "PUNDIX-BTC", + "PUNDIXUSDT": "PUNDIX-USDT", + "PUSHBTC": "PUSH-BTC", + "PUSHUSDT": "PUSH-USDT", + "PYRBTC": "PYR-BTC", + "PYRUSDT": "PYR-USDT", + "QIBTC": "QI-BTC", + "QIUSDT": "QI-USDT", + "QKCBTC": "QKC-BTC", + "QKCETH": "QKC-ETH", + "QNTUSDT": "QNT-USDT", + "QRDOETH": "QRDO-ETH", + "QRDOUSDT": "QRDO-USDT", + "QTUMBTC": "QTUM-BTC", + "QUICKBTC": "QUICK-BTC", + "QUICKUSDT": "QUICK-USDT", + "RBTCBTC": "RBTC-BTC", + "REAPUSDT": "REAP-USDT", + "REEFBTC": "REEF-BTC", + "REEFUSDT": "REEF-USDT", + "RENUSDT": "REN-USDT", + "REPBTC": "REP-BTC", + "REPETH": "REP-ETH", + "REPUSDT": "REP-USDT", + "REQBTC": "REQ-BTC", + "REQETH": "REQ-ETH", + "REQUSDT": "REQ-USDT", + "REVVBTC": "REVV-BTC", + "REVVUSDT": "REVV-USDT", + "RFOXUSDT": "RFOX-USDT", + "RFUELUSDT": "RFUEL-USDT", + "RIFBTC": "RIF-BTC", + "RLCBTC": "RLC-BTC", + "RLCUSDT": "RLC-USDT", + "RLYUSDT": "RLY-USDT", + "RMRKUSDT": "RMRK-USDT", + "RNDRBTC": "RNDR-BTC", + "RNDRUSDT": "RNDR-USDT", + "ROOBEEBTC": "ROOBEE-BTC", + "ROSEUSDT": "ROSE-USDT", + "ROSNUSDT": "ROSN-USDT", + "ROUTEUSDT": "ROUTE-USDT", + "RSRBTC": "RSR-BTC", + "RSRUSDT": "RSR-USDT", + "RUNEBTC": "RUNE-BTC", + "RUNEUSDT": "RUNE-USDT", + "RUSDT": "R-USDT", + "SAND3LUSDT": "SAND3L-USDT", + "SAND3SUSDT": "SAND3S-USDT", + "SANDUSDT": "SAND-USDT", + "SCLPBTC": "SCLP-BTC", + "SCLPUSDT": "SCLP-USDT", + "SDAOETH": "SDAO-ETH", + "SDAOUSDT": "SDAO-USDT", + "SDNETH": "SDN-ETH", + "SDNUSDT": "SDN-USDT", + "SENSOBTC": "SENSO-BTC", + "SENSOUSDT": "SENSO-USDT", + "SFPBTC": "SFP-BTC", + "SFPUSDT": "SFP-USDT", + "SFUNDUSDT": "SFUND-USDT", + "SHABTC": "SHA-BTC", + "SHAUSDT": "SHA-USDT", + "SHFTBTC": "SHFT-BTC", + "SHFTUSDT": "SHFT-USDT", + "SHIBDOGE": "SHIB-DOGE", + "SHIBUSDT": "SHIB-USDT", + "SHILLUSDT": "SHILL-USDT", + "SHRBTC": "SHR-BTC", + "SHRUSDT": "SHR-USDT", + "SKEYUSDT": "SKEY-USDT", + "SKLBTC": "SKL-BTC", + "SKLUSDT": "SKL-USDT", + "SKUBTC": "SKU-BTC", + "SKUUSDT": "SKU-USDT", + "SLIMUSDT": "SLIM-USDT", + "SLPUSDT": "SLP-USDT", + "SNTBTC": "SNT-BTC", + "SNTETH": "SNT-ETH", + "SNTVTBTC": "SNTVT-BTC", + "SNTVTETH": "SNTVT-ETH", + "SNXBTC": "SNX-BTC", + "SNXETH": "SNX-ETH", + "SNXUSDT": "SNX-USDT", + "SNXUST": "SNX-UST", + "SOL3LUSDT": "SOL3L-USDT", + "SOL3SUSDT": "SOL3S-USDT", + "SOLRUSDT": "SOLR-USDT", + "SOLUSDT": "SOL-USDT", + "SOLUST": "SOL-UST", + "SOLVEBTC": "SOLVE-BTC", + "SOLVEUSDT": "SOLVE-USDT", + "SOULBTC": "SOUL-BTC", + "SOULETH": "SOUL-ETH", + "SOULUSDT": "SOUL-USDT", + "SOVUSDT": "SOV-USDT", + "SPIUSDT": "SPI-USDT", + "SRKBTC": "SRK-BTC", + "SRKUSDT": "SRK-USDT", + "SRMBTC": "SRM-BTC", + "SRMUSDT": "SRM-USDT", + "STCBTC": "STC-BTC", + "STCUSDT": "STC-USDT", + "STMXUSDT": "STMX-USDT", + "STNDETH": "STND-ETH", + "STNDUSDT": "STND-USDT", + "STORJBTC": "STORJ-BTC", + "STORJETH": "STORJ-ETH", + "STORJUSDT": "STORJ-USDT", + "STRKBTC": "STRK-BTC", + "STRKETH": "STRK-ETH", + "STRONGUSDT": "STRONG-USDT", + "STXBTC": "STX-BTC", + "STXUSDT": "STX-USDT", + "SUKUBTC": "SUKU-BTC", + "SUKUUSDT": "SUKU-USDT", + "SUNUSDT": "SUN-USDT", + "SUPERBTC": "SUPER-BTC", + "SUPERUSDT": "SUPER-USDT", + "SUSDBTC": "SUSD-BTC", + "SUSDETH": "SUSD-ETH", + "SUSDUSDT": "SUSD-USDT", + "SUSHI3LUSDT": "SUSHI3L-USDT", + "SUSHI3SUSDT": "SUSHI3S-USDT", + "SUSHIUSDT": "SUSHI-USDT", + "SUTERBTC": "SUTER-BTC", + "SUTERUSDT": "SUTER-USDT", + "SWASHUSDT": "SWASH-USDT", + "SWINGBYBTC": "SWINGBY-BTC", + "SWINGBYUSDT": "SWINGBY-USDT", + "SWPUSDT": "SWP-USDT", + "SXPBTC": "SXP-BTC", + "SXPUSDT": "SXP-USDT", + "SYLOUSDT": "SYLO-USDT", + "TARAETH": "TARA-ETH", + "TARAUSDT": "TARA-USDT", + "TCPUSDT": "TCP-USDT", + "TELBTC": "TEL-BTC", + "TELETH": "TEL-ETH", + "TELUSDT": "TEL-USDT", + "THETAUSDT": "THETA-USDT", + "TIDALUSDT": "TIDAL-USDT", + "TIMEBTC": "TIME-BTC", + "TIMEETH": "TIME-ETH", + "TKOBTC": "TKO-BTC", + "TKOUSDT": "TKO-USDT", + "TKYBTC": "TKY-BTC", + "TKYETH": "TKY-ETH", + "TKYUSDT": "TKY-USDT", + "TLMBTC": "TLM-BTC", + "TLMETH": "TLM-ETH", + "TLMUSDT": "TLM-USDT", + "TLOSBTC": "TLOS-BTC", + "TLOSUSDT": "TLOS-USDT", + "TOKOBTC": "TOKO-BTC", + "TOKOKCS": "TOKO-KCS", + "TOKOUSDT": "TOKO-USDT", + "TOMOBTC": "TOMO-BTC", + "TOMOETH": "TOMO-ETH", + "TOMOUSDT": "TOMO-USDT", + "TONEBTC": "TONE-BTC", + "TONEETH": "TONE-ETH", + "TONEUSDT": "TONE-USDT", + "TOWERBTC": "TOWER-BTC", + "TOWERUSDT": "TOWER-USDT", + "TRACBTC": "TRAC-BTC", + "TRACETH": "TRAC-ETH", + "TRADEBTC": "TRADE-BTC", + "TRADEUSDT": "TRADE-USDT", + "TRBBTC": "TRB-BTC", + "TRBUSDT": "TRB-USDT", + "TRIASBTC": "TRIAS-BTC", + "TRIASUSDT": "TRIAS-USDT", + "TRIBEUSDT": "TRIBE-USDT", + "TRUBTC": "TRU-BTC", + "TRUUSDT": "TRU-USDT", + "TRVLUSDT": "TRVL-USDT", + "TRXBTC": "TRX-BTC", + "TRXETH": "TRX-ETH", + "TRXKCS": "TRX-KCS", + "TRXUSDT": "TRX-USDT", + "TVKBTC": "TVK-BTC", + "TVKUSDT": "TVK-USDT", + "TWTBTC": "TWT-BTC", + "TWTUSDT": "TWT-USDT", + "TXAUSDC": "TXA-USDC", + "TXAUSDT": "TXA-USDT", + "UBXETH": "UBX-ETH", + "UBXTUSDT": "UBXT-USDT", + "UBXUSDT": "UBX-USDT", + "UDOOETH": "UDOO-ETH", + "UFOUSDT": "UFO-USDT", + "UMAUSDT": "UMA-USDT", + "UMBUSDT": "UMB-USDT", + "UNBUSDT": "UNB-USDT", + "UNFIUSDT": "UNFI-USDT", + "UNI3LUSDT": "UNI3L-USDT", + "UNI3SUSDT": "UNI3S-USDT", + "UNICUSDT": "UNIC-USDT", + "UNIKCS": "UNI-KCS", + "UNIUSDT": "UNI-USDT", + "UNOBTC": "UNO-BTC", + "UNOUSDT": "UNO-USDT", + "UOSBTC": "UOS-BTC", + "UOSUSDT": "UOS-USDT", + "UQCBTC": "UQC-BTC", + "UQCETH": "UQC-ETH", + "USDCUSDT": "USDC-USDT", + "USDCUST": "USDC-UST", + "USDJUSDT": "USDJ-USDT", + "USDNUSDT": "USDN-USDT", + "USDTDAI": "USDT-DAI", + "USDTPAX": "USDT-PAX", + "USDTTUSD": "USDT-TUSD", + "USDTUSDC": "USDT-USDC", + "USDTUST": "USDT-UST", + "UTKBTC": "UTK-BTC", + "UTKETH": "UTK-ETH", + "VAIUSDT": "VAI-USDT", + "VEEDBTC": "VEED-BTC", + "VEEDUSDT": "VEED-USDT", + "VEGAETH": "VEGA-ETH", + "VEGAUSDT": "VEGA-USDT", + "VELOUSDT": "VELO-USDT", + "VET3LUSDT": "VET3L-USDT", + "VET3SUSDT": "VET3S-USDT", + "VETBTC": "VET-BTC", + "VETETH": "VET-ETH", + "VETKCS": "VET-KCS", + "VETUSDT": "VET-USDT", + "VIDBTC": "VID-BTC", + "VIDTBTC": "VIDT-BTC", + "VIDTUSDT": "VIDT-USDT", + "VIDUSDT": "VID-USDT", + "VLXBTC": "VLX-BTC", + "VLXUSDT": "VLX-USDT", + "VRABTC": "VRA-BTC", + "VRAUSDT": "VRA-USDT", + "VRUSDT": "VR-USDT", + "VSYSBTC": "VSYS-BTC", + "VSYSUSDT": "VSYS-USDT", + "VXVUSDT": "VXV-USDT", + "WANBTC": "WAN-BTC", + "WANETH": "WAN-ETH", + "WAVESBTC": "WAVES-BTC", + "WAVESUSDT": "WAVES-USDT", + "WAXBTC": "WAX-BTC", + "WAXETH": "WAX-ETH", + "WAXUSDT": "WAX-USDT", + "WBTCBTC": "WBTC-BTC", + "WBTCETH": "WBTC-ETH", + "WESTBTC": "WEST-BTC", + "WESTUSDT": "WEST-USDT", + "WILDUSDT": "WILD-USDT", + "WINBTC": "WIN-BTC", + "WINTRX": "WIN-TRX", + "WINUSDT": "WIN-USDT", + "WNCGBTC": "WNCG-BTC", + "WNCGUSDT": "WNCG-USDT", + "WNXMBTC": "WNXM-BTC", + "WNXMUSDT": "WNXM-USDT", + "WOMUSDT": "WOM-USDT", + "WOOUSDT": "WOO-USDT", + "WRXBTC": "WRX-BTC", + "WRXUSDT": "WRX-USDT", + "WSIENNAUSDT": "WSIENNA-USDT", + "WTCBTC": "WTC-BTC", + "WXTBTC": "WXT-BTC", + "WXTUSDT": "WXT-USDT", + "XAVAUSDT": "XAVA-USDT", + "XCADUSDT": "XCAD-USDT", + "XCHUSDT": "XCH-USDT", + "XCURBTC": "XCUR-BTC", + "XCURUSDT": "XCUR-USDT", + "XDBBTC": "XDB-BTC", + "XDBUSDT": "XDB-USDT", + "XDCBTC": "XDC-BTC", + "XDCETH": "XDC-ETH", + "XDCUSDT": "XDC-USDT", + "XECUSDT": "XEC-USDT", + "XEDBTC": "XED-BTC", + "XEDUSDT": "XED-USDT", + "XEMBTC": "XEM-BTC", + "XEMUSDT": "XEM-USDT", + "XHVBTC": "XHV-BTC", + "XHVUSDT": "XHV-USDT", + "XLMBTC": "XLM-BTC", + "XLMETH": "XLM-ETH", + "XLMKCS": "XLM-KCS", + "XLMUSDT": "XLM-USDT", + "XMRBTC": "XMR-BTC", + "XMRETH": "XMR-ETH", + "XMRUSDT": "XMR-USDT", + "XNLUSDT": "XNL-USDT", + "XPRBTC": "XPR-BTC", + "XPRTUSDT": "XPRT-USDT", + "XPRUSDT": "XPR-USDT", + "XRP3LUSDT": "XRP3L-USDT", + "XRP3SUSDT": "XRP3S-USDT", + "XRPBTC": "XRP-BTC", + "XRPETH": "XRP-ETH", + "XRPKCS": "XRP-KCS", + "XRPPAX": "XRP-PAX", + "XRPTUSD": "XRP-TUSD", + "XRPUSDC": "XRP-USDC", + "XRPUSDT": "XRP-USDT", + "XSRUSDT": "XSR-USDT", + "XTAGUSDT": "XTAG-USDT", + "XTMUSDT": "XTM-USDT", + "XTZBTC": "XTZ-BTC", + "XTZKCS": "XTZ-KCS", + "XTZUSDT": "XTZ-USDT", + "XVSBTC": "XVS-BTC", + "XVSUSDT": "XVS-USDT", + "XYMBTC": "XYM-BTC", + "XYMUSDT": "XYM-USDT", + "XYOBTC": "XYO-BTC", + "XYOETH": "XYO-ETH", + "XYOUSDT": "XYO-USDT", + "YFDAIBTC": "YFDAI-BTC", + "YFDAIUSDT": "YFDAI-USDT", + "YFIUSDT": "YFI-USDT", + "YFIUST": "YFI-UST", + "YGGUSDT": "YGG-USDT", + "YLDUSDT": "YLD-USDT", + "YOPETH": "YOP-ETH", + "YOPUSDT": "YOP-USDT", + "ZCXBTC": "ZCX-BTC", + "ZCXUSDT": "ZCX-USDT", + "ZECBTC": "ZEC-BTC", + "ZECKCS": "ZEC-KCS", + "ZECUSDT": "ZEC-USDT", + "ZEEUSDT": "ZEE-USDT", + "ZENUSDT": "ZEN-USDT", + "ZILBTC": "ZIL-BTC", + "ZILETH": "ZIL-ETH", + "ZILUSDT": "ZIL-USDT", + "ZKTUSDT": "ZKT-USDT", + "ZORTUSDT": "ZORT-USDT", + "ZRXBTC": "ZRX-BTC", + "ZRXETH": "ZRX-ETH", } func toLocalSymbol(symbol string) string { diff --git a/pkg/exchange/max/convert.go b/pkg/exchange/max/convert.go index 08903f1c4b..dd1c8b39d3 100644 --- a/pkg/exchange/max/convert.go +++ b/pkg/exchange/max/convert.go @@ -5,8 +5,6 @@ import ( "strings" "time" - "github.com/pkg/errors" - "github.com/c9s/bbgo/pkg/exchange/max/maxapi" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" @@ -168,15 +166,9 @@ func toGlobalOrders(maxOrders []max.Order) (orders []types.Order, err error) { } func toGlobalOrder(maxOrder max.Order) (*types.Order, error) { - executedVolume, err := fixedpoint.NewFromString(maxOrder.ExecutedVolume) - if err != nil { - return nil, errors.Wrapf(err, "parse executed_volume failed: %+v", maxOrder) - } - - remainingVolume, err := fixedpoint.NewFromString(maxOrder.RemainingVolume) - if err != nil { - return nil, errors.Wrapf(err, "parse remaining volume failed: %+v", maxOrder) - } + executedVolume := maxOrder.ExecutedVolume + remainingVolume := maxOrder.RemainingVolume + isMargin := maxOrder.WalletType == max.WalletTypeMargin return &types.Order{ SubmitOrder: types.SubmitOrder{ @@ -184,62 +176,43 @@ func toGlobalOrder(maxOrder max.Order) (*types.Order, error) { Symbol: toGlobalSymbol(maxOrder.Market), Side: toGlobalSideType(maxOrder.Side), Type: toGlobalOrderType(maxOrder.OrderType), - Quantity: fixedpoint.MustNewFromString(maxOrder.Volume), - Price: fixedpoint.MustNewFromString(maxOrder.Price), - TimeInForce: "GTC", // MAX only supports GTC + Quantity: maxOrder.Volume, + Price: maxOrder.Price, + TimeInForce: types.TimeInForceGTC, // MAX only supports GTC GroupID: maxOrder.GroupID, }, Exchange: types.ExchangeMax, - IsWorking: maxOrder.State == "wait", + IsWorking: maxOrder.State == max.OrderStateWait, OrderID: maxOrder.ID, Status: toGlobalOrderStatus(maxOrder.State, executedVolume, remainingVolume), ExecutedQuantity: executedVolume, - CreationTime: types.Time(maxOrder.CreatedAtMs.Time()), - UpdateTime: types.Time(maxOrder.CreatedAtMs.Time()), + CreationTime: types.Time(maxOrder.CreatedAt.Time()), + UpdateTime: types.Time(maxOrder.CreatedAt.Time()), + IsMargin: isMargin, + IsIsolated: false, // isolated margin is not supported }, nil } func toGlobalTrade(t max.Trade) (*types.Trade, error) { - // skip trade ID that is the same. however this should not happen - var side = toGlobalSideType(t.Side) - - // trade time - mts := t.CreatedAtMilliSeconds - - price, err := fixedpoint.NewFromString(t.Price) - if err != nil { - return nil, err - } - - quantity, err := fixedpoint.NewFromString(t.Volume) - if err != nil { - return nil, err - } - - quoteQuantity, err := fixedpoint.NewFromString(t.Funds) - if err != nil { - return nil, err - } - - fee, err := fixedpoint.NewFromString(t.Fee) - if err != nil { - return nil, err - } - + isMargin := t.WalletType == max.WalletTypeMargin + side := toGlobalSideType(t.Side) return &types.Trade{ ID: t.ID, OrderID: t.OrderID, - Price: price, + Price: t.Price, Symbol: toGlobalSymbol(t.Market), - Exchange: "max", - Quantity: quantity, + Exchange: types.ExchangeMax, + Quantity: t.Volume, Side: side, IsBuyer: t.IsBuyer(), IsMaker: t.IsMaker(), - Fee: fee, + Fee: t.Fee, FeeCurrency: toGlobalCurrency(t.FeeCurrency), - QuoteQuantity: quoteQuantity, - Time: types.Time(mts), + QuoteQuantity: t.Funds, + Time: types.Time(t.CreatedAt), + IsMargin: isMargin, + IsIsolated: false, + IsFutures: false, }, nil } @@ -306,16 +279,6 @@ func convertWebSocketTrade(t max.TradeUpdate) (*types.Trade, error) { } func convertWebSocketOrderUpdate(u max.OrderUpdate) (*types.Order, error) { - executedVolume, err := fixedpoint.NewFromString(u.ExecutedVolume) - if err != nil { - return nil, err - } - - remainingVolume, err := fixedpoint.NewFromString(u.RemainingVolume) - if err != nil { - return nil, err - } - timeInForce := types.TimeInForceGTC if u.OrderType == max.OrderTypeIOCLimit { timeInForce = types.TimeInForceIOC @@ -327,16 +290,16 @@ func convertWebSocketOrderUpdate(u max.OrderUpdate) (*types.Order, error) { Symbol: toGlobalSymbol(u.Market), Side: toGlobalSideType(u.Side), Type: toGlobalOrderType(u.OrderType), - Quantity: fixedpoint.MustNewFromString(u.Volume), - Price: fixedpoint.MustNewFromString(u.Price), - StopPrice: fixedpoint.MustNewFromString(u.StopPrice), + Quantity: u.Volume, + Price: u.Price, + StopPrice: u.StopPrice, TimeInForce: timeInForce, // MAX only supports GTC GroupID: u.GroupID, }, Exchange: types.ExchangeMax, OrderID: u.ID, - Status: toGlobalOrderStatus(u.State, executedVolume, remainingVolume), - ExecutedQuantity: executedVolume, + Status: toGlobalOrderStatus(u.State, u.ExecutedVolume, u.RemainingVolume), + ExecutedQuantity: u.ExecutedVolume, CreationTime: types.Time(time.Unix(0, u.CreatedAtMs*int64(time.Millisecond))), UpdateTime: types.Time(time.Unix(0, u.CreatedAtMs*int64(time.Millisecond))), }, nil diff --git a/pkg/exchange/max/exchange.go b/pkg/exchange/max/exchange.go index d4359ee2a1..36f352b5a5 100644 --- a/pkg/exchange/max/exchange.go +++ b/pkg/exchange/max/exchange.go @@ -15,9 +15,9 @@ import ( "golang.org/x/time/rate" maxapi "github.com/c9s/bbgo/pkg/exchange/max/maxapi" + v3 "github.com/c9s/bbgo/pkg/exchange/max/maxapi/v3" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" - "github.com/c9s/bbgo/pkg/util" ) // closedOrderQueryLimiter is used for the closed orders query rate limit, 1 request per second @@ -29,8 +29,13 @@ var marketDataLimiter = rate.NewLimiter(rate.Every(2*time.Second), 10) var log = logrus.WithField("exchange", "max") type Exchange struct { - client *maxapi.RestClient + types.MarginSettings + key, secret string + client *maxapi.RestClient + + v3order *v3.OrderService + v3margin *v3.MarginService } func New(key, secret string) *Exchange { @@ -44,7 +49,10 @@ func New(key, secret string) *Exchange { return &Exchange{ client: client, key: key, - secret: secret, + // pragma: allowlist nextline secret + secret: secret, + v3order: &v3.OrderService{Client: client}, + v3margin: &v3.MarginService{Client: client}, } } @@ -155,16 +163,53 @@ func (e *Exchange) QueryMarkets(ctx context.Context) (types.MarketMap, error) { } func (e *Exchange) NewStream() types.Stream { - return NewStream(e.key, e.secret) + stream := NewStream(e.key, e.secret) + stream.MarginSettings = e.MarginSettings + return stream +} + +func (e *Exchange) QueryOrderTrades(ctx context.Context, q types.OrderQuery) ([]types.Trade, error) { + if q.OrderID == "" { + return nil, errors.New("max.QueryOrder: OrderID is required parameter") + } + + orderID, err := strconv.ParseInt(q.OrderID, 10, 64) + if err != nil { + return nil, err + } + + maxTrades, err := e.v3order.NewGetOrderTradesRequest().OrderID(uint64(orderID)).Do(ctx) + if err != nil { + return nil, err + } + + var trades []types.Trade + for _, t := range maxTrades { + localTrade, err := toGlobalTrade(t) + if err != nil { + log.WithError(err).Errorf("can not convert trade: %+v", t) + continue + } + + trades = append(trades, *localTrade) + } + + // ensure everything is sorted ascending + trades = types.SortTradesAscending(trades) + return trades, nil } func (e *Exchange) QueryOrder(ctx context.Context, q types.OrderQuery) (*types.Order, error) { + if q.OrderID == "" { + return nil, errors.New("max.QueryOrder: OrderID is required parameter") + } + orderID, err := strconv.ParseInt(q.OrderID, 10, 64) if err != nil { return nil, err } - maxOrder, err := e.client.OrderService.NewGetOrderRequest().Id(uint64(orderID)).Do(ctx) + maxOrder, err := e.v3order.NewGetOrderRequest().Id(uint64(orderID)).Do(ctx) if err != nil { return nil, err } @@ -173,7 +218,13 @@ func (e *Exchange) QueryOrder(ctx context.Context, q types.OrderQuery) (*types.O } func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders []types.Order, err error) { - maxOrders, err := e.client.OrderService.Open(toLocalSymbol(symbol), maxapi.QueryOrderOptions{}) + market := toLocalSymbol(symbol) + walletType := maxapi.WalletTypeSpot + if e.MarginSettings.IsMargin { + walletType = maxapi.WalletTypeMargin + } + + maxOrders, err := e.v3order.NewGetWalletOpenOrdersRequest(walletType).Market(market).Do(ctx) if err != nil { return orders, err } @@ -194,14 +245,7 @@ func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders [ func (e *Exchange) QueryClosedOrders(ctx context.Context, symbol string, since, until time.Time, lastOrderID uint64) ([]types.Order, error) { log.Warn("!!!MAX EXCHANGE API NOTICE!!!") log.Warn("the since/until conditions will not be effected on closed orders query, max exchange does not support time-range-based query") - - if v, ok := util.GetEnvVarBool("MAX_QUERY_CLOSED_ORDERS_ALL"); v && ok { - log.Warn("MAX_QUERY_CLOSED_ORDERS_ALL is set, we will fetch all closed orders from the first order") - return e.queryClosedOrdersByLastOrderID(ctx, symbol, 1) - } - return e.queryClosedOrdersByLastOrderID(ctx, symbol, lastOrderID) - // return e.queryRecentlyClosedOrders(ctx, symbol, since, until) } func (e *Exchange) queryClosedOrdersByLastOrderID(ctx context.Context, symbol string, lastOrderID uint64) (orders []types.Order, err error) { @@ -209,13 +253,20 @@ func (e *Exchange) queryClosedOrdersByLastOrderID(ctx context.Context, symbol st return orders, err } - req := e.client.OrderService.NewGetOrderHistoryRequest() - req.Market(toLocalSymbol(symbol)) + market := toLocalSymbol(symbol) + walletType := maxapi.WalletTypeSpot + if e.MarginSettings.IsMargin { + walletType = maxapi.WalletTypeMargin + } + + req := e.v3order.NewGetWalletOrderHistoryRequest(walletType).Market(market) if lastOrderID == 0 { lastOrderID = 1 } req.FromID(lastOrderID) + req.Limit(1000) + maxOrders, err := req.Do(ctx) if err != nil { return orders, err @@ -228,154 +279,20 @@ func (e *Exchange) queryClosedOrdersByLastOrderID(ctx context.Context, symbol st continue } - log.Debugf("max order %d %s %v %s %s", order.OrderID, order.Symbol, order.Price, order.Status, order.CreationTime.Time().Format(time.StampMilli)) orders = append(orders, *order) } - // always sort the orders by creation time - sort.Slice(orders, func(i, j int) bool { - return orders[i].CreationTime.Before(orders[j].CreationTime.Time()) - }) - + orders = types.SortOrdersAscending(orders) return orders, nil } -// queryRecentlyClosedOrders is deprecated -func (e *Exchange) queryRecentlyClosedOrders(ctx context.Context, symbol string, since time.Time, until time.Time) (orders []types.Order, err error) { - limit := 1000 // max limit = 1000, default 100 - orderIDs := make(map[uint64]struct{}, limit*2) - maxPages := 10 - - if v, ok := util.GetEnvVarInt("MAX_QUERY_CLOSED_ORDERS_LIMIT"); ok { - limit = v - } - - if v, ok := util.GetEnvVarInt("MAX_QUERY_CLOSED_ORDERS_NUM_OF_PAGES"); ok { - maxPages = v - } - - log.Warnf("fetching recently closed orders, maximum %d pages to fetch", maxPages) - log.Warnf("note that, some MAX orders might be missing if you did not sync the closed orders for a while") - -queryRecentlyClosedOrders: - for page := 1; page < maxPages; page++ { - if err := closedOrderQueryLimiter.Wait(ctx); err != nil { - return orders, err - } - - log.Infof("querying %s closed orders from page %d ~ ", symbol, page) - maxOrders, err2 := e.client.OrderService.Closed(toLocalSymbol(symbol), maxapi.QueryOrderOptions{ - Limit: limit, - Page: page, - OrderBy: "desc", - }) - if err2 != nil { - err = multierr.Append(err, err2) - break queryRecentlyClosedOrders - } - - // no recent orders - if len(maxOrders) == 0 { - break queryRecentlyClosedOrders - } - - log.Debugf("fetched %d orders", len(maxOrders)) - for _, maxOrder := range maxOrders { - if maxOrder.CreatedAtMs.Time().Before(since) { - log.Debugf("skip orders with creation time before %s, found %s", since, maxOrder.CreatedAtMs.Time()) - break queryRecentlyClosedOrders - } - - if maxOrder.CreatedAtMs.Time().After(until) { - log.Debugf("skip orders with creation time after %s, found %s", until, maxOrder.CreatedAtMs.Time()) - continue - } - - order, err2 := toGlobalOrder(maxOrder) - if err2 != nil { - err = multierr.Append(err, err2) - continue - } - - if _, ok := orderIDs[order.OrderID]; ok { - log.Debugf("skipping duplicated order: %d", order.OrderID) - } - - log.Debugf("max order %d %s %v %s %s", order.OrderID, order.Symbol, order.Price, order.Status, order.CreationTime.Time().Format(time.StampMilli)) - - orderIDs[order.OrderID] = struct{}{} - orders = append(orders, *order) - } - } - - // ensure everything is ascending ordered - log.Debugf("sorting %d orders", len(orders)) - sort.Slice(orders, func(i, j int) bool { - return orders[i].CreationTime.Time().Before(orders[j].CreationTime.Time()) - }) - - return orders, err -} - -func (e *Exchange) queryAllClosedOrders(ctx context.Context, symbol string, since time.Time, until time.Time) (orders []types.Order, err error) { - limit := 1000 // max limit = 1000, default 100 - orderIDs := make(map[uint64]struct{}, limit*2) - page := 1 - for { - if err := closedOrderQueryLimiter.Wait(ctx); err != nil { - return nil, err - } - - log.Infof("querying %s closed orders from page %d ~ ", symbol, page) - maxOrders, err := e.client.OrderService.Closed(toLocalSymbol(symbol), maxapi.QueryOrderOptions{ - Limit: limit, - Page: page, - }) - if err != nil { - return orders, err - } - - if len(maxOrders) == 0 { - return orders, err - } - - // ensure everything is ascending ordered - sort.Slice(maxOrders, func(i, j int) bool { - return maxOrders[i].CreatedAtMs.Time().Before(maxOrders[j].CreatedAtMs.Time()) - }) - - log.Debugf("%d orders", len(maxOrders)) - for _, maxOrder := range maxOrders { - if maxOrder.CreatedAtMs.Time().Before(since) { - log.Debugf("skip orders with creation time before %s, found %s", since, maxOrder.CreatedAtMs.Time()) - continue - } - - if maxOrder.CreatedAtMs.Time().After(until) { - return orders, err - } - - order, err := toGlobalOrder(maxOrder) - if err != nil { - return orders, err - } - - if _, ok := orderIDs[order.OrderID]; ok { - log.Debugf("skipping duplicated order: %d", order.OrderID) - } - - orderIDs[order.OrderID] = struct{}{} - orders = append(orders, *order) - log.Debugf("order %+v", order) - } - page++ +func (e *Exchange) CancelAllOrders(ctx context.Context) ([]types.Order, error) { + walletType := maxapi.WalletTypeSpot + if e.MarginSettings.IsMargin { + walletType = maxapi.WalletTypeMargin } - return orders, err -} - -func (e *Exchange) CancelAllOrders(ctx context.Context) ([]types.Order, error) { - var req = e.client.OrderService.NewOrderCancelAllRequest() + req := e.v3order.NewCancelWalletOrderAllRequest(walletType) var maxOrders, err = req.Do(ctx) if err != nil { return nil, err @@ -385,10 +302,16 @@ func (e *Exchange) CancelAllOrders(ctx context.Context) ([]types.Order, error) { } func (e *Exchange) CancelOrdersBySymbol(ctx context.Context, symbol string) ([]types.Order, error) { - var req = e.client.OrderService.NewOrderCancelAllRequest() - req.Market(toLocalSymbol(symbol)) + market := toLocalSymbol(symbol) + walletType := maxapi.WalletTypeSpot + if e.MarginSettings.IsMargin { + walletType = maxapi.WalletTypeMargin + } - var maxOrders, err = req.Do(ctx) + req := e.v3order.NewCancelWalletOrderAllRequest(walletType) + req.Market(market) + + maxOrders, err := req.Do(ctx) if err != nil { return nil, err } @@ -397,10 +320,15 @@ func (e *Exchange) CancelOrdersBySymbol(ctx context.Context, symbol string) ([]t } func (e *Exchange) CancelOrdersByGroupID(ctx context.Context, groupID uint32) ([]types.Order, error) { - var req = e.client.OrderService.NewOrderCancelAllRequest() + walletType := maxapi.WalletTypeSpot + if e.MarginSettings.IsMargin { + walletType = maxapi.WalletTypeMargin + } + + req := e.v3order.NewCancelWalletOrderAllRequest(walletType) req.GroupID(groupID) - var maxOrders, err = req.Do(ctx) + maxOrders, err := req.Do(ctx) if err != nil { return nil, err } @@ -409,6 +337,11 @@ func (e *Exchange) CancelOrdersByGroupID(ctx context.Context, groupID uint32) ([ } func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) (err2 error) { + walletType := maxapi.WalletTypeSpot + if e.MarginSettings.IsMargin { + walletType = maxapi.WalletTypeMargin + } + var groupIDs = make(map[uint32]struct{}) var orphanOrders []types.Order for _, o := range orders { @@ -421,7 +354,7 @@ func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) (err if len(groupIDs) > 0 { for groupID := range groupIDs { - var req = e.client.OrderService.NewOrderCancelAllRequest() + req := e.v3order.NewCancelWalletOrderAllRequest(walletType) req.GroupID(groupID) if _, err := req.Do(ctx); err != nil { @@ -432,7 +365,7 @@ func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) (err } for _, o := range orphanOrders { - var req = e.client.OrderService.NewOrderCancelRequest() + req := e.v3order.NewCancelOrderRequest() if o.OrderID > 0 { req.Id(o.OrderID) } else if len(o.ClientOrderID) > 0 && o.ClientOrderID != types.NoClientOrderID { @@ -450,7 +383,7 @@ func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) (err return err2 } -func toMaxSubmitOrder(o types.SubmitOrder) (*maxapi.Order, error) { +func toMaxSubmitOrder(o types.SubmitOrder) (*maxapi.SubmitOrder, error) { symbol := toLocalSymbol(o.Symbol) orderType, err := toLocalOrderType(o.Type) if err != nil { @@ -469,12 +402,11 @@ func toMaxSubmitOrder(o types.SubmitOrder) (*maxapi.Order, error) { quantityString = o.Quantity.String() } - maxOrder := maxapi.Order{ + maxOrder := maxapi.SubmitOrder{ Market: symbol, Side: toLocalSideType(o.Side), OrderType: orderType, - // Price: priceInString, - Volume: quantityString, + Volume: quantityString, } if o.GroupID > 0 { @@ -512,7 +444,7 @@ func toMaxSubmitOrder(o types.SubmitOrder) (*maxapi.Order, error) { return &maxOrder, nil } -func (e *Exchange) Withdrawal(ctx context.Context, asset string, amount fixedpoint.Value, address string, options *types.WithdrawalOptions) error { +func (e *Exchange) Withdraw(ctx context.Context, asset string, amount fixedpoint.Value, address string, options *types.WithdrawalOptions) error { asset = toLocalCurrency(asset) addresses, err := e.client.WithdrawalService.NewGetWithdrawalAddressesRequest(). @@ -553,87 +485,73 @@ func (e *Exchange) Withdrawal(ctx context.Context, asset string, amount fixedpoi return nil } -func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) (createdOrders types.OrderSlice, err error) { - if len(orders) > 1 && len(orders) < 15 { - var ordersBySymbol = map[string][]maxapi.Order{} - for _, o := range orders { - maxOrder, err := toMaxSubmitOrder(o) - if err != nil { - return nil, err - } - - ordersBySymbol[maxOrder.Market] = append(ordersBySymbol[maxOrder.Market], *maxOrder) - } - - for symbol, orders := range ordersBySymbol { - req := e.client.OrderService.NewCreateMultiOrderRequest() - req.Market(symbol) - req.AddOrders(orders...) - - orderResponses, err := req.Do(ctx) - if err != nil { - return createdOrders, err - } - - for _, resp := range *orderResponses { - if len(resp.Error) > 0 { - log.Errorf("multi-order submit error: %s", resp.Error) - continue - } - - o, err := toGlobalOrder(resp.Order) - if err != nil { - return createdOrders, err - } - - createdOrders = append(createdOrders, *o) - } - } +func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (createdOrder *types.Order, err error) { + walletType := maxapi.WalletTypeSpot + if e.MarginSettings.IsMargin { + walletType = maxapi.WalletTypeMargin + } - return createdOrders, nil + o := order + orderType, err := toLocalOrderType(o.Type) + if err != nil { + return createdOrder, err } - for _, order := range orders { - maxOrder, err := toMaxSubmitOrder(order) - if err != nil { - return createdOrders, err - } + // case IOC type + if orderType == maxapi.OrderTypeLimit && o.TimeInForce == types.TimeInForceIOC { + orderType = maxapi.OrderTypeIOCLimit + } - req := e.client.OrderService.NewCreateOrderRequest(). - Market(maxOrder.Market). - Side(maxOrder.Side). - Volume(maxOrder.Volume). - OrderType(string(maxOrder.OrderType)) + var quantityString string + if o.Market.Symbol != "" { + quantityString = o.Market.FormatQuantity(o.Quantity) + } else { + quantityString = o.Quantity.String() + } - if len(maxOrder.ClientOID) > 0 { - req.ClientOrderID(maxOrder.ClientOID) - } + clientOrderID := NewClientOrderID(o.ClientOrderID) - if len(maxOrder.Price) > 0 { - req.Price(maxOrder.Price) - } + req := e.v3order.NewCreateWalletOrderRequest(walletType) + req.Market(toLocalSymbol(o.Symbol)). + Side(toLocalSideType(o.Side)). + Volume(quantityString). + OrderType(orderType). + ClientOrderID(clientOrderID) - if len(maxOrder.StopPrice) > 0 { - req.StopPrice(maxOrder.StopPrice) + switch o.Type { + case types.OrderTypeStopLimit, types.OrderTypeLimit, types.OrderTypeLimitMaker: + var priceInString string + if o.Market.Symbol != "" { + priceInString = o.Market.FormatPrice(o.Price) + } else { + priceInString = o.Price.String() } + req.Price(priceInString) + } - retOrder, err := req.Do(ctx) - if err != nil { - return createdOrders, err - } - if retOrder == nil { - return createdOrders, errors.New("returned nil order") + // set stop price field for limit orders + switch o.Type { + case types.OrderTypeStopLimit, types.OrderTypeStopMarket: + var priceInString string + if o.Market.Symbol != "" { + priceInString = o.Market.FormatPrice(o.StopPrice) + } else { + priceInString = o.StopPrice.String() } + req.StopPrice(priceInString) + } - createdOrder, err := toGlobalOrder(*retOrder) - if err != nil { - return createdOrders, err - } + retOrder, err := req.Do(ctx) + if err != nil { + return createdOrder, err + } - createdOrders = append(createdOrders, *createdOrder) + if retOrder == nil { + return createdOrder, errors.New("returned nil order") } - return createdOrders, err + createdOrder, err = toGlobalOrder(*retOrder) + return createdOrder, err } // PlatformFeeCurrency @@ -656,20 +574,6 @@ func (e *Exchange) QueryAccount(ctx context.Context) (*types.Account, error) { return nil, err } - userInfo, err := e.client.AccountService.NewGetMeRequest().Do(ctx) - if err != nil { - return nil, err - } - - var balances = make(types.BalanceMap) - for _, a := range userInfo.Accounts { - balances[toGlobalCurrency(a.Currency)] = types.Balance{ - Currency: toGlobalCurrency(a.Currency), - Available: a.Balance, - Locked: a.Locked, - } - } - vipLevel, err := e.client.AccountService.NewGetVipLevelRequest().Do(ctx) if err != nil { return nil, err @@ -678,15 +582,68 @@ func (e *Exchange) QueryAccount(ctx context.Context) (*types.Account, error) { // MAX returns the fee rate in the following format: // "maker_fee": 0.0005 -> 0.05% // "taker_fee": 0.0015 -> 0.15% + a := &types.Account{ + AccountType: types.AccountTypeSpot, + MarginLevel: fixedpoint.Zero, MakerFeeRate: fixedpoint.NewFromFloat(vipLevel.Current.MakerFee), // 0.15% = 0.0015 TakerFeeRate: fixedpoint.NewFromFloat(vipLevel.Current.TakerFee), // 0.15% = 0.0015 } + balances, err := e.QueryAccountBalances(ctx) + if err != nil { + return nil, err + } a.UpdateBalances(balances) + + if e.MarginSettings.IsMargin { + a.AccountType = types.AccountTypeMargin + + req := e.v3margin.NewGetMarginADRatioRequest() + adRatio, err := req.Do(ctx) + if err != nil { + return a, err + } + + a.MarginLevel = adRatio.AdRatio + a.TotalAccountValue = adRatio.AssetInUsdt + } + return a, nil } +func (e *Exchange) QueryAccountBalances(ctx context.Context) (types.BalanceMap, error) { + if err := accountQueryLimiter.Wait(ctx); err != nil { + return nil, err + } + + walletType := maxapi.WalletTypeSpot + if e.MarginSettings.IsMargin { + walletType = maxapi.WalletTypeMargin + } + + req := e.v3order.NewGetWalletAccountsRequest(walletType) + accounts, err := req.Do(ctx) + if err != nil { + return nil, err + } + + var balances = make(types.BalanceMap) + for _, b := range accounts { + cur := toGlobalCurrency(b.Currency) + balances[cur] = types.Balance{ + Currency: cur, + Available: b.Balance, + Locked: b.Locked, + NetAsset: b.Balance.Add(b.Locked).Sub(b.Debt), + Borrowed: b.Borrowed, + Interest: b.Interest, + } + } + + return balances, nil +} + func (e *Exchange) QueryWithdrawHistory(ctx context.Context, asset string, since, until time.Time) (allWithdraws []types.Withdraw, err error) { startTime := since limit := 1000 @@ -848,39 +805,22 @@ func (e *Exchange) QueryDepositHistory(ctx context.Context, asset string, since, return allDeposits, err } -func (e *Exchange) QueryAccountBalances(ctx context.Context) (types.BalanceMap, error) { - if err := accountQueryLimiter.Wait(ctx); err != nil { - return nil, err - } - - accounts, err := e.client.AccountService.NewGetAccountsRequest().Do(ctx) - if err != nil { - return nil, err - } - - var balances = make(types.BalanceMap) - - for _, a := range accounts { - balances[toGlobalCurrency(a.Currency)] = types.Balance{ - Currency: toGlobalCurrency(a.Currency), - Available: a.Balance, - Locked: a.Locked, - } - } - - return balances, nil -} - func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *types.TradeQueryOptions) (trades []types.Trade, err error) { if err := tradeQueryLimiter.Wait(ctx); err != nil { return nil, err } - req := e.client.TradeService.NewGetPrivateTradeRequest() - req.Market(toLocalSymbol(symbol)) + market := toLocalSymbol(symbol) + walletType := maxapi.WalletTypeSpot + if e.MarginSettings.IsMargin { + walletType = maxapi.WalletTypeMargin + } + + req := e.v3order.NewGetWalletTradesRequest(walletType) + req.Market(market) if options.Limit > 0 { - req.Limit(options.Limit) + req.Limit(uint64(options.Limit)) } else { req.Limit(1000) } @@ -888,12 +828,9 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type // MAX uses exclusive last trade ID // the timestamp parameter is used for reverse order, we can't use it. if options.LastTradeID > 0 { - req.From(int64(options.LastTradeID)) + req.From(options.LastTradeID) } - // make it compatible with binance, we need the last trade id for the next page. - req.OrderBy("asc") - maxTrades, err := req.Do(ctx) if err != nil { return nil, err @@ -1019,3 +956,77 @@ func (e *Exchange) QueryAveragePrice(ctx context.Context, symbol string) (fixedp return fixedpoint.MustNewFromString(ticker.Sell). Add(fixedpoint.MustNewFromString(ticker.Buy)).Div(Two), nil } + +func (e *Exchange) RepayMarginAsset(ctx context.Context, asset string, amount fixedpoint.Value) error { + req := e.v3margin.NewMarginRepayRequest() + req.Currency(toLocalCurrency(asset)) + req.Amount(amount.String()) + resp, err := req.Do(ctx) + if err != nil { + return err + } + + log.Infof("margin repay: %v", resp) + return nil +} + +func (e *Exchange) BorrowMarginAsset(ctx context.Context, asset string, amount fixedpoint.Value) error { + req := e.v3margin.NewMarginLoanRequest() + req.Currency(toLocalCurrency(asset)) + req.Amount(amount.String()) + resp, err := req.Do(ctx) + if err != nil { + return err + } + + log.Infof("margin borrow: %v", resp) + return nil +} + +func (e *Exchange) QueryMarginAssetMaxBorrowable(ctx context.Context, asset string) (amount fixedpoint.Value, err error) { + req := e.v3margin.NewGetMarginBorrowingLimitsRequest() + resp, err := req.Do(ctx) + if err != nil { + return fixedpoint.Zero, err + } + + limits := *resp + if limit, ok := limits[toLocalCurrency(asset)]; ok { + return limit, nil + } + + err = fmt.Errorf("borrowing limit of %s not found", asset) + return amount, err +} + +// DefaultFeeRates returns the MAX VIP 0 fee schedule +// See also https://max-vip-zh.maicoin.com/ +func (e *Exchange) DefaultFeeRates() types.ExchangeFee { + return types.ExchangeFee{ + MakerFeeRate: fixedpoint.NewFromFloat(0.01 * 0.045), // 0.045% + TakerFeeRate: fixedpoint.NewFromFloat(0.01 * 0.150), // 0.15% + } +} + +var SupportedIntervals = map[types.Interval]int{ + types.Interval1m: 1 * 60, + types.Interval5m: 5 * 60, + types.Interval15m: 15 * 60, + types.Interval30m: 30 * 60, + types.Interval1h: 60 * 60, + types.Interval2h: 60 * 60 * 2, + types.Interval4h: 60 * 60 * 4, + types.Interval6h: 60 * 60 * 6, + types.Interval12h: 60 * 60 * 12, + types.Interval1d: 60 * 60 * 24, + types.Interval3d: 60 * 60 * 24 * 3, +} + +func (e *Exchange) SupportedInterval() map[types.Interval]int { + return SupportedIntervals +} + +func (e *Exchange) IsSupportedInterval(interval types.Interval) bool { + _, ok := SupportedIntervals[interval] + return ok +} diff --git a/pkg/exchange/max/maxapi/account.go b/pkg/exchange/max/maxapi/account.go index b1c4d4d13a..26ce9326ab 100644 --- a/pkg/exchange/max/maxapi/account.go +++ b/pkg/exchange/max/maxapi/account.go @@ -2,6 +2,7 @@ package max //go:generate -command GetRequest requestgen -method GET //go:generate -command PostRequest requestgen -method POST +//go:generate -command DeleteRequest requestgen -method DELETE import ( "github.com/c9s/requestgen" @@ -10,27 +11,27 @@ import ( ) type AccountService struct { - client *RestClient + client requestgen.AuthenticatedAPIClient } // Account is for max rest api v2, Balance and Type will be conflict with types.PrivateBalanceUpdate type Account struct { - Currency string `json:"currency"` - Balance fixedpoint.Value `json:"balance"` - Locked fixedpoint.Value `json:"locked"` - Type string `json:"type"` + Type string `json:"type"` + Currency string `json:"currency"` + Balance fixedpoint.Value `json:"balance"` + Locked fixedpoint.Value `json:"locked"` + + // v3 fields for M wallet + Debt fixedpoint.Value `json:"debt"` + Principal fixedpoint.Value `json:"principal"` + Borrowed fixedpoint.Value `json:"borrowed"` + Interest fixedpoint.Value `json:"interest"` + + // v2 fields FiatCurrency string `json:"fiat_currency"` FiatBalance fixedpoint.Value `json:"fiat_balance"` } -// Balance is for kingfisher -type Balance struct { - Currency string - Available int64 - Locked int64 - Total int64 -} - type UserBank struct { Branch string `json:"branch"` Name string `json:"name"` @@ -102,16 +103,6 @@ func (s *AccountService) NewGetAccountsRequest() *GetAccountsRequest { return &GetAccountsRequest{client: s.client} } -//go:generate GetRequest -url "v2/members/me" -type GetMeRequest -responseType .UserInfo -type GetMeRequest struct { - client requestgen.AuthenticatedAPIClient -} - -// NewGetMeRequest returns the current user info by the current used MAX key and secret -func (s *AccountService) NewGetMeRequest() *GetMeRequest { - return &GetMeRequest{client: s.client} -} - type Deposit struct { Currency string `json:"currency"` CurrencyVersion string `json:"currency_version"` // "eth" diff --git a/pkg/exchange/max/maxapi/account_test.go b/pkg/exchange/max/maxapi/account_test.go index f828eb1c48..e082586be4 100644 --- a/pkg/exchange/max/maxapi/account_test.go +++ b/pkg/exchange/max/maxapi/account_test.go @@ -110,4 +110,3 @@ func TestAccountService_NewGetDepositHistoryRequest(t *testing.T) { assert.NotEmpty(t, deposits) t.Logf("deposits: %+v", deposits) } - diff --git a/pkg/exchange/max/maxapi/auth.go b/pkg/exchange/max/maxapi/auth.go index c353c56dd0..15629dca89 100644 --- a/pkg/exchange/max/maxapi/auth.go +++ b/pkg/exchange/max/maxapi/auth.go @@ -1,11 +1,12 @@ package max type AuthMessage struct { - Action string `json:"action"` - APIKey string `json:"apiKey"` - Nonce int64 `json:"nonce"` - Signature string `json:"signature"` - ID string `json:"id"` + Action string `json:"action,omitempty"` + APIKey string `json:"apiKey,omitempty"` + Nonce int64 `json:"nonce,omitempty"` + Signature string `json:"signature,omitempty"` + ID string `json:"id,omitempty"` + Filters []string `json:"filters,omitempty"` } type AuthEvent struct { diff --git a/pkg/exchange/max/maxapi/cancel_order_request.go b/pkg/exchange/max/maxapi/cancel_order_request.go new file mode 100644 index 0000000000..3c77188e63 --- /dev/null +++ b/pkg/exchange/max/maxapi/cancel_order_request.go @@ -0,0 +1,19 @@ +package max + +//go:generate -command GetRequest requestgen -method GET +//go:generate -command PostRequest requestgen -method POST +//go:generate -command DeleteRequest requestgen -method DELETE + +import "github.com/c9s/requestgen" + +func (s *OrderService) NewCancelOrderRequest() *CancelOrderRequest { + return &CancelOrderRequest{client: s.client} +} + +//go:generate PostRequest -url "/api/v2/order/delete" -type CancelOrderRequest -responseType .Order +type CancelOrderRequest struct { + client requestgen.AuthenticatedAPIClient + + id *uint64 `param:"id,omitempty"` + clientOrderID *string `param:"client_oid,omitempty"` +} diff --git a/pkg/exchange/max/maxapi/cancel_order_request_requestgen.go b/pkg/exchange/max/maxapi/cancel_order_request_requestgen.go new file mode 100644 index 0000000000..402204c55e --- /dev/null +++ b/pkg/exchange/max/maxapi/cancel_order_request_requestgen.go @@ -0,0 +1,163 @@ +// Code generated by "requestgen -method POST -url /api/v2/order/delete -type CancelOrderRequest -responseType .Order"; DO NOT EDIT. + +package max + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +func (c *CancelOrderRequest) Id(id uint64) *CancelOrderRequest { + c.id = &id + return c +} + +func (c *CancelOrderRequest) ClientOrderID(clientOrderID string) *CancelOrderRequest { + c.clientOrderID = &clientOrderID + return c +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (c *CancelOrderRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (c *CancelOrderRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check id field -> json key id + if c.id != nil { + id := *c.id + + // assign parameter of id + params["id"] = id + } else { + } + // check clientOrderID field -> json key client_oid + if c.clientOrderID != nil { + clientOrderID := *c.clientOrderID + + // assign parameter of clientOrderID + params["client_oid"] = clientOrderID + } else { + } + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (c *CancelOrderRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := c.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if c.isVarSlice(_v) { + c.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (c *CancelOrderRequest) GetParametersJSON() ([]byte, error) { + params, err := c.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (c *CancelOrderRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (c *CancelOrderRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (c *CancelOrderRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (c *CancelOrderRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (c *CancelOrderRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := c.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (c *CancelOrderRequest) Do(ctx context.Context) (*Order, error) { + + params, err := c.GetParameters() + if err != nil { + return nil, err + } + query := url.Values{} + + apiURL := "/api/v2/order/delete" + + req, err := c.client.NewAuthenticatedRequest(ctx, "POST", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := c.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse Order + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return &apiResponse, nil +} diff --git a/pkg/exchange/max/maxapi/create_order_request.go b/pkg/exchange/max/maxapi/create_order_request.go new file mode 100644 index 0000000000..6646e3a472 --- /dev/null +++ b/pkg/exchange/max/maxapi/create_order_request.go @@ -0,0 +1,26 @@ +package max + +import "github.com/c9s/requestgen" + +//go:generate -command GetRequest requestgen -method GET +//go:generate -command PostRequest requestgen -method POST +//go:generate -command DeleteRequest requestgen -method DELETE + +//go:generate PostRequest -url "/api/v2/orders" -type CreateOrderRequest -responseType .Order +type CreateOrderRequest struct { + client requestgen.AuthenticatedAPIClient + + market string `param:"market,required"` + side string `param:"side,required"` + volume string `param:"volume,required"` + orderType OrderType `param:"ord_type"` + + price *string `param:"price"` + stopPrice *string `param:"stop_price"` + clientOrderID *string `param:"client_oid"` + groupID *string `param:"group_id"` +} + +func (s *OrderService) NewCreateOrderRequest() *CreateOrderRequest { + return &CreateOrderRequest{client: s.client} +} diff --git a/pkg/exchange/max/maxapi/create_order_request_requestgen.go b/pkg/exchange/max/maxapi/create_order_request_requestgen.go index 300b9fe9f6..2edd002d89 100644 --- a/pkg/exchange/max/maxapi/create_order_request_requestgen.go +++ b/pkg/exchange/max/maxapi/create_order_request_requestgen.go @@ -1,4 +1,4 @@ -// Code generated by "requestgen -method POST -url v2/orders -type CreateOrderRequest -responseType .Order"; DO NOT EDIT. +// Code generated by "requestgen -method POST -url /api/v2/orders -type CreateOrderRequest -responseType .Order"; DO NOT EDIT. package max @@ -26,7 +26,7 @@ func (c *CreateOrderRequest) Volume(volume string) *CreateOrderRequest { return c } -func (c *CreateOrderRequest) OrderType(orderType string) *CreateOrderRequest { +func (c *CreateOrderRequest) OrderType(orderType OrderType) *CreateOrderRequest { c.orderType = orderType return c } @@ -56,8 +56,8 @@ func (c *CreateOrderRequest) GetQueryParameters() (url.Values, error) { var params = map[string]interface{}{} query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) } return query, nil @@ -149,13 +149,13 @@ func (c *CreateOrderRequest) GetParametersQuery() (url.Values, error) { return query, err } - for k, v := range params { - if c.isVarSlice(v) { - c.iterateSlice(v, func(it interface{}) { - query.Add(k+"[]", fmt.Sprintf("%v", it)) + for _k, _v := range params { + if c.isVarSlice(_v) { + c.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) }) } else { - query.Add(k, fmt.Sprintf("%v", v)) + query.Add(_k, fmt.Sprintf("%v", _v)) } } @@ -180,24 +180,24 @@ func (c *CreateOrderRequest) GetSlugParameters() (map[string]interface{}, error) } func (c *CreateOrderRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) } return url } -func (c *CreateOrderRequest) iterateSlice(slice interface{}, f func(it interface{})) { +func (c *CreateOrderRequest) iterateSlice(slice interface{}, _f func(it interface{})) { sliceValue := reflect.ValueOf(slice) - for i := 0; i < sliceValue.Len(); i++ { - it := sliceValue.Index(i).Interface() - f(it) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) } } -func (c *CreateOrderRequest) isVarSlice(v interface{}) bool { - rt := reflect.TypeOf(v) +func (c *CreateOrderRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) switch rt.Kind() { case reflect.Slice: return true @@ -212,8 +212,8 @@ func (c *CreateOrderRequest) GetSlugsMap() (map[string]string, error) { return slugs, nil } - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) } return slugs, nil @@ -227,7 +227,7 @@ func (c *CreateOrderRequest) Do(ctx context.Context) (*Order, error) { } query := url.Values{} - apiURL := "v2/orders" + apiURL := "/api/v2/orders" req, err := c.client.NewAuthenticatedRequest(ctx, "POST", apiURL, query, params) if err != nil { diff --git a/pkg/exchange/max/maxapi/get_k_lines_request.go b/pkg/exchange/max/maxapi/get_k_lines_request.go new file mode 100644 index 0000000000..710b6d8730 --- /dev/null +++ b/pkg/exchange/max/maxapi/get_k_lines_request.go @@ -0,0 +1,27 @@ +package max + +import ( + "time" + + "github.com/c9s/requestgen" +) + +//go:generate -command GetRequest requestgen -method GET +//go:generate -command PostRequest requestgen -method POST +//go:generate -command DeleteRequest requestgen -method DELETE + +type KLineData []float64 + +//go:generate GetRequest -url "/api/v2/k" -type GetKLinesRequest -responseType []KLineData +type GetKLinesRequest struct { + client requestgen.APIClient + + market string `param:"market,required"` + limit *int `param:"limit"` + period *int `param:"period"` + timestamp time.Time `param:"timestamp,seconds"` +} + +func (s *PublicService) NewGetKLinesRequest() *GetKLinesRequest { + return &GetKLinesRequest{client: s.client} +} diff --git a/pkg/exchange/max/maxapi/get_k_lines_request_requestgen.go b/pkg/exchange/max/maxapi/get_k_lines_request_requestgen.go new file mode 100644 index 0000000000..595184c9dc --- /dev/null +++ b/pkg/exchange/max/maxapi/get_k_lines_request_requestgen.go @@ -0,0 +1,193 @@ +// Code generated by "requestgen -method GET -url /api/v2/k -type GetKLinesRequest -responseType []KLineData"; DO NOT EDIT. + +package max + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" + "strconv" + "time" +) + +func (g *GetKLinesRequest) Market(market string) *GetKLinesRequest { + g.market = market + return g +} + +func (g *GetKLinesRequest) Limit(limit int) *GetKLinesRequest { + g.limit = &limit + return g +} + +func (g *GetKLinesRequest) Period(period int) *GetKLinesRequest { + g.period = &period + return g +} + +func (g *GetKLinesRequest) Timestamp(timestamp time.Time) *GetKLinesRequest { + g.timestamp = timestamp + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetKLinesRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetKLinesRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check market field -> json key market + market := g.market + + // TEMPLATE check-required + if len(market) == 0 { + return nil, fmt.Errorf("market is required, empty string given") + } + // END TEMPLATE check-required + + // assign parameter of market + params["market"] = market + // check limit field -> json key limit + if g.limit != nil { + limit := *g.limit + + // assign parameter of limit + params["limit"] = limit + } else { + } + // check period field -> json key period + if g.period != nil { + period := *g.period + + // assign parameter of period + params["period"] = period + } else { + } + // check timestamp field -> json key timestamp + timestamp := g.timestamp + + // assign parameter of timestamp + // convert time.Time to seconds time stamp + params["timestamp"] = strconv.FormatInt(timestamp.Unix(), 10) + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetKLinesRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetKLinesRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetKLinesRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetKLinesRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetKLinesRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetKLinesRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetKLinesRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetKLinesRequest) Do(ctx context.Context) ([]KLineData, error) { + + // empty params for GET operation + var params interface{} + query, err := g.GetParametersQuery() + if err != nil { + return nil, err + } + + apiURL := "/api/v2/k" + + req, err := g.client.NewRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse []KLineData + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return apiResponse, nil +} diff --git a/pkg/exchange/max/maxapi/get_order_history_request_requestgen.go b/pkg/exchange/max/maxapi/get_order_history_request_requestgen.go deleted file mode 100644 index 51ef9e0bbd..0000000000 --- a/pkg/exchange/max/maxapi/get_order_history_request_requestgen.go +++ /dev/null @@ -1,174 +0,0 @@ -// Code generated by "requestgen -method GET -url v2/orders/history -type GetOrderHistoryRequest -responseType []Order"; DO NOT EDIT. - -package max - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "reflect" - "regexp" -) - -func (g *GetOrderHistoryRequest) Market(market string) *GetOrderHistoryRequest { - g.market = market - return g -} - -func (g *GetOrderHistoryRequest) FromID(fromID uint64) *GetOrderHistoryRequest { - g.fromID = &fromID - return g -} - -func (g *GetOrderHistoryRequest) Limit(limit uint) *GetOrderHistoryRequest { - g.limit = &limit - return g -} - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (g *GetOrderHistoryRequest) GetQueryParameters() (url.Values, error) { - var params = map[string]interface{}{} - - query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParameters builds and checks the parameters and return the result in a map object -func (g *GetOrderHistoryRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - // check market field -> json key market - market := g.market - - // assign parameter of market - params["market"] = market - // check fromID field -> json key from_id - if g.fromID != nil { - fromID := *g.fromID - - // assign parameter of fromID - params["from_id"] = fromID - } else { - } - // check limit field -> json key limit - if g.limit != nil { - limit := *g.limit - - // assign parameter of limit - params["limit"] = limit - } else { - } - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (g *GetOrderHistoryRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := g.GetParameters() - if err != nil { - return query, err - } - - for k, v := range params { - if g.isVarSlice(v) { - g.iterateSlice(v, func(it interface{}) { - query.Add(k+"[]", fmt.Sprintf("%v", it)) - }) - } else { - query.Add(k, fmt.Sprintf("%v", v)) - } - } - - return query, nil -} - -// GetParametersJSON converts the parameters from GetParameters into the JSON format -func (g *GetOrderHistoryRequest) GetParametersJSON() ([]byte, error) { - params, err := g.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} - -// GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (g *GetOrderHistoryRequest) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -func (g *GetOrderHistoryRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) - } - - return url -} - -func (g *GetOrderHistoryRequest) iterateSlice(slice interface{}, f func(it interface{})) { - sliceValue := reflect.ValueOf(slice) - for i := 0; i < sliceValue.Len(); i++ { - it := sliceValue.Index(i).Interface() - f(it) - } -} - -func (g *GetOrderHistoryRequest) isVarSlice(v interface{}) bool { - rt := reflect.TypeOf(v) - switch rt.Kind() { - case reflect.Slice: - return true - } - return false -} - -func (g *GetOrderHistoryRequest) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := g.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) - } - - return slugs, nil -} - -func (g *GetOrderHistoryRequest) Do(ctx context.Context) ([]Order, error) { - - // empty params for GET operation - var params interface{} - query, err := g.GetParametersQuery() - if err != nil { - return nil, err - } - - apiURL := "v2/orders/history" - - req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := g.client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse []Order - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - return apiResponse, nil -} diff --git a/pkg/exchange/max/maxapi/get_orders_request_requestgen.go b/pkg/exchange/max/maxapi/get_orders_request_requestgen.go deleted file mode 100644 index 36ef447016..0000000000 --- a/pkg/exchange/max/maxapi/get_orders_request_requestgen.go +++ /dev/null @@ -1,240 +0,0 @@ -// Code generated by "requestgen -method GET -url v2/orders -type GetOrdersRequest -responseType []Order"; DO NOT EDIT. - -package max - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "reflect" - "regexp" -) - -func (g *GetOrdersRequest) Market(market string) *GetOrdersRequest { - g.market = market - return g -} - -func (g *GetOrdersRequest) Side(side string) *GetOrdersRequest { - g.side = &side - return g -} - -func (g *GetOrdersRequest) GroupID(groupID uint32) *GetOrdersRequest { - g.groupID = &groupID - return g -} - -func (g *GetOrdersRequest) Offset(offset int) *GetOrdersRequest { - g.offset = &offset - return g -} - -func (g *GetOrdersRequest) Limit(limit int) *GetOrdersRequest { - g.limit = &limit - return g -} - -func (g *GetOrdersRequest) Page(page int) *GetOrdersRequest { - g.page = &page - return g -} - -func (g *GetOrdersRequest) OrderBy(orderBy string) *GetOrdersRequest { - g.orderBy = &orderBy - return g -} - -func (g *GetOrdersRequest) State(state []OrderState) *GetOrdersRequest { - g.state = state - return g -} - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (g *GetOrdersRequest) GetQueryParameters() (url.Values, error) { - var params = map[string]interface{}{} - - query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParameters builds and checks the parameters and return the result in a map object -func (g *GetOrdersRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - // check market field -> json key market - market := g.market - - // assign parameter of market - params["market"] = market - // check side field -> json key side - if g.side != nil { - side := *g.side - - // assign parameter of side - params["side"] = side - } else { - } - // check groupID field -> json key groupID - if g.groupID != nil { - groupID := *g.groupID - - // assign parameter of groupID - params["groupID"] = groupID - } else { - } - // check offset field -> json key offset - if g.offset != nil { - offset := *g.offset - - // assign parameter of offset - params["offset"] = offset - } else { - } - // check limit field -> json key limit - if g.limit != nil { - limit := *g.limit - - // assign parameter of limit - params["limit"] = limit - } else { - } - // check page field -> json key page - if g.page != nil { - page := *g.page - - // assign parameter of page - params["page"] = page - } else { - } - // check orderBy field -> json key order_by - if g.orderBy != nil { - orderBy := *g.orderBy - - // assign parameter of orderBy - params["order_by"] = orderBy - } else { - orderBy := "desc" - - // assign parameter of orderBy - params["order_by"] = orderBy - } - // check state field -> json key state - state := g.state - - // assign parameter of state - params["state"] = state - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (g *GetOrdersRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := g.GetParameters() - if err != nil { - return query, err - } - - for k, v := range params { - if g.isVarSlice(v) { - g.iterateSlice(v, func(it interface{}) { - query.Add(k+"[]", fmt.Sprintf("%v", it)) - }) - } else { - query.Add(k, fmt.Sprintf("%v", v)) - } - } - - return query, nil -} - -// GetParametersJSON converts the parameters from GetParameters into the JSON format -func (g *GetOrdersRequest) GetParametersJSON() ([]byte, error) { - params, err := g.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} - -// GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (g *GetOrdersRequest) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -func (g *GetOrdersRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) - } - - return url -} - -func (g *GetOrdersRequest) iterateSlice(slice interface{}, f func(it interface{})) { - sliceValue := reflect.ValueOf(slice) - for i := 0; i < sliceValue.Len(); i++ { - it := sliceValue.Index(i).Interface() - f(it) - } -} - -func (g *GetOrdersRequest) isVarSlice(v interface{}) bool { - rt := reflect.TypeOf(v) - switch rt.Kind() { - case reflect.Slice: - return true - } - return false -} - -func (g *GetOrdersRequest) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := g.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) - } - - return slugs, nil -} - -func (g *GetOrdersRequest) Do(ctx context.Context) ([]Order, error) { - - // empty params for GET operation - var params interface{} - query, err := g.GetParametersQuery() - if err != nil { - return nil, err - } - - apiURL := "v2/orders" - - req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := g.client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse []Order - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - return apiResponse, nil -} diff --git a/pkg/exchange/max/maxapi/get_private_trades_request_requestgen.go b/pkg/exchange/max/maxapi/get_private_trades_request_requestgen.go deleted file mode 100644 index 1951d196da..0000000000 --- a/pkg/exchange/max/maxapi/get_private_trades_request_requestgen.go +++ /dev/null @@ -1,242 +0,0 @@ -// Code generated by "requestgen -method GET -url v2/trades/my -type GetPrivateTradesRequest -responseType []Trade"; DO NOT EDIT. - -package max - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "reflect" - "regexp" - "strconv" - "time" -) - -func (p *GetPrivateTradesRequest) Market(market string) *GetPrivateTradesRequest { - p.market = market - return p -} - -func (p *GetPrivateTradesRequest) Timestamp(timestamp time.Time) *GetPrivateTradesRequest { - p.timestamp = ×tamp - return p -} - -func (p *GetPrivateTradesRequest) From(from int64) *GetPrivateTradesRequest { - p.from = &from - return p -} - -func (p *GetPrivateTradesRequest) To(to int64) *GetPrivateTradesRequest { - p.to = &to - return p -} - -func (p *GetPrivateTradesRequest) OrderBy(orderBy string) *GetPrivateTradesRequest { - p.orderBy = &orderBy - return p -} - -func (p *GetPrivateTradesRequest) Pagination(pagination bool) *GetPrivateTradesRequest { - p.pagination = &pagination - return p -} - -func (p *GetPrivateTradesRequest) Limit(limit int64) *GetPrivateTradesRequest { - p.limit = &limit - return p -} - -func (p *GetPrivateTradesRequest) Offset(offset int64) *GetPrivateTradesRequest { - p.offset = &offset - return p -} - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (p *GetPrivateTradesRequest) GetQueryParameters() (url.Values, error) { - var params = map[string]interface{}{} - - query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParameters builds and checks the parameters and return the result in a map object -func (p *GetPrivateTradesRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - // check market field -> json key market - market := p.market - - // assign parameter of market - params["market"] = market - // check timestamp field -> json key timestamp - if p.timestamp != nil { - timestamp := *p.timestamp - - // assign parameter of timestamp - // convert time.Time to seconds time stamp - params["timestamp"] = strconv.FormatInt(timestamp.Unix(), 10) - } else { - } - // check from field -> json key from - if p.from != nil { - from := *p.from - - // assign parameter of from - params["from"] = from - } else { - } - // check to field -> json key to - if p.to != nil { - to := *p.to - - // assign parameter of to - params["to"] = to - } else { - } - // check orderBy field -> json key order_by - if p.orderBy != nil { - orderBy := *p.orderBy - - // assign parameter of orderBy - params["order_by"] = orderBy - } else { - } - // check pagination field -> json key pagination - if p.pagination != nil { - pagination := *p.pagination - - // assign parameter of pagination - params["pagination"] = pagination - } else { - } - // check limit field -> json key limit - if p.limit != nil { - limit := *p.limit - - // assign parameter of limit - params["limit"] = limit - } else { - } - // check offset field -> json key offset - if p.offset != nil { - offset := *p.offset - - // assign parameter of offset - params["offset"] = offset - } else { - } - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (p *GetPrivateTradesRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := p.GetParameters() - if err != nil { - return query, err - } - - for k, v := range params { - if p.isVarSlice(v) { - p.iterateSlice(v, func(it interface{}) { - query.Add(k+"[]", fmt.Sprintf("%v", it)) - }) - } else { - query.Add(k, fmt.Sprintf("%v", v)) - } - } - - return query, nil -} - -// GetParametersJSON converts the parameters from GetParameters into the JSON format -func (p *GetPrivateTradesRequest) GetParametersJSON() ([]byte, error) { - params, err := p.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} - -// GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (p *GetPrivateTradesRequest) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -func (p *GetPrivateTradesRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) - } - - return url -} - -func (p *GetPrivateTradesRequest) iterateSlice(slice interface{}, f func(it interface{})) { - sliceValue := reflect.ValueOf(slice) - for i := 0; i < sliceValue.Len(); i++ { - it := sliceValue.Index(i).Interface() - f(it) - } -} - -func (p *GetPrivateTradesRequest) isVarSlice(v interface{}) bool { - rt := reflect.TypeOf(v) - switch rt.Kind() { - case reflect.Slice: - return true - } - return false -} - -func (p *GetPrivateTradesRequest) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := p.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) - } - - return slugs, nil -} - -func (p *GetPrivateTradesRequest) Do(ctx context.Context) ([]Trade, error) { - - // empty params for GET operation - var params interface{} - query, err := p.GetParametersQuery() - if err != nil { - return nil, err - } - - apiURL := "v2/trades/my" - - req, err := p.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := p.client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse []Trade - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - return apiResponse, nil -} diff --git a/pkg/exchange/max/maxapi/order.go b/pkg/exchange/max/maxapi/order.go index a412ce7c76..fa76096b7b 100644 --- a/pkg/exchange/max/maxapi/order.go +++ b/pkg/exchange/max/maxapi/order.go @@ -4,38 +4,18 @@ package max //go:generate -command PostRequest requestgen -method POST import ( - "context" - "net/url" - "time" - "github.com/c9s/requestgen" - "github.com/pkg/errors" + "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) -var relUrlV2Order *url.URL -var relUrlV2Orders *url.URL -var relUrlV2OrdersClear *url.URL -var relUrlV2OrdersDelete *url.URL -var relUrlV2OrdersMultiOneByOne, relUrlV2OrderDelete *url.URL - -func mustParseURL(s string) *url.URL { - u, err := url.Parse(s) - if err != nil { - panic(err) - } - return u -} +type WalletType string -func init() { - relUrlV2Order = mustParseURL("v2/order") - relUrlV2OrderDelete = mustParseURL("v2/order/delete") - relUrlV2Orders = mustParseURL("v2/orders") - relUrlV2OrdersClear = mustParseURL("v2/orders/clear") - relUrlV2OrdersDelete = mustParseURL("v2/orders/delete") - relUrlV2OrdersMultiOneByOne = mustParseURL("v2/orders/multi/onebyone") -} +const ( + WalletTypeSpot WalletType = "spot" + WalletTypeMargin WalletType = "m" +) type OrderStateToQuery int @@ -79,277 +59,36 @@ type QueryOrderOptions struct { // OrderService manages the Order endpoint. type OrderService struct { - client *RestClient + client requestgen.AuthenticatedAPIClient +} + +type SubmitOrder struct { + Side string `json:"side"` + Market string `json:"market"` + Price string `json:"price"` + StopPrice string `json:"stop_price,omitempty"` + OrderType OrderType `json:"ord_type"` + Volume string `json:"volume"` + GroupID uint32 `json:"group_id,omitempty"` + ClientOID string `json:"client_oid,omitempty"` } // Order represents one returned order (POST order/GET order/GET orders) on the max platform. type Order struct { ID uint64 `json:"id,omitempty"` + WalletType WalletType `json:"wallet_type,omitempty"` Side string `json:"side"` OrderType OrderType `json:"ord_type"` - Price string `json:"price,omitempty"` - StopPrice string `json:"stop_price,omitempty"` - AveragePrice string `json:"avg_price,omitempty"` + Price fixedpoint.Value `json:"price,omitempty"` + StopPrice fixedpoint.Value `json:"stop_price,omitempty"` + AveragePrice fixedpoint.Value `json:"avg_price,omitempty"` State OrderState `json:"state,omitempty"` Market string `json:"market,omitempty"` - Volume string `json:"volume"` - RemainingVolume string `json:"remaining_volume,omitempty"` - ExecutedVolume string `json:"executed_volume,omitempty"` + Volume fixedpoint.Value `json:"volume"` + RemainingVolume fixedpoint.Value `json:"remaining_volume,omitempty"` + ExecutedVolume fixedpoint.Value `json:"executed_volume,omitempty"` TradesCount int64 `json:"trades_count,omitempty"` GroupID uint32 `json:"group_id,omitempty"` ClientOID string `json:"client_oid,omitempty"` - CreatedAt time.Time `json:"-" db:"created_at"` - CreatedAtMs types.MillisecondTimestamp `json:"created_at_in_ms,omitempty"` - InsertedAt time.Time `json:"-" db:"inserted_at"` -} - -// Open returns open orders -func (s *OrderService) Closed(market string, options QueryOrderOptions) ([]Order, error) { - req := s.NewGetOrdersRequest() - req.Market(market) - req.State([]OrderState{OrderStateDone, OrderStateCancel}) - - if options.GroupID > 0 { - req.GroupID(uint32(options.GroupID)) - } - if options.Offset > 0 { - req.Offset(options.Offset) - } - if options.Limit > 0 { - req.Limit(options.Limit) - } - - if options.Page > 0 { - req.Page(options.Page) - } - - if len(options.OrderBy) > 0 { - req.OrderBy(options.OrderBy) - } - - return req.Do(context.Background()) -} - -// Open returns open orders -func (s *OrderService) Open(market string, options QueryOrderOptions) ([]Order, error) { - req := s.NewGetOrdersRequest() - req.Market(market) - // state default ot wait and convert - - if options.GroupID > 0 { - req.GroupID(uint32(options.GroupID)) - } - - return req.Do(context.Background()) -} - -//go:generate GetRequest -url "v2/orders/history" -type GetOrderHistoryRequest -responseType []Order -type GetOrderHistoryRequest struct { - client requestgen.AuthenticatedAPIClient - - market string `param:"market"` - fromID *uint64 `param:"from_id"` - limit *uint `param:"limit"` -} - -func (s *OrderService) NewGetOrderHistoryRequest() *GetOrderHistoryRequest { - return &GetOrderHistoryRequest{ - client: s.client, - } -} - -//go:generate GetRequest -url "v2/orders" -type GetOrdersRequest -responseType []Order -type GetOrdersRequest struct { - client requestgen.AuthenticatedAPIClient - - market string `param:"market"` - side *string `param:"side"` - groupID *uint32 `param:"groupID"` - offset *int `param:"offset"` - limit *int `param:"limit"` - page *int `param:"page"` - orderBy *string `param:"order_by" default:"desc"` - state []OrderState `param:"state"` -} - -func (s *OrderService) NewGetOrdersRequest() *GetOrdersRequest { - return &GetOrdersRequest{ - client: s.client, - } -} - -// All returns all orders for the authenticated account. -func (s *OrderService) All(market string, limit, page int, states ...OrderState) ([]Order, error) { - payload := map[string]interface{}{ - "market": market, - "limit": limit, - "page": page, - "state": states, - "order_by": "desc", - } - - req, err := s.client.newAuthenticatedRequest(context.Background(), "GET", "v2/orders", nil, payload, relUrlV2Orders) - if err != nil { - return nil, err - } - - response, err := s.client.SendRequest(req) - if err != nil { - return nil, err - } - - var orders []Order - if err := response.DecodeJSON(&orders); err != nil { - return nil, err - } - - return orders, nil -} - -// Options carry the option fields for REST API -type Options map[string]interface{} - -// Create multiple order in a single request -func (s *OrderService) CreateMulti(market string, orders []Order) (*MultiOrderResponse, error) { - req := s.NewCreateMultiOrderRequest() - req.Market(market) - req.AddOrders(orders...) - return req.Do(context.Background()) -} - -//go:generate PostRequest -url "v2/orders/clear" -type OrderCancelAllRequest -responseType []Order -type OrderCancelAllRequest struct { - client requestgen.AuthenticatedAPIClient - - side *string `param:"side"` - market *string `param:"market"` - groupID *uint32 `param:"groupID"` -} - -func (s *OrderService) NewOrderCancelAllRequest() *OrderCancelAllRequest { - return &OrderCancelAllRequest{client: s.client} -} - -//go:generate PostRequest -url "v2/order/delete" -type OrderCancelRequest -responseType .Order -type OrderCancelRequest struct { - client requestgen.AuthenticatedAPIClient - - id *uint64 `param:"id,omitempty"` - clientOrderID *string `param:"client_oid,omitempty"` -} - -func (s *OrderService) NewOrderCancelRequest() *OrderCancelRequest { - return &OrderCancelRequest{client: s.client} -} - -//go:generate GetRequest -url "v2/order" -type GetOrderRequest -responseType .Order -type GetOrderRequest struct { - client requestgen.AuthenticatedAPIClient - - id *uint64 `param:"id,omitempty"` - clientOrderID *string `param:"client_oid,omitempty"` -} - -func (s *OrderService) NewGetOrderRequest() *GetOrderRequest { - return &GetOrderRequest{client: s.client} -} - -type MultiOrderRequestParams struct { - *PrivateRequestParams - - Market string `json:"market"` - Orders []Order `json:"orders"` -} - -type MultiOrderResponse []struct { - Error string `json:"error,omitempty"` - Order Order `json:"order,omitempty"` -} - -type CreateMultiOrderRequest struct { - client *RestClient - - market *string - groupID *uint32 - orders []Order -} - -func (r *CreateMultiOrderRequest) GroupID(groupID uint32) *CreateMultiOrderRequest { - r.groupID = &groupID - return r -} - -func (r *CreateMultiOrderRequest) Market(market string) *CreateMultiOrderRequest { - r.market = &market - return r -} - -func (r *CreateMultiOrderRequest) AddOrders(orders ...Order) *CreateMultiOrderRequest { - r.orders = append(r.orders, orders...) - return r -} - -func (r *CreateMultiOrderRequest) Do(ctx context.Context) (multiOrderResponse *MultiOrderResponse, err error) { - var payload = map[string]interface{}{} - - if r.market != nil { - payload["market"] = r.market - } else { - return nil, errors.New("parameter market is required") - } - - if r.groupID != nil { - payload["group_id"] = r.groupID - } - - if len(r.orders) == 0 { - return nil, errors.New("parameter orders can not be empty") - } - - // clear group id - for i := range r.orders { - r.orders[i].GroupID = 0 - } - - payload["orders"] = r.orders - - req, err := r.client.newAuthenticatedRequest(context.Background(), "POST", "v2/orders/multi/onebyone", nil, payload, relUrlV2OrdersMultiOneByOne) - if err != nil { - return multiOrderResponse, errors.Wrapf(err, "order create error") - } - - response, err := r.client.SendRequest(req) - if err != nil { - return multiOrderResponse, err - } - - multiOrderResponse = &MultiOrderResponse{} - if errJson := response.DecodeJSON(multiOrderResponse); errJson != nil { - return multiOrderResponse, errJson - } - - return multiOrderResponse, err -} - -func (s *OrderService) NewCreateMultiOrderRequest() *CreateMultiOrderRequest { - return &CreateMultiOrderRequest{client: s.client} -} - -//go:generate PostRequest -url "v2/orders" -type CreateOrderRequest -responseType .Order -type CreateOrderRequest struct { - client requestgen.AuthenticatedAPIClient - - market string `param:"market,required"` - side string `param:"side,required"` - volume string `param:"volume,required"` - orderType string `param:"ord_type"` - - price *string `param:"price"` - stopPrice *string `param:"stop_price"` - clientOrderID *string `param:"client_oid"` - groupID *string `param:"group_id"` -} - -func (s *OrderService) NewCreateOrderRequest() *CreateOrderRequest { - return &CreateOrderRequest{client: s.client} + CreatedAt types.MillisecondTimestamp `json:"created_at"` } diff --git a/pkg/exchange/max/maxapi/order_cancel_all_request_requestgen.go b/pkg/exchange/max/maxapi/order_cancel_all_request_requestgen.go deleted file mode 100644 index f1fdac3276..0000000000 --- a/pkg/exchange/max/maxapi/order_cancel_all_request_requestgen.go +++ /dev/null @@ -1,176 +0,0 @@ -// Code generated by "requestgen -method POST -url v2/orders/clear -type OrderCancelAllRequest -responseType []Order"; DO NOT EDIT. - -package max - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "reflect" - "regexp" -) - -func (o *OrderCancelAllRequest) Side(side string) *OrderCancelAllRequest { - o.side = &side - return o -} - -func (o *OrderCancelAllRequest) Market(market string) *OrderCancelAllRequest { - o.market = &market - return o -} - -func (o *OrderCancelAllRequest) GroupID(groupID uint32) *OrderCancelAllRequest { - o.groupID = &groupID - return o -} - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (o *OrderCancelAllRequest) GetQueryParameters() (url.Values, error) { - var params = map[string]interface{}{} - - query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParameters builds and checks the parameters and return the result in a map object -func (o *OrderCancelAllRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - // check side field -> json key side - if o.side != nil { - side := *o.side - - // assign parameter of side - params["side"] = side - } else { - } - // check market field -> json key market - if o.market != nil { - market := *o.market - - // assign parameter of market - params["market"] = market - } else { - } - // check groupID field -> json key groupID - if o.groupID != nil { - groupID := *o.groupID - - // assign parameter of groupID - params["groupID"] = groupID - } else { - } - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (o *OrderCancelAllRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := o.GetParameters() - if err != nil { - return query, err - } - - for k, v := range params { - if o.isVarSlice(v) { - o.iterateSlice(v, func(it interface{}) { - query.Add(k+"[]", fmt.Sprintf("%v", it)) - }) - } else { - query.Add(k, fmt.Sprintf("%v", v)) - } - } - - return query, nil -} - -// GetParametersJSON converts the parameters from GetParameters into the JSON format -func (o *OrderCancelAllRequest) GetParametersJSON() ([]byte, error) { - params, err := o.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} - -// GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (o *OrderCancelAllRequest) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -func (o *OrderCancelAllRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) - } - - return url -} - -func (o *OrderCancelAllRequest) iterateSlice(slice interface{}, f func(it interface{})) { - sliceValue := reflect.ValueOf(slice) - for i := 0; i < sliceValue.Len(); i++ { - it := sliceValue.Index(i).Interface() - f(it) - } -} - -func (o *OrderCancelAllRequest) isVarSlice(v interface{}) bool { - rt := reflect.TypeOf(v) - switch rt.Kind() { - case reflect.Slice: - return true - } - return false -} - -func (o *OrderCancelAllRequest) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := o.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) - } - - return slugs, nil -} - -func (o *OrderCancelAllRequest) Do(ctx context.Context) ([]Order, error) { - - params, err := o.GetParameters() - if err != nil { - return nil, err - } - query := url.Values{} - - apiURL := "v2/orders/clear" - - req, err := o.client.NewAuthenticatedRequest(ctx, "POST", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := o.client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse []Order - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - return apiResponse, nil -} diff --git a/pkg/exchange/max/maxapi/order_cancel_request_requestgen.go b/pkg/exchange/max/maxapi/order_cancel_request_requestgen.go deleted file mode 100644 index 1854837c5d..0000000000 --- a/pkg/exchange/max/maxapi/order_cancel_request_requestgen.go +++ /dev/null @@ -1,163 +0,0 @@ -// Code generated by "requestgen -method POST -url v2/order/delete -type OrderCancelRequest -responseType .Order"; DO NOT EDIT. - -package max - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "reflect" - "regexp" -) - -func (o *OrderCancelRequest) Id(id uint64) *OrderCancelRequest { - o.id = &id - return o -} - -func (o *OrderCancelRequest) ClientOrderID(clientOrderID string) *OrderCancelRequest { - o.clientOrderID = &clientOrderID - return o -} - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (o *OrderCancelRequest) GetQueryParameters() (url.Values, error) { - var params = map[string]interface{}{} - - query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParameters builds and checks the parameters and return the result in a map object -func (o *OrderCancelRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - // check id field -> json key id - if o.id != nil { - id := *o.id - - // assign parameter of id - params["id"] = id - } else { - } - // check clientOrderID field -> json key client_oid - if o.clientOrderID != nil { - clientOrderID := *o.clientOrderID - - // assign parameter of clientOrderID - params["client_oid"] = clientOrderID - } else { - } - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (o *OrderCancelRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := o.GetParameters() - if err != nil { - return query, err - } - - for k, v := range params { - if o.isVarSlice(v) { - o.iterateSlice(v, func(it interface{}) { - query.Add(k+"[]", fmt.Sprintf("%v", it)) - }) - } else { - query.Add(k, fmt.Sprintf("%v", v)) - } - } - - return query, nil -} - -// GetParametersJSON converts the parameters from GetParameters into the JSON format -func (o *OrderCancelRequest) GetParametersJSON() ([]byte, error) { - params, err := o.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} - -// GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (o *OrderCancelRequest) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -func (o *OrderCancelRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) - } - - return url -} - -func (o *OrderCancelRequest) iterateSlice(slice interface{}, f func(it interface{})) { - sliceValue := reflect.ValueOf(slice) - for i := 0; i < sliceValue.Len(); i++ { - it := sliceValue.Index(i).Interface() - f(it) - } -} - -func (o *OrderCancelRequest) isVarSlice(v interface{}) bool { - rt := reflect.TypeOf(v) - switch rt.Kind() { - case reflect.Slice: - return true - } - return false -} - -func (o *OrderCancelRequest) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := o.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) - } - - return slugs, nil -} - -func (o *OrderCancelRequest) Do(ctx context.Context) (*Order, error) { - - params, err := o.GetParameters() - if err != nil { - return nil, err - } - query := url.Values{} - - apiURL := "v2/order/delete" - - req, err := o.client.NewAuthenticatedRequest(ctx, "POST", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := o.client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse Order - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - return &apiResponse, nil -} diff --git a/pkg/exchange/max/maxapi/order_test.go b/pkg/exchange/max/maxapi/order_test.go index a69d920bad..0bfb887daf 100644 --- a/pkg/exchange/max/maxapi/order_test.go +++ b/pkg/exchange/max/maxapi/order_test.go @@ -1,12 +1,9 @@ package max import ( - "context" "os" "regexp" "testing" - - "github.com/stretchr/testify/assert" ) func maskSecret(s string) string { @@ -25,97 +22,3 @@ func integrationTestConfigured(t *testing.T, prefix string) (key, secret string, } return key, secret, ok } - -func TestOrderService_GetOrdersRequest(t *testing.T) { - key, secret, ok := integrationTestConfigured(t, "MAX") - if !ok { - t.SkipNow() - } - - ctx := context.Background() - - client := NewRestClient(ProductionAPIURL) - client.Auth(key, secret) - - req3 := client.OrderService.NewGetOrdersRequest() - req3.State([]OrderState{OrderStateDone, OrderStateFinalizing}) - // req3.State([]OrderState{OrderStateDone}) - req3.Market("btcusdt") - orders, err := req3.Do(ctx) - if assert.NoError(t, err) { - t.Logf("orders: %+v", orders) - - assert.NotNil(t, orders) - if assert.NotEmptyf(t, orders, "got %d orders", len(orders)) { - for _, order := range orders { - assert.Contains(t, []OrderState{OrderStateDone, OrderStateFinalizing}, order.State) - } - } - } -} - -func TestOrderService_GetOrdersRequest_SingleState(t *testing.T) { - key, secret, ok := integrationTestConfigured(t, "MAX") - if !ok { - t.SkipNow() - } - - ctx := context.Background() - - client := NewRestClient(ProductionAPIURL) - client.Auth(key, secret) - - req3 := client.OrderService.NewGetOrdersRequest() - req3.State([]OrderState{OrderStateDone}) - req3.Market("btcusdt") - orders, err := req3.Do(ctx) - assert.NoError(t, err) - assert.NotNil(t, orders) -} - -func TestOrderService_GetOrderHistoryRequest(t *testing.T) { - key, secret, ok := integrationTestConfigured(t, "MAX") - if !ok { - t.SkipNow() - } - - ctx := context.Background() - - client := NewRestClient(ProductionAPIURL) - client.Auth(key, secret) - - req := client.OrderService.NewGetOrderHistoryRequest() - req.Market("btcusdt") - req.FromID(1) - orders, err := req.Do(ctx) - assert.NoError(t, err) - assert.NotNil(t, orders) -} - -func TestOrderService(t *testing.T) { - key, secret, ok := integrationTestConfigured(t, "MAX") - if !ok { - t.SkipNow() - } - - ctx := context.Background() - - client := NewRestClient(ProductionAPIURL) - client.Auth(key, secret) - req := client.OrderService.NewCreateOrderRequest() - order, err := req.Market("btcusdt"). - Price("10000"). - Volume("0.001"). - OrderType("limit"). - Side("buy").Do(ctx) - - if assert.NoError(t, err) { - assert.NotNil(t, order) - req2 := client.OrderService.NewOrderCancelRequest() - req2.Id(order.ID) - cancelResp, err := req2.Do(ctx) - assert.NoError(t, err) - t.Logf("cancelResponse: %+v", cancelResp) - } - -} diff --git a/pkg/exchange/max/maxapi/public.go b/pkg/exchange/max/maxapi/public.go index 6b228a8bcc..4591a3274c 100644 --- a/pkg/exchange/max/maxapi/public.go +++ b/pkg/exchange/max/maxapi/public.go @@ -3,12 +3,11 @@ package max import ( "context" "fmt" - "io/ioutil" "net/url" - "strconv" "strings" "time" + "github.com/c9s/requestgen" "github.com/pkg/errors" "github.com/valyala/fastjson" @@ -17,18 +16,20 @@ import ( ) type PublicService struct { - client *RestClient + client requestgen.AuthenticatedAPIClient } type Market struct { - ID string `json:"id"` - Name string `json:"name"` - BaseUnit string `json:"base_unit"` - BaseUnitPrecision int `json:"base_unit_precision"` - QuoteUnit string `json:"quote_unit"` - QuoteUnitPrecision int `json:"quote_unit_precision"` + ID string `json:"id"` + Name string `json:"name"` + Status string `json:"market_status"` // active + BaseUnit string `json:"base_unit"` + BaseUnitPrecision int `json:"base_unit_precision"` + QuoteUnit string `json:"quote_unit"` + QuoteUnitPrecision int `json:"quote_unit_precision"` MinBaseAmount fixedpoint.Value `json:"min_base_amount"` MinQuoteAmount fixedpoint.Value `json:"min_quote_amount"` + SupportMargin bool `json:"m_wallet_supported"` } type Ticker struct { @@ -233,46 +234,39 @@ func (k KLine) KLine() types.KLine { } func (s *PublicService) KLines(symbol string, resolution string, start time.Time, limit int) ([]KLine, error) { - queries := url.Values{} - queries.Set("market", symbol) - interval, err := ParseInterval(resolution) if err != nil { return nil, err } - queries.Set("period", strconv.Itoa(int(interval))) - - nilTime := time.Time{} - if start != nilTime { - queries.Set("timestamp", strconv.FormatInt(start.Unix(), 10)) - } - - if limit > 0 { - queries.Set("limit", strconv.Itoa(limit)) // default to 30, max limit = 10,000 - } - - req, err := s.client.NewRequest(context.Background(), "GET", fmt.Sprintf("%s/k", s.client.BaseURL), queries, nil) - if err != nil { - return nil, fmt.Errorf("request build error: %s", err.Error()) - } - - resp, err := s.client.Do(req) - if err != nil { - return nil, fmt.Errorf("request failed: %s", err.Error()) - } - defer func() { - if err := resp.Body.Close(); err != nil { - logger.WithError(err).Error("failed to close resp body") - } - }() - - body, err := ioutil.ReadAll(resp.Body) + req := s.NewGetKLinesRequest() + req.Market(symbol).Period(int(interval)).Timestamp(start).Limit(limit) + data, err := req.Do(context.Background()) if err != nil { return nil, err } - return parseKLines(body, symbol, resolution, interval) + var kLines []KLine + for _, slice := range data { + ts := int64(slice[0]) + startTime := time.Unix(ts, 0) + endTime := startTime.Add(time.Duration(interval)*time.Minute - time.Millisecond) + isClosed := time.Now().Before(endTime) + kLines = append(kLines, KLine{ + Symbol: symbol, + Interval: resolution, + StartTime: startTime, + EndTime: endTime, + Open: fixedpoint.NewFromFloat(slice[1]), + High: fixedpoint.NewFromFloat(slice[2]), + Low: fixedpoint.NewFromFloat(slice[3]), + Close: fixedpoint.NewFromFloat(slice[4]), + Volume: fixedpoint.NewFromFloat(slice[5]), + Closed: isClosed, + }) + } + return kLines, nil + // return parseKLines(resp.Body, symbol, resolution, interval) } func parseKLines(payload []byte, symbol, resolution string, interval Interval) (klines []KLine, err error) { diff --git a/pkg/exchange/max/maxapi/restapi.go b/pkg/exchange/max/maxapi/restapi.go index ef26fd1edf..5e0deeeade 100644 --- a/pkg/exchange/max/maxapi/restapi.go +++ b/pkg/exchange/max/maxapi/restapi.go @@ -1,7 +1,6 @@ package max import ( - "bytes" "context" "crypto/hmac" "crypto/sha256" @@ -9,7 +8,6 @@ import ( "encoding/hex" "encoding/json" "fmt" - "io/ioutil" "math" "net" "net/http" @@ -40,18 +38,12 @@ const ( TimestampSince = 1535760000 ) -var debugRequestDump = false -var debugMaxRequestPayload = false -var addUserAgentHeader = true - var httpTransportMaxIdleConnsPerHost = http.DefaultMaxIdleConnsPerHost var httpTransportMaxIdleConns = 100 var httpTransportIdleConnTimeout = 90 * time.Second +var disableUserAgentHeader = false func init() { - debugMaxRequestPayload, _ = util.GetEnvVarBool("DEBUG_MAX_REQUEST_PAYLOAD") - debugRequestDump, _ = util.GetEnvVarBool("DEBUG_MAX_REQUEST") - addUserAgentHeader, _ = util.GetEnvVarBool("DISABLE_MAX_USER_AGENT_HEADER") if val, ok := util.GetEnvVarInt("HTTP_TRANSPORT_MAX_IDLE_CONNS_PER_HOST"); ok { httpTransportMaxIdleConnsPerHost = val @@ -60,9 +52,14 @@ func init() { if val, ok := util.GetEnvVarInt("HTTP_TRANSPORT_MAX_IDLE_CONNS"); ok { httpTransportMaxIdleConns = val } + if val, ok := util.GetEnvVarDuration("HTTP_TRANSPORT_IDLE_CONN_TIMEOUT"); ok { httpTransportIdleConnTimeout = val } + + if val, ok := util.GetEnvVarBool("DISABLE_MAX_USER_AGENT_HEADER"); ok { + disableUserAgentHeader = val + } } var logger = log.WithField("exchange", "max") @@ -80,14 +77,33 @@ var serverTimestamp = time.Now().Unix() // reqCount is used for nonce, this variable counts the API request count. var reqCount int64 = 1 -type RestClient struct { - client *http.Client +// create an isolated http httpTransport rather than the default one +var httpTransport = &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: 10 * time.Second, + KeepAlive: 30 * time.Second, + }).DialContext, + + // ForceAttemptHTTP2: true, + // DisableCompression: false, + + MaxIdleConns: httpTransportMaxIdleConns, + MaxIdleConnsPerHost: httpTransportMaxIdleConnsPerHost, + IdleConnTimeout: httpTransportIdleConnTimeout, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, +} + +var defaultHttpClient = &http.Client{ + Timeout: defaultHTTPTimeout, + Transport: httpTransport, +} - BaseURL *url.URL +type RestClient struct { + requestgen.BaseAPIClient - // Authentication - APIKey string - APISecret string + APIKey, APISecret string AccountService *AccountService PublicService *PublicService @@ -95,21 +111,19 @@ type RestClient struct { OrderService *OrderService RewardService *RewardService WithdrawalService *WithdrawalService - // OrderBookService *OrderBookService - // MaxTokenService *MaxTokenService - // MaxKLineService *KLineService - // CreditService *CreditService } -func NewRestClientWithHttpClient(baseURL string, httpClient *http.Client) *RestClient { +func NewRestClient(baseURL string) *RestClient { u, err := url.Parse(baseURL) if err != nil { panic(err) } var client = &RestClient{ - client: httpClient, - BaseURL: u, + BaseAPIClient: requestgen.BaseAPIClient{ + HttpClient: defaultHttpClient, + BaseURL: u, + }, } client.AccountService = &AccountService{client} @@ -119,38 +133,16 @@ func NewRestClientWithHttpClient(baseURL string, httpClient *http.Client) *RestC client.RewardService = &RewardService{client} client.WithdrawalService = &WithdrawalService{client} - // client.MaxTokenService = &MaxTokenService{client} + // defaultHttpClient.MaxTokenService = &MaxTokenService{defaultHttpClient} client.initNonce() return client } -func NewRestClient(baseURL string) *RestClient { - // create an isolated http transport rather than the default one - transport := &http.Transport{ - Proxy: http.ProxyFromEnvironment, - DialContext: (&net.Dialer{ - Timeout: 10 * time.Second, - KeepAlive: 30 * time.Second, - }).DialContext, - ForceAttemptHTTP2: true, - MaxIdleConns: httpTransportMaxIdleConns, - MaxIdleConnsPerHost: httpTransportMaxIdleConnsPerHost, - IdleConnTimeout: httpTransportIdleConnTimeout, - TLSHandshakeTimeout: 10 * time.Second, - ExpectContinueTimeout: 1 * time.Second, - } - - client := &http.Client{ - Timeout: defaultHTTPTimeout, - Transport: transport, - } - - return NewRestClientWithHttpClient(baseURL, client) -} - // Auth sets api key and secret for usage is requests that requires authentication. func (c *RestClient) Auth(key string, secret string) *RestClient { + // pragma: allowlist nextline secret c.APIKey = key + // pragma: allowlist nextline secret c.APISecret = secret return c } @@ -175,45 +167,20 @@ func (c *RestClient) getNonce() int64 { return (seconds+timeOffset)*1000 - 1 + int64(math.Mod(float64(rc), 1000.0)) } -// NewRequest create new API request. Relative url can be provided in refURL. -func (c *RestClient) NewRequest(ctx context.Context, method string, refURL string, params url.Values, payload interface{}) (*http.Request, error) { - rel, err := url.Parse(refURL) - if err != nil { - return nil, err - } - - if params != nil { - rel.RawQuery = params.Encode() - } - - var req *http.Request - u := c.BaseURL.ResolveReference(rel) - - body, err := castPayload(payload) - if err != nil { - return nil, err - } - - req, err = http.NewRequest(method, u.String(), bytes.NewReader(body)) - if err != nil { - return nil, err - } - - req = req.WithContext(ctx) - - if addUserAgentHeader { - req.Header.Add("User-Agent", UserAgent) - } - - return req, nil -} - func (c *RestClient) NewAuthenticatedRequest(ctx context.Context, m string, refURL string, params url.Values, payload interface{}) (*http.Request, error) { return c.newAuthenticatedRequest(ctx, m, refURL, params, payload, nil) } // newAuthenticatedRequest creates new http request for authenticated routes. func (c *RestClient) newAuthenticatedRequest(ctx context.Context, m string, refURL string, params url.Values, data interface{}, rel *url.URL) (*http.Request, error) { + if len(c.APIKey) == 0 { + return nil, errors.New("empty api key") + } + + if len(c.APISecret) == 0 { + return nil, errors.New("empty api secret") + } + var err error if rel == nil { rel, err = url.Parse(refURL) @@ -223,22 +190,13 @@ func (c *RestClient) newAuthenticatedRequest(ctx context.Context, m string, refU } var p []byte - var payload map[string]interface{} + var payload = map[string]interface{}{ + "nonce": c.getNonce(), + "path": c.BaseURL.ResolveReference(rel).Path, + } switch d := data.(type) { - - case nil: - payload = map[string]interface{}{ - "nonce": c.getNonce(), - "path": c.BaseURL.ResolveReference(rel).Path, - } - case map[string]interface{}: - payload = map[string]interface{}{ - "nonce": c.getNonce(), - "path": c.BaseURL.ResolveReference(rel).Path, - } - for k, v := range d { payload[k] = v } @@ -258,22 +216,6 @@ func (c *RestClient) newAuthenticatedRequest(ctx context.Context, m string, refU return nil, err } - if debugMaxRequestPayload { - log.Infof("request payload: %s", p) - } - - if err != nil { - return nil, err - } - - if len(c.APIKey) == 0 { - return nil, errors.New("empty api key") - } - - if len(c.APISecret) == 0 { - return nil, errors.New("empty api secret") - } - req, err := c.NewRequest(ctx, m, refURL, params, p) if err != nil { return nil, err @@ -286,95 +228,18 @@ func (c *RestClient) newAuthenticatedRequest(ctx context.Context, m string, refU req.Header.Add("X-MAX-PAYLOAD", encoded) req.Header.Add("X-MAX-SIGNATURE", signPayload(encoded, c.APISecret)) - if debugRequestDump { - dump, err2 := httputil.DumpRequestOut(req, true) - if err2 != nil { - log.Errorf("dump request error: %v", err2) - } else { - fmt.Printf("REQUEST:\n%s", dump) - } + if disableUserAgentHeader { + req.Header.Set("USER-AGENT", "") + } else { + req.Header.Set("USER-AGENT", UserAgent) } - return req, nil -} - -func signPayload(payload string, secret string) string { - var sig = hmac.New(sha256.New, []byte(secret)) - _, err := sig.Write([]byte(payload)) - if err != nil { - return "" - } - return hex.EncodeToString(sig.Sum(nil)) -} - -func (c *RestClient) Do(req *http.Request) (resp *http.Response, err error) { - return c.client.Do(req) -} - -// SendRequest sends the request to the API server and handle the response -func (c *RestClient) SendRequest(req *http.Request) (*requestgen.Response, error) { - resp, err := c.client.Do(req) - if err != nil { - return nil, err + if false { + out, _ := httputil.DumpRequestOut(req, true) + fmt.Println(string(out)) } - // newResponse reads the response body and return a new Response object - response, err := requestgen.NewResponse(resp) - if err != nil { - return response, err - } - - // Check error, if there is an error, return the ErrorResponse struct type - if response.IsError() { - errorResponse, err := ToErrorResponse(response) - if err != nil { - return response, err - } - return response, errorResponse - } - - return response, nil -} - -func (c *RestClient) sendAuthenticatedRequest(m string, refURL string, data map[string]interface{}) (*requestgen.Response, error) { - req, err := c.newAuthenticatedRequest(nil, m, refURL, nil, data, nil) - if err != nil { - return nil, err - } - response, err := c.SendRequest(req) - if err != nil { - return nil, err - } - return response, err -} - -// get sends GET http request to the api endpoint, the urlPath must start with a slash '/' -func (c *RestClient) get(urlPath string, values url.Values) ([]byte, error) { - var reqURL = c.BaseURL.String() + urlPath - - // Create request - req, err := http.NewRequest("GET", reqURL, nil) - if err != nil { - return nil, fmt.Errorf("could not init request: %s", err.Error()) - } - - req.URL.RawQuery = values.Encode() - req.Header.Add("User-Agent", UserAgent) - - // Execute request - resp, err := c.client.Do(req) - if err != nil { - return nil, fmt.Errorf("could not execute request: %s", err.Error()) - } - defer resp.Body.Close() - - // Load request - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("could not read response: %s", err.Error()) - } - - return body, nil + return req, nil } // ErrorResponse is the custom error type that is returned if the API returns an @@ -439,3 +304,12 @@ func castPayload(payload interface{}) ([]byte, error) { body, err := json.Marshal(payload) return body, err } + +func signPayload(payload string, secret string) string { + var sig = hmac.New(sha256.New, []byte(secret)) + _, err := sig.Write([]byte(payload)) + if err != nil { + return "" + } + return hex.EncodeToString(sig.Sum(nil)) +} diff --git a/pkg/exchange/max/maxapi/reward.go b/pkg/exchange/max/maxapi/reward.go index 685580e055..5777618007 100644 --- a/pkg/exchange/max/maxapi/reward.go +++ b/pkg/exchange/max/maxapi/reward.go @@ -123,7 +123,7 @@ func (reward Reward) Reward() (*types.Reward, error) { } type RewardService struct { - client *RestClient + client requestgen.AuthenticatedAPIClient } func (s *RewardService) NewGetRewardsRequest() *GetRewardsRequest { diff --git a/pkg/exchange/max/maxapi/trade.go b/pkg/exchange/max/maxapi/trade.go index 093ce2e390..33f4eb70fe 100644 --- a/pkg/exchange/max/maxapi/trade.go +++ b/pkg/exchange/max/maxapi/trade.go @@ -10,6 +10,7 @@ import ( "github.com/c9s/requestgen" + "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) @@ -26,21 +27,24 @@ type TradeInfo struct { Ask *MarkerInfo `json:"ask,omitempty"` } +type Liquidity string + // Trade represents one returned trade on the max platform. type Trade struct { - ID uint64 `json:"id" db:"exchange_id"` - Price string `json:"price" db:"price"` - Volume string `json:"volume" db:"volume"` - Funds string `json:"funds"` - Market string `json:"market" db:"market"` - MarketName string `json:"market_name"` - CreatedAt int64 `json:"created_at"` - CreatedAtMilliSeconds types.MillisecondTimestamp `json:"created_at_in_ms"` - Side string `json:"side" db:"side"` - OrderID uint64 `json:"order_id"` - Fee string `json:"fee" db:"fee"` // float number as string - FeeCurrency string `json:"fee_currency" db:"fee_currency"` - Info TradeInfo `json:"info,omitempty"` + ID uint64 `json:"id" db:"exchange_id"` + WalletType WalletType `json:"wallet_type,omitempty"` + Price fixedpoint.Value `json:"price"` + Volume fixedpoint.Value `json:"volume"` + Funds fixedpoint.Value `json:"funds"` + Market string `json:"market"` + MarketName string `json:"market_name"` + CreatedAt types.MillisecondTimestamp `json:"created_at"` + Side string `json:"side"` + OrderID uint64 `json:"order_id"` + Fee fixedpoint.Value `json:"fee"` // float number as string + FeeCurrency string `json:"fee_currency"` + Liquidity Liquidity `json:"liquidity"` + Info TradeInfo `json:"info,omitempty"` } func (t Trade) IsBuyer() bool { @@ -63,7 +67,7 @@ type QueryTradeOptions struct { } type TradeService struct { - client *RestClient + client requestgen.AuthenticatedAPIClient } func (options *QueryTradeOptions) Map() map[string]interface{} { @@ -129,16 +133,16 @@ type PrivateRequestParams struct { type GetPrivateTradesRequest struct { client requestgen.AuthenticatedAPIClient - market string `param:"market"` + market string `param:"market"` // nolint:golint,structcheck // timestamp is the seconds elapsed since Unix epoch, set to return trades executed before the time only - timestamp *time.Time `param:"timestamp,seconds"` + timestamp *time.Time `param:"timestamp,seconds"` // nolint:golint,structcheck // From field is a trade id, set ot return trades created after the trade - from *int64 `param:"from"` + from *int64 `param:"from"` // nolint:golint,structcheck // To field trade id, set to return trades created before the trade - to *int64 `param:"to"` + to *int64 `param:"to"` // nolint:golint,structcheck orderBy *string `param:"order_by"` @@ -148,4 +152,3 @@ type GetPrivateTradesRequest struct { offset *int64 `param:"offset"` } - diff --git a/pkg/exchange/max/maxapi/trade_test.go b/pkg/exchange/max/maxapi/trade_test.go deleted file mode 100644 index 8287ee05fe..0000000000 --- a/pkg/exchange/max/maxapi/trade_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package max - -import ( - "context" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -func TestTradeService(t *testing.T) { - key, secret, ok := integrationTestConfigured(t, "MAX") - if !ok { - t.SkipNow() - } - - ctx := context.Background() - - client := NewRestClient(ProductionAPIURL) - client.Auth(key, secret) - - t.Run("default timestamp", func(t *testing.T) { - req := client.TradeService.NewGetPrivateTradeRequest() - until := time.Now().AddDate(0, -6, 0) - - trades, err := req.Market("btcusdt"). - Timestamp(until). - Do(ctx) - if assert.NoError(t, err) { - assert.NotEmptyf(t, trades, "got %d trades", len(trades)) - for _, td := range trades { - t.Logf("trade: %+v", td) - assert.True(t, td.CreatedAtMilliSeconds.Time().Before(until)) - } - } - }) - - t.Run("desc and pagination = false", func(t *testing.T) { - req := client.TradeService.NewGetPrivateTradeRequest() - trades, err := req.Market("btcusdt"). - Pagination(false). - OrderBy("asc"). - Do(ctx) - - if assert.NoError(t, err) { - assert.NotEmptyf(t, trades, "got %d trades", len(trades)) - for _, td := range trades { - t.Logf("trade: %+v", td) - } - } - }) -} diff --git a/pkg/exchange/max/maxapi/userdata.go b/pkg/exchange/max/maxapi/userdata.go index b2ade66008..8fe3e3c5d0 100644 --- a/pkg/exchange/max/maxapi/userdata.go +++ b/pkg/exchange/max/maxapi/userdata.go @@ -2,9 +2,11 @@ package max import ( "encoding/json" + "fmt" "strings" "github.com/pkg/errors" + log "github.com/sirupsen/logrus" "github.com/valyala/fastjson" "github.com/c9s/bbgo/pkg/fixedpoint" @@ -22,22 +24,23 @@ type OrderUpdate struct { Side string `json:"sd"` OrderType OrderType `json:"ot"` - Price string `json:"p"` - StopPrice string `json:"sp"` + Price fixedpoint.Value `json:"p"` + StopPrice fixedpoint.Value `json:"sp"` - Volume string `json:"v"` - AveragePrice string `json:"ap"` - State OrderState `json:"S"` - Market string `json:"M"` + Volume fixedpoint.Value `json:"v"` + AveragePrice fixedpoint.Value `json:"ap"` + State OrderState `json:"S"` + Market string `json:"M"` - RemainingVolume string `json:"rv"` - ExecutedVolume string `json:"ev"` + RemainingVolume fixedpoint.Value `json:"rv"` + ExecutedVolume fixedpoint.Value `json:"ev"` TradesCount int64 `json:"tc"` GroupID uint32 `json:"gi"` ClientOID string `json:"ci"` CreatedAtMs int64 `json:"T"` + UpdateTime int64 `json:"TU"` } type OrderUpdateEvent struct { @@ -46,35 +49,20 @@ type OrderUpdateEvent struct { Orders []OrderUpdate `json:"o"` } -func parserOrderUpdate(v *fastjson.Value) OrderUpdate { - return OrderUpdate{ - Event: string(v.GetStringBytes("e")), - ID: v.GetUint64("i"), - Side: string(v.GetStringBytes("sd")), - Market: string(v.GetStringBytes("M")), - OrderType: OrderType(v.GetStringBytes("ot")), - State: OrderState(v.GetStringBytes("S")), - Price: string(v.GetStringBytes("p")), - StopPrice: string(v.GetStringBytes("sp")), - AveragePrice: string(v.GetStringBytes("ap")), - Volume: string(v.GetStringBytes("v")), - RemainingVolume: string(v.GetStringBytes("rv")), - ExecutedVolume: string(v.GetStringBytes("ev")), - TradesCount: v.GetInt64("tc"), - GroupID: uint32(v.GetInt("gi")), - ClientOID: string(v.GetStringBytes("ci")), - CreatedAtMs: v.GetInt64("T"), - } -} - func parseOrderUpdateEvent(v *fastjson.Value) *OrderUpdateEvent { var e OrderUpdateEvent e.Event = string(v.GetStringBytes("e")) e.Timestamp = v.GetInt64("T") for _, ov := range v.GetArray("o") { - o := parserOrderUpdate(ov) - e.Orders = append(e.Orders, o) + var o = ov.String() + var u OrderUpdate + if err := json.Unmarshal([]byte(o), &u); err != nil { + log.WithError(err).Error("parse error") + continue + } + + e.Orders = append(e.Orders, u) } return &e @@ -92,8 +80,14 @@ func parserOrderSnapshotEvent(v *fastjson.Value) *OrderSnapshotEvent { e.Timestamp = v.GetInt64("T") for _, ov := range v.GetArray("o") { - o := parserOrderUpdate(ov) - e.Orders = append(e.Orders, o) + var o = ov.String() + var u OrderUpdate + if err := json.Unmarshal([]byte(o), &u); err != nil { + log.WithError(err).Error("parse error") + continue + } + + e.Orders = append(e.Orders, u) } return &e @@ -109,6 +103,7 @@ type TradeUpdate struct { Fee string `json:"f"` FeeCurrency string `json:"fc"` Timestamp int64 `json:"T"` + UpdateTime int64 `json:"TU"` OrderID uint64 `json:"oi"` @@ -125,6 +120,7 @@ func parseTradeUpdate(v *fastjson.Value) TradeUpdate { Fee: string(v.GetStringBytes("f")), FeeCurrency: string(v.GetStringBytes("fc")), Timestamp: v.GetInt64("T"), + UpdateTime: v.GetInt64("TU"), OrderID: v.GetUint64("oi"), Maker: v.GetBool("m"), } @@ -198,22 +194,76 @@ func parseAuthEvent(v *fastjson.Value) (*AuthEvent, error) { return &e, err } +type ADRatio struct { + ADRatio fixedpoint.Value `json:"ad"` + AssetInUSDT fixedpoint.Value `json:"as"` + DebtInUSDT fixedpoint.Value `json:"db"` + IndexPrices []struct { + Market string `json:"M"` + Price fixedpoint.Value `json:"p"` + } `json:"idxp"` + TU types.MillisecondTimestamp `json:"TU"` +} + +func (r *ADRatio) String() string { + return fmt.Sprintf("ADRatio: %v Asset: %v USDT, Debt: %v USDT (Mark Prices: %+v)", r.ADRatio, r.AssetInUSDT, r.DebtInUSDT, r.IndexPrices) +} + +type ADRatioEvent struct { + ADRatio ADRatio `json:"ad"` +} + +func parseADRatioEvent(v *fastjson.Value) (*ADRatioEvent, error) { + o := v.String() + e := ADRatioEvent{} + err := json.Unmarshal([]byte(o), &e) + return &e, err +} + +type Debt struct { + Currency string `json:"cu"` + DebtPrincipal fixedpoint.Value `json:"dbp"` + DebtInterest fixedpoint.Value `json:"dbi"` + TU types.MillisecondTimestamp `json:"TU"` +} + +func (d *Debt) String() string { + return fmt.Sprintf("Debt %s %v (Interest %v)", d.Currency, d.DebtPrincipal, d.DebtInterest) +} + +type DebtEvent struct { + Debts []Debt `json:"db"` +} + +func parseDebts(v *fastjson.Value) (*DebtEvent, error) { + o := v.String() + e := DebtEvent{} + err := json.Unmarshal([]byte(o), &e) + return &e, err +} + func ParseUserEvent(v *fastjson.Value) (interface{}, error) { eventType := string(v.GetStringBytes("e")) switch eventType { - case "order_snapshot": + case "order_snapshot", "mwallet_order_snapshot": return parserOrderSnapshotEvent(v), nil - case "order_update": + case "order_update", "mwallet_order_update": return parseOrderUpdateEvent(v), nil - case "trade_snapshot": + case "trade_snapshot", "mwallet_trade_snapshot": return parseTradeSnapshotEvent(v), nil - case "trade_update": + case "trade_update", "mwallet_trade_update": return parseTradeUpdateEvent(v), nil - case "account_snapshot", "account_update": + case "ad_ratio_snapshot", "ad_ratio_update": + return parseADRatioEvent(v) + + case "borrowing_snapshot", "borrowing_update": + return parseDebts(v) + + case "account_snapshot", "account_update", "mwallet_account_snapshot", "mwallet_account_update": var e AccountUpdateEvent o := v.String() err := json.Unmarshal([]byte(o), &e) diff --git a/pkg/exchange/max/maxapi/v3/cancel_order_request.go b/pkg/exchange/max/maxapi/v3/cancel_order_request.go new file mode 100644 index 0000000000..67bbaa52c8 --- /dev/null +++ b/pkg/exchange/max/maxapi/v3/cancel_order_request.go @@ -0,0 +1,19 @@ +package v3 + +//go:generate -command GetRequest requestgen -method GET +//go:generate -command PostRequest requestgen -method POST +//go:generate -command DeleteRequest requestgen -method DELETE + +import "github.com/c9s/requestgen" + +func (s *OrderService) NewCancelOrderRequest() *CancelOrderRequest { + return &CancelOrderRequest{client: s.Client} +} + +//go:generate DeleteRequest -url "/api/v3/order" -type CancelOrderRequest -responseType .Order +type CancelOrderRequest struct { + client requestgen.AuthenticatedAPIClient + + id *uint64 `param:"id,omitempty"` + clientOrderID *string `param:"client_oid,omitempty"` +} diff --git a/pkg/exchange/max/maxapi/v3/cancel_order_request_requestgen.go b/pkg/exchange/max/maxapi/v3/cancel_order_request_requestgen.go new file mode 100644 index 0000000000..12d9c684e4 --- /dev/null +++ b/pkg/exchange/max/maxapi/v3/cancel_order_request_requestgen.go @@ -0,0 +1,164 @@ +// Code generated by "requestgen -method DELETE -url /api/v3/order -type CancelOrderRequest -responseType .Order"; DO NOT EDIT. + +package v3 + +import ( + "context" + "encoding/json" + "fmt" + "github.com/c9s/bbgo/pkg/exchange/max/maxapi" + "net/url" + "reflect" + "regexp" +) + +func (c *CancelOrderRequest) Id(id uint64) *CancelOrderRequest { + c.id = &id + return c +} + +func (c *CancelOrderRequest) ClientOrderID(clientOrderID string) *CancelOrderRequest { + c.clientOrderID = &clientOrderID + return c +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (c *CancelOrderRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (c *CancelOrderRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check id field -> json key id + if c.id != nil { + id := *c.id + + // assign parameter of id + params["id"] = id + } else { + } + // check clientOrderID field -> json key client_oid + if c.clientOrderID != nil { + clientOrderID := *c.clientOrderID + + // assign parameter of clientOrderID + params["client_oid"] = clientOrderID + } else { + } + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (c *CancelOrderRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := c.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if c.isVarSlice(_v) { + c.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (c *CancelOrderRequest) GetParametersJSON() ([]byte, error) { + params, err := c.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (c *CancelOrderRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (c *CancelOrderRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (c *CancelOrderRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (c *CancelOrderRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (c *CancelOrderRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := c.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (c *CancelOrderRequest) Do(ctx context.Context) (*max.Order, error) { + + params, err := c.GetParameters() + if err != nil { + return nil, err + } + query := url.Values{} + + apiURL := "/api/v3/order" + + req, err := c.client.NewAuthenticatedRequest(ctx, "DELETE", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := c.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse max.Order + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return &apiResponse, nil +} diff --git a/pkg/exchange/max/maxapi/v3/cancel_wallet_order_all_request.go b/pkg/exchange/max/maxapi/v3/cancel_wallet_order_all_request.go new file mode 100644 index 0000000000..04825d3866 --- /dev/null +++ b/pkg/exchange/max/maxapi/v3/cancel_wallet_order_all_request.go @@ -0,0 +1,21 @@ +package v3 + +import "github.com/c9s/requestgen" + +//go:generate -command GetRequest requestgen -method GET +//go:generate -command PostRequest requestgen -method POST +//go:generate -command DeleteRequest requestgen -method DELETE + +func (s *OrderService) NewCancelWalletOrderAllRequest(walletType WalletType) *CancelWalletOrderAllRequest { + return &CancelWalletOrderAllRequest{client: s.Client, walletType: walletType} +} + +//go:generate DeleteRequest -url "/api/v3/wallet/:walletType/orders" -type CancelWalletOrderAllRequest -responseType []Order +type CancelWalletOrderAllRequest struct { + client requestgen.AuthenticatedAPIClient + + walletType WalletType `param:"walletType,slug,required"` + side *string `param:"side"` + market *string `param:"market"` + groupID *uint32 `param:"groupID"` +} diff --git a/pkg/exchange/max/maxapi/v3/cancel_wallet_order_all_request_requestgen.go b/pkg/exchange/max/maxapi/v3/cancel_wallet_order_all_request_requestgen.go new file mode 100644 index 0000000000..0015693f3e --- /dev/null +++ b/pkg/exchange/max/maxapi/v3/cancel_wallet_order_all_request_requestgen.go @@ -0,0 +1,199 @@ +// Code generated by "requestgen -method DELETE -url /api/v3/wallet/:walletType/orders -type CancelWalletOrderAllRequest -responseType []Order"; DO NOT EDIT. + +package v3 + +import ( + "context" + "encoding/json" + "fmt" + "github.com/c9s/bbgo/pkg/exchange/max/maxapi" + "net/url" + "reflect" + "regexp" +) + +func (c *CancelWalletOrderAllRequest) Side(side string) *CancelWalletOrderAllRequest { + c.side = &side + return c +} + +func (c *CancelWalletOrderAllRequest) Market(market string) *CancelWalletOrderAllRequest { + c.market = &market + return c +} + +func (c *CancelWalletOrderAllRequest) GroupID(groupID uint32) *CancelWalletOrderAllRequest { + c.groupID = &groupID + return c +} + +func (c *CancelWalletOrderAllRequest) WalletType(walletType max.WalletType) *CancelWalletOrderAllRequest { + c.walletType = walletType + return c +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (c *CancelWalletOrderAllRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (c *CancelWalletOrderAllRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check side field -> json key side + if c.side != nil { + side := *c.side + + // assign parameter of side + params["side"] = side + } else { + } + // check market field -> json key market + if c.market != nil { + market := *c.market + + // assign parameter of market + params["market"] = market + } else { + } + // check groupID field -> json key groupID + if c.groupID != nil { + groupID := *c.groupID + + // assign parameter of groupID + params["groupID"] = groupID + } else { + } + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (c *CancelWalletOrderAllRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := c.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if c.isVarSlice(_v) { + c.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (c *CancelWalletOrderAllRequest) GetParametersJSON() ([]byte, error) { + params, err := c.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (c *CancelWalletOrderAllRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check walletType field -> json key walletType + walletType := c.walletType + + // TEMPLATE check-required + if len(walletType) == 0 { + return nil, fmt.Errorf("walletType is required, empty string given") + } + // END TEMPLATE check-required + + // assign parameter of walletType + params["walletType"] = walletType + + return params, nil +} + +func (c *CancelWalletOrderAllRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (c *CancelWalletOrderAllRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (c *CancelWalletOrderAllRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (c *CancelWalletOrderAllRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := c.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (c *CancelWalletOrderAllRequest) Do(ctx context.Context) ([]max.Order, error) { + + params, err := c.GetParameters() + if err != nil { + return nil, err + } + query := url.Values{} + + apiURL := "/api/v3/wallet/:walletType/orders" + slugs, err := c.GetSlugsMap() + if err != nil { + return nil, err + } + + apiURL = c.applySlugsToUrl(apiURL, slugs) + + req, err := c.client.NewAuthenticatedRequest(ctx, "DELETE", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := c.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse []max.Order + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return apiResponse, nil +} diff --git a/pkg/exchange/max/maxapi/v3/create_wallet_order_request.go b/pkg/exchange/max/maxapi/v3/create_wallet_order_request.go new file mode 100644 index 0000000000..1c9c589912 --- /dev/null +++ b/pkg/exchange/max/maxapi/v3/create_wallet_order_request.go @@ -0,0 +1,27 @@ +package v3 + +import "github.com/c9s/requestgen" + +//go:generate -command GetRequest requestgen -method GET +//go:generate -command PostRequest requestgen -method POST +//go:generate -command DeleteRequest requestgen -method DELETE + +//go:generate PostRequest -url "/api/v3/wallet/:walletType/order" -type CreateWalletOrderRequest -responseType .Order +type CreateWalletOrderRequest struct { + client requestgen.AuthenticatedAPIClient + + walletType WalletType `param:"walletType,slug,required"` + market string `param:"market,required"` + side string `param:"side,required"` + volume string `param:"volume,required"` + orderType OrderType `param:"ord_type"` + + price *string `param:"price"` + stopPrice *string `param:"stop_price"` + clientOrderID *string `param:"client_oid"` + groupID *string `param:"group_id"` +} + +func (s *OrderService) NewCreateWalletOrderRequest(walletType WalletType) *CreateWalletOrderRequest { + return &CreateWalletOrderRequest{client: s.Client, walletType: walletType} +} diff --git a/pkg/exchange/max/maxapi/v3/create_wallet_order_request_requestgen.go b/pkg/exchange/max/maxapi/v3/create_wallet_order_request_requestgen.go new file mode 100644 index 0000000000..79bd83a207 --- /dev/null +++ b/pkg/exchange/max/maxapi/v3/create_wallet_order_request_requestgen.go @@ -0,0 +1,270 @@ +// Code generated by "requestgen -method POST -url /api/v3/wallet/:walletType/order -type CreateWalletOrderRequest -responseType .Order"; DO NOT EDIT. + +package v3 + +import ( + "context" + "encoding/json" + "fmt" + "github.com/c9s/bbgo/pkg/exchange/max/maxapi" + "net/url" + "reflect" + "regexp" +) + +func (c *CreateWalletOrderRequest) Market(market string) *CreateWalletOrderRequest { + c.market = market + return c +} + +func (c *CreateWalletOrderRequest) Side(side string) *CreateWalletOrderRequest { + c.side = side + return c +} + +func (c *CreateWalletOrderRequest) Volume(volume string) *CreateWalletOrderRequest { + c.volume = volume + return c +} + +func (c *CreateWalletOrderRequest) OrderType(orderType max.OrderType) *CreateWalletOrderRequest { + c.orderType = orderType + return c +} + +func (c *CreateWalletOrderRequest) Price(price string) *CreateWalletOrderRequest { + c.price = &price + return c +} + +func (c *CreateWalletOrderRequest) StopPrice(stopPrice string) *CreateWalletOrderRequest { + c.stopPrice = &stopPrice + return c +} + +func (c *CreateWalletOrderRequest) ClientOrderID(clientOrderID string) *CreateWalletOrderRequest { + c.clientOrderID = &clientOrderID + return c +} + +func (c *CreateWalletOrderRequest) GroupID(groupID string) *CreateWalletOrderRequest { + c.groupID = &groupID + return c +} + +func (c *CreateWalletOrderRequest) WalletType(walletType max.WalletType) *CreateWalletOrderRequest { + c.walletType = walletType + return c +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (c *CreateWalletOrderRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (c *CreateWalletOrderRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check market field -> json key market + market := c.market + + // TEMPLATE check-required + if len(market) == 0 { + return nil, fmt.Errorf("market is required, empty string given") + } + // END TEMPLATE check-required + + // assign parameter of market + params["market"] = market + // check side field -> json key side + side := c.side + + // TEMPLATE check-required + if len(side) == 0 { + return nil, fmt.Errorf("side is required, empty string given") + } + // END TEMPLATE check-required + + // assign parameter of side + params["side"] = side + // check volume field -> json key volume + volume := c.volume + + // TEMPLATE check-required + if len(volume) == 0 { + return nil, fmt.Errorf("volume is required, empty string given") + } + // END TEMPLATE check-required + + // assign parameter of volume + params["volume"] = volume + // check orderType field -> json key ord_type + orderType := c.orderType + + // assign parameter of orderType + params["ord_type"] = orderType + // check price field -> json key price + if c.price != nil { + price := *c.price + + // assign parameter of price + params["price"] = price + } else { + } + // check stopPrice field -> json key stop_price + if c.stopPrice != nil { + stopPrice := *c.stopPrice + + // assign parameter of stopPrice + params["stop_price"] = stopPrice + } else { + } + // check clientOrderID field -> json key client_oid + if c.clientOrderID != nil { + clientOrderID := *c.clientOrderID + + // assign parameter of clientOrderID + params["client_oid"] = clientOrderID + } else { + } + // check groupID field -> json key group_id + if c.groupID != nil { + groupID := *c.groupID + + // assign parameter of groupID + params["group_id"] = groupID + } else { + } + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (c *CreateWalletOrderRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := c.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if c.isVarSlice(_v) { + c.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (c *CreateWalletOrderRequest) GetParametersJSON() ([]byte, error) { + params, err := c.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (c *CreateWalletOrderRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check walletType field -> json key walletType + walletType := c.walletType + + // TEMPLATE check-required + if len(walletType) == 0 { + return nil, fmt.Errorf("walletType is required, empty string given") + } + // END TEMPLATE check-required + + // assign parameter of walletType + params["walletType"] = walletType + + return params, nil +} + +func (c *CreateWalletOrderRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (c *CreateWalletOrderRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (c *CreateWalletOrderRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (c *CreateWalletOrderRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := c.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (c *CreateWalletOrderRequest) Do(ctx context.Context) (*max.Order, error) { + + params, err := c.GetParameters() + if err != nil { + return nil, err + } + query := url.Values{} + + apiURL := "/api/v3/wallet/:walletType/order" + slugs, err := c.GetSlugsMap() + if err != nil { + return nil, err + } + + apiURL = c.applySlugsToUrl(apiURL, slugs) + + req, err := c.client.NewAuthenticatedRequest(ctx, "POST", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := c.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse max.Order + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return &apiResponse, nil +} diff --git a/pkg/exchange/max/maxapi/v3/get_margin_ad_ratio_request.go b/pkg/exchange/max/maxapi/v3/get_margin_ad_ratio_request.go new file mode 100644 index 0000000000..f01cc7c2bb --- /dev/null +++ b/pkg/exchange/max/maxapi/v3/get_margin_ad_ratio_request.go @@ -0,0 +1,26 @@ +package v3 + +import ( + "github.com/c9s/requestgen" + + "github.com/c9s/bbgo/pkg/fixedpoint" +) + +//go:generate -command GetRequest requestgen -method GET +//go:generate -command PostRequest requestgen -method POST +//go:generate -command DeleteRequest requestgen -method DELETE + +func (s *MarginService) NewGetMarginADRatioRequest() *GetMarginADRatioRequest { + return &GetMarginADRatioRequest{client: s.Client} +} + +type ADRatio struct { + AdRatio fixedpoint.Value `json:"ad_ratio"` + AssetInUsdt fixedpoint.Value `json:"asset_in_usdt"` + DebtInUsdt fixedpoint.Value `json:"debt_in_usdt"` +} + +//go:generate GetRequest -url "/api/v3/wallet/m/ad_ratio" -type GetMarginADRatioRequest -responseType .ADRatio +type GetMarginADRatioRequest struct { + client requestgen.AuthenticatedAPIClient +} diff --git a/pkg/exchange/max/maxapi/get_me_request_requestgen.go b/pkg/exchange/max/maxapi/v3/get_margin_ad_ratio_request_requestgen.go similarity index 50% rename from pkg/exchange/max/maxapi/get_me_request_requestgen.go rename to pkg/exchange/max/maxapi/v3/get_margin_ad_ratio_request_requestgen.go index 1d73178439..cf54325a93 100644 --- a/pkg/exchange/max/maxapi/get_me_request_requestgen.go +++ b/pkg/exchange/max/maxapi/v3/get_margin_ad_ratio_request_requestgen.go @@ -1,6 +1,6 @@ -// Code generated by "requestgen -method GET -url v2/members/me -type GetMeRequest -responseType .UserInfo"; DO NOT EDIT. +// Code generated by "requestgen -method GET -url /api/v3/wallet/m/ad_ratio -type GetMarginADRatioRequest -responseType .ADRatio"; DO NOT EDIT. -package max +package v3 import ( "context" @@ -12,26 +12,26 @@ import ( ) // GetQueryParameters builds and checks the query parameters and returns url.Values -func (g *GetMeRequest) GetQueryParameters() (url.Values, error) { +func (g *GetMarginADRatioRequest) GetQueryParameters() (url.Values, error) { var params = map[string]interface{}{} query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) } return query, nil } // GetParameters builds and checks the parameters and return the result in a map object -func (g *GetMeRequest) GetParameters() (map[string]interface{}, error) { +func (g *GetMarginADRatioRequest) GetParameters() (map[string]interface{}, error) { var params = map[string]interface{}{} return params, nil } // GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (g *GetMeRequest) GetParametersQuery() (url.Values, error) { +func (g *GetMarginADRatioRequest) GetParametersQuery() (url.Values, error) { query := url.Values{} params, err := g.GetParameters() @@ -39,13 +39,13 @@ func (g *GetMeRequest) GetParametersQuery() (url.Values, error) { return query, err } - for k, v := range params { - if g.isVarSlice(v) { - g.iterateSlice(v, func(it interface{}) { - query.Add(k+"[]", fmt.Sprintf("%v", it)) + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) }) } else { - query.Add(k, fmt.Sprintf("%v", v)) + query.Add(_k, fmt.Sprintf("%v", _v)) } } @@ -53,7 +53,7 @@ func (g *GetMeRequest) GetParametersQuery() (url.Values, error) { } // GetParametersJSON converts the parameters from GetParameters into the JSON format -func (g *GetMeRequest) GetParametersJSON() ([]byte, error) { +func (g *GetMarginADRatioRequest) GetParametersJSON() ([]byte, error) { params, err := g.GetParameters() if err != nil { return nil, err @@ -63,31 +63,31 @@ func (g *GetMeRequest) GetParametersJSON() ([]byte, error) { } // GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (g *GetMeRequest) GetSlugParameters() (map[string]interface{}, error) { +func (g *GetMarginADRatioRequest) GetSlugParameters() (map[string]interface{}, error) { var params = map[string]interface{}{} return params, nil } -func (g *GetMeRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) +func (g *GetMarginADRatioRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) } return url } -func (g *GetMeRequest) iterateSlice(slice interface{}, f func(it interface{})) { +func (g *GetMarginADRatioRequest) iterateSlice(slice interface{}, _f func(it interface{})) { sliceValue := reflect.ValueOf(slice) - for i := 0; i < sliceValue.Len(); i++ { - it := sliceValue.Index(i).Interface() - f(it) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) } } -func (g *GetMeRequest) isVarSlice(v interface{}) bool { - rt := reflect.TypeOf(v) +func (g *GetMarginADRatioRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) switch rt.Kind() { case reflect.Slice: return true @@ -95,27 +95,27 @@ func (g *GetMeRequest) isVarSlice(v interface{}) bool { return false } -func (g *GetMeRequest) GetSlugsMap() (map[string]string, error) { +func (g *GetMarginADRatioRequest) GetSlugsMap() (map[string]string, error) { slugs := map[string]string{} params, err := g.GetSlugParameters() if err != nil { return slugs, nil } - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) } return slugs, nil } -func (g *GetMeRequest) Do(ctx context.Context) (*UserInfo, error) { +func (g *GetMarginADRatioRequest) Do(ctx context.Context) (*ADRatio, error) { // no body params var params interface{} query := url.Values{} - apiURL := "v2/members/me" + apiURL := "/api/v3/wallet/m/ad_ratio" req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) if err != nil { @@ -127,7 +127,7 @@ func (g *GetMeRequest) Do(ctx context.Context) (*UserInfo, error) { return nil, err } - var apiResponse UserInfo + var apiResponse ADRatio if err := response.DecodeJSON(&apiResponse); err != nil { return nil, err } diff --git a/pkg/exchange/max/maxapi/v3/get_margin_borrowing_limits_request_requestgen.go b/pkg/exchange/max/maxapi/v3/get_margin_borrowing_limits_request_requestgen.go new file mode 100644 index 0000000000..4c631eab9e --- /dev/null +++ b/pkg/exchange/max/maxapi/v3/get_margin_borrowing_limits_request_requestgen.go @@ -0,0 +1,135 @@ +// Code generated by "requestgen -method GET -url /api/v3/wallet/m/limits -type GetMarginBorrowingLimitsRequest -responseType .MarginBorrowingLimitMap"; DO NOT EDIT. + +package v3 + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetMarginBorrowingLimitsRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetMarginBorrowingLimitsRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetMarginBorrowingLimitsRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetMarginBorrowingLimitsRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetMarginBorrowingLimitsRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetMarginBorrowingLimitsRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetMarginBorrowingLimitsRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetMarginBorrowingLimitsRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetMarginBorrowingLimitsRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetMarginBorrowingLimitsRequest) Do(ctx context.Context) (*MarginBorrowingLimitMap, error) { + + // no body params + var params interface{} + query := url.Values{} + + apiURL := "/api/v3/wallet/m/limits" + + req, err := g.client.NewRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse MarginBorrowingLimitMap + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return &apiResponse, nil +} diff --git a/pkg/exchange/max/maxapi/v3/get_margin_interest_history_request_requestgen.go b/pkg/exchange/max/maxapi/v3/get_margin_interest_history_request_requestgen.go new file mode 100644 index 0000000000..60002d36bc --- /dev/null +++ b/pkg/exchange/max/maxapi/v3/get_margin_interest_history_request_requestgen.go @@ -0,0 +1,203 @@ +// Code generated by "requestgen -method GET -url /api/v3/wallet/m/interests/history/:currency -type GetMarginInterestHistoryRequest -responseType []MarginInterestRecord"; DO NOT EDIT. + +package v3 + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" + "strconv" + "time" +) + +func (g *GetMarginInterestHistoryRequest) StartTime(startTime time.Time) *GetMarginInterestHistoryRequest { + g.startTime = &startTime + return g +} + +func (g *GetMarginInterestHistoryRequest) EndTime(endTime time.Time) *GetMarginInterestHistoryRequest { + g.endTime = &endTime + return g +} + +func (g *GetMarginInterestHistoryRequest) Limit(limit int) *GetMarginInterestHistoryRequest { + g.limit = &limit + return g +} + +func (g *GetMarginInterestHistoryRequest) Currency(currency string) *GetMarginInterestHistoryRequest { + g.currency = currency + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetMarginInterestHistoryRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetMarginInterestHistoryRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check startTime field -> json key startTime + if g.startTime != nil { + startTime := *g.startTime + + // assign parameter of startTime + // convert time.Time to milliseconds time stamp + params["startTime"] = strconv.FormatInt(startTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check endTime field -> json key endTime + if g.endTime != nil { + endTime := *g.endTime + + // assign parameter of endTime + // convert time.Time to milliseconds time stamp + params["endTime"] = strconv.FormatInt(endTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check limit field -> json key limit + if g.limit != nil { + limit := *g.limit + + // assign parameter of limit + params["limit"] = limit + } else { + } + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetMarginInterestHistoryRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetMarginInterestHistoryRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetMarginInterestHistoryRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check currency field -> json key currency + currency := g.currency + + // TEMPLATE check-required + if len(currency) == 0 { + return nil, fmt.Errorf("currency is required, empty string given") + } + // END TEMPLATE check-required + + // assign parameter of currency + params["currency"] = currency + + return params, nil +} + +func (g *GetMarginInterestHistoryRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetMarginInterestHistoryRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetMarginInterestHistoryRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetMarginInterestHistoryRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetMarginInterestHistoryRequest) Do(ctx context.Context) ([]MarginInterestRecord, error) { + + // empty params for GET operation + var params interface{} + query, err := g.GetParametersQuery() + if err != nil { + return nil, err + } + + apiURL := "/api/v3/wallet/m/interests/history/:currency" + slugs, err := g.GetSlugsMap() + if err != nil { + return nil, err + } + + apiURL = g.applySlugsToUrl(apiURL, slugs) + + req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse []MarginInterestRecord + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return apiResponse, nil +} diff --git a/pkg/exchange/max/maxapi/v3/get_margin_interest_rates_request_requestgen.go b/pkg/exchange/max/maxapi/v3/get_margin_interest_rates_request_requestgen.go new file mode 100644 index 0000000000..6de0e5eaa0 --- /dev/null +++ b/pkg/exchange/max/maxapi/v3/get_margin_interest_rates_request_requestgen.go @@ -0,0 +1,135 @@ +// Code generated by "requestgen -method GET -url /api/v3/wallet/m/interest_rates -type GetMarginInterestRatesRequest -responseType .MarginInterestRateMap"; DO NOT EDIT. + +package v3 + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetMarginInterestRatesRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetMarginInterestRatesRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetMarginInterestRatesRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetMarginInterestRatesRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetMarginInterestRatesRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetMarginInterestRatesRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetMarginInterestRatesRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetMarginInterestRatesRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetMarginInterestRatesRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetMarginInterestRatesRequest) Do(ctx context.Context) (*MarginInterestRateMap, error) { + + // no body params + var params interface{} + query := url.Values{} + + apiURL := "/api/v3/wallet/m/interest_rates" + + req, err := g.client.NewRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse MarginInterestRateMap + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return &apiResponse, nil +} diff --git a/pkg/exchange/max/maxapi/v3/get_margin_liquidation_history_request_requestgen.go b/pkg/exchange/max/maxapi/v3/get_margin_liquidation_history_request_requestgen.go new file mode 100644 index 0000000000..257b8c8e7e --- /dev/null +++ b/pkg/exchange/max/maxapi/v3/get_margin_liquidation_history_request_requestgen.go @@ -0,0 +1,181 @@ +// Code generated by "requestgen -method GET -url /api/v3/wallet/m/liquidations -type GetMarginLiquidationHistoryRequest -responseType []LiquidationRecord"; DO NOT EDIT. + +package v3 + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" + "strconv" + "time" +) + +func (g *GetMarginLiquidationHistoryRequest) StartTime(startTime time.Time) *GetMarginLiquidationHistoryRequest { + g.startTime = &startTime + return g +} + +func (g *GetMarginLiquidationHistoryRequest) EndTime(endTime time.Time) *GetMarginLiquidationHistoryRequest { + g.endTime = &endTime + return g +} + +func (g *GetMarginLiquidationHistoryRequest) Limit(limit int) *GetMarginLiquidationHistoryRequest { + g.limit = &limit + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetMarginLiquidationHistoryRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetMarginLiquidationHistoryRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check startTime field -> json key startTime + if g.startTime != nil { + startTime := *g.startTime + + // assign parameter of startTime + // convert time.Time to milliseconds time stamp + params["startTime"] = strconv.FormatInt(startTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check endTime field -> json key endTime + if g.endTime != nil { + endTime := *g.endTime + + // assign parameter of endTime + // convert time.Time to milliseconds time stamp + params["endTime"] = strconv.FormatInt(endTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check limit field -> json key limit + if g.limit != nil { + limit := *g.limit + + // assign parameter of limit + params["limit"] = limit + } else { + } + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetMarginLiquidationHistoryRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetMarginLiquidationHistoryRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetMarginLiquidationHistoryRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetMarginLiquidationHistoryRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetMarginLiquidationHistoryRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetMarginLiquidationHistoryRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetMarginLiquidationHistoryRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetMarginLiquidationHistoryRequest) Do(ctx context.Context) ([]LiquidationRecord, error) { + + // empty params for GET operation + var params interface{} + query, err := g.GetParametersQuery() + if err != nil { + return nil, err + } + + apiURL := "/api/v3/wallet/m/liquidations" + + req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse []LiquidationRecord + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return apiResponse, nil +} diff --git a/pkg/exchange/max/maxapi/v3/get_margin_loan_history_request_requestgen.go b/pkg/exchange/max/maxapi/v3/get_margin_loan_history_request_requestgen.go new file mode 100644 index 0000000000..e0ca63db00 --- /dev/null +++ b/pkg/exchange/max/maxapi/v3/get_margin_loan_history_request_requestgen.go @@ -0,0 +1,203 @@ +// Code generated by "requestgen -method GET -url /api/v3/wallet/m/loans/:currency -type GetMarginLoanHistoryRequest -responseType []LoanRecord"; DO NOT EDIT. + +package v3 + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" + "strconv" + "time" +) + +func (g *GetMarginLoanHistoryRequest) StartTime(startTime time.Time) *GetMarginLoanHistoryRequest { + g.startTime = &startTime + return g +} + +func (g *GetMarginLoanHistoryRequest) EndTime(endTime time.Time) *GetMarginLoanHistoryRequest { + g.endTime = &endTime + return g +} + +func (g *GetMarginLoanHistoryRequest) Limit(limit int) *GetMarginLoanHistoryRequest { + g.limit = &limit + return g +} + +func (g *GetMarginLoanHistoryRequest) Currency(currency string) *GetMarginLoanHistoryRequest { + g.currency = currency + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetMarginLoanHistoryRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetMarginLoanHistoryRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check startTime field -> json key startTime + if g.startTime != nil { + startTime := *g.startTime + + // assign parameter of startTime + // convert time.Time to milliseconds time stamp + params["startTime"] = strconv.FormatInt(startTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check endTime field -> json key endTime + if g.endTime != nil { + endTime := *g.endTime + + // assign parameter of endTime + // convert time.Time to milliseconds time stamp + params["endTime"] = strconv.FormatInt(endTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check limit field -> json key limit + if g.limit != nil { + limit := *g.limit + + // assign parameter of limit + params["limit"] = limit + } else { + } + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetMarginLoanHistoryRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetMarginLoanHistoryRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetMarginLoanHistoryRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check currency field -> json key currency + currency := g.currency + + // TEMPLATE check-required + if len(currency) == 0 { + return nil, fmt.Errorf("currency is required, empty string given") + } + // END TEMPLATE check-required + + // assign parameter of currency + params["currency"] = currency + + return params, nil +} + +func (g *GetMarginLoanHistoryRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetMarginLoanHistoryRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetMarginLoanHistoryRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetMarginLoanHistoryRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetMarginLoanHistoryRequest) Do(ctx context.Context) ([]LoanRecord, error) { + + // empty params for GET operation + var params interface{} + query, err := g.GetParametersQuery() + if err != nil { + return nil, err + } + + apiURL := "/api/v3/wallet/m/loans/:currency" + slugs, err := g.GetSlugsMap() + if err != nil { + return nil, err + } + + apiURL = g.applySlugsToUrl(apiURL, slugs) + + req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse []LoanRecord + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return apiResponse, nil +} diff --git a/pkg/exchange/max/maxapi/v3/get_margin_repayment_history_request_requestgen.go b/pkg/exchange/max/maxapi/v3/get_margin_repayment_history_request_requestgen.go new file mode 100644 index 0000000000..83edcd7fdb --- /dev/null +++ b/pkg/exchange/max/maxapi/v3/get_margin_repayment_history_request_requestgen.go @@ -0,0 +1,203 @@ +// Code generated by "requestgen -method GET -url /api/v3/wallet/m/repayments/:currency -type GetMarginRepaymentHistoryRequest -responseType []RepaymentRecord"; DO NOT EDIT. + +package v3 + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" + "strconv" + "time" +) + +func (g *GetMarginRepaymentHistoryRequest) StartTime(startTime time.Time) *GetMarginRepaymentHistoryRequest { + g.startTime = &startTime + return g +} + +func (g *GetMarginRepaymentHistoryRequest) EndTime(endTime time.Time) *GetMarginRepaymentHistoryRequest { + g.endTime = &endTime + return g +} + +func (g *GetMarginRepaymentHistoryRequest) Limit(limit int) *GetMarginRepaymentHistoryRequest { + g.limit = &limit + return g +} + +func (g *GetMarginRepaymentHistoryRequest) Currency(currency string) *GetMarginRepaymentHistoryRequest { + g.currency = currency + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetMarginRepaymentHistoryRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetMarginRepaymentHistoryRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check startTime field -> json key startTime + if g.startTime != nil { + startTime := *g.startTime + + // assign parameter of startTime + // convert time.Time to milliseconds time stamp + params["startTime"] = strconv.FormatInt(startTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check endTime field -> json key endTime + if g.endTime != nil { + endTime := *g.endTime + + // assign parameter of endTime + // convert time.Time to milliseconds time stamp + params["endTime"] = strconv.FormatInt(endTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check limit field -> json key limit + if g.limit != nil { + limit := *g.limit + + // assign parameter of limit + params["limit"] = limit + } else { + } + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetMarginRepaymentHistoryRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetMarginRepaymentHistoryRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetMarginRepaymentHistoryRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check currency field -> json key currency + currency := g.currency + + // TEMPLATE check-required + if len(currency) == 0 { + return nil, fmt.Errorf("currency is required, empty string given") + } + // END TEMPLATE check-required + + // assign parameter of currency + params["currency"] = currency + + return params, nil +} + +func (g *GetMarginRepaymentHistoryRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetMarginRepaymentHistoryRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetMarginRepaymentHistoryRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetMarginRepaymentHistoryRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetMarginRepaymentHistoryRequest) Do(ctx context.Context) ([]RepaymentRecord, error) { + + // empty params for GET operation + var params interface{} + query, err := g.GetParametersQuery() + if err != nil { + return nil, err + } + + apiURL := "/api/v3/wallet/m/repayments/:currency" + slugs, err := g.GetSlugsMap() + if err != nil { + return nil, err + } + + apiURL = g.applySlugsToUrl(apiURL, slugs) + + req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse []RepaymentRecord + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return apiResponse, nil +} diff --git a/pkg/exchange/max/maxapi/v3/get_order_request.go b/pkg/exchange/max/maxapi/v3/get_order_request.go new file mode 100644 index 0000000000..94de554210 --- /dev/null +++ b/pkg/exchange/max/maxapi/v3/get_order_request.go @@ -0,0 +1,19 @@ +package v3 + +//go:generate -command GetRequest requestgen -method GET +//go:generate -command PostRequest requestgen -method POST +//go:generate -command DeleteRequest requestgen -method DELETE + +import "github.com/c9s/requestgen" + +func (s *OrderService) NewGetOrderRequest() *GetOrderRequest { + return &GetOrderRequest{client: s.Client} +} + +//go:generate GetRequest -url "/api/v3/order" -type GetOrderRequest -responseType .Order +type GetOrderRequest struct { + client requestgen.AuthenticatedAPIClient + + id *uint64 `param:"id,omitempty"` + clientOrderID *string `param:"client_oid,omitempty"` +} diff --git a/pkg/exchange/max/maxapi/get_order_request_requestgen.go b/pkg/exchange/max/maxapi/v3/get_order_request_requestgen.go similarity index 73% rename from pkg/exchange/max/maxapi/get_order_request_requestgen.go rename to pkg/exchange/max/maxapi/v3/get_order_request_requestgen.go index 4f495fbe0a..ae8c39e019 100644 --- a/pkg/exchange/max/maxapi/get_order_request_requestgen.go +++ b/pkg/exchange/max/maxapi/v3/get_order_request_requestgen.go @@ -1,11 +1,12 @@ -// Code generated by "requestgen -method GET -url v2/order -type GetOrderRequest -responseType .Order"; DO NOT EDIT. +// Code generated by "requestgen -method GET -url /api/v3/order -type GetOrderRequest -responseType .Order"; DO NOT EDIT. -package max +package v3 import ( "context" "encoding/json" "fmt" + "github.com/c9s/bbgo/pkg/exchange/max/maxapi" "net/url" "reflect" "regexp" @@ -26,8 +27,8 @@ func (g *GetOrderRequest) GetQueryParameters() (url.Values, error) { var params = map[string]interface{}{} query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) } return query, nil @@ -65,13 +66,13 @@ func (g *GetOrderRequest) GetParametersQuery() (url.Values, error) { return query, err } - for k, v := range params { - if g.isVarSlice(v) { - g.iterateSlice(v, func(it interface{}) { - query.Add(k+"[]", fmt.Sprintf("%v", it)) + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) }) } else { - query.Add(k, fmt.Sprintf("%v", v)) + query.Add(_k, fmt.Sprintf("%v", _v)) } } @@ -96,24 +97,24 @@ func (g *GetOrderRequest) GetSlugParameters() (map[string]interface{}, error) { } func (g *GetOrderRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) } return url } -func (g *GetOrderRequest) iterateSlice(slice interface{}, f func(it interface{})) { +func (g *GetOrderRequest) iterateSlice(slice interface{}, _f func(it interface{})) { sliceValue := reflect.ValueOf(slice) - for i := 0; i < sliceValue.Len(); i++ { - it := sliceValue.Index(i).Interface() - f(it) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) } } -func (g *GetOrderRequest) isVarSlice(v interface{}) bool { - rt := reflect.TypeOf(v) +func (g *GetOrderRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) switch rt.Kind() { case reflect.Slice: return true @@ -128,14 +129,14 @@ func (g *GetOrderRequest) GetSlugsMap() (map[string]string, error) { return slugs, nil } - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) } return slugs, nil } -func (g *GetOrderRequest) Do(ctx context.Context) (*Order, error) { +func (g *GetOrderRequest) Do(ctx context.Context) (*max.Order, error) { // empty params for GET operation var params interface{} @@ -144,7 +145,7 @@ func (g *GetOrderRequest) Do(ctx context.Context) (*Order, error) { return nil, err } - apiURL := "v2/order" + apiURL := "/api/v3/order" req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) if err != nil { @@ -156,7 +157,7 @@ func (g *GetOrderRequest) Do(ctx context.Context) (*Order, error) { return nil, err } - var apiResponse Order + var apiResponse max.Order if err := response.DecodeJSON(&apiResponse); err != nil { return nil, err } diff --git a/pkg/exchange/max/maxapi/v3/get_order_trades_request.go b/pkg/exchange/max/maxapi/v3/get_order_trades_request.go new file mode 100644 index 0000000000..4ab982b02f --- /dev/null +++ b/pkg/exchange/max/maxapi/v3/get_order_trades_request.go @@ -0,0 +1,19 @@ +package v3 + +//go:generate -command GetRequest requestgen -method GET +//go:generate -command PostRequest requestgen -method POST +//go:generate -command DeleteRequest requestgen -method DELETE + +import "github.com/c9s/requestgen" + +func (s *OrderService) NewGetOrderTradesRequest() *GetOrderTradesRequest { + return &GetOrderTradesRequest{client: s.Client} +} + +//go:generate GetRequest -url "/api/v3/order/trades" -type GetOrderTradesRequest -responseType []Trade +type GetOrderTradesRequest struct { + client requestgen.AuthenticatedAPIClient + + orderID *uint64 `param:"order_id,omitempty"` + clientOrderID *string `param:"client_oid,omitempty"` +} diff --git a/pkg/exchange/max/maxapi/v3/get_order_trades_request_requestgen.go b/pkg/exchange/max/maxapi/v3/get_order_trades_request_requestgen.go new file mode 100644 index 0000000000..10bd1cd448 --- /dev/null +++ b/pkg/exchange/max/maxapi/v3/get_order_trades_request_requestgen.go @@ -0,0 +1,165 @@ +// Code generated by "requestgen -method GET -url /api/v3/order/trades -type GetOrderTradesRequest -responseType []Trade"; DO NOT EDIT. + +package v3 + +import ( + "context" + "encoding/json" + "fmt" + "github.com/c9s/bbgo/pkg/exchange/max/maxapi" + "net/url" + "reflect" + "regexp" +) + +func (g *GetOrderTradesRequest) OrderID(orderID uint64) *GetOrderTradesRequest { + g.orderID = &orderID + return g +} + +func (g *GetOrderTradesRequest) ClientOrderID(clientOrderID string) *GetOrderTradesRequest { + g.clientOrderID = &clientOrderID + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetOrderTradesRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetOrderTradesRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check orderID field -> json key order_id + if g.orderID != nil { + orderID := *g.orderID + + // assign parameter of orderID + params["order_id"] = orderID + } else { + } + // check clientOrderID field -> json key client_oid + if g.clientOrderID != nil { + clientOrderID := *g.clientOrderID + + // assign parameter of clientOrderID + params["client_oid"] = clientOrderID + } else { + } + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetOrderTradesRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetOrderTradesRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetOrderTradesRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetOrderTradesRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetOrderTradesRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetOrderTradesRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetOrderTradesRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetOrderTradesRequest) Do(ctx context.Context) ([]max.Trade, error) { + + // empty params for GET operation + var params interface{} + query, err := g.GetParametersQuery() + if err != nil { + return nil, err + } + + apiURL := "/api/v3/order/trades" + + req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse []max.Trade + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return apiResponse, nil +} diff --git a/pkg/exchange/max/maxapi/v3/get_wallet_accounts_request.go b/pkg/exchange/max/maxapi/v3/get_wallet_accounts_request.go new file mode 100644 index 0000000000..20b2ebdd8c --- /dev/null +++ b/pkg/exchange/max/maxapi/v3/get_wallet_accounts_request.go @@ -0,0 +1,18 @@ +package v3 + +import "github.com/c9s/requestgen" + +//go:generate -command GetRequest requestgen -method GET +//go:generate -command PostRequest requestgen -method POST +//go:generate -command DeleteRequest requestgen -method DELETE + +func (s *OrderService) NewGetWalletAccountsRequest(walletType WalletType) *GetWalletAccountsRequest { + return &GetWalletAccountsRequest{client: s.Client, walletType: walletType} +} + +//go:generate GetRequest -url "/api/v3/wallet/:walletType/accounts" -type GetWalletAccountsRequest -responseType []Account +type GetWalletAccountsRequest struct { + client requestgen.AuthenticatedAPIClient + + walletType WalletType `param:"walletType,slug,required"` +} diff --git a/pkg/exchange/max/maxapi/v3/get_wallet_accounts_request_requestgen.go b/pkg/exchange/max/maxapi/v3/get_wallet_accounts_request_requestgen.go new file mode 100644 index 0000000000..7c5c1ff04a --- /dev/null +++ b/pkg/exchange/max/maxapi/v3/get_wallet_accounts_request_requestgen.go @@ -0,0 +1,158 @@ +// Code generated by "requestgen -method GET -url /api/v3/wallet/:walletType/accounts -type GetWalletAccountsRequest -responseType []Account"; DO NOT EDIT. + +package v3 + +import ( + "context" + "encoding/json" + "fmt" + "github.com/c9s/bbgo/pkg/exchange/max/maxapi" + "net/url" + "reflect" + "regexp" +) + +func (g *GetWalletAccountsRequest) WalletType(walletType max.WalletType) *GetWalletAccountsRequest { + g.walletType = walletType + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetWalletAccountsRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetWalletAccountsRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetWalletAccountsRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetWalletAccountsRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetWalletAccountsRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check walletType field -> json key walletType + walletType := g.walletType + + // TEMPLATE check-required + if len(walletType) == 0 { + return nil, fmt.Errorf("walletType is required, empty string given") + } + // END TEMPLATE check-required + + // assign parameter of walletType + params["walletType"] = walletType + + return params, nil +} + +func (g *GetWalletAccountsRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetWalletAccountsRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetWalletAccountsRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetWalletAccountsRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetWalletAccountsRequest) Do(ctx context.Context) ([]max.Account, error) { + + // no body params + var params interface{} + query := url.Values{} + + apiURL := "/api/v3/wallet/:walletType/accounts" + slugs, err := g.GetSlugsMap() + if err != nil { + return nil, err + } + + apiURL = g.applySlugsToUrl(apiURL, slugs) + + req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse []max.Account + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return apiResponse, nil +} diff --git a/pkg/exchange/max/maxapi/v3/get_wallet_open_orders_request.go b/pkg/exchange/max/maxapi/v3/get_wallet_open_orders_request.go new file mode 100644 index 0000000000..3841becabc --- /dev/null +++ b/pkg/exchange/max/maxapi/v3/get_wallet_open_orders_request.go @@ -0,0 +1,19 @@ +package v3 + +import "github.com/c9s/requestgen" + +//go:generate -command GetRequest requestgen -method GET +//go:generate -command PostRequest requestgen -method POST +//go:generate -command DeleteRequest requestgen -method DELETE + +func (s *OrderService) NewGetWalletOpenOrdersRequest(walletType WalletType) *GetWalletOpenOrdersRequest { + return &GetWalletOpenOrdersRequest{client: s.Client, walletType: walletType} +} + +//go:generate GetRequest -url "/api/v3/wallet/:walletType/orders/open" -type GetWalletOpenOrdersRequest -responseType []Order +type GetWalletOpenOrdersRequest struct { + client requestgen.AuthenticatedAPIClient + + walletType WalletType `param:"walletType,slug,required"` + market string `param:"market,required"` +} diff --git a/pkg/exchange/max/maxapi/v3/get_wallet_open_orders_request_requestgen.go b/pkg/exchange/max/maxapi/v3/get_wallet_open_orders_request_requestgen.go new file mode 100644 index 0000000000..8121085b25 --- /dev/null +++ b/pkg/exchange/max/maxapi/v3/get_wallet_open_orders_request_requestgen.go @@ -0,0 +1,177 @@ +// Code generated by "requestgen -method GET -url /api/v3/wallet/:walletType/orders/open -type GetWalletOpenOrdersRequest -responseType []Order"; DO NOT EDIT. + +package v3 + +import ( + "context" + "encoding/json" + "fmt" + "github.com/c9s/bbgo/pkg/exchange/max/maxapi" + "net/url" + "reflect" + "regexp" +) + +func (g *GetWalletOpenOrdersRequest) Market(market string) *GetWalletOpenOrdersRequest { + g.market = market + return g +} + +func (g *GetWalletOpenOrdersRequest) WalletType(walletType max.WalletType) *GetWalletOpenOrdersRequest { + g.walletType = walletType + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetWalletOpenOrdersRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetWalletOpenOrdersRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check market field -> json key market + market := g.market + + // TEMPLATE check-required + if len(market) == 0 { + return nil, fmt.Errorf("market is required, empty string given") + } + // END TEMPLATE check-required + + // assign parameter of market + params["market"] = market + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetWalletOpenOrdersRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetWalletOpenOrdersRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetWalletOpenOrdersRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check walletType field -> json key walletType + walletType := g.walletType + + // TEMPLATE check-required + if len(walletType) == 0 { + return nil, fmt.Errorf("walletType is required, empty string given") + } + // END TEMPLATE check-required + + // assign parameter of walletType + params["walletType"] = walletType + + return params, nil +} + +func (g *GetWalletOpenOrdersRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetWalletOpenOrdersRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetWalletOpenOrdersRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetWalletOpenOrdersRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetWalletOpenOrdersRequest) Do(ctx context.Context) ([]max.Order, error) { + + // empty params for GET operation + var params interface{} + query, err := g.GetParametersQuery() + if err != nil { + return nil, err + } + + apiURL := "/api/v3/wallet/:walletType/orders/open" + slugs, err := g.GetSlugsMap() + if err != nil { + return nil, err + } + + apiURL = g.applySlugsToUrl(apiURL, slugs) + + req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse []max.Order + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return apiResponse, nil +} diff --git a/pkg/exchange/max/maxapi/v3/get_wallet_order_history_request.go b/pkg/exchange/max/maxapi/v3/get_wallet_order_history_request.go new file mode 100644 index 0000000000..cf6f804575 --- /dev/null +++ b/pkg/exchange/max/maxapi/v3/get_wallet_order_history_request.go @@ -0,0 +1,22 @@ +package v3 + +import "github.com/c9s/requestgen" + +//go:generate -command GetRequest requestgen -method GET +//go:generate -command PostRequest requestgen -method POST +//go:generate -command DeleteRequest requestgen -method DELETE + +func (s *OrderService) NewGetWalletOrderHistoryRequest(walletType WalletType) *GetWalletOrderHistoryRequest { + return &GetWalletOrderHistoryRequest{client: s.Client, walletType: walletType} +} + +//go:generate GetRequest -url "/api/v3/wallet/:walletType/orders/history" -type GetWalletOrderHistoryRequest -responseType []Order +type GetWalletOrderHistoryRequest struct { + client requestgen.AuthenticatedAPIClient + + walletType WalletType `param:"walletType,slug,required"` + + market string `param:"market,required"` + fromID *uint64 `param:"from_id"` + limit *uint `param:"limit"` +} diff --git a/pkg/exchange/max/maxapi/v3/get_wallet_order_history_request_requestgen.go b/pkg/exchange/max/maxapi/v3/get_wallet_order_history_request_requestgen.go new file mode 100644 index 0000000000..c6b7393dc5 --- /dev/null +++ b/pkg/exchange/max/maxapi/v3/get_wallet_order_history_request_requestgen.go @@ -0,0 +1,203 @@ +// Code generated by "requestgen -method GET -url /api/v3/wallet/:walletType/orders/history -type GetWalletOrderHistoryRequest -responseType []Order"; DO NOT EDIT. + +package v3 + +import ( + "context" + "encoding/json" + "fmt" + "github.com/c9s/bbgo/pkg/exchange/max/maxapi" + "net/url" + "reflect" + "regexp" +) + +func (g *GetWalletOrderHistoryRequest) Market(market string) *GetWalletOrderHistoryRequest { + g.market = market + return g +} + +func (g *GetWalletOrderHistoryRequest) FromID(fromID uint64) *GetWalletOrderHistoryRequest { + g.fromID = &fromID + return g +} + +func (g *GetWalletOrderHistoryRequest) Limit(limit uint) *GetWalletOrderHistoryRequest { + g.limit = &limit + return g +} + +func (g *GetWalletOrderHistoryRequest) WalletType(walletType max.WalletType) *GetWalletOrderHistoryRequest { + g.walletType = walletType + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetWalletOrderHistoryRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetWalletOrderHistoryRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check market field -> json key market + market := g.market + + // TEMPLATE check-required + if len(market) == 0 { + return nil, fmt.Errorf("market is required, empty string given") + } + // END TEMPLATE check-required + + // assign parameter of market + params["market"] = market + // check fromID field -> json key from_id + if g.fromID != nil { + fromID := *g.fromID + + // assign parameter of fromID + params["from_id"] = fromID + } else { + } + // check limit field -> json key limit + if g.limit != nil { + limit := *g.limit + + // assign parameter of limit + params["limit"] = limit + } else { + } + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetWalletOrderHistoryRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetWalletOrderHistoryRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetWalletOrderHistoryRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check walletType field -> json key walletType + walletType := g.walletType + + // TEMPLATE check-required + if len(walletType) == 0 { + return nil, fmt.Errorf("walletType is required, empty string given") + } + // END TEMPLATE check-required + + // assign parameter of walletType + params["walletType"] = walletType + + return params, nil +} + +func (g *GetWalletOrderHistoryRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetWalletOrderHistoryRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetWalletOrderHistoryRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetWalletOrderHistoryRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetWalletOrderHistoryRequest) Do(ctx context.Context) ([]max.Order, error) { + + // empty params for GET operation + var params interface{} + query, err := g.GetParametersQuery() + if err != nil { + return nil, err + } + + apiURL := "/api/v3/wallet/:walletType/orders/history" + slugs, err := g.GetSlugsMap() + if err != nil { + return nil, err + } + + apiURL = g.applySlugsToUrl(apiURL, slugs) + + req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse []max.Order + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return apiResponse, nil +} diff --git a/pkg/exchange/max/maxapi/v3/get_wallet_trades_request.go b/pkg/exchange/max/maxapi/v3/get_wallet_trades_request.go new file mode 100644 index 0000000000..e4804a7c14 --- /dev/null +++ b/pkg/exchange/max/maxapi/v3/get_wallet_trades_request.go @@ -0,0 +1,28 @@ +package v3 + +//go:generate -command GetRequest requestgen -method GET +//go:generate -command PostRequest requestgen -method POST +//go:generate -command DeleteRequest requestgen -method DELETE + +import ( + "time" + + "github.com/c9s/requestgen" +) + +func (s *OrderService) NewGetWalletTradesRequest(walletType WalletType) *GetWalletTradesRequest { + return &GetWalletTradesRequest{client: s.Client, walletType: walletType} +} + +//go:generate GetRequest -url "/api/v3/wallet/:walletType/trades" -type GetWalletTradesRequest -responseType []Trade +type GetWalletTradesRequest struct { + client requestgen.AuthenticatedAPIClient + + walletType WalletType `param:"walletType,slug,required"` + + market string `param:"market,required"` + from *uint64 `param:"from_id"` + startTime *time.Time `param:"start_time,milliseconds"` + endTime *time.Time `param:"end_time,milliseconds"` + limit *uint64 `param:"limit"` +} diff --git a/pkg/exchange/max/maxapi/v3/get_wallet_trades_request_requestgen.go b/pkg/exchange/max/maxapi/v3/get_wallet_trades_request_requestgen.go new file mode 100644 index 0000000000..2fdf94c400 --- /dev/null +++ b/pkg/exchange/max/maxapi/v3/get_wallet_trades_request_requestgen.go @@ -0,0 +1,233 @@ +// Code generated by "requestgen -method GET -url /api/v3/wallet/:walletType/trades -type GetWalletTradesRequest -responseType []Trade"; DO NOT EDIT. + +package v3 + +import ( + "context" + "encoding/json" + "fmt" + "github.com/c9s/bbgo/pkg/exchange/max/maxapi" + "net/url" + "reflect" + "regexp" + "strconv" + "time" +) + +func (g *GetWalletTradesRequest) Market(market string) *GetWalletTradesRequest { + g.market = market + return g +} + +func (g *GetWalletTradesRequest) From(from uint64) *GetWalletTradesRequest { + g.from = &from + return g +} + +func (g *GetWalletTradesRequest) StartTime(startTime time.Time) *GetWalletTradesRequest { + g.startTime = &startTime + return g +} + +func (g *GetWalletTradesRequest) EndTime(endTime time.Time) *GetWalletTradesRequest { + g.endTime = &endTime + return g +} + +func (g *GetWalletTradesRequest) Limit(limit uint64) *GetWalletTradesRequest { + g.limit = &limit + return g +} + +func (g *GetWalletTradesRequest) WalletType(walletType max.WalletType) *GetWalletTradesRequest { + g.walletType = walletType + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetWalletTradesRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetWalletTradesRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check market field -> json key market + market := g.market + + // TEMPLATE check-required + if len(market) == 0 { + return nil, fmt.Errorf("market is required, empty string given") + } + // END TEMPLATE check-required + + // assign parameter of market + params["market"] = market + // check from field -> json key from_id + if g.from != nil { + from := *g.from + + // assign parameter of from + params["from_id"] = from + } else { + } + // check startTime field -> json key start_time + if g.startTime != nil { + startTime := *g.startTime + + // assign parameter of startTime + // convert time.Time to milliseconds time stamp + params["start_time"] = strconv.FormatInt(startTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check endTime field -> json key end_time + if g.endTime != nil { + endTime := *g.endTime + + // assign parameter of endTime + // convert time.Time to milliseconds time stamp + params["end_time"] = strconv.FormatInt(endTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check limit field -> json key limit + if g.limit != nil { + limit := *g.limit + + // assign parameter of limit + params["limit"] = limit + } else { + } + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetWalletTradesRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetWalletTradesRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetWalletTradesRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check walletType field -> json key walletType + walletType := g.walletType + + // TEMPLATE check-required + if len(walletType) == 0 { + return nil, fmt.Errorf("walletType is required, empty string given") + } + // END TEMPLATE check-required + + // assign parameter of walletType + params["walletType"] = walletType + + return params, nil +} + +func (g *GetWalletTradesRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetWalletTradesRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetWalletTradesRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetWalletTradesRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (g *GetWalletTradesRequest) Do(ctx context.Context) ([]max.Trade, error) { + + // empty params for GET operation + var params interface{} + query, err := g.GetParametersQuery() + if err != nil { + return nil, err + } + + apiURL := "/api/v3/wallet/:walletType/trades" + slugs, err := g.GetSlugsMap() + if err != nil { + return nil, err + } + + apiURL = g.applySlugsToUrl(apiURL, slugs) + + req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse []max.Trade + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return apiResponse, nil +} diff --git a/pkg/exchange/max/maxapi/v3/margin.go b/pkg/exchange/max/maxapi/v3/margin.go new file mode 100644 index 0000000000..e69422a268 --- /dev/null +++ b/pkg/exchange/max/maxapi/v3/margin.go @@ -0,0 +1,160 @@ +package v3 + +//go:generate -command GetRequest requestgen -method GET +//go:generate -command PostRequest requestgen -method POST +//go:generate -command DeleteRequest requestgen -method DELETE + +import ( + "time" + + "github.com/c9s/requestgen" + + maxapi "github.com/c9s/bbgo/pkg/exchange/max/maxapi" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +type MarginService struct { + Client *maxapi.RestClient +} + +func (s *MarginService) NewGetMarginInterestRatesRequest() *GetMarginInterestRatesRequest { + return &GetMarginInterestRatesRequest{client: s.Client} +} + +func (s *MarginService) NewGetMarginBorrowingLimitsRequest() *GetMarginBorrowingLimitsRequest { + return &GetMarginBorrowingLimitsRequest{client: s.Client} +} + +func (s *MarginService) NewGetMarginInterestHistoryRequest(currency string) *GetMarginInterestHistoryRequest { + return &GetMarginInterestHistoryRequest{client: s.Client, currency: currency} +} + +func (s *MarginService) NewGetMarginLiquidationHistoryRequest() *GetMarginLiquidationHistoryRequest { + return &GetMarginLiquidationHistoryRequest{client: s.Client} +} + +func (s *MarginService) NewGetMarginLoanHistoryRequest() *GetMarginLoanHistoryRequest { + return &GetMarginLoanHistoryRequest{client: s.Client} +} + +func (s *MarginService) NewMarginRepayRequest() *MarginRepayRequest { + return &MarginRepayRequest{client: s.Client} +} + +func (s *MarginService) NewMarginLoanRequest() *MarginLoanRequest { + return &MarginLoanRequest{client: s.Client} +} + +type MarginInterestRate struct { + HourlyInterestRate fixedpoint.Value `json:"hourly_interest_rate"` + NextHourlyInterestRate fixedpoint.Value `json:"next_hourly_interest_rate"` +} + +type MarginInterestRateMap map[string]MarginInterestRate + +//go:generate GetRequest -url "/api/v3/wallet/m/interest_rates" -type GetMarginInterestRatesRequest -responseType .MarginInterestRateMap +type GetMarginInterestRatesRequest struct { + client requestgen.APIClient +} + +type MarginBorrowingLimitMap map[string]fixedpoint.Value + +//go:generate GetRequest -url "/api/v3/wallet/m/limits" -type GetMarginBorrowingLimitsRequest -responseType .MarginBorrowingLimitMap +type GetMarginBorrowingLimitsRequest struct { + client requestgen.APIClient +} + +type MarginInterestRecord struct { + Currency string `json:"currency"` + Amount fixedpoint.Value `json:"amount"` + InterestRate fixedpoint.Value `json:"interest_rate"` + CreatedAt types.MillisecondTimestamp `json:"created_at"` +} + +//go:generate GetRequest -url "/api/v3/wallet/m/interests/history/:currency" -type GetMarginInterestHistoryRequest -responseType []MarginInterestRecord +type GetMarginInterestHistoryRequest struct { + client requestgen.AuthenticatedAPIClient + + currency string `param:"currency,slug,required"` + startTime *time.Time `param:"startTime,milliseconds"` + endTime *time.Time `param:"endTime,milliseconds"` + limit *int `param:"limit"` +} + +type LiquidationRecord struct { + SN string `json:"sn"` + AdRatio fixedpoint.Value `json:"ad_ratio"` + ExpectedAdRatio fixedpoint.Value `json:"expected_ad_ratio"` + CreatedAt types.MillisecondTimestamp `json:"created_at"` + State LiquidationState `json:"state"` +} + +type LiquidationState string + +const ( + LiquidationStateProcessing LiquidationState = "processing" + LiquidationStateDebt LiquidationState = "debt" + LiquidationStateLiquidated LiquidationState = "liquidated" +) + +//go:generate GetRequest -url "/api/v3/wallet/m/liquidations" -type GetMarginLiquidationHistoryRequest -responseType []LiquidationRecord +type GetMarginLiquidationHistoryRequest struct { + client requestgen.AuthenticatedAPIClient + startTime *time.Time `param:"startTime,milliseconds"` + endTime *time.Time `param:"endTime,milliseconds"` + limit *int `param:"limit"` +} + +type RepaymentRecord struct { + SN string `json:"sn"` + Currency string `json:"currency"` + Amount fixedpoint.Value `json:"amount"` + Principal fixedpoint.Value `json:"principal"` + Interest fixedpoint.Value `json:"interest"` + CreatedAt types.MillisecondTimestamp `json:"created_at"` + State string `json:"state"` +} + +//go:generate GetRequest -url "/api/v3/wallet/m/repayments/:currency" -type GetMarginRepaymentHistoryRequest -responseType []RepaymentRecord +type GetMarginRepaymentHistoryRequest struct { + client requestgen.AuthenticatedAPIClient + currency string `param:"currency,slug,required"` + + startTime *time.Time `param:"startTime,milliseconds"` + endTime *time.Time `param:"endTime,milliseconds"` + limit *int `param:"limit"` +} + +type LoanRecord struct { + SN string `json:"sn"` + Currency string `json:"currency"` + Amount fixedpoint.Value `json:"amount"` + State string `json:"state"` + CreatedAt types.MillisecondTimestamp `json:"created_at"` + InterestRate fixedpoint.Value `json:"interest_rate"` +} + +//go:generate GetRequest -url "/api/v3/wallet/m/loans/:currency" -type GetMarginLoanHistoryRequest -responseType []LoanRecord +type GetMarginLoanHistoryRequest struct { + client requestgen.AuthenticatedAPIClient + currency string `param:"currency,slug,required"` + + startTime *time.Time `param:"startTime,milliseconds"` + endTime *time.Time `param:"endTime,milliseconds"` + limit *int `param:"limit"` +} + +//go:generate PostRequest -url "/api/v3/wallet/m/loan/:currency" -type MarginLoanRequest -responseType .LoanRecord +type MarginLoanRequest struct { + client requestgen.AuthenticatedAPIClient + currency string `param:"currency,slug,required"` + amount string `param:"amount"` +} + +//go:generate PostRequest -url "/api/v3/wallet/m/repayment/:currency" -type MarginRepayRequest -responseType .RepaymentRecord +type MarginRepayRequest struct { + client requestgen.AuthenticatedAPIClient + currency string `param:"currency,slug,required"` + amount string `param:"amount"` +} diff --git a/pkg/exchange/max/maxapi/v3/margin_loan_request_requestgen.go b/pkg/exchange/max/maxapi/v3/margin_loan_request_requestgen.go new file mode 100644 index 0000000000..8f3e73466c --- /dev/null +++ b/pkg/exchange/max/maxapi/v3/margin_loan_request_requestgen.go @@ -0,0 +1,169 @@ +// Code generated by "requestgen -method POST -url /api/v3/wallet/m/loan/:currency -type MarginLoanRequest -responseType .LoanRecord"; DO NOT EDIT. + +package v3 + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +func (m *MarginLoanRequest) Amount(amount string) *MarginLoanRequest { + m.amount = amount + return m +} + +func (m *MarginLoanRequest) Currency(currency string) *MarginLoanRequest { + m.currency = currency + return m +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (m *MarginLoanRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (m *MarginLoanRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check amount field -> json key amount + amount := m.amount + + // assign parameter of amount + params["amount"] = amount + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (m *MarginLoanRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := m.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if m.isVarSlice(_v) { + m.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (m *MarginLoanRequest) GetParametersJSON() ([]byte, error) { + params, err := m.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (m *MarginLoanRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check currency field -> json key currency + currency := m.currency + + // TEMPLATE check-required + if len(currency) == 0 { + return nil, fmt.Errorf("currency is required, empty string given") + } + // END TEMPLATE check-required + + // assign parameter of currency + params["currency"] = currency + + return params, nil +} + +func (m *MarginLoanRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (m *MarginLoanRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (m *MarginLoanRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (m *MarginLoanRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := m.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (m *MarginLoanRequest) Do(ctx context.Context) (*LoanRecord, error) { + + params, err := m.GetParameters() + if err != nil { + return nil, err + } + query := url.Values{} + + apiURL := "/api/v3/wallet/m/loan/:currency" + slugs, err := m.GetSlugsMap() + if err != nil { + return nil, err + } + + apiURL = m.applySlugsToUrl(apiURL, slugs) + + req, err := m.client.NewAuthenticatedRequest(ctx, "POST", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := m.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse LoanRecord + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return &apiResponse, nil +} diff --git a/pkg/exchange/max/maxapi/v3/margin_repay_request_requestgen.go b/pkg/exchange/max/maxapi/v3/margin_repay_request_requestgen.go new file mode 100644 index 0000000000..a84beb4756 --- /dev/null +++ b/pkg/exchange/max/maxapi/v3/margin_repay_request_requestgen.go @@ -0,0 +1,169 @@ +// Code generated by "requestgen -method POST -url /api/v3/wallet/m/repayment/:currency -type MarginRepayRequest -responseType .RepaymentRecord"; DO NOT EDIT. + +package v3 + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +func (m *MarginRepayRequest) Amount(amount string) *MarginRepayRequest { + m.amount = amount + return m +} + +func (m *MarginRepayRequest) Currency(currency string) *MarginRepayRequest { + m.currency = currency + return m +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (m *MarginRepayRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (m *MarginRepayRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check amount field -> json key amount + amount := m.amount + + // assign parameter of amount + params["amount"] = amount + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (m *MarginRepayRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := m.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if m.isVarSlice(_v) { + m.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (m *MarginRepayRequest) GetParametersJSON() ([]byte, error) { + params, err := m.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (m *MarginRepayRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check currency field -> json key currency + currency := m.currency + + // TEMPLATE check-required + if len(currency) == 0 { + return nil, fmt.Errorf("currency is required, empty string given") + } + // END TEMPLATE check-required + + // assign parameter of currency + params["currency"] = currency + + return params, nil +} + +func (m *MarginRepayRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (m *MarginRepayRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (m *MarginRepayRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (m *MarginRepayRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := m.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (m *MarginRepayRequest) Do(ctx context.Context) (*RepaymentRecord, error) { + + params, err := m.GetParameters() + if err != nil { + return nil, err + } + query := url.Values{} + + apiURL := "/api/v3/wallet/m/repayment/:currency" + slugs, err := m.GetSlugsMap() + if err != nil { + return nil, err + } + + apiURL = m.applySlugsToUrl(apiURL, slugs) + + req, err := m.client.NewAuthenticatedRequest(ctx, "POST", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := m.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse RepaymentRecord + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return &apiResponse, nil +} diff --git a/pkg/exchange/max/maxapi/v3/order.go b/pkg/exchange/max/maxapi/v3/order.go new file mode 100644 index 0000000000..df3a63a606 --- /dev/null +++ b/pkg/exchange/max/maxapi/v3/order.go @@ -0,0 +1,24 @@ +package v3 + +//go:generate -command GetRequest requestgen -method GET +//go:generate -command PostRequest requestgen -method POST +//go:generate -command DeleteRequest requestgen -method DELETE + +import ( + "github.com/c9s/requestgen" + + maxapi "github.com/c9s/bbgo/pkg/exchange/max/maxapi" +) + +// create type alias +type WalletType = maxapi.WalletType +type OrderType = maxapi.OrderType + +type Order = maxapi.Order +type Trade = maxapi.Trade +type Account = maxapi.Account + +// OrderService manages the Order endpoint. +type OrderService struct { + Client requestgen.AuthenticatedAPIClient +} diff --git a/pkg/exchange/max/maxapi/websocket.go b/pkg/exchange/max/maxapi/websocket.go index 1008138c47..241adda6a9 100644 --- a/pkg/exchange/max/maxapi/websocket.go +++ b/pkg/exchange/max/maxapi/websocket.go @@ -28,6 +28,3 @@ type WebsocketCommand struct { Action string `json:"action"` Subscriptions []Subscription `json:"subscriptions,omitempty"` } - -var SubscribeAction = "subscribe" -var UnsubscribeAction = "unsubscribe" diff --git a/pkg/exchange/max/maxapi/withdrawal.go b/pkg/exchange/max/maxapi/withdrawal.go index b5f30ee0e7..93d63bae50 100644 --- a/pkg/exchange/max/maxapi/withdrawal.go +++ b/pkg/exchange/max/maxapi/withdrawal.go @@ -56,12 +56,12 @@ type WithdrawalAddress struct { //go:generate GetRequest -url "v2/withdraw_addresses" -type GetWithdrawalAddressesRequest -responseType []WithdrawalAddress type GetWithdrawalAddressesRequest struct { - client requestgen.AuthenticatedAPIClient - currency string `param:"currency,required"` + client requestgen.AuthenticatedAPIClient + currency string `param:"currency,required"` } type WithdrawalService struct { - client *RestClient + client requestgen.AuthenticatedAPIClient } func (s *WithdrawalService) NewGetWithdrawalAddressesRequest() *GetWithdrawalAddressesRequest { diff --git a/pkg/exchange/max/stream.go b/pkg/exchange/max/stream.go index 385e24b04e..2a850a047e 100644 --- a/pkg/exchange/max/stream.go +++ b/pkg/exchange/max/stream.go @@ -18,6 +18,7 @@ import ( //go:generate callbackgen -type Stream type Stream struct { types.StandardStream + types.MarginSettings key, secret string @@ -32,6 +33,8 @@ type Stream struct { tradeSnapshotEventCallbacks []func(e max.TradeSnapshotEvent) orderUpdateEventCallbacks []func(e max.OrderUpdateEvent) orderSnapshotEventCallbacks []func(e max.OrderSnapshotEvent) + adRatioEventCallbacks []func(e max.ADRatioEvent) + debtEventCallbacks []func(e max.DebtEvent) accountSnapshotEventCallbacks []func(e max.AccountSnapshotEvent) accountUpdateEventCallbacks []func(e max.AccountUpdateEvent) @@ -41,12 +44,12 @@ func NewStream(key, secret string) *Stream { stream := &Stream{ StandardStream: types.NewStandardStream(), key: key, - secret: secret, + // pragma: allowlist nextline secret + secret: secret, } stream.SetEndpointCreator(stream.getEndpoint) stream.SetParser(max.ParseMessage) stream.SetDispatcher(stream.dispatchEvent) - stream.OnConnect(stream.handleConnect) stream.OnKLineEvent(stream.handleKLineEvent) stream.OnOrderSnapshotEvent(stream.handleOrderSnapshotEvent) @@ -92,20 +95,38 @@ func (s *Stream) handleConnect() { Channel: string(sub.Channel), Market: toLocalSymbol(sub.Symbol), Depth: depth, - Resolution: sub.Options.Interval, + Resolution: sub.Options.Interval.String(), }) } - s.Conn.WriteJSON(cmd) + if err := s.Conn.WriteJSON(cmd); err != nil { + log.WithError(err).Error("failed to send subscription request") + } + } else { + var filters []string + if s.MarginSettings.IsMargin { + filters = []string{ + "mwallet_order", + "mwallet_trade", + "mwallet_account", + "ad_ratio", + "borrowing", + } + } + nonce := time.Now().UnixNano() / int64(time.Millisecond) auth := &max.AuthMessage{ - Action: "auth", + // pragma: allowlist nextline secret + Action: "auth", + // pragma: allowlist nextline secret APIKey: s.key, Nonce: nonce, Signature: signPayload(fmt.Sprintf("%d", nonce), s.secret), ID: uuid.New().String(), + Filters: filters, } + if err := s.Conn.WriteJSON(auth); err != nil { log.WithError(err).Error("failed to send auth request") } @@ -240,8 +261,14 @@ func (s *Stream) dispatchEvent(e interface{}) { case *max.OrderUpdateEvent: s.EmitOrderUpdateEvent(*e) + case *max.ADRatioEvent: + s.EmitAdRatioEvent(*e) + + case *max.DebtEvent: + s.EmitDebtEvent(*e) + default: - log.Errorf("unsupported %T event: %+v", e, e) + log.Warnf("unhandled %T event: %+v", e, e) } } diff --git a/pkg/exchange/max/stream_callbacks.go b/pkg/exchange/max/stream_callbacks.go index 9c37eae0b7..3f556ef3cb 100644 --- a/pkg/exchange/max/stream_callbacks.go +++ b/pkg/exchange/max/stream_callbacks.go @@ -106,6 +106,26 @@ func (s *Stream) EmitOrderSnapshotEvent(e max.OrderSnapshotEvent) { } } +func (s *Stream) OnAdRatioEvent(cb func(e max.ADRatioEvent)) { + s.adRatioEventCallbacks = append(s.adRatioEventCallbacks, cb) +} + +func (s *Stream) EmitAdRatioEvent(e max.ADRatioEvent) { + for _, cb := range s.adRatioEventCallbacks { + cb(e) + } +} + +func (s *Stream) OnDebtEvent(cb func(e max.DebtEvent)) { + s.debtEventCallbacks = append(s.debtEventCallbacks, cb) +} + +func (s *Stream) EmitDebtEvent(e max.DebtEvent) { + for _, cb := range s.debtEventCallbacks { + cb(e) + } +} + func (s *Stream) OnAccountSnapshotEvent(cb func(e max.AccountSnapshotEvent)) { s.accountSnapshotEventCallbacks = append(s.accountSnapshotEventCallbacks, cb) } diff --git a/pkg/exchange/max/ticker_test.go b/pkg/exchange/max/ticker_test.go index 68defb2f61..6ef459e1a7 100644 --- a/pkg/exchange/max/ticker_test.go +++ b/pkg/exchange/max/ticker_test.go @@ -31,7 +31,6 @@ func TestExchange_QueryTickers_SomeSymbols(t *testing.T) { return } - e := New(key, secret) got, err := e.QueryTickers(context.Background(), "BTCUSDT", "ETHUSDT") if assert.NoError(t, err) { diff --git a/pkg/exchange/okex/convert.go b/pkg/exchange/okex/convert.go index e4c7fd0eac..968544729b 100644 --- a/pkg/exchange/okex/convert.go +++ b/pkg/exchange/okex/convert.go @@ -2,6 +2,7 @@ package okex import ( "fmt" + "regexp" "strconv" "strings" @@ -16,7 +17,7 @@ func toGlobalSymbol(symbol string) string { return strings.ReplaceAll(symbol, "-", "") } -////go:generate sh -c "echo \"package okex\nvar spotSymbolMap = map[string]string{\n\" $(curl -s -L 'https://okex.com/api/v5/public/instruments?instType=SPOT' | jq -r '.data[] | \"\\(.instId | sub(\"-\" ; \"\") | tojson ): \\( .instId | tojson),\n\"') \"\n}\" > symbols.go" +// //go:generate sh -c "echo \"package okex\nvar spotSymbolMap = map[string]string{\n\" $(curl -s -L 'https://okex.com/api/v5/public/instruments?instType=SPOT' | jq -r '.data[] | \"\\(.instId | sub(\"-\" ; \"\") | tojson ): \\( .instId | tojson),\n\"') \"\n}\" > symbols.go" //go:generate go run gensymbols.go func toLocalSymbol(symbol string) string { if s, ok := spotSymbolMap[symbol]; ok { @@ -67,18 +68,19 @@ var CandleChannels = []string{ "candle30m", "candle15m", "candle5m", "candle3m", "candle1m", } -func convertIntervalToCandle(interval string) string { - switch interval { +func convertIntervalToCandle(interval types.Interval) string { + s := interval.String() + switch s { case "1h", "2h", "4h", "6h", "12h", "1d", "3d": - return "candle" + strings.ToUpper(interval) + return "candle" + strings.ToUpper(s) case "1m", "5m", "15m", "30m": - return "candle" + interval + return "candle" + s } - return "candle" + interval + return "candle" + s } func convertSubscription(s types.Subscription) (WebsocketSubscription, error) { @@ -268,3 +270,10 @@ func toGlobalOrderType(orderType okexapi.OrderType) (types.OrderType, error) { } return "", fmt.Errorf("unknown or unsupported okex order type: %s", orderType) } + +func toLocalInterval(src string) string { + var re = regexp.MustCompile(`\d+[hdw]`) + return re.ReplaceAllStringFunc(src, func(w string) string { + return strings.ToUpper(w) + }) +} diff --git a/pkg/exchange/okex/exchange.go b/pkg/exchange/okex/exchange.go index 4a80f5826f..0bebbc62f7 100644 --- a/pkg/exchange/okex/exchange.go +++ b/pkg/exchange/okex/exchange.go @@ -8,12 +8,15 @@ import ( "github.com/pkg/errors" "github.com/sirupsen/logrus" + "golang.org/x/time/rate" "github.com/c9s/bbgo/pkg/exchange/okex/okexapi" - "github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" ) +var marketDataLimiter = rate.NewLimiter(rate.Every(time.Second/10), 1) + // OKB is the platform currency of OKEx, pre-allocate static string here const OKB = "OKB" @@ -35,7 +38,8 @@ func New(key, secret, passphrase string) *Exchange { } return &Exchange{ - key: key, + key: key, + // pragma: allowlist nextline secret secret: secret, passphrase: passphrase, client: client, @@ -155,78 +159,97 @@ func (e *Exchange) QueryAccountBalances(ctx context.Context) (types.BalanceMap, return balanceMap, nil } -func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) (createdOrders types.OrderSlice, err error) { - var reqs []*okexapi.PlaceOrderRequest - for _, order := range orders { - orderReq := e.client.TradeService.NewPlaceOrderRequest() +func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (*types.Order, error) { + orderReq := e.client.TradeService.NewPlaceOrderRequest() - orderType, err := toLocalOrderType(order.Type) - if err != nil { - return nil, err - } + orderType, err := toLocalOrderType(order.Type) + if err != nil { + return nil, err + } - orderReq.InstrumentID(toLocalSymbol(order.Symbol)) - orderReq.Side(toLocalSideType(order.Side)) + orderReq.InstrumentID(toLocalSymbol(order.Symbol)) + orderReq.Side(toLocalSideType(order.Side)) + + if order.Market.Symbol != "" { + orderReq.Quantity(order.Market.FormatQuantity(order.Quantity)) + } else { + // TODO report error + orderReq.Quantity(order.Quantity.FormatString(8)) + } + // set price field for limit orders + switch order.Type { + case types.OrderTypeStopLimit, types.OrderTypeLimit: if order.Market.Symbol != "" { - orderReq.Quantity(order.Market.FormatQuantity(order.Quantity)) + orderReq.Price(order.Market.FormatPrice(order.Price)) } else { // TODO report error - orderReq.Quantity(order.Quantity.FormatString(8)) - } - - // set price field for limit orders - switch order.Type { - case types.OrderTypeStopLimit, types.OrderTypeLimit: - if order.Market.Symbol != "" { - orderReq.Price(order.Market.FormatPrice(order.Price)) - } else { - // TODO report error - orderReq.Price(order.Price.FormatString(8)) - } + orderReq.Price(order.Price.FormatString(8)) } + } - switch order.TimeInForce { - case "FOK": - orderReq.OrderType(okexapi.OrderTypeFOK) - case "IOC": - orderReq.OrderType(okexapi.OrderTypeIOC) - default: - orderReq.OrderType(orderType) - } + switch order.TimeInForce { + case "FOK": + orderReq.OrderType(okexapi.OrderTypeFOK) + case "IOC": + orderReq.OrderType(okexapi.OrderTypeIOC) + default: + orderReq.OrderType(orderType) + } - reqs = append(reqs, orderReq) + orderHead, err := orderReq.Do(ctx) + if err != nil { + return nil, err } - batchReq := e.client.TradeService.NewBatchPlaceOrderRequest() - batchReq.Add(reqs...) - orderHeads, err := batchReq.Do(ctx) + orderID, err := strconv.ParseInt(orderHead.OrderID, 10, 64) if err != nil { return nil, err } - for idx, orderHead := range orderHeads { - orderID, err := strconv.ParseInt(orderHead.OrderID, 10, 64) + return &types.Order{ + SubmitOrder: order, + Exchange: types.ExchangeOKEx, + OrderID: uint64(orderID), + Status: types.OrderStatusNew, + ExecutedQuantity: fixedpoint.Zero, + IsWorking: true, + CreationTime: types.Time(time.Now()), + UpdateTime: types.Time(time.Now()), + IsMargin: false, + IsIsolated: false, + }, nil + + // TODO: move this to batch place orders interface + /* + batchReq := e.client.TradeService.NewBatchPlaceOrderRequest() + batchReq.Add(reqs...) + orderHeads, err := batchReq.Do(ctx) if err != nil { - return createdOrders, err + return nil, err } - submitOrder := orders[idx] - createdOrders = append(createdOrders, types.Order{ - SubmitOrder: submitOrder, - Exchange: types.ExchangeOKEx, - OrderID: uint64(orderID), - Status: types.OrderStatusNew, - ExecutedQuantity: fixedpoint.Zero, - IsWorking: true, - CreationTime: types.Time(time.Now()), - UpdateTime: types.Time(time.Now()), - IsMargin: false, - IsIsolated: false, - }) - } + for idx, orderHead := range orderHeads { + orderID, err := strconv.ParseInt(orderHead.OrderID, 10, 64) + if err != nil { + return createdOrder, err + } - return createdOrders, nil + submitOrder := order[idx] + createdOrder = append(createdOrder, types.Order{ + SubmitOrder: submitOrder, + Exchange: types.ExchangeOKEx, + OrderID: uint64(orderID), + Status: types.OrderStatusNew, + ExecutedQuantity: fixedpoint.Zero, + IsWorking: true, + CreationTime: types.Time(time.Now()), + UpdateTime: types.Time(time.Now()), + IsMargin: false, + IsIsolated: false, + }) + } + */ } func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders []types.Order, err error) { @@ -272,15 +295,21 @@ func (e *Exchange) NewStream() types.Stream { } func (e *Exchange) QueryKLines(ctx context.Context, symbol string, interval types.Interval, options types.KLineQueryOptions) ([]types.KLine, error) { + if err := marketDataLimiter.Wait(ctx); err != nil { + return nil, err + } + + intervalParam := toLocalInterval(interval.String()) + req := e.client.MarketDataService.NewCandlesticksRequest(toLocalSymbol(symbol)) - req.Bar(interval.String()) + req.Bar(intervalParam) if options.StartTime != nil { - req.After(options.StartTime.UnixNano() / int64(time.Millisecond)) + req.After(options.StartTime.Unix()) } if options.EndTime != nil { - req.Before(options.EndTime.UnixNano() / int64(time.Millisecond)) + req.Before(options.EndTime.Unix()) } candles, err := req.Do(ctx) diff --git a/pkg/exchange/okex/gensymbols.go b/pkg/exchange/okex/gensymbols.go index 4838f8f596..be27065e1a 100644 --- a/pkg/exchange/okex/gensymbols.go +++ b/pkg/exchange/okex/gensymbols.go @@ -1,3 +1,4 @@ +//go:build ignore // +build ignore package main diff --git a/pkg/exchange/okex/okexapi/client.go b/pkg/exchange/okex/okexapi/client.go index 5f23a0094f..626e841603 100644 --- a/pkg/exchange/okex/okexapi/client.go +++ b/pkg/exchange/okex/okexapi/client.go @@ -91,6 +91,7 @@ func NewClient() *RestClient { func (c *RestClient) Auth(key, secret, passphrase string) { c.Key = key + // pragma: allowlist nextline secret c.Secret = secret c.Passphrase = passphrase } diff --git a/pkg/exchange/okex/okexapi/market.go b/pkg/exchange/okex/okexapi/market.go index 178c68f6cb..b9b46c43f2 100644 --- a/pkg/exchange/okex/okexapi/market.go +++ b/pkg/exchange/okex/okexapi/market.go @@ -25,15 +25,15 @@ type Candle struct { type CandlesticksRequest struct { client *RestClient - instId string + instId string `param:"instId"` - limit *int + limit *int `param:"limit"` - bar *string + bar *string `param:"bar"` - after *int64 + after *int64 `param:"after,seconds"` - before *int64 + before *int64 `param:"before,seconds"` } func (r *CandlesticksRequest) After(after int64) *CandlesticksRequest { diff --git a/pkg/exchange/okex/stream.go b/pkg/exchange/okex/stream.go index d803a6d303..348f486540 100644 --- a/pkg/exchange/okex/stream.go +++ b/pkg/exchange/okex/stream.go @@ -28,9 +28,9 @@ type Stream struct { client *okexapi.RestClient // public callbacks - candleEventCallbacks []func(candle Candle) - bookEventCallbacks []func(book BookEvent) - eventCallbacks []func(event WebSocketEvent) + candleEventCallbacks []func(candle Candle) + bookEventCallbacks []func(book BookEvent) + eventCallbacks []func(event WebSocketEvent) accountEventCallbacks []func(account okexapi.Account) orderDetailsEventCallbacks []func(orderDetails []okexapi.OrderDetails) diff --git a/pkg/exchange/okex/symbols.go b/pkg/exchange/okex/symbols.go index b8f0bce3de..dfbff11566 100644 --- a/pkg/exchange/okex/symbols.go +++ b/pkg/exchange/okex/symbols.go @@ -2,516 +2,515 @@ package okex var spotSymbolMap = map[string]string{ - "1INCHETH": "1INCH-ETH", - "1INCHUSDT": "1INCH-USDT", - "AACUSDT": "AAC-USDT", - "AAVEBTC": "AAVE-BTC", - "AAVEETH": "AAVE-ETH", - "AAVEUSDT": "AAVE-USDT", - "ABTBTC": "ABT-BTC", - "ABTETH": "ABT-ETH", - "ABTUSDT": "ABT-USDT", - "ACTBTC": "ACT-BTC", - "ACTUSDT": "ACT-USDT", - "ADABTC": "ADA-BTC", - "ADAETH": "ADA-ETH", - "ADAUSDT": "ADA-USDT", - "AEBTC": "AE-BTC", - "AEETH": "AE-ETH", - "AERGOBTC": "AERGO-BTC", - "AERGOUSDT": "AERGO-USDT", - "AEUSDT": "AE-USDT", - "AKITAUSDT": "AKITA-USDT", - "ALGOBTC": "ALGO-BTC", - "ALGOUSDK": "ALGO-USDK", - "ALGOUSDT": "ALGO-USDT", - "ALPHABTC": "ALPHA-BTC", - "ALPHAUSDT": "ALPHA-USDT", - "ALVUSDT": "ALV-USDT", - "ANCUSDT": "ANC-USDT", - "ANTBTC": "ANT-BTC", - "ANTUSDT": "ANT-USDT", - "ANWUSDT": "ANW-USDT", - "API3ETH": "API3-ETH", - "API3USDT": "API3-USDT", - "APIXUSDT": "APIX-USDT", - "APMUSDT": "APM-USDT", - "ARDRBTC": "ARDR-BTC", - "ARKBTC": "ARK-BTC", - "ARKUSDT": "ARK-USDT", - "ASTUSDT": "AST-USDT", - "ATOMBTC": "ATOM-BTC", - "ATOMETH": "ATOM-ETH", - "ATOMUSDT": "ATOM-USDT", + "1INCHETH": "1INCH-ETH", + "1INCHUSDT": "1INCH-USDT", + "AACUSDT": "AAC-USDT", + "AAVEBTC": "AAVE-BTC", + "AAVEETH": "AAVE-ETH", + "AAVEUSDT": "AAVE-USDT", + "ABTBTC": "ABT-BTC", + "ABTETH": "ABT-ETH", + "ABTUSDT": "ABT-USDT", + "ACTBTC": "ACT-BTC", + "ACTUSDT": "ACT-USDT", + "ADABTC": "ADA-BTC", + "ADAETH": "ADA-ETH", + "ADAUSDT": "ADA-USDT", + "AEBTC": "AE-BTC", + "AEETH": "AE-ETH", + "AERGOBTC": "AERGO-BTC", + "AERGOUSDT": "AERGO-USDT", + "AEUSDT": "AE-USDT", + "AKITAUSDT": "AKITA-USDT", + "ALGOBTC": "ALGO-BTC", + "ALGOUSDK": "ALGO-USDK", + "ALGOUSDT": "ALGO-USDT", + "ALPHABTC": "ALPHA-BTC", + "ALPHAUSDT": "ALPHA-USDT", + "ALVUSDT": "ALV-USDT", + "ANCUSDT": "ANC-USDT", + "ANTBTC": "ANT-BTC", + "ANTUSDT": "ANT-USDT", + "ANWUSDT": "ANW-USDT", + "API3ETH": "API3-ETH", + "API3USDT": "API3-USDT", + "APIXUSDT": "APIX-USDT", + "APMUSDT": "APM-USDT", + "ARDRBTC": "ARDR-BTC", + "ARKBTC": "ARK-BTC", + "ARKUSDT": "ARK-USDT", + "ASTUSDT": "AST-USDT", + "ATOMBTC": "ATOM-BTC", + "ATOMETH": "ATOM-ETH", + "ATOMUSDT": "ATOM-USDT", "AUCTIONUSDT": "AUCTION-USDT", - "AVAXBTC": "AVAX-BTC", - "AVAXETH": "AVAX-ETH", - "AVAXUSDT": "AVAX-USDT", - "BADGERBTC": "BADGER-BTC", - "BADGERUSDT": "BADGER-USDT", - "BALBTC": "BAL-BTC", - "BALUSDT": "BAL-USDT", - "BANDUSDT": "BAND-USDT", - "BATBTC": "BAT-BTC", - "BATUSDT": "BAT-USDT", - "BCDBTC": "BCD-BTC", - "BCDUSDT": "BCD-USDT", - "BCHABTC": "BCHA-BTC", - "BCHAUSDT": "BCHA-USDT", - "BCHBTC": "BCH-BTC", - "BCHUSDC": "BCH-USDC", - "BCHUSDK": "BCH-USDK", - "BCHUSDT": "BCH-USDT", - "BCXBTC": "BCX-BTC", - "BETHETH": "BETH-ETH", - "BETHUSDT": "BETH-USDT", - "BHPBTC": "BHP-BTC", - "BHPUSDT": "BHP-USDT", - "BLOCUSDT": "BLOC-USDT", - "BNTBTC": "BNT-BTC", - "BNTUSDT": "BNT-USDT", - "BOXUSDT": "BOX-USDT", - "BSVBTC": "BSV-BTC", - "BSVUSDC": "BSV-USDC", - "BSVUSDK": "BSV-USDK", - "BSVUSDT": "BSV-USDT", - "BTCDAI": "BTC-DAI", - "BTCUSDC": "BTC-USDC", - "BTCUSDK": "BTC-USDK", - "BTCUSDT": "BTC-USDT", - "BTGBTC": "BTG-BTC", - "BTGUSDT": "BTG-USDT", - "BTMBTC": "BTM-BTC", - "BTMETH": "BTM-ETH", - "BTMUSDT": "BTM-USDT", - "BTTBTC": "BTT-BTC", - "BTTUSDT": "BTT-USDT", - "CELOBTC": "CELO-BTC", - "CELOUSDT": "CELO-USDT", - "CELRUSDT": "CELR-USDT", - "CELUSDT": "CEL-USDT", - "CFXBTC": "CFX-BTC", - "CFXUSDT": "CFX-USDT", - "CHATUSDT": "CHAT-USDT", - "CHZBTC": "CHZ-BTC", - "CHZUSDT": "CHZ-USDT", - "CMTBTC": "CMT-BTC", - "CMTETH": "CMT-ETH", - "CMTUSDT": "CMT-USDT", - "CNTMUSDT": "CNTM-USDT", - "COMPBTC": "COMP-BTC", - "COMPUSDT": "COMP-USDT", - "CONVUSDT": "CONV-USDT", - "COVERUSDT": "COVER-USDT", - "CROBTC": "CRO-BTC", - "CROUSDK": "CRO-USDK", - "CROUSDT": "CRO-USDT", - "CRVBTC": "CRV-BTC", - "CRVETH": "CRV-ETH", - "CRVUSDT": "CRV-USDT", - "CSPRUSDT": "CSPR-USDT", - "CTCBTC": "CTC-BTC", - "CTCUSDT": "CTC-USDT", - "CTXCBTC": "CTXC-BTC", - "CTXCETH": "CTXC-ETH", - "CTXCUSDT": "CTXC-USDT", - "CVCBTC": "CVC-BTC", - "CVCUSDT": "CVC-USDT", - "CVPUSDT": "CVP-USDT", - "CVTBTC": "CVT-BTC", - "CVTUSDT": "CVT-USDT", - "DAIUSDT": "DAI-USDT", - "DAOUSDT": "DAO-USDT", - "DASHBTC": "DASH-BTC", - "DASHETH": "DASH-ETH", - "DASHUSDT": "DASH-USDT", - "DCRBTC": "DCR-BTC", - "DCRUSDT": "DCR-USDT", - "DEPUSDK": "DEP-USDK", - "DEPUSDT": "DEP-USDT", - "DGBBTC": "DGB-BTC", - "DGBUSDT": "DGB-USDT", - "DHTETH": "DHT-ETH", - "DHTUSDT": "DHT-USDT", - "DIAETH": "DIA-ETH", - "DIAUSDT": "DIA-USDT", - "DMDUSDT": "DMD-USDT", - "DMGUSDT": "DMG-USDT", - "DNABTC": "DNA-BTC", - "DNAUSDT": "DNA-USDT", - "DOGEBTC": "DOGE-BTC", - "DOGEETH": "DOGE-ETH", - "DOGEUSDK": "DOGE-USDK", - "DOGEUSDT": "DOGE-USDT", - "DORAUSDT": "DORA-USDT", - "DOTBTC": "DOT-BTC", - "DOTETH": "DOT-ETH", - "DOTUSDT": "DOT-USDT", - "ECUSDK": "EC-USDK", - "ECUSDT": "EC-USDT", - "EGLDBTC": "EGLD-BTC", - "EGLDUSDT": "EGLD-USDT", - "EGTBTC": "EGT-BTC", - "EGTETH": "EGT-ETH", - "EGTUSDT": "EGT-USDT", - "ELFBTC": "ELF-BTC", - "ELFUSDT": "ELF-USDT", - "EMUSDK": "EM-USDK", - "EMUSDT": "EM-USDT", - "ENJBTC": "ENJ-BTC", - "ENJUSDT": "ENJ-USDT", - "EOSBTC": "EOS-BTC", - "EOSETH": "EOS-ETH", - "EOSUSDC": "EOS-USDC", - "EOSUSDK": "EOS-USDK", - "EOSUSDT": "EOS-USDT", - "ETCBTC": "ETC-BTC", - "ETCETH": "ETC-ETH", - "ETCOKB": "ETC-OKB", - "ETCUSDC": "ETC-USDC", - "ETCUSDK": "ETC-USDK", - "ETCUSDT": "ETC-USDT", - "ETHBTC": "ETH-BTC", - "ETHDAI": "ETH-DAI", - "ETHUSDC": "ETH-USDC", - "ETHUSDK": "ETH-USDK", - "ETHUSDT": "ETH-USDT", - "ETMUSDT": "ETM-USDT", - "EXEUSDT": "EXE-USDT", - "FAIRUSDT": "FAIR-USDT", - "FILBTC": "FIL-BTC", - "FILETH": "FIL-ETH", - "FILUSDT": "FIL-USDT", - "FLMUSDT": "FLM-USDT", - "FLOWBTC": "FLOW-BTC", - "FLOWETH": "FLOW-ETH", - "FLOWUSDT": "FLOW-USDT", - "FORTHBTC": "FORTH-BTC", - "FORTHUSDT": "FORTH-USDT", - "FRONTETH": "FRONT-ETH", - "FRONTUSDT": "FRONT-USDT", - "FSNUSDK": "FSN-USDK", - "FSNUSDT": "FSN-USDT", - "FTMUSDK": "FTM-USDK", - "FTMUSDT": "FTM-USDT", - "FUNBTC": "FUN-BTC", - "GALUSDT": "GAL-USDT", - "GASBTC": "GAS-BTC", - "GASETH": "GAS-ETH", - "GASUSDT": "GAS-USDT", - "GHSTETH": "GHST-ETH", - "GHSTUSDT": "GHST-USDT", - "GLMBTC": "GLM-BTC", - "GLMUSDT": "GLM-USDT", - "GNXBTC": "GNX-BTC", - "GRTBTC": "GRT-BTC", - "GRTUSDT": "GRT-USDT", - "GTOBTC": "GTO-BTC", - "GTOUSDT": "GTO-USDT", - "GUSDBTC": "GUSD-BTC", - "GUSDUSDT": "GUSD-USDT", - "HBARBTC": "HBAR-BTC", - "HBARUSDK": "HBAR-USDK", - "HBARUSDT": "HBAR-USDT", - "HCBTC": "HC-BTC", - "HCUSDT": "HC-USDT", - "HDAOUSDK": "HDAO-USDK", - "HDAOUSDT": "HDAO-USDT", - "HEGICETH": "HEGIC-ETH", - "HEGICUSDT": "HEGIC-USDT", - "ICPBTC": "ICP-BTC", - "ICPUSDT": "ICP-USDT", - "ICXBTC": "ICX-BTC", - "ICXUSDT": "ICX-USDT", - "INTBTC": "INT-BTC", - "INTETH": "INT-ETH", - "INTUSDT": "INT-USDT", - "INXUSDT": "INX-USDT", - "IOSTBTC": "IOST-BTC", - "IOSTETH": "IOST-ETH", - "IOSTUSDT": "IOST-USDT", - "IOTABTC": "IOTA-BTC", - "IOTAUSDT": "IOTA-USDT", - "IQUSDT": "IQ-USDT", - "ITCUSDT": "ITC-USDT", - "JFIUSDT": "JFI-USDT", - "JSTUSDT": "JST-USDT", - "KANETH": "KAN-ETH", - "KANUSDT": "KAN-USDT", - "KCASHBTC": "KCASH-BTC", - "KCASHETH": "KCASH-ETH", - "KCASHUSDT": "KCASH-USDT", - "KINEUSDT": "KINE-USDT", - "KISHUUSDT": "KISHU-USDT", - "KLAYBTC": "KLAY-BTC", - "KLAYUSDT": "KLAY-USDT", - "KNCBTC": "KNC-BTC", - "KNCUSDT": "KNC-USDT", - "KONOUSDT": "KONO-USDT", - "KP3RUSDT": "KP3R-USDT", - "KSMBTC": "KSM-BTC", - "KSMETH": "KSM-ETH", - "KSMUSDT": "KSM-USDT", - "LAMBUSDK": "LAMB-USDK", - "LAMBUSDT": "LAMB-USDT", - "LATUSDT": "LAT-USDT", - "LBAUSDT": "LBA-USDT", - "LEOUSDK": "LEO-USDK", - "LEOUSDT": "LEO-USDT", - "LETUSDT": "LET-USDT", - "LINKBTC": "LINK-BTC", - "LINKETH": "LINK-ETH", - "LINKUSDT": "LINK-USDT", - "LMCHUSDT": "LMCH-USDT", - "LONETH": "LON-ETH", - "LONUSDT": "LON-USDT", - "LOONBTC": "LOON-BTC", - "LOONUSDT": "LOON-USDT", - "LPTUSDT": "LPT-USDT", - "LRCBTC": "LRC-BTC", - "LRCUSDT": "LRC-USDT", - "LSKBTC": "LSK-BTC", - "LSKUSDT": "LSK-USDT", - "LTCBTC": "LTC-BTC", - "LTCETH": "LTC-ETH", - "LTCOKB": "LTC-OKB", - "LTCUSDC": "LTC-USDC", - "LTCUSDK": "LTC-USDK", - "LTCUSDT": "LTC-USDT", - "LUNABTC": "LUNA-BTC", - "LUNAUSDT": "LUNA-USDT", - "MANABTC": "MANA-BTC", - "MANAETH": "MANA-ETH", - "MANAUSDT": "MANA-USDT", - "MASKUSDT": "MASK-USDT", - "MATICUSDT": "MATIC-USDT", - "MCOBTC": "MCO-BTC", - "MCOETH": "MCO-ETH", - "MCOUSDT": "MCO-USDT", - "MDAUSDT": "MDA-USDT", - "MDTUSDT": "MDT-USDT", - "MEMEUSDT": "MEME-USDT", - "MIRUSDT": "MIR-USDT", - "MITHBTC": "MITH-BTC", - "MITHETH": "MITH-ETH", - "MITHUSDT": "MITH-USDT", - "MKRBTC": "MKR-BTC", - "MKRETH": "MKR-ETH", - "MKRUSDT": "MKR-USDT", - "MLNUSDT": "MLN-USDT", - "MOFBTC": "MOF-BTC", - "MOFUSDT": "MOF-USDT", - "MXCUSDT": "MXC-USDT", - "MXTUSDT": "MXT-USDT", - "NANOBTC": "NANO-BTC", - "NANOUSDT": "NANO-USDT", - "NASBTC": "NAS-BTC", - "NASUSDT": "NAS-USDT", - "NDNUSDK": "NDN-USDK", - "NDNUSDT": "NDN-USDT", - "NEARBTC": "NEAR-BTC", - "NEARETH": "NEAR-ETH", - "NEARUSDT": "NEAR-USDT", - "NEOBTC": "NEO-BTC", - "NEOETH": "NEO-ETH", - "NEOUSDT": "NEO-USDT", - "NMRUSDT": "NMR-USDT", - "NUBTC": "NU-BTC", - "NULSBTC": "NULS-BTC", - "NULSETH": "NULS-ETH", - "NULSUSDT": "NULS-USDT", - "NUUSDT": "NU-USDT", - "OKBBTC": "OKB-BTC", - "OKBETH": "OKB-ETH", - "OKBUSDC": "OKB-USDC", - "OKBUSDK": "OKB-USDK", - "OKBUSDT": "OKB-USDT", - "OKTBTC": "OKT-BTC", - "OKTETH": "OKT-ETH", - "OKTUSDT": "OKT-USDT", - "OMETH": "OM-ETH", - "OMGBTC": "OMG-BTC", - "OMGUSDT": "OMG-USDT", - "OMUSDT": "OM-USDT", - "ONTBTC": "ONT-BTC", - "ONTETH": "ONT-ETH", - "ONTUSDT": "ONT-USDT", - "ORBSUSDK": "ORBS-USDK", - "ORBSUSDT": "ORBS-USDT", - "ORSUSDT": "ORS-USDT", - "OXTUSDT": "OXT-USDT", - "PAXBTC": "PAX-BTC", - "PAXUSDT": "PAX-USDT", - "PAYBTC": "PAY-BTC", - "PAYUSDT": "PAY-USDT", - "PERPUSDT": "PERP-USDT", - "PHAETH": "PHA-ETH", - "PHAUSDT": "PHA-USDT", - "PICKLEUSDT": "PICKLE-USDT", - "PLGUSDK": "PLG-USDK", - "PLGUSDT": "PLG-USDT", - "PMABTC": "PMA-BTC", - "PMAUSDK": "PMA-USDK", - "PNKUSDT": "PNK-USDT", - "POLSETH": "POLS-ETH", - "POLSUSDT": "POLS-USDT", - "PPTUSDT": "PPT-USDT", - "PROPSETH": "PROPS-ETH", - "PROPSUSDT": "PROPS-USDT", - "PRQUSDT": "PRQ-USDT", - "PSTBTC": "PST-BTC", - "PSTUSDT": "PST-USDT", - "QTUMBTC": "QTUM-BTC", - "QTUMETH": "QTUM-ETH", - "QTUMUSDT": "QTUM-USDT", - "QUNBTC": "QUN-BTC", - "QUNUSDT": "QUN-USDT", - "RENBTC": "REN-BTC", - "RENUSDT": "REN-USDT", - "REPETH": "REP-ETH", - "REPUSDT": "REP-USDT", - "RFUELETH": "RFUEL-ETH", - "RFUELUSDT": "RFUEL-USDT", - "RIOUSDT": "RIO-USDT", - "RNTUSDT": "RNT-USDT", - "ROADUSDK": "ROAD-USDK", - "ROADUSDT": "ROAD-USDT", - "RSRBTC": "RSR-BTC", - "RSRETH": "RSR-ETH", - "RSRUSDT": "RSR-USDT", - "RVNBTC": "RVN-BTC", - "RVNUSDT": "RVN-USDT", - "SANDUSDT": "SAND-USDT", - "SBTCBTC": "SBTC-BTC", - "SCBTC": "SC-BTC", - "SCUSDT": "SC-USDT", - "SFGUSDT": "SFG-USDT", - "SHIBUSDT": "SHIB-USDT", - "SKLUSDT": "SKL-USDT", - "SNCBTC": "SNC-BTC", - "SNTBTC": "SNT-BTC", - "SNTUSDT": "SNT-USDT", - "SNXETH": "SNX-ETH", - "SNXUSDT": "SNX-USDT", - "SOCUSDT": "SOC-USDT", - "SOLBTC": "SOL-BTC", - "SOLETH": "SOL-ETH", - "SOLUSDT": "SOL-USDT", - "SRMBTC": "SRM-BTC", - "SRMUSDT": "SRM-USDT", - "STORJUSDT": "STORJ-USDT", - "STRKUSDT": "STRK-USDT", - "STXBTC": "STX-BTC", - "STXUSDT": "STX-USDT", - "SUNETH": "SUN-ETH", - "SUNUSDT": "SUN-USDT", - "SUSHIETH": "SUSHI-ETH", - "SUSHIUSDT": "SUSHI-USDT", - "SWFTCBTC": "SWFTC-BTC", - "SWFTCETH": "SWFTC-ETH", - "SWFTCUSDT": "SWFTC-USDT", - "SWRVUSDT": "SWRV-USDT", - "TAIUSDT": "TAI-USDT", - "TCTBTC": "TCT-BTC", - "TCTUSDT": "TCT-USDT", - "THETABTC": "THETA-BTC", - "THETAUSDT": "THETA-USDT", - "TMTGBTC": "TMTG-BTC", - "TMTGUSDT": "TMTG-USDT", - "TOPCUSDT": "TOPC-USDT", - "TORNETH": "TORN-ETH", - "TORNUSDT": "TORN-USDT", - "TRADEETH": "TRADE-ETH", - "TRADEUSDT": "TRADE-USDT", - "TRAUSDT": "TRA-USDT", - "TRBUSDT": "TRB-USDT", - "TRIOBTC": "TRIO-BTC", - "TRIOUSDT": "TRIO-USDT", - "TRUEBTC": "TRUE-BTC", - "TRUEUSDT": "TRUE-USDT", - "TRXBTC": "TRX-BTC", - "TRXETH": "TRX-ETH", - "TRXUSDC": "TRX-USDC", - "TRXUSDK": "TRX-USDK", - "TRXUSDT": "TRX-USDT", - "TUSDBTC": "TUSD-BTC", - "TUSDUSDT": "TUSD-USDT", - "UBTCUSDT": "UBTC-USDT", - "UMAUSDT": "UMA-USDT", - "UNIBTC": "UNI-BTC", - "UNIETH": "UNI-ETH", - "UNIUSDT": "UNI-USDT", - "USDCBTC": "USDC-BTC", - "USDCUSDT": "USDC-USDT", - "USDTUSDK": "USDT-USDK", - "UTKUSDT": "UTK-USDT", - "VALUEETH": "VALUE-ETH", - "VALUEUSDT": "VALUE-USDT", - "VELOUSDT": "VELO-USDT", - "VIBBTC": "VIB-BTC", - "VIBUSDT": "VIB-USDT", - "VITEBTC": "VITE-BTC", - "VRAUSDT": "VRA-USDT", - "VSYSBTC": "VSYS-BTC", - "VSYSUSDK": "VSYS-USDK", - "VSYSUSDT": "VSYS-USDT", - "WAVESBTC": "WAVES-BTC", - "WAVESUSDT": "WAVES-USDT", - "WBTCBTC": "WBTC-BTC", - "WBTCETH": "WBTC-ETH", - "WBTCUSDT": "WBTC-USDT", - "WGRTUSDK": "WGRT-USDK", - "WGRTUSDT": "WGRT-USDT", - "WINGUSDT": "WING-USDT", - "WNXMUSDT": "WNXM-USDT", - "WTCBTC": "WTC-BTC", - "WTCUSDT": "WTC-USDT", - "WXTBTC": "WXT-BTC", - "WXTUSDK": "WXT-USDK", - "WXTUSDT": "WXT-USDT", - "XCHBTC": "XCH-BTC", - "XCHUSDT": "XCH-USDT", - "XEMBTC": "XEM-BTC", - "XEMETH": "XEM-ETH", - "XEMUSDT": "XEM-USDT", - "XLMBTC": "XLM-BTC", - "XLMETH": "XLM-ETH", - "XLMUSDT": "XLM-USDT", - "XMRBTC": "XMR-BTC", - "XMRETH": "XMR-ETH", - "XMRUSDT": "XMR-USDT", - "XPOUSDT": "XPO-USDT", - "XPRUSDT": "XPR-USDT", - "XRPBTC": "XRP-BTC", - "XRPETH": "XRP-ETH", - "XRPOKB": "XRP-OKB", - "XRPUSDC": "XRP-USDC", - "XRPUSDK": "XRP-USDK", - "XRPUSDT": "XRP-USDT", - "XSRUSDT": "XSR-USDT", - "XTZBTC": "XTZ-BTC", - "XTZUSDT": "XTZ-USDT", - "XUCUSDT": "XUC-USDT", - "YEEUSDT": "YEE-USDT", - "YFIBTC": "YFI-BTC", - "YFIETH": "YFI-ETH", - "YFIIUSDT": "YFII-USDT", - "YFIUSDT": "YFI-USDT", - "YOUBTC": "YOU-BTC", - "YOUUSDT": "YOU-USDT", - "YOYOUSDT": "YOYO-USDT", - "ZECBTC": "ZEC-BTC", - "ZECETH": "ZEC-ETH", - "ZECUSDT": "ZEC-USDT", - "ZENBTC": "ZEN-BTC", - "ZENUSDT": "ZEN-USDT", - "ZILBTC": "ZIL-BTC", - "ZILETH": "ZIL-ETH", - "ZILUSDT": "ZIL-USDT", - "ZKSUSDT": "ZKS-USDT", - "ZRXBTC": "ZRX-BTC", - "ZRXETH": "ZRX-ETH", - "ZRXUSDT": "ZRX-USDT", - "ZYROUSDT": "ZYRO-USDT", + "AVAXBTC": "AVAX-BTC", + "AVAXETH": "AVAX-ETH", + "AVAXUSDT": "AVAX-USDT", + "BADGERBTC": "BADGER-BTC", + "BADGERUSDT": "BADGER-USDT", + "BALBTC": "BAL-BTC", + "BALUSDT": "BAL-USDT", + "BANDUSDT": "BAND-USDT", + "BATBTC": "BAT-BTC", + "BATUSDT": "BAT-USDT", + "BCDBTC": "BCD-BTC", + "BCDUSDT": "BCD-USDT", + "BCHABTC": "BCHA-BTC", + "BCHAUSDT": "BCHA-USDT", + "BCHBTC": "BCH-BTC", + "BCHUSDC": "BCH-USDC", + "BCHUSDK": "BCH-USDK", + "BCHUSDT": "BCH-USDT", + "BCXBTC": "BCX-BTC", + "BETHETH": "BETH-ETH", + "BETHUSDT": "BETH-USDT", + "BHPBTC": "BHP-BTC", + "BHPUSDT": "BHP-USDT", + "BLOCUSDT": "BLOC-USDT", + "BNTBTC": "BNT-BTC", + "BNTUSDT": "BNT-USDT", + "BOXUSDT": "BOX-USDT", + "BSVBTC": "BSV-BTC", + "BSVUSDC": "BSV-USDC", + "BSVUSDK": "BSV-USDK", + "BSVUSDT": "BSV-USDT", + "BTCDAI": "BTC-DAI", + "BTCUSDC": "BTC-USDC", + "BTCUSDK": "BTC-USDK", + "BTCUSDT": "BTC-USDT", + "BTGBTC": "BTG-BTC", + "BTGUSDT": "BTG-USDT", + "BTMBTC": "BTM-BTC", + "BTMETH": "BTM-ETH", + "BTMUSDT": "BTM-USDT", + "BTTBTC": "BTT-BTC", + "BTTUSDT": "BTT-USDT", + "CELOBTC": "CELO-BTC", + "CELOUSDT": "CELO-USDT", + "CELRUSDT": "CELR-USDT", + "CELUSDT": "CEL-USDT", + "CFXBTC": "CFX-BTC", + "CFXUSDT": "CFX-USDT", + "CHATUSDT": "CHAT-USDT", + "CHZBTC": "CHZ-BTC", + "CHZUSDT": "CHZ-USDT", + "CMTBTC": "CMT-BTC", + "CMTETH": "CMT-ETH", + "CMTUSDT": "CMT-USDT", + "CNTMUSDT": "CNTM-USDT", + "COMPBTC": "COMP-BTC", + "COMPUSDT": "COMP-USDT", + "CONVUSDT": "CONV-USDT", + "COVERUSDT": "COVER-USDT", + "CROBTC": "CRO-BTC", + "CROUSDK": "CRO-USDK", + "CROUSDT": "CRO-USDT", + "CRVBTC": "CRV-BTC", + "CRVETH": "CRV-ETH", + "CRVUSDT": "CRV-USDT", + "CSPRUSDT": "CSPR-USDT", + "CTCBTC": "CTC-BTC", + "CTCUSDT": "CTC-USDT", + "CTXCBTC": "CTXC-BTC", + "CTXCETH": "CTXC-ETH", + "CTXCUSDT": "CTXC-USDT", + "CVCBTC": "CVC-BTC", + "CVCUSDT": "CVC-USDT", + "CVPUSDT": "CVP-USDT", + "CVTBTC": "CVT-BTC", + "CVTUSDT": "CVT-USDT", + "DAIUSDT": "DAI-USDT", + "DAOUSDT": "DAO-USDT", + "DASHBTC": "DASH-BTC", + "DASHETH": "DASH-ETH", + "DASHUSDT": "DASH-USDT", + "DCRBTC": "DCR-BTC", + "DCRUSDT": "DCR-USDT", + "DEPUSDK": "DEP-USDK", + "DEPUSDT": "DEP-USDT", + "DGBBTC": "DGB-BTC", + "DGBUSDT": "DGB-USDT", + "DHTETH": "DHT-ETH", + "DHTUSDT": "DHT-USDT", + "DIAETH": "DIA-ETH", + "DIAUSDT": "DIA-USDT", + "DMDUSDT": "DMD-USDT", + "DMGUSDT": "DMG-USDT", + "DNABTC": "DNA-BTC", + "DNAUSDT": "DNA-USDT", + "DOGEBTC": "DOGE-BTC", + "DOGEETH": "DOGE-ETH", + "DOGEUSDK": "DOGE-USDK", + "DOGEUSDT": "DOGE-USDT", + "DORAUSDT": "DORA-USDT", + "DOTBTC": "DOT-BTC", + "DOTETH": "DOT-ETH", + "DOTUSDT": "DOT-USDT", + "ECUSDK": "EC-USDK", + "ECUSDT": "EC-USDT", + "EGLDBTC": "EGLD-BTC", + "EGLDUSDT": "EGLD-USDT", + "EGTBTC": "EGT-BTC", + "EGTETH": "EGT-ETH", + "EGTUSDT": "EGT-USDT", + "ELFBTC": "ELF-BTC", + "ELFUSDT": "ELF-USDT", + "EMUSDK": "EM-USDK", + "EMUSDT": "EM-USDT", + "ENJBTC": "ENJ-BTC", + "ENJUSDT": "ENJ-USDT", + "EOSBTC": "EOS-BTC", + "EOSETH": "EOS-ETH", + "EOSUSDC": "EOS-USDC", + "EOSUSDK": "EOS-USDK", + "EOSUSDT": "EOS-USDT", + "ETCBTC": "ETC-BTC", + "ETCETH": "ETC-ETH", + "ETCOKB": "ETC-OKB", + "ETCUSDC": "ETC-USDC", + "ETCUSDK": "ETC-USDK", + "ETCUSDT": "ETC-USDT", + "ETHBTC": "ETH-BTC", + "ETHDAI": "ETH-DAI", + "ETHUSDC": "ETH-USDC", + "ETHUSDK": "ETH-USDK", + "ETHUSDT": "ETH-USDT", + "ETMUSDT": "ETM-USDT", + "EXEUSDT": "EXE-USDT", + "FAIRUSDT": "FAIR-USDT", + "FILBTC": "FIL-BTC", + "FILETH": "FIL-ETH", + "FILUSDT": "FIL-USDT", + "FLMUSDT": "FLM-USDT", + "FLOWBTC": "FLOW-BTC", + "FLOWETH": "FLOW-ETH", + "FLOWUSDT": "FLOW-USDT", + "FORTHBTC": "FORTH-BTC", + "FORTHUSDT": "FORTH-USDT", + "FRONTETH": "FRONT-ETH", + "FRONTUSDT": "FRONT-USDT", + "FSNUSDK": "FSN-USDK", + "FSNUSDT": "FSN-USDT", + "FTMUSDK": "FTM-USDK", + "FTMUSDT": "FTM-USDT", + "FUNBTC": "FUN-BTC", + "GALUSDT": "GAL-USDT", + "GASBTC": "GAS-BTC", + "GASETH": "GAS-ETH", + "GASUSDT": "GAS-USDT", + "GHSTETH": "GHST-ETH", + "GHSTUSDT": "GHST-USDT", + "GLMBTC": "GLM-BTC", + "GLMUSDT": "GLM-USDT", + "GNXBTC": "GNX-BTC", + "GRTBTC": "GRT-BTC", + "GRTUSDT": "GRT-USDT", + "GTOBTC": "GTO-BTC", + "GTOUSDT": "GTO-USDT", + "GUSDBTC": "GUSD-BTC", + "GUSDUSDT": "GUSD-USDT", + "HBARBTC": "HBAR-BTC", + "HBARUSDK": "HBAR-USDK", + "HBARUSDT": "HBAR-USDT", + "HCBTC": "HC-BTC", + "HCUSDT": "HC-USDT", + "HDAOUSDK": "HDAO-USDK", + "HDAOUSDT": "HDAO-USDT", + "HEGICETH": "HEGIC-ETH", + "HEGICUSDT": "HEGIC-USDT", + "ICPBTC": "ICP-BTC", + "ICPUSDT": "ICP-USDT", + "ICXBTC": "ICX-BTC", + "ICXUSDT": "ICX-USDT", + "INTBTC": "INT-BTC", + "INTETH": "INT-ETH", + "INTUSDT": "INT-USDT", + "INXUSDT": "INX-USDT", + "IOSTBTC": "IOST-BTC", + "IOSTETH": "IOST-ETH", + "IOSTUSDT": "IOST-USDT", + "IOTABTC": "IOTA-BTC", + "IOTAUSDT": "IOTA-USDT", + "IQUSDT": "IQ-USDT", + "ITCUSDT": "ITC-USDT", + "JFIUSDT": "JFI-USDT", + "JSTUSDT": "JST-USDT", + "KANETH": "KAN-ETH", + "KANUSDT": "KAN-USDT", + "KCASHBTC": "KCASH-BTC", + "KCASHETH": "KCASH-ETH", + "KCASHUSDT": "KCASH-USDT", + "KINEUSDT": "KINE-USDT", + "KISHUUSDT": "KISHU-USDT", + "KLAYBTC": "KLAY-BTC", + "KLAYUSDT": "KLAY-USDT", + "KNCBTC": "KNC-BTC", + "KNCUSDT": "KNC-USDT", + "KONOUSDT": "KONO-USDT", + "KP3RUSDT": "KP3R-USDT", + "KSMBTC": "KSM-BTC", + "KSMETH": "KSM-ETH", + "KSMUSDT": "KSM-USDT", + "LAMBUSDK": "LAMB-USDK", + "LAMBUSDT": "LAMB-USDT", + "LATUSDT": "LAT-USDT", + "LBAUSDT": "LBA-USDT", + "LEOUSDK": "LEO-USDK", + "LEOUSDT": "LEO-USDT", + "LETUSDT": "LET-USDT", + "LINKBTC": "LINK-BTC", + "LINKETH": "LINK-ETH", + "LINKUSDT": "LINK-USDT", + "LMCHUSDT": "LMCH-USDT", + "LONETH": "LON-ETH", + "LONUSDT": "LON-USDT", + "LOONBTC": "LOON-BTC", + "LOONUSDT": "LOON-USDT", + "LPTUSDT": "LPT-USDT", + "LRCBTC": "LRC-BTC", + "LRCUSDT": "LRC-USDT", + "LSKBTC": "LSK-BTC", + "LSKUSDT": "LSK-USDT", + "LTCBTC": "LTC-BTC", + "LTCETH": "LTC-ETH", + "LTCOKB": "LTC-OKB", + "LTCUSDC": "LTC-USDC", + "LTCUSDK": "LTC-USDK", + "LTCUSDT": "LTC-USDT", + "LUNABTC": "LUNA-BTC", + "LUNAUSDT": "LUNA-USDT", + "MANABTC": "MANA-BTC", + "MANAETH": "MANA-ETH", + "MANAUSDT": "MANA-USDT", + "MASKUSDT": "MASK-USDT", + "MATICUSDT": "MATIC-USDT", + "MCOBTC": "MCO-BTC", + "MCOETH": "MCO-ETH", + "MCOUSDT": "MCO-USDT", + "MDAUSDT": "MDA-USDT", + "MDTUSDT": "MDT-USDT", + "MEMEUSDT": "MEME-USDT", + "MIRUSDT": "MIR-USDT", + "MITHBTC": "MITH-BTC", + "MITHETH": "MITH-ETH", + "MITHUSDT": "MITH-USDT", + "MKRBTC": "MKR-BTC", + "MKRETH": "MKR-ETH", + "MKRUSDT": "MKR-USDT", + "MLNUSDT": "MLN-USDT", + "MOFBTC": "MOF-BTC", + "MOFUSDT": "MOF-USDT", + "MXCUSDT": "MXC-USDT", + "MXTUSDT": "MXT-USDT", + "NANOBTC": "NANO-BTC", + "NANOUSDT": "NANO-USDT", + "NASBTC": "NAS-BTC", + "NASUSDT": "NAS-USDT", + "NDNUSDK": "NDN-USDK", + "NDNUSDT": "NDN-USDT", + "NEARBTC": "NEAR-BTC", + "NEARETH": "NEAR-ETH", + "NEARUSDT": "NEAR-USDT", + "NEOBTC": "NEO-BTC", + "NEOETH": "NEO-ETH", + "NEOUSDT": "NEO-USDT", + "NMRUSDT": "NMR-USDT", + "NUBTC": "NU-BTC", + "NULSBTC": "NULS-BTC", + "NULSETH": "NULS-ETH", + "NULSUSDT": "NULS-USDT", + "NUUSDT": "NU-USDT", + "OKBBTC": "OKB-BTC", + "OKBETH": "OKB-ETH", + "OKBUSDC": "OKB-USDC", + "OKBUSDK": "OKB-USDK", + "OKBUSDT": "OKB-USDT", + "OKTBTC": "OKT-BTC", + "OKTETH": "OKT-ETH", + "OKTUSDT": "OKT-USDT", + "OMETH": "OM-ETH", + "OMGBTC": "OMG-BTC", + "OMGUSDT": "OMG-USDT", + "OMUSDT": "OM-USDT", + "ONTBTC": "ONT-BTC", + "ONTETH": "ONT-ETH", + "ONTUSDT": "ONT-USDT", + "ORBSUSDK": "ORBS-USDK", + "ORBSUSDT": "ORBS-USDT", + "ORSUSDT": "ORS-USDT", + "OXTUSDT": "OXT-USDT", + "PAXBTC": "PAX-BTC", + "PAXUSDT": "PAX-USDT", + "PAYBTC": "PAY-BTC", + "PAYUSDT": "PAY-USDT", + "PERPUSDT": "PERP-USDT", + "PHAETH": "PHA-ETH", + "PHAUSDT": "PHA-USDT", + "PICKLEUSDT": "PICKLE-USDT", + "PLGUSDK": "PLG-USDK", + "PLGUSDT": "PLG-USDT", + "PMABTC": "PMA-BTC", + "PMAUSDK": "PMA-USDK", + "PNKUSDT": "PNK-USDT", + "POLSETH": "POLS-ETH", + "POLSUSDT": "POLS-USDT", + "PPTUSDT": "PPT-USDT", + "PROPSETH": "PROPS-ETH", + "PROPSUSDT": "PROPS-USDT", + "PRQUSDT": "PRQ-USDT", + "PSTBTC": "PST-BTC", + "PSTUSDT": "PST-USDT", + "QTUMBTC": "QTUM-BTC", + "QTUMETH": "QTUM-ETH", + "QTUMUSDT": "QTUM-USDT", + "QUNBTC": "QUN-BTC", + "QUNUSDT": "QUN-USDT", + "RENBTC": "REN-BTC", + "RENUSDT": "REN-USDT", + "REPETH": "REP-ETH", + "REPUSDT": "REP-USDT", + "RFUELETH": "RFUEL-ETH", + "RFUELUSDT": "RFUEL-USDT", + "RIOUSDT": "RIO-USDT", + "RNTUSDT": "RNT-USDT", + "ROADUSDK": "ROAD-USDK", + "ROADUSDT": "ROAD-USDT", + "RSRBTC": "RSR-BTC", + "RSRETH": "RSR-ETH", + "RSRUSDT": "RSR-USDT", + "RVNBTC": "RVN-BTC", + "RVNUSDT": "RVN-USDT", + "SANDUSDT": "SAND-USDT", + "SBTCBTC": "SBTC-BTC", + "SCBTC": "SC-BTC", + "SCUSDT": "SC-USDT", + "SFGUSDT": "SFG-USDT", + "SHIBUSDT": "SHIB-USDT", + "SKLUSDT": "SKL-USDT", + "SNCBTC": "SNC-BTC", + "SNTBTC": "SNT-BTC", + "SNTUSDT": "SNT-USDT", + "SNXETH": "SNX-ETH", + "SNXUSDT": "SNX-USDT", + "SOCUSDT": "SOC-USDT", + "SOLBTC": "SOL-BTC", + "SOLETH": "SOL-ETH", + "SOLUSDT": "SOL-USDT", + "SRMBTC": "SRM-BTC", + "SRMUSDT": "SRM-USDT", + "STORJUSDT": "STORJ-USDT", + "STRKUSDT": "STRK-USDT", + "STXBTC": "STX-BTC", + "STXUSDT": "STX-USDT", + "SUNETH": "SUN-ETH", + "SUNUSDT": "SUN-USDT", + "SUSHIETH": "SUSHI-ETH", + "SUSHIUSDT": "SUSHI-USDT", + "SWFTCBTC": "SWFTC-BTC", + "SWFTCETH": "SWFTC-ETH", + "SWFTCUSDT": "SWFTC-USDT", + "SWRVUSDT": "SWRV-USDT", + "TAIUSDT": "TAI-USDT", + "TCTBTC": "TCT-BTC", + "TCTUSDT": "TCT-USDT", + "THETABTC": "THETA-BTC", + "THETAUSDT": "THETA-USDT", + "TMTGBTC": "TMTG-BTC", + "TMTGUSDT": "TMTG-USDT", + "TOPCUSDT": "TOPC-USDT", + "TORNETH": "TORN-ETH", + "TORNUSDT": "TORN-USDT", + "TRADEETH": "TRADE-ETH", + "TRADEUSDT": "TRADE-USDT", + "TRAUSDT": "TRA-USDT", + "TRBUSDT": "TRB-USDT", + "TRIOBTC": "TRIO-BTC", + "TRIOUSDT": "TRIO-USDT", + "TRUEBTC": "TRUE-BTC", + "TRUEUSDT": "TRUE-USDT", + "TRXBTC": "TRX-BTC", + "TRXETH": "TRX-ETH", + "TRXUSDC": "TRX-USDC", + "TRXUSDK": "TRX-USDK", + "TRXUSDT": "TRX-USDT", + "TUSDBTC": "TUSD-BTC", + "TUSDUSDT": "TUSD-USDT", + "UBTCUSDT": "UBTC-USDT", + "UMAUSDT": "UMA-USDT", + "UNIBTC": "UNI-BTC", + "UNIETH": "UNI-ETH", + "UNIUSDT": "UNI-USDT", + "USDCBTC": "USDC-BTC", + "USDCUSDT": "USDC-USDT", + "USDTUSDK": "USDT-USDK", + "UTKUSDT": "UTK-USDT", + "VALUEETH": "VALUE-ETH", + "VALUEUSDT": "VALUE-USDT", + "VELOUSDT": "VELO-USDT", + "VIBBTC": "VIB-BTC", + "VIBUSDT": "VIB-USDT", + "VITEBTC": "VITE-BTC", + "VRAUSDT": "VRA-USDT", + "VSYSBTC": "VSYS-BTC", + "VSYSUSDK": "VSYS-USDK", + "VSYSUSDT": "VSYS-USDT", + "WAVESBTC": "WAVES-BTC", + "WAVESUSDT": "WAVES-USDT", + "WBTCBTC": "WBTC-BTC", + "WBTCETH": "WBTC-ETH", + "WBTCUSDT": "WBTC-USDT", + "WGRTUSDK": "WGRT-USDK", + "WGRTUSDT": "WGRT-USDT", + "WINGUSDT": "WING-USDT", + "WNXMUSDT": "WNXM-USDT", + "WTCBTC": "WTC-BTC", + "WTCUSDT": "WTC-USDT", + "WXTBTC": "WXT-BTC", + "WXTUSDK": "WXT-USDK", + "WXTUSDT": "WXT-USDT", + "XCHBTC": "XCH-BTC", + "XCHUSDT": "XCH-USDT", + "XEMBTC": "XEM-BTC", + "XEMETH": "XEM-ETH", + "XEMUSDT": "XEM-USDT", + "XLMBTC": "XLM-BTC", + "XLMETH": "XLM-ETH", + "XLMUSDT": "XLM-USDT", + "XMRBTC": "XMR-BTC", + "XMRETH": "XMR-ETH", + "XMRUSDT": "XMR-USDT", + "XPOUSDT": "XPO-USDT", + "XPRUSDT": "XPR-USDT", + "XRPBTC": "XRP-BTC", + "XRPETH": "XRP-ETH", + "XRPOKB": "XRP-OKB", + "XRPUSDC": "XRP-USDC", + "XRPUSDK": "XRP-USDK", + "XRPUSDT": "XRP-USDT", + "XSRUSDT": "XSR-USDT", + "XTZBTC": "XTZ-BTC", + "XTZUSDT": "XTZ-USDT", + "XUCUSDT": "XUC-USDT", + "YEEUSDT": "YEE-USDT", + "YFIBTC": "YFI-BTC", + "YFIETH": "YFI-ETH", + "YFIIUSDT": "YFII-USDT", + "YFIUSDT": "YFI-USDT", + "YOUBTC": "YOU-BTC", + "YOUUSDT": "YOU-USDT", + "YOYOUSDT": "YOYO-USDT", + "ZECBTC": "ZEC-BTC", + "ZECETH": "ZEC-ETH", + "ZECUSDT": "ZEC-USDT", + "ZENBTC": "ZEN-BTC", + "ZENUSDT": "ZEN-USDT", + "ZILBTC": "ZIL-BTC", + "ZILETH": "ZIL-ETH", + "ZILUSDT": "ZIL-USDT", + "ZKSUSDT": "ZKS-USDT", + "ZRXBTC": "ZRX-BTC", + "ZRXETH": "ZRX-ETH", + "ZRXUSDT": "ZRX-USDT", + "ZYROUSDT": "ZYRO-USDT", } - diff --git a/pkg/exchange/util.go b/pkg/exchange/util.go new file mode 100644 index 0000000000..6391037220 --- /dev/null +++ b/pkg/exchange/util.go @@ -0,0 +1,29 @@ +package exchange + +import "github.com/c9s/bbgo/pkg/types" + +func GetSessionAttributes(exchange types.Exchange) (isMargin, isFutures, isIsolated bool, isolatedSymbol string) { + if marginExchange, ok := exchange.(types.MarginExchange); ok { + marginSettings := marginExchange.GetMarginSettings() + isMargin = marginSettings.IsMargin + if isMargin { + isIsolated = marginSettings.IsIsolatedMargin + if marginSettings.IsIsolatedMargin { + isolatedSymbol = marginSettings.IsolatedMarginSymbol + } + } + } + + if futuresExchange, ok := exchange.(types.FuturesExchange); ok { + futuresSettings := futuresExchange.GetFuturesSettings() + isFutures = futuresSettings.IsFutures + if isFutures { + isIsolated = futuresSettings.IsIsolatedFutures + if futuresSettings.IsIsolatedFutures { + isolatedSymbol = futuresSettings.IsolatedFuturesSymbol + } + } + } + + return isMargin, isFutures, isIsolated, isolatedSymbol +} diff --git a/pkg/fixedpoint/const.go b/pkg/fixedpoint/const.go new file mode 100644 index 0000000000..86e63cd23a --- /dev/null +++ b/pkg/fixedpoint/const.go @@ -0,0 +1,7 @@ +package fixedpoint + +var ( + Two Value = NewFromInt(2) + Three Value = NewFromInt(3) + Four Value = NewFromInt(4) +) diff --git a/pkg/fixedpoint/convert.go b/pkg/fixedpoint/convert.go index 22e34a536f..1d025abf12 100644 --- a/pkg/fixedpoint/convert.go +++ b/pkg/fixedpoint/convert.go @@ -23,6 +23,8 @@ type Value int64 const Zero = Value(0) const One = Value(1e8) const NegOne = Value(-1e8) +const PosInf = Value(math.MaxInt64) +const NegInf = Value(math.MinInt64) type RoundingMode int @@ -81,6 +83,11 @@ func (v *Value) Scan(src interface{}) error { } func (v Value) Float64() float64 { + if v == PosInf { + return math.Inf(1) + } else if v == NegInf { + return math.Inf(-1) + } return float64(v) / DefaultPow } @@ -92,19 +99,34 @@ func (v Value) Abs() Value { } func (v Value) String() string { + if v == PosInf { + return "inf" + } else if v == NegInf { + return "-inf" + } return strconv.FormatFloat(float64(v)/DefaultPow, 'f', -1, 64) } func (v Value) FormatString(prec int) string { + if v == PosInf { + return "inf" + } else if v == NegInf { + return "-inf" + } pow := math.Pow10(prec) return strconv.FormatFloat( - math.Trunc(float64(v)/DefaultPow * pow) / pow, 'f', prec, 64) + math.Trunc(float64(v)/DefaultPow*pow)/pow, 'f', prec, 64) } func (v Value) Percentage() string { if v == 0 { return "0" } + if v == PosInf { + return "inf%" + } else if v == NegInf { + return "-inf%" + } return strconv.FormatFloat(float64(v)/DefaultPow*100., 'f', -1, 64) + "%" } @@ -112,9 +134,14 @@ func (v Value) FormatPercentage(prec int) string { if v == 0 { return "0" } + if v == PosInf { + return "inf%" + } else if v == NegInf { + return "-inf%" + } pow := math.Pow10(prec) result := strconv.FormatFloat( - math.Trunc(float64(v)/DefaultPow * pow * 100.) / pow, 'f', prec, 64) + math.Trunc(float64(v)/DefaultPow*pow*100.)/pow, 'f', prec, 64) return result + "%" } @@ -198,18 +225,6 @@ func (v *Value) AtomicLoad() Value { } func (v *Value) UnmarshalYAML(unmarshal func(a interface{}) error) (err error) { - var f float64 - if err = unmarshal(&f); err == nil { - *v = NewFromFloat(f) - return - } - - var i int64 - if err = unmarshal(&i); err == nil { - *v = NewFromInt(i) - return - } - var s string if err = unmarshal(&s); err == nil { nv, err2 := NewFromString(s) @@ -222,12 +237,19 @@ func (v *Value) UnmarshalYAML(unmarshal func(a interface{}) error) (err error) { return err } +func (v Value) MarshalYAML() (interface{}, error) { + return v.FormatString(DefaultPrecision), nil +} + func (v Value) MarshalJSON() ([]byte, error) { + if v.IsInf() { + return []byte("\"" + v.String() + "\""), nil + } return []byte(v.FormatString(DefaultPrecision)), nil } func (v *Value) UnmarshalJSON(data []byte) error { - if bytes.Compare(data, []byte{'n', 'u', 'l', 'l'}) == 0 { + if bytes.Equal(data, []byte{'n', 'u', 'l', 'l'}) { *v = Zero return nil } @@ -325,8 +347,9 @@ func NewFromString(input string) (Value, error) { decimalCount := 0 // if is decimal, we don't need this hasScientificNotion := false + hasIChar := false scIndex := -1 - for i, c := range(input) { + for i, c := range input { if hasDecimal { if c <= '9' && c >= '0' { decimalCount++ @@ -343,13 +366,17 @@ func NewFromString(input string) (Value, error) { scIndex = i break } + if c == 'i' || c == 'I' { + hasIChar = true + break + } } if hasDecimal { - after := input[dotIndex+1:len(input)] + after := input[dotIndex+1:] if decimalCount >= 8 { - after = after[0:8] + "." + after[8:len(after)] + after = after[0:8] + "." + after[8:] } else { - after = after[0:decimalCount] + strings.Repeat("0", 8-decimalCount) + after[decimalCount:len(after)] + after = after[0:decimalCount] + strings.Repeat("0", 8-decimalCount) + after[decimalCount:] } input = input[0:dotIndex] + after v, err := strconv.ParseFloat(input, 64) @@ -364,15 +391,25 @@ func NewFromString(input string) (Value, error) { return Value(int64(math.Trunc(v))), nil } else if hasScientificNotion { - exp, err := strconv.ParseInt(input[scIndex+1:len(input)], 10, 32) + exp, err := strconv.ParseInt(input[scIndex+1:], 10, 32) if err != nil { return 0, err } - v, err := strconv.ParseFloat(input[0:scIndex+1] + strconv.FormatInt(exp + 8, 10), 64) + v, err := strconv.ParseFloat(input[0:scIndex+1]+strconv.FormatInt(exp+8, 10), 64) if err != nil { return 0, err } return Value(int64(math.Trunc(v))), nil + } else if hasIChar { + if floatV, err := strconv.ParseFloat(input, 64); nil != err { + return 0, err + } else if math.IsInf(floatV, 1) { + return PosInf, nil + } else if math.IsInf(floatV, -1) { + return NegInf, nil + } else { + return 0, fmt.Errorf("fixedpoint.Value parse error, invalid input string %s", input) + } } else { v, err := strconv.ParseInt(input, 10, 64) if err != nil { @@ -385,7 +422,6 @@ func NewFromString(input string) (Value, error) { } return Value(v), nil } - } func MustNewFromString(input string) Value { @@ -416,6 +452,11 @@ func Must(v Value, err error) Value { } func NewFromFloat(val float64) Value { + if math.IsInf(val, 1) { + return PosInf + } else if math.IsInf(val, -1) { + return NegInf + } return Value(int64(math.Trunc(val * DefaultPow))) } @@ -423,6 +464,10 @@ func NewFromInt(val int64) Value { return Value(val * DefaultPow) } +func (a Value) IsInf() bool { + return a == PosInf || a == NegInf +} + func (a Value) MulExp(exp int) Value { return Value(int64(float64(a) * math.Pow(10, float64(exp)))) } diff --git a/pkg/fixedpoint/count.go b/pkg/fixedpoint/count.go new file mode 100644 index 0000000000..84a3d1c8c2 --- /dev/null +++ b/pkg/fixedpoint/count.go @@ -0,0 +1,13 @@ +package fixedpoint + +type Counter func(a Value) bool + +func Count(values []Value, counter Counter) int { + var c = 0 + for _, value := range values { + if counter(value) { + c++ + } + } + return c +} diff --git a/pkg/fixedpoint/dec.go b/pkg/fixedpoint/dec.go index f0cab3bfe5..2b5f3743b4 100644 --- a/pkg/fixedpoint/dec.go +++ b/pkg/fixedpoint/dec.go @@ -282,7 +282,7 @@ func (dn Value) FormatString(prec int) string { // decimal within dec := nd + e decimals := digits[dec:min(dec+prec, nd)] - return sign + digits[:dec] + "." + decimals + strings.Repeat("0", max(0, prec - len(decimals))) + return sign + digits[:dec] + "." + decimals + strings.Repeat("0", max(0, prec-len(decimals))) } else if 0 < dn.exp && dn.exp <= digitsMax { // decimal to the right if prec > 0 { @@ -350,14 +350,14 @@ func (dn Value) Percentage() string { nd := len(digits) e := int(dn.exp) - nd + 2 - if -maxLeadingZeros <= dn.exp && dn.exp <= 0 { + if -maxLeadingZeros <= dn.exp && dn.exp <= -2 { // decimal to the left return sign + "0." + strings.Repeat("0", -e-nd) + digits + "%" } else if -nd < e && e <= -1 { // decimal within dec := nd + e - return sign + digits[:dec] + "." + digits[dec:] - } else if 0 < dn.exp && dn.exp <= digitsMax { + return sign + digits[:dec] + "." + digits[dec:] + "%" + } else if -2 < dn.exp && dn.exp <= digitsMax { // decimal to the right return sign + digits + strings.Repeat("0", e) + "%" } else { @@ -403,7 +403,7 @@ func (dn Value) FormatPercentage(prec int) string { // decimal within dec := nd + e decimals := digits[dec:min(dec+prec, nd)] - return sign + digits[:dec] + "." + decimals + strings.Repeat("0", max(0, prec - len(decimals))) + "%" + return sign + digits[:dec] + "." + decimals + strings.Repeat("0", max(0, prec-len(decimals))) + "%" } else if 0 < exp && exp <= digitsMax { // decimal to the right if prec > 0 { @@ -500,7 +500,7 @@ func NewFromString(s string) (Value, error) { } r := &reader{s, 0} sign := r.getSign() - if r.matchStr("inf") { + if r.matchStrIgnoreCase("inf") { return Inf(sign), nil } coef, exp := r.getCoef() @@ -550,7 +550,7 @@ func NewFromBytes(s []byte) (Value, error) { } r := &readerBytes{s, 0} sign := r.getSign() - if r.matchStr("inf") { + if r.matchStrIgnoreCase("inf") { return Inf(sign), nil } coef, exp := r.getCoef() @@ -631,13 +631,18 @@ func (r *readerBytes) matchDigit() bool { return false } -func (r *readerBytes) matchStr(pre string) bool { - for i, c := range r.s[r.i:] { +func (r *readerBytes) matchStrIgnoreCase(pre string) bool { + pre = strings.ToLower(pre) + boundary := r.i + len(pre) + if boundary > len(r.s) { + return false + } + for i, c := range bytes.ToLower(r.s[r.i:boundary]) { if pre[i] != c { return false } } - r.i += len(pre) + r.i = boundary return true } @@ -745,9 +750,15 @@ func (r *reader) matchDigit() bool { return false } -func (r *reader) matchStr(pre string) bool { - if strings.HasPrefix(r.s[r.i:], pre) { - r.i += len(pre) +func (r *reader) matchStrIgnoreCase(pre string) bool { + boundary := r.i + len(pre) + if boundary > len(r.s) { + return false + } + data := strings.ToLower(r.s[r.i:boundary]) + pre = strings.ToLower(pre) + if data == pre { + r.i = boundary return true } return false @@ -1032,6 +1043,10 @@ func (x Value) Compare(y Value) int { return Compare(x, y) } +func (v Value) MarshalYAML() (interface{}, error) { + return v.FormatString(8), nil +} + func (v *Value) UnmarshalYAML(unmarshal func(a interface{}) error) (err error) { var f float64 if err = unmarshal(&f); err == nil { @@ -1057,6 +1072,9 @@ func (v *Value) UnmarshalYAML(unmarshal func(a interface{}) error) (err error) { // FIXME: should we limit to 8 prec? func (v Value) MarshalJSON() ([]byte, error) { + if v.IsInf() { + return []byte("\"" + v.String() + "\""), nil + } return []byte(v.FormatString(8)), nil } diff --git a/pkg/fixedpoint/dec_legacy_test.go b/pkg/fixedpoint/dec_legacy_test.go index 412c2e875a..848b77ec68 100644 --- a/pkg/fixedpoint/dec_legacy_test.go +++ b/pkg/fixedpoint/dec_legacy_test.go @@ -19,7 +19,7 @@ func TestNumFractionalDigitsLegacy(t *testing.T) { }, { name: "zero underflow", - v: MustNewFromString("1e-100"), + v: MustNewFromString("1e-100"), want: 0, }, } diff --git a/pkg/fixedpoint/dec_test.go b/pkg/fixedpoint/dec_test.go index 517ea3562e..8c72582f61 100644 --- a/pkg/fixedpoint/dec_test.go +++ b/pkg/fixedpoint/dec_test.go @@ -1,10 +1,11 @@ package fixedpoint import ( + "encoding/json" + "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v3" "math/big" "testing" - "github.com/stretchr/testify/assert" - "encoding/json" ) const Delta = 1e-9 @@ -16,7 +17,7 @@ func BenchmarkMul(b *testing.B) { for i := 0; i < b.N; i++ { x := NewFromFloat(20.0) y := NewFromFloat(20.0) - x = x.Mul(y) + x = x.Mul(y) // nolint } }) @@ -24,7 +25,7 @@ func BenchmarkMul(b *testing.B) { for i := 0; i < b.N; i++ { x := NewFromFloat(88.12345678) y := NewFromFloat(88.12345678) - x = x.Mul(y) + x = x.Mul(y) // nolint } }) @@ -32,7 +33,7 @@ func BenchmarkMul(b *testing.B) { for i := 0; i < b.N; i++ { x := big.NewFloat(20.0) y := big.NewFloat(20.0) - x = new(big.Float).Mul(x, y) + x = new(big.Float).Mul(x, y) // nolint } }) @@ -40,7 +41,7 @@ func BenchmarkMul(b *testing.B) { for i := 0; i < b.N; i++ { x := big.NewFloat(88.12345678) y := big.NewFloat(88.12345678) - x = new(big.Float).Mul(x, y) + x = new(big.Float).Mul(x, y) // nolint } }) } @@ -69,6 +70,15 @@ func TestNew(t *testing.T) { assert.Equal(t, "0.0010", f.FormatString(4)) assert.Equal(t, "0.1%", f.Percentage()) assert.Equal(t, "0.10%", f.FormatPercentage(2)) + f = NewFromFloat(0.1) + assert.Equal(t, "10%", f.Percentage()) + assert.Equal(t, "10%", f.FormatPercentage(0)) + f = NewFromFloat(0.01) + assert.Equal(t, "1%", f.Percentage()) + assert.Equal(t, "1%", f.FormatPercentage(0)) + f = NewFromFloat(0.111) + assert.Equal(t, "11.1%", f.Percentage()) + assert.Equal(t, "11.1%", f.FormatPercentage(1)) } func TestFormatString(t *testing.T) { @@ -128,6 +138,15 @@ func TestFromString(t *testing.T) { assert.Equal(t, Zero, f) f = MustNewFromString("") assert.Equal(t, Zero, f) + + for _, s := range []string{"inf", "Inf", "INF", "iNF"} { + f = MustNewFromString(s) + assert.Equal(t, PosInf, f) + f = MustNewFromString("+" + s) + assert.Equal(t, PosInf, f) + f = MustNewFromString("-" + s) + assert.Equal(t, NegInf, f) + } } func TestJson(t *testing.T) { @@ -158,16 +177,62 @@ func TestJson(t *testing.T) { assert.Equal(t, "0.00000000", p.FormatString(8)) assert.Equal(t, "0.00000000", string(e)) - _ = json.Unmarshal([]byte("0.00153917575"), &p) assert.Equal(t, "0.00153917", p.FormatString(8)) - var q Value - q = NewFromFloat(0.00153917575) + q := NewFromFloat(0.00153917575) assert.Equal(t, p, q) _ = json.Unmarshal([]byte("6e-8"), &p) _ = json.Unmarshal([]byte("0.000062"), &q) assert.Equal(t, "0.00006194", q.Sub(p).String()) + + assert.NoError(t, json.Unmarshal([]byte(`"inf"`), &p)) + assert.NoError(t, json.Unmarshal([]byte(`"+Inf"`), &q)) + assert.Equal(t, PosInf, p) + assert.Equal(t, p, q) +} + +func TestYaml(t *testing.T) { + p := MustNewFromString("0") + e, err := yaml.Marshal(p) + assert.NoError(t, err) + assert.Equal(t, "\"0.00000000\"\n", string(e)) + p = MustNewFromString("1.00000003") + e, err = yaml.Marshal(p) + assert.NoError(t, err) + assert.Equal(t, "\"1.00000003\"\n", string(e)) + p = MustNewFromString("1.000000003") + e, err = yaml.Marshal(p) + assert.NoError(t, err) + assert.Equal(t, "\"1.00000000\"\n", string(e)) + p = MustNewFromString("1.000000008") + e, err = yaml.Marshal(p) + assert.NoError(t, err) + assert.Equal(t, "\"1.00000000\"\n", string(e)) + p = MustNewFromString("0.999999999") + e, err = yaml.Marshal(p) + assert.NoError(t, err) + assert.Equal(t, "\"0.99999999\"\n", string(e)) + + p = MustNewFromString("1.2e-9") + e, err = yaml.Marshal(p) + assert.NoError(t, err) + assert.Equal(t, "0.00000000", p.FormatString(8)) + assert.Equal(t, "\"0.00000000\"\n", string(e)) + + _ = yaml.Unmarshal([]byte("0.00153917575"), &p) + assert.Equal(t, "0.00153917", p.FormatString(8)) + + q := NewFromFloat(0.00153917575) + assert.Equal(t, p, q) + _ = yaml.Unmarshal([]byte("6e-8"), &p) + _ = yaml.Unmarshal([]byte("0.000062"), &q) + assert.Equal(t, "0.00006194", q.Sub(p).String()) + + assert.NoError(t, json.Unmarshal([]byte(`"inf"`), &p)) + assert.NoError(t, json.Unmarshal([]byte(`"+Inf"`), &q)) + assert.Equal(t, PosInf, p) + assert.Equal(t, p, q) } func TestNumFractionalDigits(t *testing.T) { diff --git a/pkg/fixedpoint/filter.go b/pkg/fixedpoint/filter.go new file mode 100644 index 0000000000..bbcfc8c8a5 --- /dev/null +++ b/pkg/fixedpoint/filter.go @@ -0,0 +1,20 @@ +package fixedpoint + +type Tester func(value Value) bool + +func PositiveTester(value Value) bool { + return value.Sign() > 0 +} + +func NegativeTester(value Value) bool { + return value.Sign() < 0 +} + +func Filter(values []Value, f Tester) (slice []Value) { + for _, v := range values { + if f(v) { + slice = append(slice, v) + } + } + return slice +} diff --git a/pkg/fixedpoint/helpers.go b/pkg/fixedpoint/helpers.go new file mode 100644 index 0000000000..cb585e5c0f --- /dev/null +++ b/pkg/fixedpoint/helpers.go @@ -0,0 +1,15 @@ +package fixedpoint + +func Sum(values []Value) (s Value) { + s = Zero + for _, value := range values { + s = s.Add(value) + } + return s +} + +func Avg(values []Value) (avg Value) { + s := Sum(values) + avg = s.Div(NewFromInt(int64(len(values)))) + return avg +} diff --git a/pkg/fixedpoint/reduce.go b/pkg/fixedpoint/reduce.go new file mode 100644 index 0000000000..0b8edbf7f6 --- /dev/null +++ b/pkg/fixedpoint/reduce.go @@ -0,0 +1,25 @@ +package fixedpoint + +type Reducer func(prev, curr Value) Value + +func SumReducer(prev, curr Value) Value { + return prev.Add(curr) +} + +func Reduce(values []Value, reducer Reducer, a ...Value) Value { + init := Zero + if len(a) > 0 { + init = a[0] + } + + if len(values) == 0 { + return init + } + + r := reducer(init, values[0]) + for i := 1; i < len(values); i++ { + r = reducer(r, values[i]) + } + + return r +} diff --git a/pkg/fixedpoint/reduce_test.go b/pkg/fixedpoint/reduce_test.go new file mode 100644 index 0000000000..58000623be --- /dev/null +++ b/pkg/fixedpoint/reduce_test.go @@ -0,0 +1,37 @@ +package fixedpoint + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestReduce(t *testing.T) { + type args struct { + values []Value + init Value + reducer Reducer + } + tests := []struct { + name string + args args + want Value + }{ + { + name: "simple", + args: args{ + values: []Value{NewFromFloat(1), NewFromFloat(2), NewFromFloat(3)}, + init: NewFromFloat(0.0), + reducer: func(prev, curr Value) Value { + return prev.Add(curr) + }, + }, + want: NewFromFloat(6), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, Reduce(tt.args.values, tt.args.reducer, tt.args.init), "Reduce(%v, %v, %v)", tt.args.values, tt.args.init, tt.args.reducer) + }) + } +} diff --git a/pkg/fixedpoint/slice.go b/pkg/fixedpoint/slice.go new file mode 100644 index 0000000000..c5a7db173f --- /dev/null +++ b/pkg/fixedpoint/slice.go @@ -0,0 +1,24 @@ +package fixedpoint + +type Slice []Value + +func (s Slice) Reduce(reducer Reducer, a ...Value) Value { + return Reduce(s, reducer, a...) +} + +// Defaults to ascending sort +func (s Slice) Len() int { return len(s) } +func (s Slice) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s Slice) Less(i, j int) bool { return s[i].Compare(s[j]) < 0 } + +type Ascending []Value + +func (s Ascending) Len() int { return len(s) } +func (s Ascending) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s Ascending) Less(i, j int) bool { return s[i].Compare(s[j]) < 0 } + +type Descending []Value + +func (s Descending) Len() int { return len(s) } +func (s Descending) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s Descending) Less(i, j int) bool { return s[i].Compare(s[j]) > 0 } diff --git a/pkg/fixedpoint/slice_test.go b/pkg/fixedpoint/slice_test.go new file mode 100644 index 0000000000..082db90797 --- /dev/null +++ b/pkg/fixedpoint/slice_test.go @@ -0,0 +1,35 @@ +package fixedpoint + +import ( + "sort" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSortInterface(t *testing.T) { + slice := Slice{ + NewFromInt(7), + NewFromInt(3), + NewFromInt(1), + NewFromInt(2), + NewFromInt(5), + } + sort.Sort(slice) + assert.Equal(t, "1", slice[0].String()) + assert.Equal(t, "2", slice[1].String()) + assert.Equal(t, "3", slice[2].String()) + assert.Equal(t, "5", slice[3].String()) + + sort.Sort(Descending(slice)) + assert.Equal(t, "7", slice[0].String()) + assert.Equal(t, "5", slice[1].String()) + assert.Equal(t, "3", slice[2].String()) + assert.Equal(t, "2", slice[3].String()) + + sort.Sort(Ascending(slice)) + assert.Equal(t, "1", slice[0].String()) + assert.Equal(t, "2", slice[1].String()) + assert.Equal(t, "3", slice[2].String()) + assert.Equal(t, "5", slice[3].String()) +} diff --git a/pkg/grpc/convert.go b/pkg/grpc/convert.go index cce9e77ccd..928cbd4499 100644 --- a/pkg/grpc/convert.go +++ b/pkg/grpc/convert.go @@ -34,7 +34,7 @@ func toSubscriptions(sub *pb.Subscription) (types.Subscription, error) { Symbol: sub.Symbol, Channel: types.KLineChannel, Options: types.SubscribeOptions{ - Interval: sub.Interval, + Interval: types.Interval(sub.Interval), }, }, nil } @@ -97,14 +97,14 @@ func toSide(side pb.Side) types.SideType { func toSubmitOrders(pbOrders []*pb.SubmitOrder) (submitOrders []types.SubmitOrder) { for _, pbOrder := range pbOrders { submitOrders = append(submitOrders, types.SubmitOrder{ - ClientOrderID: pbOrder.ClientOrderId, - Symbol: pbOrder.Symbol, - Side: toSide(pbOrder.Side), - Type: toOrderType(pbOrder.OrderType), - Price: fixedpoint.MustNewFromString(pbOrder.Price), - Quantity: fixedpoint.MustNewFromString(pbOrder.Quantity), - StopPrice: fixedpoint.MustNewFromString(pbOrder.StopPrice), - TimeInForce: "", + ClientOrderID: pbOrder.ClientOrderId, + Symbol: pbOrder.Symbol, + Side: toSide(pbOrder.Side), + Type: toOrderType(pbOrder.OrderType), + Price: fixedpoint.MustNewFromString(pbOrder.Price), + Quantity: fixedpoint.MustNewFromString(pbOrder.Quantity), + StopPrice: fixedpoint.MustNewFromString(pbOrder.StopPrice), + TimeInForce: "", }) } diff --git a/pkg/grpc/server.go b/pkg/grpc/server.go index ad6df87993..c7e31bbfcf 100644 --- a/pkg/grpc/server.go +++ b/pkg/grpc/server.go @@ -9,6 +9,7 @@ import ( "github.com/pkg/errors" log "github.com/sirupsen/logrus" + "go.uber.org/multierr" "google.golang.org/grpc" "google.golang.org/grpc/reflection" @@ -46,20 +47,27 @@ func (s *TradingService) SubmitOrder(ctx context.Context, request *pb.SubmitOrde } } - createdOrders, err := session.Exchange.SubmitOrders(ctx, submitOrders...) - if err != nil { - return nil, err + createdOrders, errIdx, err := bbgo.BatchPlaceOrder(ctx, session.Exchange, submitOrders...) + if len(errIdx) > 0 { + createdOrders2, err2 := bbgo.BatchRetryPlaceOrder(ctx, session.Exchange, errIdx, submitOrders...) + if err2 != nil { + err = multierr.Append(err, err2) + } else { + createdOrders = append(createdOrders, createdOrders2...) + } } + // convert response resp := &pb.SubmitOrderResponse{ Session: sessionName, Orders: nil, } + for _, createdOrder := range createdOrders { resp.Orders = append(resp.Orders, transOrder(session, createdOrder)) } - return resp, nil + return resp, err } func (s *TradingService) CancelOrder(ctx context.Context, request *pb.CancelOrderRequest) (*pb.CancelOrderResponse, error) { diff --git a/pkg/indicator/ad.go b/pkg/indicator/ad.go index d7263a5ab7..e94f8d2463 100644 --- a/pkg/indicator/ad.go +++ b/pkg/indicator/ad.go @@ -3,6 +3,7 @@ package indicator import ( "time" + "github.com/c9s/bbgo/pkg/datatype/floats" "github.com/c9s/bbgo/pkg/types" ) @@ -14,8 +15,9 @@ Accumulation/Distribution Indicator (A/D) */ //go:generate callbackgen -type AD type AD struct { + types.SeriesBase types.IntervalWindow - Values types.Float64Slice + Values floats.Slice PrePrice float64 EndTime time.Time @@ -23,6 +25,9 @@ type AD struct { } func (inc *AD) Update(high, low, cloze, volume float64) { + if len(inc.Values) == 0 { + inc.SeriesBase.Series = inc + } var moneyFlowVolume float64 if high == low { moneyFlowVolume = 0 @@ -53,9 +58,9 @@ func (inc *AD) Length() int { return len(inc.Values) } -var _ types.Series = &AD{} +var _ types.SeriesExtend = &AD{} -func (inc *AD) calculateAndUpdate(kLines []types.KLine) { +func (inc *AD) CalculateAndUpdate(kLines []types.KLine) { for _, k := range kLines { if inc.EndTime != zeroTime && !k.EndTime.After(inc.EndTime) { continue @@ -66,12 +71,13 @@ func (inc *AD) calculateAndUpdate(kLines []types.KLine) { inc.EmitUpdate(inc.Last()) inc.EndTime = kLines[len(kLines)-1].EndTime.Time() } + func (inc *AD) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { if inc.Interval != interval { return } - inc.calculateAndUpdate(window) + inc.CalculateAndUpdate(window) } func (inc *AD) Bind(updater KLineWindowUpdater) { diff --git a/pkg/indicator/alma.go b/pkg/indicator/alma.go new file mode 100644 index 0000000000..2558344a34 --- /dev/null +++ b/pkg/indicator/alma.go @@ -0,0 +1,100 @@ +package indicator + +import ( + "math" + + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/types" +) + +// Refer: Arnaud Legoux Moving Average +// Refer: https://capital.com/arnaud-legoux-moving-average +// Also check https://github.com/DaveSkender/Stock.Indicators/blob/main/src/a-d/Alma/Alma.cs +// @param offset: Gaussian applied to the combo line. 1->ema, 0->sma +// @param sigma: the standard deviation applied to the combo line. This makes the combo line sharper +//go:generate callbackgen -type ALMA +type ALMA struct { + types.SeriesBase + types.IntervalWindow // required + Offset float64 // required: recommend to be 0.5 + Sigma int // required: recommend to be 5 + weight []float64 + sum float64 + input []float64 + Values floats.Slice + UpdateCallbacks []func(value float64) +} + +const MaxNumOfALMA = 5_000 +const MaxNumOfALMATruncateSize = 100 + +func (inc *ALMA) Update(value float64) { + if inc.weight == nil { + inc.SeriesBase.Series = inc + inc.weight = make([]float64, inc.Window) + m := inc.Offset * (float64(inc.Window) - 1.) + s := float64(inc.Window) / float64(inc.Sigma) + inc.sum = 0. + for i := 0; i < inc.Window; i++ { + diff := float64(i) - m + wt := math.Exp(-diff * diff / 2. / s / s) + inc.sum += wt + inc.weight[i] = wt + } + } + inc.input = append(inc.input, value) + if len(inc.input) >= inc.Window { + weightedSum := 0.0 + inc.input = inc.input[len(inc.input)-inc.Window:] + for i := 0; i < inc.Window; i++ { + weightedSum += inc.weight[inc.Window-i-1] * inc.input[i] + } + inc.Values.Push(weightedSum / inc.sum) + if len(inc.Values) > MaxNumOfALMA { + inc.Values = inc.Values[MaxNumOfALMATruncateSize-1:] + } + } +} + +func (inc *ALMA) Last() float64 { + if len(inc.Values) == 0 { + return 0 + } + return inc.Values[len(inc.Values)-1] +} + +func (inc *ALMA) Index(i int) float64 { + if i >= len(inc.Values) { + return 0 + } + return inc.Values[len(inc.Values)-i-1] +} + +func (inc *ALMA) Length() int { + return len(inc.Values) +} + +var _ types.SeriesExtend = &ALMA{} + +func (inc *ALMA) CalculateAndUpdate(allKLines []types.KLine) { + if inc.input == nil { + for _, k := range allKLines { + inc.Update(k.Close.Float64()) + inc.EmitUpdate(inc.Last()) + } + return + } + inc.Update(allKLines[len(allKLines)-1].Close.Float64()) + inc.EmitUpdate(inc.Last()) +} + +func (inc *ALMA) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { + if inc.Interval != interval { + return + } + inc.CalculateAndUpdate(window) +} + +func (inc *ALMA) Bind(updater KLineWindowUpdater) { + updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) +} diff --git a/pkg/indicator/alma_callbacks.go b/pkg/indicator/alma_callbacks.go new file mode 100644 index 0000000000..52d2b2f73b --- /dev/null +++ b/pkg/indicator/alma_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type ALMA"; DO NOT EDIT. + +package indicator + +import () + +func (inc *ALMA) OnUpdate(cb func(value float64)) { + inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) +} + +func (inc *ALMA) EmitUpdate(value float64) { + for _, cb := range inc.UpdateCallbacks { + cb(value) + } +} diff --git a/pkg/indicator/alma_test.go b/pkg/indicator/alma_test.go new file mode 100644 index 0000000000..a00d7113d8 --- /dev/null +++ b/pkg/indicator/alma_test.go @@ -0,0 +1,61 @@ +package indicator + +import ( + "encoding/json" + "testing" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" + "github.com/stretchr/testify/assert" +) + +/* +python: + +import pandas as pd +import pandas_ta as ta + +data = pd.Series([0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9]) +sigma = 6 +offset = 0.9 +size = 5 + +result = ta.alma(data, size, sigma, offset) +print(result) +*/ +func Test_ALMA(t *testing.T) { + var Delta = 0.01 + var randomPrices = []byte(`[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]`) + var input []fixedpoint.Value + if err := json.Unmarshal(randomPrices, &input); err != nil { + panic(err) + } + tests := []struct { + name string + kLines []types.KLine + want float64 + next float64 + all int + }{ + { + name: "random_case", + kLines: buildKLines(input), + want: 5.60785, + next: 4.60785, + all: 26, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + alma := ALMA{ + IntervalWindow: types.IntervalWindow{Window: 5}, + Offset: 0.9, + Sigma: 6, + } + alma.CalculateAndUpdate(tt.kLines) + assert.InDelta(t, tt.want, alma.Last(), Delta) + assert.InDelta(t, tt.next, alma.Index(1), Delta) + assert.Equal(t, tt.all, alma.Length()) + }) + } +} diff --git a/pkg/indicator/atr.go b/pkg/indicator/atr.go index 9f8bcf58f0..3a9b853656 100644 --- a/pkg/indicator/atr.go +++ b/pkg/indicator/atr.go @@ -4,13 +4,15 @@ import ( "math" "time" + "github.com/c9s/bbgo/pkg/datatype/floats" "github.com/c9s/bbgo/pkg/types" ) //go:generate callbackgen -type ATR type ATR struct { + types.SeriesBase types.IntervalWindow - PercentageVolatility types.Float64Slice + PercentageVolatility floats.Slice PreviousClose float64 RMA *RMA @@ -19,13 +21,37 @@ type ATR struct { UpdateCallbacks []func(value float64) } +var _ types.SeriesExtend = &ATR{} + +func (inc *ATR) Clone() *ATR { + out := &ATR{ + IntervalWindow: inc.IntervalWindow, + PercentageVolatility: inc.PercentageVolatility[:], + PreviousClose: inc.PreviousClose, + RMA: inc.RMA.Clone().(*RMA), + EndTime: inc.EndTime, + } + out.SeriesBase.Series = out + return out +} + +func (inc *ATR) TestUpdate(high, low, cloze float64) *ATR { + c := inc.Clone() + c.Update(high, low, cloze) + return c +} + func (inc *ATR) Update(high, low, cloze float64) { if inc.Window <= 0 { panic("window must be greater than 0") } if inc.RMA == nil { - inc.RMA = &RMA{IntervalWindow: types.IntervalWindow{Window: inc.Window}} + inc.SeriesBase.Series = inc + inc.RMA = &RMA{ + IntervalWindow: types.IntervalWindow{Window: inc.Window}, + Adjust: true, + } inc.PreviousClose = cloze return } @@ -67,31 +93,16 @@ func (inc *ATR) Length() int { if inc.RMA == nil { return 0 } - return inc.RMA.Length() -} - -var _ types.Series = &ATR{} - -func (inc *ATR) calculateAndUpdate(kLines []types.KLine) { - for _, k := range kLines { - if inc.EndTime != zeroTime && !k.EndTime.After(inc.EndTime) { - continue - } - inc.Update(k.High.Float64(), k.Low.Float64(), k.Close.Float64()) - } - inc.EmitUpdate(inc.Last()) - inc.EndTime = kLines[len(kLines)-1].EndTime.Time() + return inc.RMA.Length() } -func (inc *ATR) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { - if inc.Interval != interval { +func (inc *ATR) PushK(k types.KLine) { + if inc.EndTime != zeroTime && !k.EndTime.After(inc.EndTime) { return } - inc.calculateAndUpdate(window) -} - -func (inc *ATR) Bind(updater KLineWindowUpdater) { - updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) + inc.Update(k.High.Float64(), k.Low.Float64(), k.Close.Float64()) + inc.EndTime = k.EndTime.Time() + inc.EmitUpdate(inc.Last()) } diff --git a/pkg/indicator/atr_callbacks.go b/pkg/indicator/atr_callbacks.go index 67952ad71c..addc133bbb 100644 --- a/pkg/indicator/atr_callbacks.go +++ b/pkg/indicator/atr_callbacks.go @@ -4,12 +4,12 @@ package indicator import () -func (A *ATR) OnUpdate(cb func(value float64)) { - A.UpdateCallbacks = append(A.UpdateCallbacks, cb) +func (inc *ATR) OnUpdate(cb func(value float64)) { + inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) } -func (A *ATR) EmitUpdate(value float64) { - for _, cb := range A.UpdateCallbacks { +func (inc *ATR) EmitUpdate(value float64) { + for _, cb := range inc.UpdateCallbacks { cb(value) } } diff --git a/pkg/indicator/atr_test.go b/pkg/indicator/atr_test.go index 63f1899926..b5cb138e98 100644 --- a/pkg/indicator/atr_test.go +++ b/pkg/indicator/atr_test.go @@ -9,6 +9,25 @@ import ( "github.com/c9s/bbgo/pkg/types" ) +/* +python + +import pandas as pd +import pandas_ta as ta + +data = { + "high": [40145.0, 40186.36, 40196.39, 40344.6, 40245.48, 40273.24, 40464.0, 40699.0, 40627.48, 40436.31, 40370.0, 40376.8, 40227.03, 40056.52, 39721.7, 39597.94, 39750.15, 39927.0, 40289.02, 40189.0], + "low": [39870.71, 39834.98, 39866.31, 40108.31, 40016.09, 40094.66, 40105.0, 40196.48, 40154.99, 39800.0, 39959.21, 39922.98, 39940.02, 39632.0, 39261.39, 39254.63, 39473.91, 39555.51, 39819.0, 40006.84], + "close": [40105.78, 39935.23, 40183.97, 40182.03, 40212.26, 40149.99, 40378.0, 40618.37, 40401.03, 39990.39, 40179.13, 40097.23, 40014.72, 39667.85, 39303.1, 39519.99, +39693.79, 39827.96, 40074.94, 40059.84] +} + +high = pd.Series(data['high']) +low = pd.Series(data['low']) +close = pd.Series(data['close']) +result = ta.atr(high, low, close, length=14) +print(result) +*/ func Test_calculateATR(t *testing.T) { var bytes = []byte(`{ "high": [40145.0, 40186.36, 40196.39, 40344.6, 40245.48, 40273.24, 40464.0, 40699.0, 40627.48, 40436.31, 40370.0, 40376.8, 40227.03, 40056.52, 39721.7, 39597.94, 39750.15, 39927.0, 40289.02, 40189.0], @@ -35,14 +54,17 @@ func Test_calculateATR(t *testing.T) { name: "test_binance_btcusdt_1h", kLines: buildKLines(bytes), window: 14, - want: 364.048648, + want: 367.913903, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { atr := &ATR{IntervalWindow: types.IntervalWindow{Window: tt.window}} - atr.calculateAndUpdate(tt.kLines) + for _, k := range tt.kLines { + atr.PushK(k) + } + got := atr.Last() diff := math.Trunc((got-tt.want)*100) / 100 if diff != 0 { diff --git a/pkg/indicator/atrp.go b/pkg/indicator/atrp.go new file mode 100644 index 0000000000..03c4f2071e --- /dev/null +++ b/pkg/indicator/atrp.go @@ -0,0 +1,118 @@ +package indicator + +import ( + "math" + "time" + + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/types" +) + +// ATRP is the average true range percentage +// See also https://www.fidelity.com/learning-center/trading-investing/technical-analysis/technical-indicator-guide/atrp +// +// Calculation: +// +// ATRP = (Average True Range / Close) * 100 +// +//go:generate callbackgen -type ATRP +type ATRP struct { + types.SeriesBase + types.IntervalWindow + PercentageVolatility floats.Slice + + PreviousClose float64 + RMA *RMA + + EndTime time.Time + UpdateCallbacks []func(value float64) +} + +func (inc *ATRP) Update(high, low, cloze float64) { + if inc.Window <= 0 { + panic("window must be greater than 0") + } + + if inc.RMA == nil { + inc.SeriesBase.Series = inc + inc.RMA = &RMA{ + IntervalWindow: types.IntervalWindow{Window: inc.Window}, + Adjust: true, + } + inc.PreviousClose = cloze + return + } + + // calculate true range + trueRange := high - low + hc := math.Abs(high - inc.PreviousClose) + lc := math.Abs(low - inc.PreviousClose) + if trueRange < hc { + trueRange = hc + } + if trueRange < lc { + trueRange = lc + } + + // Note: this is the difference from ATR + trueRange = trueRange / inc.PreviousClose * 100.0 + + inc.PreviousClose = cloze + + // apply rolling moving average + inc.RMA.Update(trueRange) + atr := inc.RMA.Last() + inc.PercentageVolatility.Push(atr / cloze) +} + +func (inc *ATRP) Last() float64 { + if inc.RMA == nil { + return 0 + } + return inc.RMA.Last() +} + +func (inc *ATRP) Index(i int) float64 { + if inc.RMA == nil { + return 0 + } + return inc.RMA.Index(i) +} + +func (inc *ATRP) Length() int { + if inc.RMA == nil { + return 0 + } + return inc.RMA.Length() +} + +var _ types.SeriesExtend = &ATRP{} + +func (inc *ATRP) PushK(k types.KLine) { + inc.Update(k.High.Float64(), k.Low.Float64(), k.Close.Float64()) +} + +func (inc *ATRP) CalculateAndUpdate(kLines []types.KLine) { + for _, k := range kLines { + if inc.EndTime != zeroTime && !k.EndTime.After(inc.EndTime) { + continue + } + + inc.PushK(k) + } + + inc.EmitUpdate(inc.Last()) + inc.EndTime = kLines[len(kLines)-1].EndTime.Time() +} + +func (inc *ATRP) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { + if inc.Interval != interval { + return + } + + inc.CalculateAndUpdate(window) +} + +func (inc *ATRP) Bind(updater KLineWindowUpdater) { + updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) +} diff --git a/pkg/indicator/atrp_callbacks.go b/pkg/indicator/atrp_callbacks.go new file mode 100644 index 0000000000..daaba836de --- /dev/null +++ b/pkg/indicator/atrp_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type ATRP"; DO NOT EDIT. + +package indicator + +import () + +func (inc *ATRP) OnUpdate(cb func(value float64)) { + inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) +} + +func (inc *ATRP) EmitUpdate(value float64) { + for _, cb := range inc.UpdateCallbacks { + cb(value) + } +} diff --git a/pkg/indicator/boll.go b/pkg/indicator/boll.go index 894e1ac901..712481ffcb 100644 --- a/pkg/indicator/boll.go +++ b/pkg/indicator/boll.go @@ -3,9 +3,7 @@ package indicator import ( "time" - log "github.com/sirupsen/logrus" - "gonum.org/v1/gonum/stat" - + "github.com/c9s/bbgo/pkg/datatype/floats" "github.com/c9s/bbgo/pkg/types" ) @@ -26,13 +24,14 @@ Bollinger Bands Technical indicator guide: type BOLL struct { types.IntervalWindow - // times of Std, generally it's 2 + // K is the multiplier of Std, generally it's 2 K float64 - SMA types.Float64Slice - StdDev types.Float64Slice - UpBand types.Float64Slice - DownBand types.Float64Slice + SMA *SMA + StdDev *StdDev + + UpBand floats.Slice + DownBand floats.Slice EndTime time.Time @@ -41,20 +40,20 @@ type BOLL struct { type BandType int -func (inc *BOLL) GetUpBand() types.Series { - return &inc.UpBand +func (inc *BOLL) GetUpBand() types.SeriesExtend { + return types.NewSeries(&inc.UpBand) } -func (inc *BOLL) GetDownBand() types.Series { - return &inc.DownBand +func (inc *BOLL) GetDownBand() types.SeriesExtend { + return types.NewSeries(&inc.DownBand) } -func (inc *BOLL) GetSMA() types.Series { - return &inc.SMA +func (inc *BOLL) GetSMA() types.SeriesExtend { + return types.NewSeries(inc.SMA) } -func (inc *BOLL) GetStdDev() types.Series { - return &inc.StdDev +func (inc *BOLL) GetStdDev() types.SeriesExtend { + return inc.StdDev } func (inc *BOLL) LastUpBand() float64 { @@ -73,64 +72,58 @@ func (inc *BOLL) LastDownBand() float64 { return inc.DownBand[len(inc.DownBand)-1] } -func (inc *BOLL) LastStdDev() float64 { - if len(inc.StdDev) == 0 { - return 0.0 +func (inc *BOLL) Update(value float64) { + if inc.SMA == nil { + inc.SMA = &SMA{IntervalWindow: inc.IntervalWindow} } - return inc.StdDev[len(inc.StdDev)-1] -} - -func (inc *BOLL) LastSMA() float64 { - if len(inc.SMA) > 0 { - return inc.SMA[len(inc.SMA)-1] + if inc.StdDev == nil { + inc.StdDev = &StdDev{IntervalWindow: inc.IntervalWindow} } - return 0.0 -} -func (inc *BOLL) calculateAndUpdate(kLines []types.KLine) { - if len(kLines) < inc.Window { - return - } + inc.SMA.Update(value) + inc.StdDev.Update(value) - var index = len(kLines) - 1 - var kline = kLines[index] + var sma = inc.SMA.Last() + var stdDev = inc.StdDev.Last() + var band = inc.K * stdDev - if inc.EndTime != zeroTime && kline.EndTime.Before(inc.EndTime) { - return - } + var upBand = sma + band + var downBand = sma - band - var recentK = kLines[index-(inc.Window-1) : index+1] - sma, err := calculateSMA(recentK, inc.Window, KLineClosePriceMapper) - if err != nil { - log.WithError(err).Error("SMA error") - return - } + inc.UpBand.Push(upBand) + inc.DownBand.Push(downBand) +} - inc.SMA.Push(sma) +func (inc *BOLL) BindK(target KLineClosedEmitter, symbol string, interval types.Interval) { + target.OnKLineClosed(types.KLineWith(symbol, interval, inc.PushK)) +} - var prices []float64 - for _, k := range recentK { - prices = append(prices, k.Close.Float64()) +func (inc *BOLL) PushK(k types.KLine) { + if inc.EndTime != zeroTime && k.EndTime.Before(inc.EndTime) { + return } + inc.Update(k.Close.Float64()) + inc.EndTime = k.EndTime.Time() + inc.EmitUpdate(inc.SMA.Last(), inc.UpBand.Last(), inc.DownBand.Last()) +} - var std = stat.StdDev(prices, nil) - inc.StdDev.Push(std) - - var band = inc.K * std - - var upBand = sma + band - inc.UpBand.Push(upBand) - - var downBand = sma - band - inc.DownBand.Push(downBand) +func (inc *BOLL) LoadK(allKLines []types.KLine) { + for _, k := range allKLines { + inc.PushK(k) + } - // update end time - inc.EndTime = kLines[index].EndTime.Time() + inc.EmitUpdate(inc.SMA.Last(), inc.UpBand.Last(), inc.DownBand.Last()) +} - // log.Infof("update boll: sma=%f, up=%f, down=%f", sma, upBand, downBand) +func (inc *BOLL) CalculateAndUpdate(allKLines []types.KLine) { + if inc.SMA == nil { + inc.LoadK(allKLines) + return + } - inc.EmitUpdate(sma, upBand, downBand) + var last = allKLines[len(allKLines)-1] + inc.PushK(last) } func (inc *BOLL) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { @@ -142,7 +135,7 @@ func (inc *BOLL) handleKLineWindowUpdate(interval types.Interval, window types.K return } - inc.calculateAndUpdate(window) + inc.CalculateAndUpdate(window) } func (inc *BOLL) Bind(updater KLineWindowUpdater) { diff --git a/pkg/indicator/boll_test.go b/pkg/indicator/boll_test.go new file mode 100644 index 0000000000..7bc8c574c2 --- /dev/null +++ b/pkg/indicator/boll_test.go @@ -0,0 +1,69 @@ +package indicator + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +/* +python: + +import numpy as np +import pandas as pd + +np.random.seed(1) + +window = 14 +n = 100 + +s = pd.Series(10 + np.sin(2 * np.pi * np.arange(n) / n) + np.random.rand(n)) +print(s.tolist()) + +std = s.rolling(window).std() +ma = s.rolling(window).mean() + +boll_up = ma + std +boll_down = ma - std +print(boll_up) +print(boll_down) +*/ +func TestBOLL(t *testing.T) { + var Delta = 4e-2 + var randomPrices = []byte(`[10.417022004702574, 10.783115012971471, 10.12544760838165, 10.489713887217565, 10.395445777981967, 10.401355589143744, 10.55438476406235, 10.77134001860812, 10.878521148332386, 11.074643528982353, 11.006979766695768, 11.322643490145449, 10.888999355660205, 11.607086063812357, 10.797900835973715, 11.47948450455335, 11.261632727869141, 11.434996508489617, 11.045213991061253, 11.12787797497313, 11.75180108497069, 11.936844736848029, 11.295711428887932, 11.684437316983791, 11.87441588072431, 11.894606663503847, 11.08307093979805, 11.03116948454736, 11.152117670293258, 11.846725664558043, 11.049403350128204, 11.350884110893302, 11.862716582616521, 11.40947196501688, 11.536205039452488, 11.12453262538101, 11.457014170457374, 11.563594299318783, 10.70283538327288, 11.387568304693657, 11.576646341198968, 11.283992449358836, 10.76219766616612, 11.215058620016562, 10.471350559262321, 10.756910520550854, 11.157285390257952, 10.480995462959404, 10.413108572150653, 10.192819091647591, 10.019366957870297, 10.616045013410577, 10.086294882435753, 10.078165344786502, 10.242883272115485, 9.744345550742134, 10.205993052807335, 9.720949283340737, 10.107551862801568, 10.163931565041935, 9.514549176535352, 9.776631998070878, 10.009853051799057, 9.685210642105492, 9.279440216170297, 9.726879411540565, 9.819466719717774, 9.638582432014445, 10.039767703524793, 9.656778554613743, 9.95234539899273, 9.168891543017606, 9.15698909652207, 9.815276587395047, 9.399650108557262, 9.165354197116933, 9.929481851967761, 9.355651158431028, 9.768524852407467, 9.75741482422182, 9.932249574910657, 9.693895721167358, 9.846115381561317, 9.47259166193398, 9.425599966263011, 10.086869223821118, 9.657577947095504, 10.235871419726973, 9.97889439188976, 9.984271730460431, 9.526960720660902, 10.413662463728075, 9.968158459378225, 10.152610322822058, 10.040012250076602, 9.92800998586808, 10.654689633397398, 10.386298172086562, 9.877537093466854, 10.55435439409141]`) + var input []fixedpoint.Value + if err := json.Unmarshal(randomPrices, &input); err != nil { + panic(err) + } + tests := []struct { + name string + kLines []types.KLine + window int + k float64 + up float64 + down float64 + }{ + { + name: "random_case", + kLines: buildKLines(input), + window: 14, + k: 1, + up: 10.421434, + down: 9.772696, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + boll := BOLL{IntervalWindow: types.IntervalWindow{Window: tt.window}, K: tt.k} + boll.CalculateAndUpdate(tt.kLines) + assert.InDelta(t, tt.up, boll.UpBand.Last(), Delta) + assert.InDelta(t, tt.down, boll.DownBand.Last(), Delta) + }) + } + +} diff --git a/pkg/indicator/cci.go b/pkg/indicator/cci.go new file mode 100644 index 0000000000..ee3190d0a3 --- /dev/null +++ b/pkg/indicator/cci.go @@ -0,0 +1,109 @@ +package indicator + +import ( + "math" + + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/types" +) + +// Refer: Commodity Channel Index +// Refer URL: http://www.andrewshamlet.net/2017/07/08/python-tutorial-cci +// with modification of ddof=0 to let standard deviation to be divided by N instead of N-1 +//go:generate callbackgen -type CCI +type CCI struct { + types.SeriesBase + types.IntervalWindow + Input floats.Slice + TypicalPrice floats.Slice + MA floats.Slice + Values floats.Slice + + UpdateCallbacks []func(value float64) +} + +func (inc *CCI) Update(value float64) { + if len(inc.TypicalPrice) == 0 { + inc.SeriesBase.Series = inc + inc.TypicalPrice.Push(value) + inc.Input.Push(value) + return + } else if len(inc.TypicalPrice) > MaxNumOfEWMA { + inc.TypicalPrice = inc.TypicalPrice[MaxNumOfEWMATruncateSize-1:] + inc.Input = inc.Input[MaxNumOfEWMATruncateSize-1:] + } + + inc.Input.Push(value) + tp := inc.TypicalPrice.Last() - inc.Input.Index(inc.Window) + value + inc.TypicalPrice.Push(tp) + if len(inc.Input) < inc.Window { + return + } + ma := tp / float64(inc.Window) + inc.MA.Push(ma) + if len(inc.MA) > MaxNumOfEWMA { + inc.MA = inc.MA[MaxNumOfEWMATruncateSize-1:] + } + md := 0. + for i := 0; i < inc.Window; i++ { + diff := inc.Input.Index(i) - ma + md += diff * diff + } + md = math.Sqrt(md / float64(inc.Window)) + + cci := (value - ma) / (0.015 * md) + + inc.Values.Push(cci) + if len(inc.Values) > MaxNumOfEWMA { + inc.Values = inc.Values[MaxNumOfEWMATruncateSize-1:] + } +} + +func (inc *CCI) Last() float64 { + if len(inc.Values) == 0 { + return 0 + } + return inc.Values[len(inc.Values)-1] +} + +func (inc *CCI) Index(i int) float64 { + if i >= len(inc.Values) { + return 0 + } + return inc.Values[len(inc.Values)-1-i] +} + +func (inc *CCI) Length() int { + return len(inc.Values) +} + +var _ types.SeriesExtend = &CCI{} + +func (inc *CCI) PushK(k types.KLine) { + inc.Update(k.High.Add(k.Low).Add(k.Close).Div(three).Float64()) +} + +func (inc *CCI) CalculateAndUpdate(allKLines []types.KLine) { + if inc.TypicalPrice.Length() == 0 { + for _, k := range allKLines { + inc.PushK(k) + inc.EmitUpdate(inc.Last()) + } + } else { + k := allKLines[len(allKLines)-1] + inc.PushK(k) + inc.EmitUpdate(inc.Last()) + } +} + +func (inc *CCI) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { + if inc.Interval != interval { + return + } + + inc.CalculateAndUpdate(window) +} + +func (inc *CCI) Bind(updater KLineWindowUpdater) { + updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) +} diff --git a/pkg/indicator/cci_callbacks.go b/pkg/indicator/cci_callbacks.go new file mode 100644 index 0000000000..52251a1f90 --- /dev/null +++ b/pkg/indicator/cci_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type CCI"; DO NOT EDIT. + +package indicator + +import () + +func (inc *CCI) OnUpdate(cb func(value float64)) { + inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) +} + +func (inc *CCI) EmitUpdate(value float64) { + for _, cb := range inc.UpdateCallbacks { + cb(value) + } +} diff --git a/pkg/indicator/cci_test.go b/pkg/indicator/cci_test.go new file mode 100644 index 0000000000..4aeca6fcab --- /dev/null +++ b/pkg/indicator/cci_test.go @@ -0,0 +1,37 @@ +package indicator + +import ( + "encoding/json" + "testing" + + "github.com/c9s/bbgo/pkg/types" + "github.com/stretchr/testify/assert" +) + +/* +python: + +import pandas as pd +s = pd.Series([0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9]) +cci = pd.Series((s - s.rolling(16).mean()) / (0.015 * s.rolling(16).std(ddof=0)), name="CCI") +print(cci) +*/ +func Test_CCI(t *testing.T) { + var randomPrices = []byte(`[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]`) + var input []float64 + var Delta = 4.3e-2 + if err := json.Unmarshal(randomPrices, &input); err != nil { + panic(err) + } + t.Run("random_case", func(t *testing.T) { + cci := CCI{IntervalWindow: types.IntervalWindow{Window: 16}} + for _, value := range input { + cci.Update(value) + } + + last := cci.Last() + assert.InDelta(t, 93.250481, last, Delta) + assert.InDelta(t, 81.813449, cci.Index(1), Delta) + assert.Equal(t, 50-16+1, cci.Length()) + }) +} diff --git a/pkg/indicator/cma.go b/pkg/indicator/cma.go index 8040c87072..37dad3f8c7 100644 --- a/pkg/indicator/cma.go +++ b/pkg/indicator/cma.go @@ -1,6 +1,7 @@ package indicator import ( + "github.com/c9s/bbgo/pkg/datatype/floats" "github.com/c9s/bbgo/pkg/types" ) @@ -8,18 +9,23 @@ import ( // Refer: https://en.wikipedia.org/wiki/Moving_average //go:generate callbackgen -type CA type CA struct { - Interval types.Interval - Values types.Float64Slice - length float64 + types.SeriesBase + Interval types.Interval + Values floats.Slice + length float64 UpdateCallbacks []func(value float64) } func (inc *CA) Update(x float64) { + if len(inc.Values) == 0 { + inc.SeriesBase.Series = inc + } newVal := (inc.Values.Last()*inc.length + x) / (inc.length + 1.) inc.length += 1 inc.Values.Push(newVal) if len(inc.Values) > MaxNumOfEWMA { inc.Values = inc.Values[MaxNumOfEWMATruncateSize-1:] + inc.length = float64(len(inc.Values)) } } @@ -41,11 +47,15 @@ func (inc *CA) Length() int { return len(inc.Values) } -var _ types.Series = &CA{} +var _ types.SeriesExtend = &CA{} + +func (inc *CA) PushK(k types.KLine) { + inc.Update(k.Close.Float64()) +} -func (inc *CA) calculateAndUpdate(allKLines []types.KLine) { +func (inc *CA) CalculateAndUpdate(allKLines []types.KLine) { for _, k := range allKLines { - inc.Update(k.Close.Float64()) + inc.PushK(k) inc.EmitUpdate(inc.Last()) } } @@ -55,7 +65,7 @@ func (inc *CA) handleKLineWindowUpdate(interval types.Interval, window types.KLi return } - inc.calculateAndUpdate(window) + inc.CalculateAndUpdate(window) } func (inc *CA) Bind(updater KLineWindowUpdater) { diff --git a/pkg/indicator/const.go b/pkg/indicator/const.go new file mode 100644 index 0000000000..7764c75dd2 --- /dev/null +++ b/pkg/indicator/const.go @@ -0,0 +1,11 @@ +package indicator + +import ( + "time" + + "github.com/c9s/bbgo/pkg/fixedpoint" +) + +var three = fixedpoint.NewFromInt(3) + +var zeroTime = time.Time{} diff --git a/pkg/indicator/dema.go b/pkg/indicator/dema.go index bc476134a2..601cb8380b 100644 --- a/pkg/indicator/dema.go +++ b/pkg/indicator/dema.go @@ -1,6 +1,7 @@ package indicator import ( + "github.com/c9s/bbgo/pkg/datatype/floats" "github.com/c9s/bbgo/pkg/types" ) @@ -10,17 +11,36 @@ import ( //go:generate callbackgen -type DEMA type DEMA struct { types.IntervalWindow - Values types.Float64Slice + types.SeriesBase + Values floats.Slice a1 *EWMA a2 *EWMA UpdateCallbacks []func(value float64) } +func (inc *DEMA) Clone() *DEMA { + out := &DEMA{ + IntervalWindow: inc.IntervalWindow, + Values: inc.Values[:], + a1: inc.a1.Clone(), + a2: inc.a2.Clone(), + } + out.SeriesBase.Series = out + return out +} + +func (inc *DEMA) TestUpdate(value float64) *DEMA { + out := inc.Clone() + out.Update(value) + return out +} + func (inc *DEMA) Update(value float64) { if len(inc.Values) == 0 { - inc.a1 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}} - inc.a2 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}} + inc.SeriesBase.Series = inc + inc.a1 = &EWMA{IntervalWindow: inc.IntervalWindow} + inc.a2 = &EWMA{IntervalWindow: inc.IntervalWindow} } inc.a1.Update(value) @@ -46,16 +66,22 @@ func (inc *DEMA) Length() int { return len(inc.Values) } -var _ types.Series = &DEMA{} +var _ types.SeriesExtend = &DEMA{} + +func (inc *DEMA) PushK(k types.KLine) { + inc.Update(k.Close.Float64()) +} -func (inc *DEMA) calculateAndUpdate(allKLines []types.KLine) { +func (inc *DEMA) CalculateAndUpdate(allKLines []types.KLine) { if inc.a1 == nil { for _, k := range allKLines { - inc.Update(k.Close.Float64()) + inc.PushK(k) inc.EmitUpdate(inc.Last()) } } else { - inc.Update(allKLines[len(allKLines)-1].Close.Float64()) + // last k + k := allKLines[len(allKLines)-1] + inc.PushK(k) inc.EmitUpdate(inc.Last()) } } @@ -65,7 +91,7 @@ func (inc *DEMA) handleKLineWindowUpdate(interval types.Interval, window types.K return } - inc.calculateAndUpdate(window) + inc.CalculateAndUpdate(window) } func (inc *DEMA) Bind(updater KLineWindowUpdater) { diff --git a/pkg/indicator/dema_test.go b/pkg/indicator/dema_test.go index c58429672c..99da987b98 100644 --- a/pkg/indicator/dema_test.go +++ b/pkg/indicator/dema_test.go @@ -45,7 +45,7 @@ func Test_DEMA(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { dema := DEMA{IntervalWindow: types.IntervalWindow{Window: 16}} - dema.calculateAndUpdate(tt.kLines) + dema.CalculateAndUpdate(tt.kLines) last := dema.Last() assert.InDelta(t, tt.want, last, Delta) assert.InDelta(t, tt.next, dema.Index(1), Delta) diff --git a/pkg/indicator/dmi.go b/pkg/indicator/dmi.go new file mode 100644 index 0000000000..d3352bfc55 --- /dev/null +++ b/pkg/indicator/dmi.go @@ -0,0 +1,122 @@ +package indicator + +import ( + "math" + + "github.com/c9s/bbgo/pkg/types" +) + +// Refer: https://www.investopedia.com/terms/d/dmi.asp +// Refer: https://github.com/twopirllc/pandas-ta/blob/main/pandas_ta/trend/adx.py +// +// Directional Movement Index +// an indicator developed by J. Welles Wilder in 1978 that identifies in which +// direction the price of an asset is moving. +//go:generate callbackgen -type DMI +type DMI struct { + types.IntervalWindow + + ADXSmoothing int + atr *ATR + DMP types.UpdatableSeriesExtend + DMN types.UpdatableSeriesExtend + DIPlus *types.Queue + DIMinus *types.Queue + ADX types.UpdatableSeriesExtend + PrevHigh, PrevLow float64 + + updateCallbacks []func(diplus, diminus, adx float64) +} + +func (inc *DMI) Update(high, low, cloze float64) { + if inc.DMP == nil || inc.DMN == nil { + inc.DMP = &RMA{IntervalWindow: inc.IntervalWindow, Adjust: true} + inc.DMN = &RMA{IntervalWindow: inc.IntervalWindow, Adjust: true} + inc.ADX = &RMA{IntervalWindow: types.IntervalWindow{Window: inc.ADXSmoothing}, Adjust: true} + } + + if inc.atr == nil { + inc.atr = &ATR{IntervalWindow: inc.IntervalWindow} + inc.atr.Update(high, low, cloze) + inc.PrevHigh = high + inc.PrevLow = low + inc.DIPlus = types.NewQueue(500) + inc.DIMinus = types.NewQueue(500) + return + } + + inc.atr.Update(high, low, cloze) + up := high - inc.PrevHigh + dn := inc.PrevLow - low + inc.PrevHigh = high + inc.PrevLow = low + pos := 0.0 + if up > dn && up > 0. { + pos = up + } + + neg := 0.0 + if dn > up && dn > 0. { + neg = dn + } + + inc.DMP.Update(pos) + inc.DMN.Update(neg) + if inc.atr.Length() < inc.Window { + return + } + k := 100. / inc.atr.Last() + dmp := inc.DMP.Last() + dmn := inc.DMN.Last() + inc.DIPlus.Update(k * dmp) + inc.DIMinus.Update(k * dmn) + dx := 100. * math.Abs(dmp-dmn) / (dmp + dmn) + inc.ADX.Update(dx) + +} + +func (inc *DMI) GetDIPlus() types.SeriesExtend { + return inc.DIPlus +} + +func (inc *DMI) GetDIMinus() types.SeriesExtend { + return inc.DIMinus +} + +func (inc *DMI) GetADX() types.SeriesExtend { + return inc.ADX +} + +func (inc *DMI) Length() int { + return inc.ADX.Length() +} + +func (inc *DMI) PushK(k types.KLine) { + inc.Update(k.High.Float64(), k.Low.Float64(), k.Close.Float64()) +} + +func (inc *DMI) CalculateAndUpdate(allKLines []types.KLine) { + last := allKLines[len(allKLines)-1] + + if inc.ADX == nil { + for _, k := range allKLines { + inc.PushK(k) + inc.EmitUpdate(inc.DIPlus.Last(), inc.DIMinus.Last(), inc.ADX.Last()) + } + } else { + inc.PushK(last) + inc.EmitUpdate(inc.DIPlus.Last(), inc.DIMinus.Last(), inc.ADX.Last()) + } +} + +func (inc *DMI) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { + if inc.Interval != interval { + return + } + + inc.CalculateAndUpdate(window) +} + +func (inc *DMI) Bind(updater KLineWindowUpdater) { + updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) +} diff --git a/pkg/indicator/dmi_callbacks.go b/pkg/indicator/dmi_callbacks.go new file mode 100644 index 0000000000..ed84539075 --- /dev/null +++ b/pkg/indicator/dmi_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type DMI"; DO NOT EDIT. + +package indicator + +import () + +func (inc *DMI) OnUpdate(cb func(diplus float64, diminus float64, adx float64)) { + inc.updateCallbacks = append(inc.updateCallbacks, cb) +} + +func (inc *DMI) EmitUpdate(diplus float64, diminus float64, adx float64) { + for _, cb := range inc.updateCallbacks { + cb(diplus, diminus, adx) + } +} diff --git a/pkg/indicator/dmi_test.go b/pkg/indicator/dmi_test.go new file mode 100644 index 0000000000..d93e164409 --- /dev/null +++ b/pkg/indicator/dmi_test.go @@ -0,0 +1,87 @@ +package indicator + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" + "github.com/stretchr/testify/assert" +) + +/* +python: + +import pandas as pd +import pandas_ta as ta + +data = pd.Series([0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9]) + +high = pd.Series([100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109]) + +low = pd.Series([80,81,82,83,84,85,86,87,88,89,80,81,82,83,84,85,86,87,88,89,80,81,82,83,84,85,86,87,88,89]) + +close = pd.Series([90,91,92,93,94,95,96,97,98,99,90,91,92,93,94,95,96,97,98,99,90,91,92,93,94,95,96,97,98,99]) + +result = ta.adx(high, low, close, 5, 14) +print(result['ADX_14']) + +print(result['DMP_5']) +print(result['DMN_5']) +*/ +func Test_DMI(t *testing.T) { + var Delta = 0.001 + var highb = []byte(`[100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109]`) + var lowb = []byte(`[80,81,82,83,84,85,86,87,88,89,80,81,82,83,84,85,86,87,88,89,80,81,82,83,84,85,86,87,88,89]`) + var clozeb = []byte(`[90,91,92,93,94,95,96,97,98,99,90,91,92,93,94,95,96,97,98,99,90,91,92,93,94,95,96,97,98,99]`) + + buildKLines := func(h, l, c []byte) (klines []types.KLine) { + var hv, cv, lv []fixedpoint.Value + _ = json.Unmarshal(h, &hv) + _ = json.Unmarshal(l, &lv) + _ = json.Unmarshal(c, &cv) + if len(hv) != len(lv) || len(lv) != len(cv) { + panic(fmt.Sprintf("length not equal %v %v %v", len(hv), len(lv), len(cv))) + } + for i, hh := range hv { + kline := types.KLine{High: hh, Low: lv[i], Close: cv[i]} + klines = append(klines, kline) + } + return klines + } + + type output struct { + dip float64 + dim float64 + adx float64 + } + + tests := []struct { + name string + klines []types.KLine + want output + next output + total int + }{ + { + name: "test_dmi", + klines: buildKLines(highb, lowb, clozeb), + want: output{dip: 4.85114, dim: 1.339736, adx: 37.857156}, + next: output{dip: 4.813853, dim: 1.67532, adx: 36.111434}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dmi := &DMI{ + IntervalWindow: types.IntervalWindow{Window: 5}, + ADXSmoothing: 14, + } + dmi.CalculateAndUpdate(tt.klines) + assert.InDelta(t, dmi.GetDIPlus().Last(), tt.want.dip, Delta) + assert.InDelta(t, dmi.GetDIMinus().Last(), tt.want.dim, Delta) + assert.InDelta(t, dmi.GetADX().Last(), tt.want.adx, Delta) + }) + } + +} diff --git a/pkg/indicator/drift.go b/pkg/indicator/drift.go new file mode 100644 index 0000000000..12006191fc --- /dev/null +++ b/pkg/indicator/drift.go @@ -0,0 +1,140 @@ +package indicator + +import ( + "math" + + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/types" +) + +// Refer: https://tradingview.com/script/aDymGrFx-Drift-Study-Inspired-by-Monte-Carlo-Simulations-with-BM-KL/ +// Brownian Motion's drift factor +// could be used in Monte Carlo Simulations +//go:generate callbackgen -type Drift +type Drift struct { + types.SeriesBase + types.IntervalWindow + chng *types.Queue + Values floats.Slice + MA types.UpdatableSeriesExtend + LastValue float64 + + UpdateCallbacks []func(value float64) +} + +func (inc *Drift) Update(value float64) { + if inc.chng == nil { + inc.SeriesBase.Series = inc + if inc.MA == nil { + inc.MA = &SMA{IntervalWindow: types.IntervalWindow{Interval: inc.Interval, Window: inc.Window}} + } + inc.chng = types.NewQueue(inc.Window) + inc.LastValue = value + return + } + var chng float64 + if value == 0 { + chng = 0 + } else { + chng = math.Log(value / inc.LastValue) + inc.LastValue = value + } + inc.MA.Update(chng) + inc.chng.Update(chng) + if inc.chng.Length() >= inc.Window { + stdev := types.Stdev(inc.chng, inc.Window) + drift := inc.MA.Last() - stdev*stdev*0.5 + inc.Values.Push(drift) + } +} + +// Assume that MA is SMA +func (inc *Drift) ZeroPoint() float64 { + window := float64(inc.Window) + stdev := types.Stdev(inc.chng, inc.Window) + chng := inc.chng.Index(inc.Window - 1) + /*b := -2 * inc.MA.Last() - 2 + c := window * stdev * stdev - chng * chng + 2 * chng * (inc.MA.Last() + 1) - 2 * inc.MA.Last() * window + + root := math.Sqrt(b*b - 4*c) + K1 := (-b + root)/2 + K2 := (-b - root)/2 + N1 := math.Exp(K1) * inc.LastValue + N2 := math.Exp(K2) * inc.LastValue + if math.Abs(inc.LastValue-N1) < math.Abs(inc.LastValue-N2) { + return N1 + } else { + return N2 + }*/ + return inc.LastValue * math.Exp(window*(0.5*stdev*stdev)+chng-inc.MA.Last()*window) +} + +func (inc *Drift) Clone() (out *Drift) { + out = &Drift{ + IntervalWindow: inc.IntervalWindow, + chng: inc.chng.Clone(), + Values: inc.Values[:], + MA: types.Clone(inc.MA), + LastValue: inc.LastValue, + } + out.SeriesBase.Series = out + return out +} + +func (inc *Drift) TestUpdate(value float64) *Drift { + out := inc.Clone() + out.Update(value) + return out +} + +func (inc *Drift) Index(i int) float64 { + if inc.Values == nil { + return 0 + } + return inc.Values.Index(i) +} + +func (inc *Drift) Last() float64 { + if inc.Values.Length() == 0 { + return 0 + } + return inc.Values.Last() +} + +func (inc *Drift) Length() int { + if inc.Values == nil { + return 0 + } + return inc.Values.Length() +} + +var _ types.SeriesExtend = &Drift{} + +func (inc *Drift) PushK(k types.KLine) { + inc.Update(k.Close.Float64()) +} + +func (inc *Drift) CalculateAndUpdate(allKLines []types.KLine) { + if inc.chng == nil { + for _, k := range allKLines { + inc.PushK(k) + inc.EmitUpdate(inc.Last()) + } + } else { + k := allKLines[len(allKLines)-1] + inc.PushK(k) + inc.EmitUpdate(inc.Last()) + } +} + +func (inc *Drift) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { + if inc.Interval != interval { + return + } + + inc.CalculateAndUpdate(window) +} + +func (inc *Drift) Bind(updater KLineWindowUpdater) { + updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) +} diff --git a/pkg/indicator/drift_callbacks.go b/pkg/indicator/drift_callbacks.go new file mode 100644 index 0000000000..224ef74a4a --- /dev/null +++ b/pkg/indicator/drift_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type Drift"; DO NOT EDIT. + +package indicator + +import () + +func (inc *Drift) OnUpdate(cb func(value float64)) { + inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) +} + +func (inc *Drift) EmitUpdate(value float64) { + for _, cb := range inc.UpdateCallbacks { + cb(value) + } +} diff --git a/pkg/indicator/drift_test.go b/pkg/indicator/drift_test.go new file mode 100644 index 0000000000..9318da9d61 --- /dev/null +++ b/pkg/indicator/drift_test.go @@ -0,0 +1,40 @@ +package indicator + +import ( + "encoding/json" + "testing" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" + "github.com/stretchr/testify/assert" +) + +func Test_Drift(t *testing.T) { + var randomPrices = []byte(`[1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 4, 1, 2, 3, 4, 5, 6, 7, 8, 9, 4, 1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 4, 1, 2, 3, 4, 5, 6, 7, 8, 9]`) + var input []fixedpoint.Value + if err := json.Unmarshal(randomPrices, &input); err != nil { + panic(err) + } + tests := []struct { + name string + kLines []types.KLine + all int + }{ + { + name: "random_case", + kLines: buildKLines(input), + all: 47, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + drift := Drift{IntervalWindow: types.IntervalWindow{Window: 3}} + drift.CalculateAndUpdate(tt.kLines) + assert.Equal(t, drift.Length(), tt.all) + for _, v := range drift.Values { + assert.LessOrEqual(t, v, 1.0) + } + }) + } +} diff --git a/pkg/indicator/emv.go b/pkg/indicator/emv.go new file mode 100644 index 0000000000..cded064a88 --- /dev/null +++ b/pkg/indicator/emv.go @@ -0,0 +1,71 @@ +package indicator + +import ( + "github.com/c9s/bbgo/pkg/types" +) + +// Refer: Ease of Movement +// Refer URL: https://www.investopedia.com/terms/e/easeofmovement.asp + +//go:generate callbackgen -type EMV +type EMV struct { + types.SeriesBase + types.IntervalWindow + + prevH float64 + prevL float64 + Values *SMA + EMVScale float64 + + UpdateCallbacks []func(value float64) +} + +const DefaultEMVScale float64 = 100000000. + +func (inc *EMV) Update(high, low, vol float64) { + if inc.EMVScale == 0 { + inc.EMVScale = DefaultEMVScale + } + + if inc.prevH == 0 || inc.Values == nil { + inc.SeriesBase.Series = inc + inc.prevH = high + inc.prevL = low + inc.Values = &SMA{IntervalWindow: inc.IntervalWindow} + return + } + + distanceMoved := (high+low)/2. - (inc.prevH+inc.prevL)/2. + boxRatio := vol / inc.EMVScale / (high - low) + result := distanceMoved / boxRatio + inc.prevH = high + inc.prevL = low + inc.Values.Update(result) +} + +func (inc *EMV) Index(i int) float64 { + if inc.Values == nil { + return 0 + } + return inc.Values.Index(i) +} + +func (inc *EMV) Last() float64 { + if inc.Values == nil { + return 0 + } + return inc.Values.Last() +} + +func (inc *EMV) Length() int { + if inc.Values == nil { + return 0 + } + return inc.Values.Length() +} + +var _ types.SeriesExtend = &EMV{} + +func (inc *EMV) PushK(k types.KLine) { + inc.Update(k.High.Float64(), k.Low.Float64(), k.Volume.Float64()) +} diff --git a/pkg/indicator/emv_callbacks.go b/pkg/indicator/emv_callbacks.go new file mode 100644 index 0000000000..89afd8a998 --- /dev/null +++ b/pkg/indicator/emv_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type EMV"; DO NOT EDIT. + +package indicator + +import () + +func (inc *EMV) OnUpdate(cb func(value float64)) { + inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) +} + +func (inc *EMV) EmitUpdate(value float64) { + for _, cb := range inc.UpdateCallbacks { + cb(value) + } +} diff --git a/pkg/indicator/emv_test.go b/pkg/indicator/emv_test.go new file mode 100644 index 0000000000..03b57a7604 --- /dev/null +++ b/pkg/indicator/emv_test.go @@ -0,0 +1,34 @@ +package indicator + +import ( + "testing" + + "github.com/c9s/bbgo/pkg/types" + "github.com/stretchr/testify/assert" +) + +// data from https://school.stockcharts.com/doku.php?id=technical_indicators:ease_of_movement_emv +func Test_EMV(t *testing.T) { + var Delta = 0.01 + emv := &EMV{ + EMVScale: 100000000, + IntervalWindow: types.IntervalWindow{Window: 14}, + } + emv.Update(63.74, 62.63, 32178836) + emv.Update(64.51, 63.85, 36461672) + assert.InDelta(t, 1.8, emv.Values.rawValues.Last(), Delta) + emv.Update(64.57, 63.81, 51372680) + emv.Update(64.31, 62.62, 42476356) + emv.Update(63.43, 62.73, 29504176) + emv.Update(62.85, 61.95, 33098600) + emv.Update(62.70, 62.06, 30577960) + emv.Update(63.18, 62.69, 35693928) + emv.Update(62.47, 61.54, 49768136) + emv.Update(64.16, 63.21, 44759968) + emv.Update(64.38, 63.87, 33425504) + emv.Update(64.89, 64.29, 15895085) + emv.Update(65.25, 64.48, 37015388) + emv.Update(64.69, 63.65, 40672116) + emv.Update(64.26, 63.68, 35627200) + assert.InDelta(t, -0.03, emv.Last(), Delta) +} diff --git a/pkg/indicator/ewma.go b/pkg/indicator/ewma.go index d94fb7953e..84b92fbb5e 100644 --- a/pkg/indicator/ewma.go +++ b/pkg/indicator/ewma.go @@ -1,11 +1,9 @@ package indicator import ( - "math" "time" - log "github.com/sirupsen/logrus" - + "github.com/c9s/bbgo/pkg/datatype/floats" "github.com/c9s/bbgo/pkg/types" ) @@ -16,16 +14,36 @@ const MaxNumOfEWMATruncateSize = 100 //go:generate callbackgen -type EWMA type EWMA struct { types.IntervalWindow - Values types.Float64Slice - LastOpenTime time.Time + types.SeriesBase + + Values floats.Slice + EndTime time.Time - UpdateCallbacks []func(value float64) + updateCallbacks []func(value float64) +} + +var _ types.SeriesExtend = &EWMA{} + +func (inc *EWMA) Clone() *EWMA { + out := &EWMA{ + IntervalWindow: inc.IntervalWindow, + Values: inc.Values[:], + } + out.SeriesBase.Series = out + return out +} + +func (inc *EWMA) TestUpdate(value float64) *EWMA { + out := inc.Clone() + out.Update(value) + return out } func (inc *EWMA) Update(value float64) { var multiplier = 2.0 / float64(1+inc.Window) if len(inc.Values) == 0 { + inc.SeriesBase.Series = inc inc.Values.Push(value) return } else if len(inc.Values) > MaxNumOfEWMA { @@ -56,60 +74,17 @@ func (inc *EWMA) Length() int { return len(inc.Values) } -func (inc *EWMA) calculateAndUpdate(allKLines []types.KLine) { - if len(allKLines) < inc.Window { - // we can't calculate +func (inc *EWMA) PushK(k types.KLine) { + if inc.EndTime != zeroTime && k.EndTime.Before(inc.EndTime) { return } - var priceF = KLineClosePriceMapper - var dataLen = len(allKLines) - var multiplier = 2.0 / (float64(inc.Window) + 1) - - // init the values fromNthK the kline data - var fromNthK = 1 - if len(inc.Values) == 0 { - // for the first value, we should use the close price - inc.Values = []float64{priceF(allKLines[0])} - } else { - if len(inc.Values) >= MaxNumOfEWMA { - inc.Values = inc.Values[MaxNumOfEWMATruncateSize-1:] - } - - fromNthK = len(inc.Values) - - // update ewma with the existing values - for i := dataLen - 1; i > 0; i-- { - var k = allKLines[i] - if k.StartTime.After(inc.LastOpenTime) { - fromNthK = i - } else { - break - } - } - } - - for i := fromNthK; i < dataLen; i++ { - var k = allKLines[i] - var ewma = priceF(k)*multiplier + (1-multiplier)*inc.Values[i-1] - inc.Values.Push(ewma) - inc.LastOpenTime = k.StartTime.Time() - inc.EmitUpdate(ewma) - } - - if len(inc.Values) != dataLen { - // check error - log.Warnf("%s EMA (%d) value length (%d) != kline window length (%d)", inc.Interval, inc.Window, len(inc.Values), dataLen) - } - - v1 := math.Floor(inc.Values[len(inc.Values)-1]*100.0) / 100.0 - v2 := math.Floor(CalculateKLinesEMA(allKLines, priceF, inc.Window)*100.0) / 100.0 - if v1 != v2 { - log.Warnf("ACCUMULATED %s EMA (%d) %f != EMA %f", inc.Interval, inc.Window, v1, v2) - } + inc.Update(k.Close.Float64()) + inc.EndTime = k.EndTime.Time() + inc.EmitUpdate(inc.Last()) } -func CalculateKLinesEMA(allKLines []types.KLine, priceF KLinePriceMapper, window int) float64 { +func CalculateKLinesEMA(allKLines []types.KLine, priceF KLineValueMapper, window int) float64 { var multiplier = 2.0 / (float64(window) + 1) return ewma(MapKLinePrice(allKLines, priceF), multiplier) } @@ -123,17 +98,3 @@ func ewma(prices []float64, multiplier float64) float64 { return prices[end]*multiplier + (1-multiplier)*ewma(prices[:end], multiplier) } - -func (inc *EWMA) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { - if inc.Interval != interval { - return - } - - inc.calculateAndUpdate(window) -} - -func (inc *EWMA) Bind(updater KLineWindowUpdater) { - updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) -} - -var _ types.Series = &EWMA{} diff --git a/pkg/indicator/ewma_callbacks.go b/pkg/indicator/ewma_callbacks.go index a0458ee7c4..38fbacb26d 100644 --- a/pkg/indicator/ewma_callbacks.go +++ b/pkg/indicator/ewma_callbacks.go @@ -5,11 +5,11 @@ package indicator import () func (inc *EWMA) OnUpdate(cb func(value float64)) { - inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) + inc.updateCallbacks = append(inc.updateCallbacks, cb) } func (inc *EWMA) EmitUpdate(value float64) { - for _, cb := range inc.UpdateCallbacks { + for _, cb := range inc.updateCallbacks { cb(value) } } diff --git a/pkg/indicator/ewma_test.go b/pkg/indicator/ewma_test.go index f781f251ca..23bc812853 100644 --- a/pkg/indicator/ewma_test.go +++ b/pkg/indicator/ewma_test.go @@ -1027,7 +1027,7 @@ func buildKLines(prices []fixedpoint.Value) (klines []types.KLine) { func Test_calculateEWMA(t *testing.T) { type args struct { allKLines []types.KLine - priceF KLinePriceMapper + priceF KLineValueMapper window int } var input []fixedpoint.Value diff --git a/pkg/indicator/fisher.go b/pkg/indicator/fisher.go new file mode 100644 index 0000000000..98678afd82 --- /dev/null +++ b/pkg/indicator/fisher.go @@ -0,0 +1,73 @@ +package indicator + +import ( + "math" + + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/types" +) + +//go:generate callbackgen -type FisherTransform +type FisherTransform struct { + types.SeriesBase + types.IntervalWindow + prices *types.Queue + Values floats.Slice + + UpdateCallbacks []func(value float64) +} + +func (inc *FisherTransform) Clone() types.UpdatableSeriesExtend { + out := FisherTransform{ + IntervalWindow: inc.IntervalWindow, + prices: inc.prices.Clone(), + Values: inc.Values[:], + } + out.SeriesBase.Series = &out + return &out +} + +func (inc *FisherTransform) Update(value float64) { + if inc.prices == nil { + inc.prices = types.NewQueue(inc.Window) + inc.SeriesBase.Series = inc + } + inc.prices.Update(value) + highest := inc.prices.Highest(inc.Window) + lowest := inc.prices.Lowest(inc.Window) + if highest == lowest { + inc.Values.Update(0) + return + } + x := 2*((value-lowest)/(highest-lowest)) - 1 + if x == 1 { + x = 0.9999 + } else if x == -1 { + x = -0.9999 + } + inc.Values.Update(0.5 * math.Log((1+x)/(1-x))) + if len(inc.Values) > MaxNumOfEWMA { + inc.Values = inc.Values[MaxNumOfEWMATruncateSize-1:] + } +} + +func (inc *FisherTransform) Last() float64 { + if inc.Values == nil { + return 0.0 + } + return inc.Values.Last() +} + +func (inc *FisherTransform) Index(i int) float64 { + if inc.Values == nil { + return 0.0 + } + return inc.Values.Index(i) +} + +func (inc *FisherTransform) Length() int { + if inc.Values == nil { + return 0 + } + return inc.Values.Length() +} diff --git a/pkg/indicator/fishertransform_callbacks.go b/pkg/indicator/fishertransform_callbacks.go new file mode 100644 index 0000000000..8b1e4195e6 --- /dev/null +++ b/pkg/indicator/fishertransform_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type FisherTransform"; DO NOT EDIT. + +package indicator + +import () + +func (inc *FisherTransform) OnUpdate(cb func(value float64)) { + inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) +} + +func (inc *FisherTransform) EmitUpdate(value float64) { + for _, cb := range inc.UpdateCallbacks { + cb(value) + } +} diff --git a/pkg/indicator/ghfilter.go b/pkg/indicator/ghfilter.go new file mode 100644 index 0000000000..f5794d4d59 --- /dev/null +++ b/pkg/indicator/ghfilter.go @@ -0,0 +1,74 @@ +package indicator + +import ( + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/types" + "math" +) + +// Refer: https://jamesgoulding.com/Research_II/Ehlers/Ehlers%20(Optimal%20Tracking%20Filters).doc +// Ehler's Optimal Tracking Filter, an alpha-beta filter, also called g-h filter + +//go:generate callbackgen -type GHFilter +type GHFilter struct { + types.SeriesBase + types.IntervalWindow + a float64 // maneuverability uncertainty + b float64 // measurement uncertainty + lastMeasurement float64 + Values floats.Slice + + UpdateCallbacks []func(value float64) +} + +func (inc *GHFilter) Update(value float64) { + inc.update(value, math.Abs(value-inc.lastMeasurement)) +} + +func (inc *GHFilter) update(value, uncertainty float64) { + if len(inc.Values) == 0 { + inc.a = 0 + inc.b = uncertainty / 2 + inc.lastMeasurement = value + inc.Values.Push(value) + return + } + multiplier := 2.0 / float64(1+inc.Window) // EMA multiplier + inc.a = multiplier*(value-inc.lastMeasurement) + (1-multiplier)*inc.a + inc.b = multiplier*uncertainty/2 + (1-multiplier)*inc.b + lambda := inc.a / inc.b + lambda2 := lambda * lambda + alpha := (-lambda2 + math.Sqrt(lambda2*lambda2+16*lambda2)) / 8 + filtered := alpha*value + (1-alpha)*inc.Values.Last() + inc.Values.Push(filtered) + inc.lastMeasurement = value +} + +func (inc *GHFilter) Index(i int) float64 { + if inc.Values == nil { + return 0.0 + } + return inc.Values.Index(i) +} + +func (inc *GHFilter) Length() int { + if inc.Values == nil { + return 0 + } + return inc.Values.Length() +} + +func (inc *GHFilter) Last() float64 { + if inc.Values == nil { + return 0.0 + } + return inc.Values.Last() +} + +// interfaces implementation check +var _ Simple = &GHFilter{} +var _ types.SeriesExtend = &GHFilter{} + +func (inc *GHFilter) PushK(k types.KLine) { + inc.update(k.Close.Float64(), k.High.Float64()-k.Low.Float64()) +} diff --git a/pkg/indicator/ghfilter_callbacks.go b/pkg/indicator/ghfilter_callbacks.go new file mode 100644 index 0000000000..345355a4e3 --- /dev/null +++ b/pkg/indicator/ghfilter_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type GHFilter"; DO NOT EDIT. + +package indicator + +import () + +func (inc *GHFilter) OnUpdate(cb func(value float64)) { + inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) +} + +func (inc *GHFilter) EmitUpdate(value float64) { + for _, cb := range inc.UpdateCallbacks { + cb(value) + } +} diff --git a/pkg/indicator/ghfilter_test.go b/pkg/indicator/ghfilter_test.go new file mode 100644 index 0000000000..ac74399335 --- /dev/null +++ b/pkg/indicator/ghfilter_test.go @@ -0,0 +1,6151 @@ +package indicator + +import ( + "encoding/json" + "math" + "testing" + + "github.com/c9s/bbgo/pkg/types" +) + +// generated from Binance 2022/07/27 00:00 +// https://www.binance.com/api/v3/klines?symbol=ETHUSDT&interval=5m&endTime=1658851200000&limit=1000 +var testGHFilterDataEthusdt5m = []byte(`[ + { + "open": 1591.11, + "high": 1593.62, + "low": 1589.04, + "close": 1590.14 + }, + { + "open": 1590.14, + "high": 1596.51, + "low": 1590.13, + "close": 1592.06 + }, + { + "open": 1592.07, + "high": 1594.41, + "low": 1586.05, + "close": 1587.02 + }, + { + "open": 1587.02, + "high": 1588.38, + "low": 1583.86, + "close": 1585.33 + }, + { + "open": 1585.34, + "high": 1595.2, + "low": 1583.74, + "close": 1594.69 + }, + { + "open": 1594.69, + "high": 1594.75, + "low": 1589.89, + "close": 1591.36 + }, + { + "open": 1591.35, + "high": 1592.55, + "low": 1586.36, + "close": 1588.95 + }, + { + "open": 1588.95, + "high": 1589.75, + "low": 1588.39, + "close": 1589.38 + }, + { + "open": 1589.38, + "high": 1589.39, + "low": 1586.17, + "close": 1588.53 + }, + { + "open": 1588.52, + "high": 1588.62, + "low": 1581.95, + "close": 1583.4 + }, + { + "open": 1583.4, + "high": 1584.67, + "low": 1582.1, + "close": 1582.36 + }, + { + "open": 1582.35, + "high": 1584.29, + "low": 1577.82, + "close": 1578.14 + }, + { + "open": 1578.14, + "high": 1581.95, + "low": 1575.72, + "close": 1581.52 + }, + { + "open": 1581.52, + "high": 1584.86, + "low": 1578.51, + "close": 1580.88 + }, + { + "open": 1580.88, + "high": 1581.77, + "low": 1578.74, + "close": 1581.11 + }, + { + "open": 1581.1, + "high": 1582.72, + "low": 1579.07, + "close": 1579.4 + }, + { + "open": 1579.4, + "high": 1580.93, + "low": 1578, + "close": 1579.6 + }, + { + "open": 1579.59, + "high": 1583.81, + "low": 1579.59, + "close": 1582.7 + }, + { + "open": 1582.7, + "high": 1583, + "low": 1577.24, + "close": 1579.45 + }, + { + "open": 1579.46, + "high": 1581.59, + "low": 1577.44, + "close": 1579.59 + }, + { + "open": 1579.58, + "high": 1581.41, + "low": 1579.22, + "close": 1580.56 + }, + { + "open": 1580.57, + "high": 1586.23, + "low": 1579.86, + "close": 1584.23 + }, + { + "open": 1584.22, + "high": 1587.36, + "low": 1584.22, + "close": 1585.15 + }, + { + "open": 1585.15, + "high": 1585.15, + "low": 1579.83, + "close": 1583.75 + }, + { + "open": 1583.74, + "high": 1592.49, + "low": 1583.45, + "close": 1587.76 + }, + { + "open": 1587.76, + "high": 1590.7, + "low": 1585.62, + "close": 1587.5 + }, + { + "open": 1587.51, + "high": 1587.51, + "low": 1579.53, + "close": 1581.16 + }, + { + "open": 1581.15, + "high": 1585.71, + "low": 1581.15, + "close": 1582.47 + }, + { + "open": 1582.46, + "high": 1582.86, + "low": 1567.58, + "close": 1571.52 + }, + { + "open": 1571.53, + "high": 1577.8, + "low": 1571.03, + "close": 1575.16 + }, + { + "open": 1575.16, + "high": 1578.06, + "low": 1572.18, + "close": 1576.66 + }, + { + "open": 1576.66, + "high": 1578, + "low": 1574.62, + "close": 1577.21 + }, + { + "open": 1577.2, + "high": 1584.57, + "low": 1576.61, + "close": 1584.05 + }, + { + "open": 1584.06, + "high": 1585.61, + "low": 1580, + "close": 1582.08 + }, + { + "open": 1582.08, + "high": 1583.4, + "low": 1579.43, + "close": 1579.43 + }, + { + "open": 1579.43, + "high": 1579.98, + "low": 1574.53, + "close": 1575.06 + }, + { + "open": 1575.06, + "high": 1578.52, + "low": 1574.57, + "close": 1576.49 + }, + { + "open": 1576.5, + "high": 1577, + "low": 1572.5, + "close": 1573.26 + }, + { + "open": 1573.26, + "high": 1579.41, + "low": 1573.06, + "close": 1578.35 + }, + { + "open": 1578.35, + "high": 1585, + "low": 1577.16, + "close": 1584.32 + }, + { + "open": 1584.31, + "high": 1587.97, + "low": 1580.67, + "close": 1585.7 + }, + { + "open": 1585.7, + "high": 1588.35, + "low": 1584.37, + "close": 1585.95 + }, + { + "open": 1585.94, + "high": 1587.09, + "low": 1580.66, + "close": 1580.97 + }, + { + "open": 1580.97, + "high": 1583.38, + "low": 1577, + "close": 1581.64 + }, + { + "open": 1581.64, + "high": 1586.79, + "low": 1581.22, + "close": 1585.42 + }, + { + "open": 1585.42, + "high": 1585.42, + "low": 1581.67, + "close": 1582.37 + }, + { + "open": 1582.38, + "high": 1584.86, + "low": 1581.01, + "close": 1581.02 + }, + { + "open": 1581.03, + "high": 1582.05, + "low": 1578.99, + "close": 1579.46 + }, + { + "open": 1579.46, + "high": 1579.89, + "low": 1566.85, + "close": 1567.99 + }, + { + "open": 1567.99, + "high": 1567.99, + "low": 1553.2, + "close": 1554.87 + }, + { + "open": 1554.87, + "high": 1558, + "low": 1546.9, + "close": 1550.4 + }, + { + "open": 1550.4, + "high": 1554.98, + "low": 1546.27, + "close": 1549.67 + }, + { + "open": 1549.68, + "high": 1555, + "low": 1546.97, + "close": 1553.88 + }, + { + "open": 1553.89, + "high": 1557.86, + "low": 1553.6, + "close": 1557.85 + }, + { + "open": 1557.86, + "high": 1558.37, + "low": 1554.9, + "close": 1556.3 + }, + { + "open": 1556.31, + "high": 1557.4, + "low": 1552.81, + "close": 1557.18 + }, + { + "open": 1557.18, + "high": 1563.78, + "low": 1556.5, + "close": 1562.72 + }, + { + "open": 1562.72, + "high": 1564.11, + "low": 1558.76, + "close": 1560.64 + }, + { + "open": 1560.64, + "high": 1562.31, + "low": 1560.5, + "close": 1561.24 + }, + { + "open": 1561.25, + "high": 1565.69, + "low": 1561.23, + "close": 1564.79 + }, + { + "open": 1564.79, + "high": 1565.33, + "low": 1558.23, + "close": 1559.9 + }, + { + "open": 1559.89, + "high": 1561.77, + "low": 1555.87, + "close": 1560.79 + }, + { + "open": 1560.78, + "high": 1562.07, + "low": 1557.89, + "close": 1560.36 + }, + { + "open": 1560.36, + "high": 1561.2, + "low": 1556.13, + "close": 1558.26 + }, + { + "open": 1558.25, + "high": 1563.12, + "low": 1558.25, + "close": 1562.35 + }, + { + "open": 1562.36, + "high": 1564.02, + "low": 1561.76, + "close": 1563.32 + }, + { + "open": 1563.31, + "high": 1564.29, + "low": 1557.79, + "close": 1559.87 + }, + { + "open": 1559.86, + "high": 1562.71, + "low": 1558.77, + "close": 1559.8 + }, + { + "open": 1559.81, + "high": 1559.91, + "low": 1557.6, + "close": 1559.19 + }, + { + "open": 1559.2, + "high": 1559.95, + "low": 1554.3, + "close": 1557.16 + }, + { + "open": 1557.16, + "high": 1557.17, + "low": 1536.25, + "close": 1541.89 + }, + { + "open": 1541.89, + "high": 1544.39, + "low": 1538.55, + "close": 1539.33 + }, + { + "open": 1539.33, + "high": 1546.28, + "low": 1533.67, + "close": 1543.99 + }, + { + "open": 1543.99, + "high": 1544.5, + "low": 1538.21, + "close": 1539.17 + }, + { + "open": 1539.17, + "high": 1543.33, + "low": 1537.73, + "close": 1543 + }, + { + "open": 1543.2, + "high": 1544, + "low": 1535.81, + "close": 1541.12 + }, + { + "open": 1541.12, + "high": 1541.13, + "low": 1534.12, + "close": 1536.89 + }, + { + "open": 1536.9, + "high": 1539.09, + "low": 1528.25, + "close": 1531.02 + }, + { + "open": 1531.01, + "high": 1532.91, + "low": 1525.28, + "close": 1532.32 + }, + { + "open": 1532.33, + "high": 1537.58, + "low": 1532.32, + "close": 1535.07 + }, + { + "open": 1535.06, + "high": 1541.28, + "low": 1535.06, + "close": 1539.52 + }, + { + "open": 1539.53, + "high": 1539.85, + "low": 1533.37, + "close": 1536.22 + }, + { + "open": 1536.21, + "high": 1536.22, + "low": 1524.81, + "close": 1527.09 + }, + { + "open": 1527.1, + "high": 1529.2, + "low": 1520.62, + "close": 1525.04 + }, + { + "open": 1525.04, + "high": 1528.2, + "low": 1522.12, + "close": 1523.72 + }, + { + "open": 1523.71, + "high": 1525.54, + "low": 1519, + "close": 1522.82 + }, + { + "open": 1522.82, + "high": 1524.98, + "low": 1521, + "close": 1522.19 + }, + { + "open": 1522.19, + "high": 1524.27, + "low": 1512.68, + "close": 1513.27 + }, + { + "open": 1513.26, + "high": 1514.55, + "low": 1501.65, + "close": 1514.14 + }, + { + "open": 1514.14, + "high": 1524.43, + "low": 1513.03, + "close": 1520.08 + }, + { + "open": 1520.08, + "high": 1525.07, + "low": 1518.63, + "close": 1520.61 + }, + { + "open": 1520.61, + "high": 1525.68, + "low": 1517.43, + "close": 1524.86 + }, + { + "open": 1524.86, + "high": 1525.04, + "low": 1519.65, + "close": 1520.26 + }, + { + "open": 1520.27, + "high": 1521.19, + "low": 1517.91, + "close": 1518.46 + }, + { + "open": 1518.46, + "high": 1525.25, + "low": 1518.46, + "close": 1524.83 + }, + { + "open": 1524.84, + "high": 1526.94, + "low": 1521.69, + "close": 1521.9 + }, + { + "open": 1521.9, + "high": 1524.8, + "low": 1519.25, + "close": 1519.88 + }, + { + "open": 1519.88, + "high": 1520.5, + "low": 1517.33, + "close": 1518.76 + }, + { + "open": 1518.76, + "high": 1522.64, + "low": 1518.14, + "close": 1520.46 + }, + { + "open": 1520.46, + "high": 1522.96, + "low": 1518.63, + "close": 1522.52 + }, + { + "open": 1522.51, + "high": 1522.52, + "low": 1519.19, + "close": 1520.61 + }, + { + "open": 1520.61, + "high": 1526.26, + "low": 1519.52, + "close": 1526.1 + }, + { + "open": 1526.11, + "high": 1530.26, + "low": 1524.63, + "close": 1528.65 + }, + { + "open": 1528.65, + "high": 1529.64, + "low": 1521.24, + "close": 1523.18 + }, + { + "open": 1523.19, + "high": 1525.92, + "low": 1521.79, + "close": 1525.6 + }, + { + "open": 1525.61, + "high": 1525.79, + "low": 1522.78, + "close": 1525.31 + }, + { + "open": 1525.31, + "high": 1529.6, + "low": 1525.3, + "close": 1528.93 + }, + { + "open": 1528.93, + "high": 1530.33, + "low": 1527.02, + "close": 1527.61 + }, + { + "open": 1527.6, + "high": 1534, + "low": 1527.6, + "close": 1533.98 + }, + { + "open": 1533.98, + "high": 1537.3, + "low": 1532.24, + "close": 1536.49 + }, + { + "open": 1536.5, + "high": 1536.5, + "low": 1531.65, + "close": 1532.92 + }, + { + "open": 1532.92, + "high": 1532.92, + "low": 1529.1, + "close": 1529.58 + }, + { + "open": 1529.58, + "high": 1535.97, + "low": 1528.13, + "close": 1532.48 + }, + { + "open": 1532.49, + "high": 1533.45, + "low": 1530.29, + "close": 1531.01 + }, + { + "open": 1531.02, + "high": 1532.56, + "low": 1524.11, + "close": 1524.37 + }, + { + "open": 1524.36, + "high": 1534.58, + "low": 1524.28, + "close": 1529.52 + }, + { + "open": 1529.52, + "high": 1530.55, + "low": 1521.72, + "close": 1522.54 + }, + { + "open": 1522.55, + "high": 1526.64, + "low": 1522.45, + "close": 1526.49 + }, + { + "open": 1526.48, + "high": 1530.2, + "low": 1525.07, + "close": 1526.92 + }, + { + "open": 1526.92, + "high": 1527.65, + "low": 1525, + "close": 1526.44 + }, + { + "open": 1526.43, + "high": 1527.68, + "low": 1525.46, + "close": 1526.05 + }, + { + "open": 1526.05, + "high": 1526.27, + "low": 1516.23, + "close": 1516.52 + }, + { + "open": 1516.23, + "high": 1520.67, + "low": 1509.21, + "close": 1520.48 + }, + { + "open": 1520.48, + "high": 1530.83, + "low": 1519.77, + "close": 1529.59 + }, + { + "open": 1529.6, + "high": 1531.12, + "low": 1526.99, + "close": 1531.11 + }, + { + "open": 1531.12, + "high": 1533.79, + "low": 1529.25, + "close": 1531.72 + }, + { + "open": 1531.73, + "high": 1532.96, + "low": 1528.52, + "close": 1529.64 + }, + { + "open": 1529.64, + "high": 1530.49, + "low": 1523.16, + "close": 1524.37 + }, + { + "open": 1524.38, + "high": 1524.58, + "low": 1517.86, + "close": 1521.06 + }, + { + "open": 1521.07, + "high": 1530.49, + "low": 1515.75, + "close": 1526.14 + }, + { + "open": 1526.13, + "high": 1526.98, + "low": 1521.57, + "close": 1523.57 + }, + { + "open": 1523.56, + "high": 1523.68, + "low": 1520.39, + "close": 1521.17 + }, + { + "open": 1521.18, + "high": 1521.36, + "low": 1516.78, + "close": 1516.79 + }, + { + "open": 1516.79, + "high": 1521.81, + "low": 1516.48, + "close": 1520.2 + }, + { + "open": 1520.2, + "high": 1524.79, + "low": 1516.97, + "close": 1523.67 + }, + { + "open": 1523.67, + "high": 1527.82, + "low": 1522.79, + "close": 1525.77 + }, + { + "open": 1525.77, + "high": 1527.68, + "low": 1520.25, + "close": 1524.24 + }, + { + "open": 1524.23, + "high": 1530.88, + "low": 1523.28, + "close": 1529.87 + }, + { + "open": 1529.88, + "high": 1532.92, + "low": 1527.6, + "close": 1530.66 + }, + { + "open": 1530.65, + "high": 1531.32, + "low": 1528.31, + "close": 1530.85 + }, + { + "open": 1530.85, + "high": 1532.99, + "low": 1527.78, + "close": 1528.68 + }, + { + "open": 1528.69, + "high": 1529.92, + "low": 1527.13, + "close": 1527.14 + }, + { + "open": 1527.13, + "high": 1527.14, + "low": 1518.31, + "close": 1521.16 + }, + { + "open": 1521.16, + "high": 1530.26, + "low": 1521.15, + "close": 1526.67 + }, + { + "open": 1526.68, + "high": 1528.17, + "low": 1522.22, + "close": 1522.33 + }, + { + "open": 1522.33, + "high": 1526.23, + "low": 1521.09, + "close": 1523.59 + }, + { + "open": 1523.59, + "high": 1523.99, + "low": 1517.48, + "close": 1518.86 + }, + { + "open": 1518.85, + "high": 1523.43, + "low": 1513.25, + "close": 1521.57 + }, + { + "open": 1521.58, + "high": 1521.58, + "low": 1511.11, + "close": 1513.4 + }, + { + "open": 1513.4, + "high": 1515.26, + "low": 1507.7, + "close": 1508.31 + }, + { + "open": 1508.31, + "high": 1512.48, + "low": 1503.49, + "close": 1505.89 + }, + { + "open": 1505.88, + "high": 1509.76, + "low": 1494.63, + "close": 1500.13 + }, + { + "open": 1500.13, + "high": 1510.52, + "low": 1498.39, + "close": 1507.22 + }, + { + "open": 1507.21, + "high": 1508, + "low": 1495.51, + "close": 1501.06 + }, + { + "open": 1501.06, + "high": 1506.84, + "low": 1500.04, + "close": 1504.99 + }, + { + "open": 1505, + "high": 1507.4, + "low": 1497.16, + "close": 1498.46 + }, + { + "open": 1498.46, + "high": 1505.37, + "low": 1495, + "close": 1501.44 + }, + { + "open": 1501.36, + "high": 1504.4, + "low": 1500.27, + "close": 1500.55 + }, + { + "open": 1500.55, + "high": 1502.52, + "low": 1496.63, + "close": 1501.29 + }, + { + "open": 1501.29, + "high": 1501.88, + "low": 1496, + "close": 1496.37 + }, + { + "open": 1496.37, + "high": 1506.67, + "low": 1488, + "close": 1505.21 + }, + { + "open": 1505.21, + "high": 1508.6, + "low": 1502.24, + "close": 1508 + }, + { + "open": 1507.99, + "high": 1514.07, + "low": 1507.03, + "close": 1512.2 + }, + { + "open": 1512.2, + "high": 1513.73, + "low": 1510.89, + "close": 1512.64 + }, + { + "open": 1512.65, + "high": 1514.52, + "low": 1508.88, + "close": 1513.17 + }, + { + "open": 1513.17, + "high": 1513.95, + "low": 1511.68, + "close": 1511.94 + }, + { + "open": 1511.94, + "high": 1512.69, + "low": 1508, + "close": 1508.97 + }, + { + "open": 1508.97, + "high": 1511.92, + "low": 1508, + "close": 1511.71 + }, + { + "open": 1511.71, + "high": 1512.21, + "low": 1502.06, + "close": 1502.3 + }, + { + "open": 1502.29, + "high": 1505.5, + "low": 1499.75, + "close": 1503.8 + }, + { + "open": 1503.8, + "high": 1510.52, + "low": 1497.04, + "close": 1499.02 + }, + { + "open": 1499.03, + "high": 1500.56, + "low": 1497.35, + "close": 1499.88 + }, + { + "open": 1499.88, + "high": 1507.12, + "low": 1498.43, + "close": 1499 + }, + { + "open": 1498.99, + "high": 1501.4, + "low": 1489.93, + "close": 1493.7 + }, + { + "open": 1493.71, + "high": 1495.73, + "low": 1490.72, + "close": 1493.73 + }, + { + "open": 1493.72, + "high": 1495.82, + "low": 1492.44, + "close": 1493.23 + }, + { + "open": 1493.23, + "high": 1501.75, + "low": 1493.06, + "close": 1501.54 + }, + { + "open": 1501.54, + "high": 1506.81, + "low": 1500.45, + "close": 1506.61 + }, + { + "open": 1506.6, + "high": 1507.9, + "low": 1505.1, + "close": 1505.95 + }, + { + "open": 1505.95, + "high": 1509.42, + "low": 1505.69, + "close": 1508.9 + }, + { + "open": 1508.9, + "high": 1516.09, + "low": 1508.3, + "close": 1513.84 + }, + { + "open": 1513.83, + "high": 1516.35, + "low": 1510.74, + "close": 1512 + }, + { + "open": 1512, + "high": 1516.35, + "low": 1511.43, + "close": 1513.33 + }, + { + "open": 1513.25, + "high": 1518.68, + "low": 1511.56, + "close": 1517.19 + }, + { + "open": 1517.18, + "high": 1524.05, + "low": 1516.45, + "close": 1517.64 + }, + { + "open": 1517.64, + "high": 1519.51, + "low": 1514.37, + "close": 1518.03 + }, + { + "open": 1518.04, + "high": 1520.21, + "low": 1516.06, + "close": 1518.91 + }, + { + "open": 1518.91, + "high": 1519.8, + "low": 1516.06, + "close": 1518.1 + }, + { + "open": 1518.11, + "high": 1518.11, + "low": 1515.6, + "close": 1516.43 + }, + { + "open": 1516.43, + "high": 1521.28, + "low": 1515.79, + "close": 1519.82 + }, + { + "open": 1519.81, + "high": 1519.98, + "low": 1518.42, + "close": 1519.68 + }, + { + "open": 1519.69, + "high": 1521.68, + "low": 1518.67, + "close": 1520.28 + }, + { + "open": 1520.29, + "high": 1521.65, + "low": 1519.08, + "close": 1520.24 + }, + { + "open": 1520.25, + "high": 1527.76, + "low": 1520.24, + "close": 1526.28 + }, + { + "open": 1526.27, + "high": 1526.99, + "low": 1522.67, + "close": 1525.11 + }, + { + "open": 1525.1, + "high": 1529.05, + "low": 1523.62, + "close": 1525.51 + }, + { + "open": 1525.5, + "high": 1525.51, + "low": 1520.4, + "close": 1521.62 + }, + { + "open": 1521.62, + "high": 1525.97, + "low": 1521.59, + "close": 1523.41 + }, + { + "open": 1523.42, + "high": 1524.16, + "low": 1523, + "close": 1523.81 + }, + { + "open": 1523.8, + "high": 1523.99, + "low": 1522, + "close": 1523.43 + }, + { + "open": 1523.42, + "high": 1524.99, + "low": 1523.42, + "close": 1524.78 + }, + { + "open": 1524.79, + "high": 1525.35, + "low": 1523.06, + "close": 1523.24 + }, + { + "open": 1523.24, + "high": 1523.24, + "low": 1518.44, + "close": 1520.29 + }, + { + "open": 1520.28, + "high": 1521.95, + "low": 1518.01, + "close": 1521.07 + }, + { + "open": 1521.08, + "high": 1521.3, + "low": 1519.22, + "close": 1519.35 + }, + { + "open": 1519.35, + "high": 1519.63, + "low": 1516.3, + "close": 1517.68 + }, + { + "open": 1517.67, + "high": 1518.24, + "low": 1515.23, + "close": 1516.39 + }, + { + "open": 1516.39, + "high": 1520.22, + "low": 1515.31, + "close": 1519.56 + }, + { + "open": 1519.55, + "high": 1524.64, + "low": 1518, + "close": 1522.74 + }, + { + "open": 1522.74, + "high": 1523.93, + "low": 1520.21, + "close": 1520.29 + }, + { + "open": 1520.29, + "high": 1523.26, + "low": 1520.1, + "close": 1522.73 + }, + { + "open": 1522.74, + "high": 1541.63, + "low": 1522.73, + "close": 1539.67 + }, + { + "open": 1539.67, + "high": 1541.92, + "low": 1535.13, + "close": 1538.82 + }, + { + "open": 1538.82, + "high": 1547.2, + "low": 1538.27, + "close": 1545.55 + }, + { + "open": 1545.55, + "high": 1550, + "low": 1543.77, + "close": 1545.59 + }, + { + "open": 1545.6, + "high": 1546.69, + "low": 1539.57, + "close": 1539.68 + }, + { + "open": 1539.67, + "high": 1543.83, + "low": 1538.46, + "close": 1542.91 + }, + { + "open": 1542.91, + "high": 1545.89, + "low": 1542.34, + "close": 1543.44 + }, + { + "open": 1543.43, + "high": 1544.62, + "low": 1541.84, + "close": 1541.85 + }, + { + "open": 1541.85, + "high": 1554.35, + "low": 1539.93, + "close": 1545.74 + }, + { + "open": 1545.77, + "high": 1554.47, + "low": 1545, + "close": 1549.46 + }, + { + "open": 1549.46, + "high": 1552.24, + "low": 1549.45, + "close": 1551.24 + }, + { + "open": 1551.25, + "high": 1554.87, + "low": 1550.63, + "close": 1551.87 + }, + { + "open": 1551.86, + "high": 1553.53, + "low": 1545.58, + "close": 1546.72 + }, + { + "open": 1546.71, + "high": 1552.74, + "low": 1546.65, + "close": 1551.27 + }, + { + "open": 1551.26, + "high": 1555.26, + "low": 1549.71, + "close": 1551.45 + }, + { + "open": 1551.44, + "high": 1553.45, + "low": 1548.31, + "close": 1548.54 + }, + { + "open": 1548.54, + "high": 1549.16, + "low": 1546.57, + "close": 1547.39 + }, + { + "open": 1547.38, + "high": 1549.99, + "low": 1546.86, + "close": 1548.82 + }, + { + "open": 1548.82, + "high": 1554.04, + "low": 1544.92, + "close": 1552.11 + }, + { + "open": 1552.11, + "high": 1553.01, + "low": 1548.42, + "close": 1548.67 + }, + { + "open": 1548.66, + "high": 1577.24, + "low": 1548.66, + "close": 1568.11 + }, + { + "open": 1568.11, + "high": 1569.11, + "low": 1562, + "close": 1563.15 + }, + { + "open": 1563.16, + "high": 1572.49, + "low": 1562.7, + "close": 1566.75 + }, + { + "open": 1566.76, + "high": 1567.67, + "low": 1563.83, + "close": 1564.03 + }, + { + "open": 1564.03, + "high": 1566.14, + "low": 1561.79, + "close": 1563.28 + }, + { + "open": 1563.27, + "high": 1569.75, + "low": 1562.43, + "close": 1569.75 + }, + { + "open": 1569.75, + "high": 1571.84, + "low": 1566.17, + "close": 1569.27 + }, + { + "open": 1569.27, + "high": 1569.28, + "low": 1563.78, + "close": 1563.84 + }, + { + "open": 1563.84, + "high": 1565.98, + "low": 1563.84, + "close": 1564.01 + }, + { + "open": 1564.01, + "high": 1566, + "low": 1562.21, + "close": 1563.5 + }, + { + "open": 1563.51, + "high": 1566.46, + "low": 1562.51, + "close": 1564.33 + }, + { + "open": 1564.34, + "high": 1566.17, + "low": 1564.07, + "close": 1565.09 + }, + { + "open": 1565.09, + "high": 1570.37, + "low": 1565.09, + "close": 1567.3 + }, + { + "open": 1567.29, + "high": 1567.3, + "low": 1564.01, + "close": 1564.01 + }, + { + "open": 1564.01, + "high": 1564.08, + "low": 1560.55, + "close": 1560.71 + }, + { + "open": 1560.71, + "high": 1565.8, + "low": 1560.71, + "close": 1563.98 + }, + { + "open": 1563.97, + "high": 1566.38, + "low": 1563.86, + "close": 1564.27 + }, + { + "open": 1564.28, + "high": 1564.71, + "low": 1560.12, + "close": 1561.13 + }, + { + "open": 1561.13, + "high": 1561.66, + "low": 1551.32, + "close": 1556.13 + }, + { + "open": 1556.13, + "high": 1562.78, + "low": 1549.89, + "close": 1559.92 + }, + { + "open": 1559.92, + "high": 1559.97, + "low": 1545.67, + "close": 1552.36 + }, + { + "open": 1552.37, + "high": 1554.87, + "low": 1549.84, + "close": 1550.13 + }, + { + "open": 1550.12, + "high": 1555.85, + "low": 1549.76, + "close": 1553.41 + }, + { + "open": 1553.41, + "high": 1562.56, + "low": 1553.08, + "close": 1560.89 + }, + { + "open": 1560.9, + "high": 1561.67, + "low": 1556.7, + "close": 1557.88 + }, + { + "open": 1557.87, + "high": 1559.82, + "low": 1555.63, + "close": 1558.56 + }, + { + "open": 1558.56, + "high": 1558.91, + "low": 1555.59, + "close": 1557.08 + }, + { + "open": 1557.09, + "high": 1557.56, + "low": 1554.21, + "close": 1555.63 + }, + { + "open": 1555.62, + "high": 1556.44, + "low": 1553.5, + "close": 1556.34 + }, + { + "open": 1556.34, + "high": 1560.77, + "low": 1555.66, + "close": 1559.82 + }, + { + "open": 1559.83, + "high": 1567.93, + "low": 1559.82, + "close": 1561.87 + }, + { + "open": 1561.88, + "high": 1567.23, + "low": 1561.69, + "close": 1564.58 + }, + { + "open": 1564.58, + "high": 1565.55, + "low": 1561.26, + "close": 1561.58 + }, + { + "open": 1561.58, + "high": 1563.41, + "low": 1557.54, + "close": 1557.74 + }, + { + "open": 1557.73, + "high": 1559.22, + "low": 1556.8, + "close": 1557.69 + }, + { + "open": 1557.7, + "high": 1565.46, + "low": 1557.69, + "close": 1565.18 + }, + { + "open": 1565.18, + "high": 1566.39, + "low": 1563.34, + "close": 1564.35 + }, + { + "open": 1564.35, + "high": 1565.13, + "low": 1561.71, + "close": 1561.71 + }, + { + "open": 1561.72, + "high": 1561.85, + "low": 1557.41, + "close": 1558.38 + }, + { + "open": 1558.38, + "high": 1559.17, + "low": 1552.3, + "close": 1554.71 + }, + { + "open": 1554.7, + "high": 1555.89, + "low": 1552.21, + "close": 1553.36 + }, + { + "open": 1553.35, + "high": 1556.24, + "low": 1551.78, + "close": 1555.12 + }, + { + "open": 1555.12, + "high": 1557.49, + "low": 1553.78, + "close": 1554.54 + }, + { + "open": 1554.54, + "high": 1554.55, + "low": 1545.75, + "close": 1550.29 + }, + { + "open": 1550.29, + "high": 1554.52, + "low": 1549.21, + "close": 1552.37 + }, + { + "open": 1552.38, + "high": 1554.16, + "low": 1551.93, + "close": 1552.33 + }, + { + "open": 1552.33, + "high": 1553.41, + "low": 1551.41, + "close": 1551.65 + }, + { + "open": 1551.65, + "high": 1552.49, + "low": 1551, + "close": 1551.51 + }, + { + "open": 1551.51, + "high": 1556.79, + "low": 1550.86, + "close": 1553.86 + }, + { + "open": 1553.85, + "high": 1557.95, + "low": 1553.28, + "close": 1555.2 + }, + { + "open": 1555.19, + "high": 1555.45, + "low": 1546.9, + "close": 1553.41 + }, + { + "open": 1553.4, + "high": 1554.25, + "low": 1551.34, + "close": 1551.35 + }, + { + "open": 1551.35, + "high": 1553.57, + "low": 1551.1, + "close": 1551.67 + }, + { + "open": 1551.67, + "high": 1555.66, + "low": 1550.68, + "close": 1554.05 + }, + { + "open": 1554.09, + "high": 1560.4, + "low": 1554.09, + "close": 1559.55 + }, + { + "open": 1559.56, + "high": 1561.81, + "low": 1558.47, + "close": 1561.8 + }, + { + "open": 1561.81, + "high": 1561.81, + "low": 1558.59, + "close": 1559.39 + }, + { + "open": 1559.39, + "high": 1560.98, + "low": 1558.91, + "close": 1558.95 + }, + { + "open": 1558.96, + "high": 1563.63, + "low": 1557.85, + "close": 1558.69 + }, + { + "open": 1558.7, + "high": 1561.62, + "low": 1556.87, + "close": 1561.25 + }, + { + "open": 1561.25, + "high": 1572, + "low": 1560.1, + "close": 1564.23 + }, + { + "open": 1564.22, + "high": 1565.96, + "low": 1563.01, + "close": 1564.81 + }, + { + "open": 1564.81, + "high": 1579, + "low": 1563.25, + "close": 1577.63 + }, + { + "open": 1577.63, + "high": 1592.55, + "low": 1575.76, + "close": 1591.16 + }, + { + "open": 1591.17, + "high": 1603.78, + "low": 1590.69, + "close": 1594.31 + }, + { + "open": 1594.31, + "high": 1600.91, + "low": 1593.95, + "close": 1594.48 + }, + { + "open": 1594.47, + "high": 1599.53, + "low": 1589.52, + "close": 1590.67 + }, + { + "open": 1590.66, + "high": 1597.42, + "low": 1586.33, + "close": 1597.12 + }, + { + "open": 1597.12, + "high": 1608.5, + "low": 1596.08, + "close": 1607.22 + }, + { + "open": 1607.23, + "high": 1608.55, + "low": 1601.27, + "close": 1602.19 + }, + { + "open": 1602.19, + "high": 1604.2, + "low": 1599.23, + "close": 1601.92 + }, + { + "open": 1601.92, + "high": 1603.39, + "low": 1599.15, + "close": 1601.33 + }, + { + "open": 1601.33, + "high": 1604.92, + "low": 1600.37, + "close": 1604.71 + }, + { + "open": 1604.72, + "high": 1604.8, + "low": 1600.12, + "close": 1600.68 + }, + { + "open": 1600.69, + "high": 1605.89, + "low": 1600.23, + "close": 1604.66 + }, + { + "open": 1604.66, + "high": 1619.05, + "low": 1604.36, + "close": 1607.94 + }, + { + "open": 1607.95, + "high": 1613.84, + "low": 1605.04, + "close": 1612.56 + }, + { + "open": 1612.57, + "high": 1619.78, + "low": 1611.98, + "close": 1618.5 + }, + { + "open": 1618.49, + "high": 1619.35, + "low": 1612, + "close": 1614.67 + }, + { + "open": 1614.67, + "high": 1614.68, + "low": 1608.16, + "close": 1608.9 + }, + { + "open": 1608.89, + "high": 1612.96, + "low": 1608.89, + "close": 1610.35 + }, + { + "open": 1610.36, + "high": 1615.02, + "low": 1610.23, + "close": 1613.92 + }, + { + "open": 1613.91, + "high": 1614.82, + "low": 1611.6, + "close": 1612.52 + }, + { + "open": 1612.51, + "high": 1613.49, + "low": 1606.76, + "close": 1610.14 + }, + { + "open": 1610.14, + "high": 1615.15, + "low": 1608.17, + "close": 1613.23 + }, + { + "open": 1613.23, + "high": 1619.99, + "low": 1613.23, + "close": 1615.83 + }, + { + "open": 1615.84, + "high": 1617.05, + "low": 1610.28, + "close": 1610.39 + }, + { + "open": 1610.39, + "high": 1612.38, + "low": 1606.69, + "close": 1608.64 + }, + { + "open": 1608.64, + "high": 1611.02, + "low": 1608.16, + "close": 1610.05 + }, + { + "open": 1610.05, + "high": 1612, + "low": 1607.59, + "close": 1608.4 + }, + { + "open": 1608.39, + "high": 1609.76, + "low": 1603.32, + "close": 1603.65 + }, + { + "open": 1603.66, + "high": 1606.98, + "low": 1603.38, + "close": 1605.03 + }, + { + "open": 1605.03, + "high": 1611.78, + "low": 1605.03, + "close": 1610.29 + }, + { + "open": 1610.29, + "high": 1611.76, + "low": 1608.83, + "close": 1609.91 + }, + { + "open": 1609.9, + "high": 1609.93, + "low": 1604.31, + "close": 1605.01 + }, + { + "open": 1605.01, + "high": 1606.99, + "low": 1604.35, + "close": 1604.44 + }, + { + "open": 1604.44, + "high": 1607.45, + "low": 1599.66, + "close": 1601.09 + }, + { + "open": 1601.09, + "high": 1605, + "low": 1599.56, + "close": 1603.39 + }, + { + "open": 1603.39, + "high": 1604.36, + "low": 1601.89, + "close": 1604.17 + }, + { + "open": 1604.17, + "high": 1604.4, + "low": 1601.11, + "close": 1601.82 + }, + { + "open": 1601.82, + "high": 1602.58, + "low": 1597, + "close": 1598.54 + }, + { + "open": 1598.55, + "high": 1599.26, + "low": 1595, + "close": 1597.39 + }, + { + "open": 1597.4, + "high": 1599.58, + "low": 1595.34, + "close": 1595.5 + }, + { + "open": 1595.49, + "high": 1597.72, + "low": 1594, + "close": 1596.51 + }, + { + "open": 1596.5, + "high": 1608.06, + "low": 1596.03, + "close": 1605.83 + }, + { + "open": 1605.84, + "high": 1610.01, + "low": 1602.77, + "close": 1603.14 + }, + { + "open": 1603.13, + "high": 1605.96, + "low": 1602.02, + "close": 1605.76 + }, + { + "open": 1605.76, + "high": 1606.06, + "low": 1602.56, + "close": 1603.5 + }, + { + "open": 1603.5, + "high": 1608.17, + "low": 1603, + "close": 1606.46 + }, + { + "open": 1606.47, + "high": 1606.57, + "low": 1600.37, + "close": 1600.49 + }, + { + "open": 1600.49, + "high": 1603.15, + "low": 1599, + "close": 1602.38 + }, + { + "open": 1602.38, + "high": 1605.76, + "low": 1602.33, + "close": 1604.24 + }, + { + "open": 1604.24, + "high": 1613.6, + "low": 1603.63, + "close": 1608.86 + }, + { + "open": 1608.85, + "high": 1608.86, + "low": 1605.31, + "close": 1607.69 + }, + { + "open": 1607.69, + "high": 1611.81, + "low": 1606.26, + "close": 1606.85 + }, + { + "open": 1606.85, + "high": 1607.62, + "low": 1602.87, + "close": 1603.66 + }, + { + "open": 1603.67, + "high": 1603.9, + "low": 1600.29, + "close": 1602.19 + }, + { + "open": 1602.19, + "high": 1602.64, + "low": 1598.88, + "close": 1599.6 + }, + { + "open": 1599.6, + "high": 1602.21, + "low": 1599.59, + "close": 1601.28 + }, + { + "open": 1601.29, + "high": 1602.42, + "low": 1596.21, + "close": 1598 + }, + { + "open": 1598, + "high": 1600, + "low": 1597, + "close": 1599.99 + }, + { + "open": 1600, + "high": 1600.46, + "low": 1598.05, + "close": 1598.1 + }, + { + "open": 1598.11, + "high": 1600.46, + "low": 1597.24, + "close": 1598.62 + }, + { + "open": 1598.62, + "high": 1604.32, + "low": 1597.61, + "close": 1599.57 + }, + { + "open": 1599.58, + "high": 1603.67, + "low": 1599.05, + "close": 1602.84 + }, + { + "open": 1602.84, + "high": 1603.41, + "low": 1601.33, + "close": 1601.71 + }, + { + "open": 1601.72, + "high": 1604.38, + "low": 1600.84, + "close": 1601.06 + }, + { + "open": 1601.07, + "high": 1601.07, + "low": 1584.25, + "close": 1585.56 + }, + { + "open": 1585.57, + "high": 1590.35, + "low": 1583, + "close": 1583.3 + }, + { + "open": 1583.31, + "high": 1585.59, + "low": 1582, + "close": 1582.99 + }, + { + "open": 1582.99, + "high": 1587.47, + "low": 1580, + "close": 1585 + }, + { + "open": 1584.87, + "high": 1586.53, + "low": 1584.36, + "close": 1585.54 + }, + { + "open": 1585.53, + "high": 1592, + "low": 1583.94, + "close": 1590.4 + }, + { + "open": 1590.41, + "high": 1591.7, + "low": 1587.77, + "close": 1591.67 + }, + { + "open": 1591.67, + "high": 1627.93, + "low": 1591.67, + "close": 1619.34 + }, + { + "open": 1619.35, + "high": 1627.28, + "low": 1615.54, + "close": 1620.06 + }, + { + "open": 1620.07, + "high": 1627.91, + "low": 1616.57, + "close": 1618.04 + }, + { + "open": 1618.03, + "high": 1622.4, + "low": 1617.04, + "close": 1620.09 + }, + { + "open": 1620.09, + "high": 1628.86, + "low": 1615.37, + "close": 1615.63 + }, + { + "open": 1615.63, + "high": 1622.18, + "low": 1615.37, + "close": 1621.29 + }, + { + "open": 1621.3, + "high": 1622.8, + "low": 1620, + "close": 1620.51 + }, + { + "open": 1620.5, + "high": 1621.89, + "low": 1613.17, + "close": 1615.52 + }, + { + "open": 1615.53, + "high": 1617.4, + "low": 1614.06, + "close": 1615.32 + }, + { + "open": 1615.33, + "high": 1620.03, + "low": 1615.32, + "close": 1615.85 + }, + { + "open": 1615.84, + "high": 1619.22, + "low": 1603.38, + "close": 1606.41 + }, + { + "open": 1606.41, + "high": 1615.27, + "low": 1606.33, + "close": 1614.67 + }, + { + "open": 1614.68, + "high": 1618.39, + "low": 1614, + "close": 1617.37 + }, + { + "open": 1617.38, + "high": 1620.43, + "low": 1615.78, + "close": 1618.73 + }, + { + "open": 1618.72, + "high": 1618.73, + "low": 1610.77, + "close": 1611.04 + }, + { + "open": 1611.03, + "high": 1614.99, + "low": 1611.03, + "close": 1612.59 + }, + { + "open": 1612.59, + "high": 1613.22, + "low": 1605.77, + "close": 1606.25 + }, + { + "open": 1606.25, + "high": 1608.57, + "low": 1604.04, + "close": 1606.66 + }, + { + "open": 1606.66, + "high": 1609.22, + "low": 1594.74, + "close": 1596.49 + }, + { + "open": 1596.48, + "high": 1600.36, + "low": 1596.16, + "close": 1597.82 + }, + { + "open": 1597.82, + "high": 1598.27, + "low": 1586.09, + "close": 1587.01 + }, + { + "open": 1587.01, + "high": 1589.01, + "low": 1576.53, + "close": 1578.03 + }, + { + "open": 1578.03, + "high": 1583.88, + "low": 1575, + "close": 1579.57 + }, + { + "open": 1579.57, + "high": 1585.38, + "low": 1578.04, + "close": 1584.92 + }, + { + "open": 1584.92, + "high": 1585.58, + "low": 1579.7, + "close": 1584.64 + }, + { + "open": 1584.64, + "high": 1585.69, + "low": 1580.78, + "close": 1582.9 + }, + { + "open": 1582.91, + "high": 1582.91, + "low": 1576.04, + "close": 1578.85 + }, + { + "open": 1578.85, + "high": 1584.74, + "low": 1578.84, + "close": 1584.44 + }, + { + "open": 1584.43, + "high": 1584.94, + "low": 1575, + "close": 1579.18 + }, + { + "open": 1579.18, + "high": 1583.31, + "low": 1579.09, + "close": 1579.83 + }, + { + "open": 1579.83, + "high": 1582.08, + "low": 1573, + "close": 1577.81 + }, + { + "open": 1577.8, + "high": 1582.09, + "low": 1576.19, + "close": 1581.71 + }, + { + "open": 1581.71, + "high": 1582.97, + "low": 1579.21, + "close": 1579.75 + }, + { + "open": 1579.74, + "high": 1583.99, + "low": 1579.52, + "close": 1581.78 + }, + { + "open": 1581.79, + "high": 1583.99, + "low": 1580.27, + "close": 1580.67 + }, + { + "open": 1580.68, + "high": 1589.57, + "low": 1573.86, + "close": 1582.09 + }, + { + "open": 1582.09, + "high": 1586.32, + "low": 1578.54, + "close": 1581.81 + }, + { + "open": 1581.81, + "high": 1588.44, + "low": 1581.8, + "close": 1587.46 + }, + { + "open": 1587.46, + "high": 1618, + "low": 1587.45, + "close": 1611.24 + }, + { + "open": 1611.24, + "high": 1614.26, + "low": 1605.36, + "close": 1612.02 + }, + { + "open": 1612.02, + "high": 1616, + "low": 1608.85, + "close": 1610.58 + }, + { + "open": 1610.59, + "high": 1612.66, + "low": 1607.85, + "close": 1609.51 + }, + { + "open": 1609.51, + "high": 1611.59, + "low": 1607.13, + "close": 1610.04 + }, + { + "open": 1610.04, + "high": 1610.16, + "low": 1603.11, + "close": 1609.25 + }, + { + "open": 1609.25, + "high": 1617.77, + "low": 1605.15, + "close": 1611.78 + }, + { + "open": 1611.77, + "high": 1612.94, + "low": 1608.89, + "close": 1610.54 + }, + { + "open": 1610.53, + "high": 1610.79, + "low": 1607.76, + "close": 1609.46 + }, + { + "open": 1609.46, + "high": 1611.35, + "low": 1607.06, + "close": 1608.93 + }, + { + "open": 1608.92, + "high": 1621.42, + "low": 1608.92, + "close": 1615.58 + }, + { + "open": 1615.59, + "high": 1617.94, + "low": 1609.66, + "close": 1610.42 + }, + { + "open": 1610.41, + "high": 1613.09, + "low": 1607.94, + "close": 1610.04 + }, + { + "open": 1610.03, + "high": 1612.39, + "low": 1608.86, + "close": 1612.38 + }, + { + "open": 1612.39, + "high": 1612.39, + "low": 1606.68, + "close": 1607.45 + }, + { + "open": 1607.45, + "high": 1608.6, + "low": 1603.5, + "close": 1606.03 + }, + { + "open": 1606.03, + "high": 1608.42, + "low": 1605.16, + "close": 1606.46 + }, + { + "open": 1606.46, + "high": 1609.6, + "low": 1605.99, + "close": 1608.61 + }, + { + "open": 1608.61, + "high": 1611.82, + "low": 1608.6, + "close": 1609.35 + }, + { + "open": 1609.36, + "high": 1613.69, + "low": 1608.97, + "close": 1612.61 + }, + { + "open": 1612.61, + "high": 1615.38, + "low": 1600.05, + "close": 1605.29 + }, + { + "open": 1605.29, + "high": 1610.87, + "low": 1594.48, + "close": 1595.84 + }, + { + "open": 1595.85, + "high": 1600.32, + "low": 1593.78, + "close": 1595.82 + }, + { + "open": 1595.81, + "high": 1596.14, + "low": 1588.38, + "close": 1590.05 + }, + { + "open": 1590.05, + "high": 1595, + "low": 1587.23, + "close": 1590.98 + }, + { + "open": 1590.98, + "high": 1590.99, + "low": 1584.71, + "close": 1587.17 + }, + { + "open": 1587.16, + "high": 1591.85, + "low": 1583.51, + "close": 1590.73 + }, + { + "open": 1590.74, + "high": 1594.04, + "low": 1590, + "close": 1592.56 + }, + { + "open": 1592.55, + "high": 1596.8, + "low": 1591.81, + "close": 1593.82 + }, + { + "open": 1593.82, + "high": 1594.8, + "low": 1588.17, + "close": 1590.81 + }, + { + "open": 1590.81, + "high": 1593.02, + "low": 1590.45, + "close": 1592.96 + }, + { + "open": 1592.96, + "high": 1593.35, + "low": 1590, + "close": 1591.3 + }, + { + "open": 1591.31, + "high": 1594.96, + "low": 1590.04, + "close": 1593.73 + }, + { + "open": 1593.74, + "high": 1594.35, + "low": 1592.3, + "close": 1593.39 + }, + { + "open": 1593.4, + "high": 1607.22, + "low": 1593, + "close": 1602.44 + }, + { + "open": 1602.45, + "high": 1611.36, + "low": 1602.44, + "close": 1608.78 + }, + { + "open": 1608.79, + "high": 1613.99, + "low": 1608, + "close": 1610.29 + }, + { + "open": 1610.27, + "high": 1610.8, + "low": 1605.45, + "close": 1607.02 + }, + { + "open": 1607.01, + "high": 1607.17, + "low": 1591.77, + "close": 1594.46 + }, + { + "open": 1594.46, + "high": 1598.08, + "low": 1593.34, + "close": 1596.43 + }, + { + "open": 1596.43, + "high": 1606.97, + "low": 1596.42, + "close": 1606.51 + }, + { + "open": 1606.51, + "high": 1606.51, + "low": 1600, + "close": 1601.63 + }, + { + "open": 1601.63, + "high": 1604.92, + "low": 1600.43, + "close": 1603.69 + }, + { + "open": 1603.68, + "high": 1604.44, + "low": 1600.92, + "close": 1603.73 + }, + { + "open": 1603.73, + "high": 1604.5, + "low": 1596.63, + "close": 1599.85 + }, + { + "open": 1599.86, + "high": 1603.51, + "low": 1597.73, + "close": 1601.35 + }, + { + "open": 1601.35, + "high": 1603.01, + "low": 1598.18, + "close": 1599.56 + }, + { + "open": 1599.57, + "high": 1606, + "low": 1598.74, + "close": 1605.58 + }, + { + "open": 1605.57, + "high": 1605.59, + "low": 1600.41, + "close": 1600.46 + }, + { + "open": 1600.45, + "high": 1602.81, + "low": 1599.72, + "close": 1601.36 + }, + { + "open": 1601.36, + "high": 1602.24, + "low": 1595.8, + "close": 1596.99 + }, + { + "open": 1597, + "high": 1599, + "low": 1590.72, + "close": 1596.49 + }, + { + "open": 1596.49, + "high": 1597.31, + "low": 1593.48, + "close": 1593.61 + }, + { + "open": 1593.62, + "high": 1598.96, + "low": 1593, + "close": 1597.03 + }, + { + "open": 1597.02, + "high": 1598, + "low": 1594.17, + "close": 1596.97 + }, + { + "open": 1596.96, + "high": 1599.64, + "low": 1595.32, + "close": 1597.92 + }, + { + "open": 1597.91, + "high": 1600.45, + "low": 1597.28, + "close": 1597.64 + }, + { + "open": 1597.65, + "high": 1599.3, + "low": 1593.85, + "close": 1596.34 + }, + { + "open": 1596.34, + "high": 1607.62, + "low": 1595.76, + "close": 1603.49 + }, + { + "open": 1603.49, + "high": 1606.36, + "low": 1595, + "close": 1596.49 + }, + { + "open": 1596.48, + "high": 1604.91, + "low": 1596.1, + "close": 1604.91 + }, + { + "open": 1604.9, + "high": 1610, + "low": 1602.61, + "close": 1607.95 + }, + { + "open": 1607.94, + "high": 1614.95, + "low": 1607.59, + "close": 1613.14 + }, + { + "open": 1613.14, + "high": 1613.15, + "low": 1610.38, + "close": 1610.46 + }, + { + "open": 1610.46, + "high": 1611.95, + "low": 1607.42, + "close": 1607.63 + }, + { + "open": 1607.62, + "high": 1609.74, + "low": 1603.26, + "close": 1603.53 + }, + { + "open": 1603.53, + "high": 1604.18, + "low": 1599.56, + "close": 1600.77 + }, + { + "open": 1600.78, + "high": 1604.25, + "low": 1600.77, + "close": 1602.76 + }, + { + "open": 1602.75, + "high": 1604.05, + "low": 1601.46, + "close": 1601.89 + }, + { + "open": 1601.89, + "high": 1606.86, + "low": 1601.01, + "close": 1603.48 + }, + { + "open": 1603.49, + "high": 1605.52, + "low": 1600.24, + "close": 1600.55 + }, + { + "open": 1600.55, + "high": 1603.27, + "low": 1598.87, + "close": 1601.6 + }, + { + "open": 1601.6, + "high": 1605.65, + "low": 1600.97, + "close": 1605 + }, + { + "open": 1605, + "high": 1607.97, + "low": 1602.92, + "close": 1604.44 + }, + { + "open": 1604.44, + "high": 1606.44, + "low": 1602.58, + "close": 1605.59 + }, + { + "open": 1605.58, + "high": 1607.69, + "low": 1604.75, + "close": 1607.22 + }, + { + "open": 1607.23, + "high": 1620.12, + "low": 1607.17, + "close": 1618.72 + }, + { + "open": 1618.71, + "high": 1618.86, + "low": 1611.1, + "close": 1613.18 + }, + { + "open": 1613.19, + "high": 1613.72, + "low": 1610.16, + "close": 1612.13 + }, + { + "open": 1612.1, + "high": 1612.64, + "low": 1608.05, + "close": 1608.25 + }, + { + "open": 1608.25, + "high": 1617.11, + "low": 1608, + "close": 1614.17 + }, + { + "open": 1614.16, + "high": 1614.64, + "low": 1607.77, + "close": 1613.48 + }, + { + "open": 1613.48, + "high": 1625, + "low": 1612.54, + "close": 1618.6 + }, + { + "open": 1618.6, + "high": 1622.92, + "low": 1615.17, + "close": 1622.83 + }, + { + "open": 1622.84, + "high": 1626.94, + "low": 1620.14, + "close": 1621.06 + }, + { + "open": 1621.07, + "high": 1621.87, + "low": 1617.82, + "close": 1619.46 + }, + { + "open": 1619.45, + "high": 1634.2, + "low": 1618.5, + "close": 1627.23 + }, + { + "open": 1627.23, + "high": 1627.39, + "low": 1620.18, + "close": 1622.36 + }, + { + "open": 1622.36, + "high": 1626.93, + "low": 1616.66, + "close": 1620.82 + }, + { + "open": 1620.82, + "high": 1623.84, + "low": 1618.85, + "close": 1622.1 + }, + { + "open": 1622.1, + "high": 1622.15, + "low": 1618.52, + "close": 1620.43 + }, + { + "open": 1620.42, + "high": 1625, + "low": 1619.49, + "close": 1622 + }, + { + "open": 1622, + "high": 1625.99, + "low": 1615.31, + "close": 1617.81 + }, + { + "open": 1617.81, + "high": 1627, + "low": 1616.03, + "close": 1624.35 + }, + { + "open": 1624.36, + "high": 1629.49, + "low": 1624.35, + "close": 1626.81 + }, + { + "open": 1626.81, + "high": 1628.9, + "low": 1624.37, + "close": 1624.84 + }, + { + "open": 1624.85, + "high": 1628.28, + "low": 1622.99, + "close": 1623.1 + }, + { + "open": 1623.1, + "high": 1627.99, + "low": 1621.44, + "close": 1624.59 + }, + { + "open": 1624.6, + "high": 1626.35, + "low": 1622.57, + "close": 1623.59 + }, + { + "open": 1623.6, + "high": 1642.89, + "low": 1623.3, + "close": 1640.62 + }, + { + "open": 1640.63, + "high": 1661.98, + "low": 1634.91, + "close": 1660.48 + }, + { + "open": 1660.49, + "high": 1664.34, + "low": 1644.84, + "close": 1645.57 + }, + { + "open": 1645.58, + "high": 1646.67, + "low": 1595, + "close": 1608.22 + }, + { + "open": 1608.22, + "high": 1614.88, + "low": 1601.43, + "close": 1605.36 + }, + { + "open": 1605.36, + "high": 1609.8, + "low": 1591.92, + "close": 1592.38 + }, + { + "open": 1592.39, + "high": 1602.45, + "low": 1589.01, + "close": 1600 + }, + { + "open": 1600, + "high": 1607.26, + "low": 1599, + "close": 1606.64 + }, + { + "open": 1606.64, + "high": 1607.7, + "low": 1594.71, + "close": 1598.66 + }, + { + "open": 1598.66, + "high": 1604.57, + "low": 1595.3, + "close": 1602.48 + }, + { + "open": 1602.48, + "high": 1612.55, + "low": 1601.32, + "close": 1610.62 + }, + { + "open": 1610.63, + "high": 1615.92, + "low": 1608.01, + "close": 1610.8 + }, + { + "open": 1610.79, + "high": 1612.97, + "low": 1605.89, + "close": 1608.29 + }, + { + "open": 1608.3, + "high": 1609.65, + "low": 1605.11, + "close": 1605.12 + }, + { + "open": 1605.12, + "high": 1607.57, + "low": 1598.35, + "close": 1602.85 + }, + { + "open": 1602.85, + "high": 1602.86, + "low": 1598.44, + "close": 1599.51 + }, + { + "open": 1599.5, + "high": 1600.05, + "low": 1593.32, + "close": 1597.7 + }, + { + "open": 1597.7, + "high": 1603.62, + "low": 1596, + "close": 1598.76 + }, + { + "open": 1598.75, + "high": 1601.76, + "low": 1595.1, + "close": 1600.62 + }, + { + "open": 1600.53, + "high": 1604, + "low": 1596.81, + "close": 1602.64 + }, + { + "open": 1602.63, + "high": 1609.55, + "low": 1602, + "close": 1602.44 + }, + { + "open": 1602.45, + "high": 1603.9, + "low": 1592.51, + "close": 1593.89 + }, + { + "open": 1593.89, + "high": 1594.71, + "low": 1565.67, + "close": 1567.64 + }, + { + "open": 1567.63, + "high": 1570, + "low": 1560, + "close": 1561.39 + }, + { + "open": 1561.4, + "high": 1568.6, + "low": 1560, + "close": 1560.15 + }, + { + "open": 1560.14, + "high": 1567.72, + "low": 1558.34, + "close": 1562.48 + }, + { + "open": 1562.47, + "high": 1567.56, + "low": 1556.87, + "close": 1557.4 + }, + { + "open": 1557.39, + "high": 1562.84, + "low": 1555.8, + "close": 1559.22 + }, + { + "open": 1559.22, + "high": 1560.95, + "low": 1555.83, + "close": 1558.29 + }, + { + "open": 1558.3, + "high": 1558.71, + "low": 1552.58, + "close": 1557.29 + }, + { + "open": 1557.29, + "high": 1558.05, + "low": 1550.73, + "close": 1552.73 + }, + { + "open": 1552.73, + "high": 1556.73, + "low": 1551.93, + "close": 1555.62 + }, + { + "open": 1555.62, + "high": 1558.32, + "low": 1553.89, + "close": 1558.24 + }, + { + "open": 1558.23, + "high": 1560.73, + "low": 1555.85, + "close": 1556.41 + }, + { + "open": 1556.42, + "high": 1557.28, + "low": 1546.5, + "close": 1550.45 + }, + { + "open": 1550.44, + "high": 1553.2, + "low": 1548.64, + "close": 1551.76 + }, + { + "open": 1551.76, + "high": 1553.97, + "low": 1550.33, + "close": 1551.83 + }, + { + "open": 1551.83, + "high": 1551.84, + "low": 1536.1, + "close": 1545.23 + }, + { + "open": 1545.23, + "high": 1545.35, + "low": 1536.02, + "close": 1536.77 + }, + { + "open": 1536.78, + "high": 1545.98, + "low": 1535, + "close": 1545.85 + }, + { + "open": 1545.84, + "high": 1546.92, + "low": 1542.7, + "close": 1544.4 + }, + { + "open": 1544.41, + "high": 1544.41, + "low": 1539.46, + "close": 1542.9 + }, + { + "open": 1542.91, + "high": 1543.88, + "low": 1532.08, + "close": 1537.46 + }, + { + "open": 1537.46, + "high": 1539.81, + "low": 1528.5, + "close": 1539.6 + }, + { + "open": 1539.59, + "high": 1539.91, + "low": 1534.97, + "close": 1537.88 + }, + { + "open": 1537.89, + "high": 1538.41, + "low": 1519.26, + "close": 1529.17 + }, + { + "open": 1529.17, + "high": 1531.96, + "low": 1523.22, + "close": 1530.51 + }, + { + "open": 1530.51, + "high": 1530.53, + "low": 1516.53, + "close": 1516.54 + }, + { + "open": 1516.54, + "high": 1523, + "low": 1515.92, + "close": 1520.8 + }, + { + "open": 1520.8, + "high": 1523.87, + "low": 1519.23, + "close": 1520.13 + }, + { + "open": 1520.12, + "high": 1525.47, + "low": 1519.92, + "close": 1522.6 + }, + { + "open": 1522.59, + "high": 1523.37, + "low": 1518.67, + "close": 1520.02 + }, + { + "open": 1520.01, + "high": 1522.76, + "low": 1517.3, + "close": 1520.03 + }, + { + "open": 1520.03, + "high": 1522.47, + "low": 1514.84, + "close": 1522.2 + }, + { + "open": 1522.19, + "high": 1523.61, + "low": 1519.61, + "close": 1523.36 + }, + { + "open": 1523.36, + "high": 1523.74, + "low": 1520.13, + "close": 1523.27 + }, + { + "open": 1523.27, + "high": 1523.78, + "low": 1513.68, + "close": 1515.15 + }, + { + "open": 1515.14, + "high": 1516.51, + "low": 1508.06, + "close": 1512.14 + }, + { + "open": 1512.15, + "high": 1515.4, + "low": 1511.29, + "close": 1515.02 + }, + { + "open": 1515.02, + "high": 1516.74, + "low": 1512.8, + "close": 1515.19 + }, + { + "open": 1515.19, + "high": 1518.29, + "low": 1514.84, + "close": 1517.85 + }, + { + "open": 1517.85, + "high": 1518.5, + "low": 1509.02, + "close": 1511.14 + }, + { + "open": 1511.14, + "high": 1512.83, + "low": 1509.19, + "close": 1509.79 + }, + { + "open": 1509.78, + "high": 1510, + "low": 1502, + "close": 1504.63 + }, + { + "open": 1504.63, + "high": 1512, + "low": 1498.95, + "close": 1511.51 + }, + { + "open": 1511.51, + "high": 1512.42, + "low": 1506.71, + "close": 1506.71 + }, + { + "open": 1506.71, + "high": 1508.89, + "low": 1504.28, + "close": 1505.15 + }, + { + "open": 1505.15, + "high": 1509.41, + "low": 1499.45, + "close": 1509.01 + }, + { + "open": 1509.01, + "high": 1513.92, + "low": 1507.81, + "close": 1513.12 + }, + { + "open": 1513.12, + "high": 1517.5, + "low": 1512, + "close": 1515.83 + }, + { + "open": 1515.84, + "high": 1515.84, + "low": 1511.78, + "close": 1514.56 + }, + { + "open": 1514.56, + "high": 1514.59, + "low": 1509.96, + "close": 1512.45 + }, + { + "open": 1512.46, + "high": 1514, + "low": 1510.35, + "close": 1513.82 + }, + { + "open": 1513.82, + "high": 1516.09, + "low": 1510.83, + "close": 1513.39 + }, + { + "open": 1513.38, + "high": 1514.71, + "low": 1511.55, + "close": 1514.06 + }, + { + "open": 1514.07, + "high": 1518.42, + "low": 1514.06, + "close": 1517.45 + }, + { + "open": 1517.44, + "high": 1524.17, + "low": 1517.24, + "close": 1522.76 + }, + { + "open": 1522.76, + "high": 1522.77, + "low": 1516.74, + "close": 1519.27 + }, + { + "open": 1519.27, + "high": 1526.14, + "low": 1518.36, + "close": 1525.37 + }, + { + "open": 1525.36, + "high": 1526.16, + "low": 1523.05, + "close": 1524.78 + }, + { + "open": 1524.78, + "high": 1529.84, + "low": 1524.12, + "close": 1524.83 + }, + { + "open": 1524.83, + "high": 1527.97, + "low": 1523.69, + "close": 1525.07 + }, + { + "open": 1525.07, + "high": 1525.91, + "low": 1515.94, + "close": 1517.07 + }, + { + "open": 1517.08, + "high": 1521.93, + "low": 1514.76, + "close": 1519.75 + }, + { + "open": 1519.75, + "high": 1523.8, + "low": 1518.82, + "close": 1522.53 + }, + { + "open": 1522.52, + "high": 1527.71, + "low": 1521.24, + "close": 1527.26 + }, + { + "open": 1527.26, + "high": 1527.26, + "low": 1523.51, + "close": 1524.17 + }, + { + "open": 1524.18, + "high": 1526.33, + "low": 1522.5, + "close": 1526.02 + }, + { + "open": 1526.02, + "high": 1528.99, + "low": 1525.63, + "close": 1528.01 + }, + { + "open": 1528, + "high": 1528.98, + "low": 1524, + "close": 1527.85 + }, + { + "open": 1527.85, + "high": 1527.91, + "low": 1524.87, + "close": 1527.02 + }, + { + "open": 1527.01, + "high": 1527.77, + "low": 1525.08, + "close": 1527.53 + }, + { + "open": 1527.54, + "high": 1527.9, + "low": 1525.01, + "close": 1525.54 + }, + { + "open": 1525.54, + "high": 1526.36, + "low": 1517.7, + "close": 1521.32 + }, + { + "open": 1521.33, + "high": 1523.72, + "low": 1520.06, + "close": 1523.15 + }, + { + "open": 1523.16, + "high": 1526.22, + "low": 1521.86, + "close": 1523.7 + }, + { + "open": 1523.7, + "high": 1523.83, + "low": 1517.23, + "close": 1517.7 + }, + { + "open": 1517.69, + "high": 1519.99, + "low": 1514.45, + "close": 1515.5 + }, + { + "open": 1515.49, + "high": 1520.24, + "low": 1515.12, + "close": 1517.38 + }, + { + "open": 1517.38, + "high": 1519, + "low": 1516.54, + "close": 1517.4 + }, + { + "open": 1517.39, + "high": 1521.31, + "low": 1516.5, + "close": 1519.95 + }, + { + "open": 1519.96, + "high": 1521.47, + "low": 1514.7, + "close": 1519.12 + }, + { + "open": 1519.12, + "high": 1523.72, + "low": 1518.67, + "close": 1523.67 + }, + { + "open": 1523.66, + "high": 1528.82, + "low": 1522.18, + "close": 1528.71 + }, + { + "open": 1528.82, + "high": 1529.12, + "low": 1523.63, + "close": 1525.19 + }, + { + "open": 1525.2, + "high": 1525.2, + "low": 1518.09, + "close": 1523.17 + }, + { + "open": 1523.17, + "high": 1525.99, + "low": 1521.52, + "close": 1522.46 + }, + { + "open": 1522.47, + "high": 1522.9, + "low": 1519.17, + "close": 1521.6 + }, + { + "open": 1521.6, + "high": 1524.29, + "low": 1521.54, + "close": 1524.29 + }, + { + "open": 1524.29, + "high": 1526, + "low": 1523.3, + "close": 1524.49 + }, + { + "open": 1524.5, + "high": 1526, + "low": 1524.05, + "close": 1524.89 + }, + { + "open": 1524.88, + "high": 1526.85, + "low": 1524, + "close": 1524.81 + }, + { + "open": 1524.81, + "high": 1524.81, + "low": 1522.11, + "close": 1522.12 + }, + { + "open": 1522.12, + "high": 1525.61, + "low": 1521.27, + "close": 1521.65 + }, + { + "open": 1521.64, + "high": 1522.47, + "low": 1518.51, + "close": 1519.48 + }, + { + "open": 1519.48, + "high": 1519.48, + "low": 1516.15, + "close": 1517.9 + }, + { + "open": 1517.91, + "high": 1521.05, + "low": 1517.3, + "close": 1519.79 + }, + { + "open": 1519.79, + "high": 1519.8, + "low": 1510.7, + "close": 1513.91 + }, + { + "open": 1513.9, + "high": 1516.09, + "low": 1512.83, + "close": 1513.43 + }, + { + "open": 1513.43, + "high": 1518.21, + "low": 1513, + "close": 1516.58 + }, + { + "open": 1516.59, + "high": 1533.5, + "low": 1516.09, + "close": 1531.9 + }, + { + "open": 1531.89, + "high": 1537.27, + "low": 1530.14, + "close": 1534.55 + }, + { + "open": 1534.55, + "high": 1541.23, + "low": 1534.34, + "close": 1536.95 + }, + { + "open": 1536.96, + "high": 1542.82, + "low": 1536.28, + "close": 1539.96 + }, + { + "open": 1539.97, + "high": 1542.51, + "low": 1535.03, + "close": 1536.75 + }, + { + "open": 1536.76, + "high": 1542.68, + "low": 1533.45, + "close": 1541.74 + }, + { + "open": 1541.75, + "high": 1543.99, + "low": 1538.04, + "close": 1538.45 + }, + { + "open": 1538.44, + "high": 1539.6, + "low": 1536.86, + "close": 1537.48 + }, + { + "open": 1537.47, + "high": 1537.48, + "low": 1534.36, + "close": 1536.22 + }, + { + "open": 1536.22, + "high": 1537.19, + "low": 1534.16, + "close": 1536.47 + }, + { + "open": 1536.46, + "high": 1536.56, + "low": 1531.43, + "close": 1532.8 + }, + { + "open": 1532.81, + "high": 1536.41, + "low": 1532.68, + "close": 1536.21 + }, + { + "open": 1536.22, + "high": 1538.61, + "low": 1535.4, + "close": 1537.8 + }, + { + "open": 1537.8, + "high": 1538.88, + "low": 1535.72, + "close": 1535.99 + }, + { + "open": 1535.99, + "high": 1537.93, + "low": 1535.82, + "close": 1537.49 + }, + { + "open": 1537.48, + "high": 1537.49, + "low": 1532.8, + "close": 1533.82 + }, + { + "open": 1533.82, + "high": 1536.84, + "low": 1533.62, + "close": 1535.49 + }, + { + "open": 1535.49, + "high": 1535.76, + "low": 1534.01, + "close": 1535.42 + }, + { + "open": 1535.42, + "high": 1536, + "low": 1532.17, + "close": 1534.71 + }, + { + "open": 1534.72, + "high": 1538, + "low": 1534.71, + "close": 1537.86 + }, + { + "open": 1537.85, + "high": 1548.43, + "low": 1537.85, + "close": 1545.4 + }, + { + "open": 1545.39, + "high": 1546, + "low": 1542.09, + "close": 1544.54 + }, + { + "open": 1544.54, + "high": 1545.49, + "low": 1542.13, + "close": 1544.51 + }, + { + "open": 1544.51, + "high": 1545.82, + "low": 1543.21, + "close": 1545.81 + }, + { + "open": 1545.81, + "high": 1546.46, + "low": 1543.57, + "close": 1544.9 + }, + { + "open": 1544.9, + "high": 1548.69, + "low": 1540, + "close": 1547.55 + }, + { + "open": 1547.64, + "high": 1549.87, + "low": 1544.78, + "close": 1547.71 + }, + { + "open": 1547.7, + "high": 1549.65, + "low": 1546.11, + "close": 1547.1 + }, + { + "open": 1547.1, + "high": 1548.13, + "low": 1546.6, + "close": 1547.01 + }, + { + "open": 1547, + "high": 1548.88, + "low": 1542.95, + "close": 1548.88 + }, + { + "open": 1548.88, + "high": 1553.79, + "low": 1544.51, + "close": 1544.77 + }, + { + "open": 1544.77, + "high": 1546.59, + "low": 1543.06, + "close": 1543.48 + }, + { + "open": 1543.48, + "high": 1543.49, + "low": 1535.69, + "close": 1536.78 + }, + { + "open": 1536.78, + "high": 1542.5, + "low": 1535.31, + "close": 1541.57 + }, + { + "open": 1541.57, + "high": 1543.7, + "low": 1538.61, + "close": 1538.77 + }, + { + "open": 1538.77, + "high": 1541.47, + "low": 1537.85, + "close": 1538.86 + }, + { + "open": 1538.86, + "high": 1541.55, + "low": 1536.48, + "close": 1539.18 + }, + { + "open": 1539.19, + "high": 1541.52, + "low": 1538.97, + "close": 1539.38 + }, + { + "open": 1539.38, + "high": 1543, + "low": 1536.55, + "close": 1540.87 + }, + { + "open": 1540.86, + "high": 1543.23, + "low": 1539.09, + "close": 1539.58 + }, + { + "open": 1539.58, + "high": 1540.92, + "low": 1536.45, + "close": 1537.5 + }, + { + "open": 1537.49, + "high": 1538.34, + "low": 1533.5, + "close": 1534.83 + }, + { + "open": 1534.84, + "high": 1534.84, + "low": 1517.11, + "close": 1518.98 + }, + { + "open": 1518.98, + "high": 1525.5, + "low": 1518.64, + "close": 1523.83 + }, + { + "open": 1523.82, + "high": 1531.92, + "low": 1520.33, + "close": 1529.16 + }, + { + "open": 1529.16, + "high": 1531.5, + "low": 1524, + "close": 1530.11 + }, + { + "open": 1530.12, + "high": 1530.2, + "low": 1525.52, + "close": 1527.15 + }, + { + "open": 1527.15, + "high": 1530.94, + "low": 1526.55, + "close": 1530.8 + }, + { + "open": 1530.79, + "high": 1530.8, + "low": 1526.56, + "close": 1527.75 + }, + { + "open": 1527.75, + "high": 1530.84, + "low": 1526.98, + "close": 1527.33 + }, + { + "open": 1527.34, + "high": 1528.92, + "low": 1523.77, + "close": 1528.01 + }, + { + "open": 1528.01, + "high": 1531.35, + "low": 1527.46, + "close": 1530.32 + }, + { + "open": 1530.32, + "high": 1538.65, + "low": 1529.45, + "close": 1536.07 + }, + { + "open": 1536.08, + "high": 1536.51, + "low": 1533.75, + "close": 1535.46 + }, + { + "open": 1535.46, + "high": 1536.81, + "low": 1533.28, + "close": 1533.29 + }, + { + "open": 1533.28, + "high": 1533.97, + "low": 1522.46, + "close": 1523.87 + }, + { + "open": 1523.86, + "high": 1524.03, + "low": 1517.61, + "close": 1523.73 + }, + { + "open": 1523.72, + "high": 1537.06, + "low": 1523.21, + "close": 1532.35 + }, + { + "open": 1532.36, + "high": 1532.36, + "low": 1525.15, + "close": 1527.63 + }, + { + "open": 1527.63, + "high": 1531.98, + "low": 1521.21, + "close": 1522.82 + }, + { + "open": 1522.82, + "high": 1522.82, + "low": 1506.7, + "close": 1516.24 + }, + { + "open": 1516.24, + "high": 1527.52, + "low": 1514.14, + "close": 1523.97 + }, + { + "open": 1523.97, + "high": 1523.98, + "low": 1516.85, + "close": 1520.44 + }, + { + "open": 1520.45, + "high": 1525.98, + "low": 1517.98, + "close": 1521.25 + }, + { + "open": 1521.26, + "high": 1526.16, + "low": 1520, + "close": 1521.76 + }, + { + "open": 1521.77, + "high": 1525, + "low": 1518.05, + "close": 1518.17 + }, + { + "open": 1518.17, + "high": 1518.66, + "low": 1510.75, + "close": 1516.05 + }, + { + "open": 1516.05, + "high": 1518.01, + "low": 1509.24, + "close": 1509.65 + }, + { + "open": 1509.65, + "high": 1513.46, + "low": 1507.45, + "close": 1511.38 + }, + { + "open": 1511.37, + "high": 1517.77, + "low": 1510.69, + "close": 1513.23 + }, + { + "open": 1513.24, + "high": 1513.85, + "low": 1502.21, + "close": 1510.72 + }, + { + "open": 1510.72, + "high": 1516.44, + "low": 1508.36, + "close": 1512.4 + }, + { + "open": 1512.4, + "high": 1518.94, + "low": 1507.61, + "close": 1517.46 + }, + { + "open": 1517.45, + "high": 1523.41, + "low": 1515.15, + "close": 1522.78 + }, + { + "open": 1522.78, + "high": 1524, + "low": 1518.84, + "close": 1520.49 + }, + { + "open": 1520.49, + "high": 1523.13, + "low": 1519.01, + "close": 1521.68 + }, + { + "open": 1521.68, + "high": 1527.81, + "low": 1520.5, + "close": 1524.68 + }, + { + "open": 1524.68, + "high": 1532, + "low": 1523.05, + "close": 1527.05 + }, + { + "open": 1527.04, + "high": 1528.39, + "low": 1523, + "close": 1523.42 + }, + { + "open": 1523.41, + "high": 1523.64, + "low": 1514, + "close": 1514.71 + }, + { + "open": 1514.72, + "high": 1518, + "low": 1511.35, + "close": 1517.24 + }, + { + "open": 1517.23, + "high": 1518.75, + "low": 1514.48, + "close": 1515.82 + }, + { + "open": 1515.81, + "high": 1522.58, + "low": 1514.01, + "close": 1521.13 + }, + { + "open": 1521.13, + "high": 1530.48, + "low": 1521.11, + "close": 1527.23 + }, + { + "open": 1527.23, + "high": 1528.46, + "low": 1522.57, + "close": 1525.67 + }, + { + "open": 1525.67, + "high": 1528.64, + "low": 1525.67, + "close": 1528.35 + }, + { + "open": 1528.35, + "high": 1531.14, + "low": 1523.51, + "close": 1523.51 + }, + { + "open": 1523.51, + "high": 1528.76, + "low": 1523.48, + "close": 1527.56 + }, + { + "open": 1527.57, + "high": 1527.99, + "low": 1524.99, + "close": 1526.03 + }, + { + "open": 1526.02, + "high": 1534.93, + "low": 1524.67, + "close": 1532.6 + }, + { + "open": 1532.6, + "high": 1534.19, + "low": 1529.44, + "close": 1529.96 + }, + { + "open": 1529.96, + "high": 1531.24, + "low": 1524.14, + "close": 1525.3 + }, + { + "open": 1525.29, + "high": 1526.09, + "low": 1520.01, + "close": 1520.14 + }, + { + "open": 1520.15, + "high": 1520.51, + "low": 1517.04, + "close": 1519.66 + }, + { + "open": 1519.67, + "high": 1522.26, + "low": 1517.77, + "close": 1519.3 + }, + { + "open": 1519.31, + "high": 1523.43, + "low": 1518.31, + "close": 1520.11 + }, + { + "open": 1520.12, + "high": 1521.27, + "low": 1514.68, + "close": 1515.46 + }, + { + "open": 1515.46, + "high": 1523.5, + "low": 1515.46, + "close": 1523.21 + }, + { + "open": 1523.22, + "high": 1530.34, + "low": 1522.49, + "close": 1529.8 + }, + { + "open": 1529.8, + "high": 1530.43, + "low": 1526.66, + "close": 1527.1 + }, + { + "open": 1527.1, + "high": 1531.29, + "low": 1525.61, + "close": 1527.6 + }, + { + "open": 1527.61, + "high": 1530.92, + "low": 1527.6, + "close": 1530.26 + }, + { + "open": 1530.26, + "high": 1534.01, + "low": 1528.36, + "close": 1533 + }, + { + "open": 1532.93, + "high": 1534.94, + "low": 1529, + "close": 1529.91 + }, + { + "open": 1529.92, + "high": 1530.63, + "low": 1524.38, + "close": 1527.8 + }, + { + "open": 1527.8, + "high": 1529.94, + "low": 1524.87, + "close": 1526.36 + }, + { + "open": 1526.36, + "high": 1529.37, + "low": 1526.33, + "close": 1527.87 + }, + { + "open": 1527.87, + "high": 1529.29, + "low": 1526.79, + "close": 1527.17 + }, + { + "open": 1527.18, + "high": 1527.18, + "low": 1518.55, + "close": 1519.61 + }, + { + "open": 1519.6, + "high": 1521.47, + "low": 1517.39, + "close": 1520.78 + }, + { + "open": 1520.78, + "high": 1522.13, + "low": 1514.46, + "close": 1516.81 + }, + { + "open": 1516.8, + "high": 1517.6, + "low": 1512.82, + "close": 1513.66 + }, + { + "open": 1513.65, + "high": 1516.81, + "low": 1511.44, + "close": 1513.86 + }, + { + "open": 1513.86, + "high": 1515.47, + "low": 1507.11, + "close": 1509.29 + }, + { + "open": 1509.29, + "high": 1513.83, + "low": 1509.11, + "close": 1510.83 + }, + { + "open": 1510.83, + "high": 1518.12, + "low": 1510.83, + "close": 1515.33 + }, + { + "open": 1515.33, + "high": 1517.4, + "low": 1511.99, + "close": 1514.89 + }, + { + "open": 1514.88, + "high": 1518.67, + "low": 1513.78, + "close": 1516.93 + }, + { + "open": 1516.93, + "high": 1517.58, + "low": 1514.1, + "close": 1515.4 + }, + { + "open": 1515.4, + "high": 1518.96, + "low": 1514.65, + "close": 1518.43 + }, + { + "open": 1518.44, + "high": 1523.32, + "low": 1515.35, + "close": 1515.78 + }, + { + "open": 1515.78, + "high": 1521.43, + "low": 1515.46, + "close": 1519.21 + }, + { + "open": 1519.2, + "high": 1519.88, + "low": 1514.15, + "close": 1516.82 + }, + { + "open": 1516.82, + "high": 1517.28, + "low": 1510.85, + "close": 1513.86 + }, + { + "open": 1513.87, + "high": 1516.82, + "low": 1509.37, + "close": 1510.88 + }, + { + "open": 1510.88, + "high": 1512, + "low": 1505.9, + "close": 1505.93 + }, + { + "open": 1505.94, + "high": 1509.39, + "low": 1477.01, + "close": 1478.26 + }, + { + "open": 1478.26, + "high": 1484.64, + "low": 1474.16, + "close": 1482.69 + }, + { + "open": 1482.69, + "high": 1487.83, + "low": 1478.76, + "close": 1479.74 + }, + { + "open": 1479.74, + "high": 1481.79, + "low": 1470.8, + "close": 1474.95 + }, + { + "open": 1474.95, + "high": 1477.59, + "low": 1466.58, + "close": 1468.19 + }, + { + "open": 1468.2, + "high": 1474, + "low": 1455.17, + "close": 1466.85 + }, + { + "open": 1466.9, + "high": 1472.67, + "low": 1466.89, + "close": 1471.37 + }, + { + "open": 1471.37, + "high": 1478.04, + "low": 1467.14, + "close": 1470.07 + }, + { + "open": 1470.07, + "high": 1477.76, + "low": 1468.23, + "close": 1475.57 + }, + { + "open": 1475.56, + "high": 1491.72, + "low": 1474.88, + "close": 1488.58 + }, + { + "open": 1488.58, + "high": 1496.76, + "low": 1484.21, + "close": 1495 + }, + { + "open": 1495, + "high": 1523.99, + "low": 1492.01, + "close": 1514.98 + }, + { + "open": 1514.98, + "high": 1522.88, + "low": 1513.78, + "close": 1518.91 + }, + { + "open": 1518.91, + "high": 1522.64, + "low": 1517, + "close": 1520.67 + }, + { + "open": 1520.67, + "high": 1529.77, + "low": 1513.5, + "close": 1513.51 + }, + { + "open": 1513.51, + "high": 1520, + "low": 1513.28, + "close": 1517.2 + }, + { + "open": 1517.2, + "high": 1519.49, + "low": 1512.2, + "close": 1518.95 + }, + { + "open": 1518.95, + "high": 1519.49, + "low": 1513.26, + "close": 1515.54 + }, + { + "open": 1515.54, + "high": 1519.1, + "low": 1513.83, + "close": 1518.88 + }, + { + "open": 1518.89, + "high": 1538.51, + "low": 1518.88, + "close": 1531.01 + }, + { + "open": 1530.95, + "high": 1533.43, + "low": 1520.54, + "close": 1526.77 + }, + { + "open": 1526.77, + "high": 1530.58, + "low": 1524.92, + "close": 1527.44 + }, + { + "open": 1527.44, + "high": 1528.44, + "low": 1521.03, + "close": 1523.01 + }, + { + "open": 1523.01, + "high": 1524.56, + "low": 1517.95, + "close": 1521.65 + }, + { + "open": 1521.63, + "high": 1521.75, + "low": 1516.33, + "close": 1520.27 + }, + { + "open": 1520.28, + "high": 1523.83, + "low": 1518.53, + "close": 1523.21 + }, + { + "open": 1523.21, + "high": 1524, + "low": 1520.51, + "close": 1522.06 + }, + { + "open": 1522.05, + "high": 1524, + "low": 1519.38, + "close": 1520.38 + }, + { + "open": 1520.37, + "high": 1521.46, + "low": 1519.64, + "close": 1520.5 + }, + { + "open": 1520.5, + "high": 1525.95, + "low": 1520.49, + "close": 1523.01 + }, + { + "open": 1523, + "high": 1523.38, + "low": 1520.25, + "close": 1520.78 + }, + { + "open": 1520.77, + "high": 1521.39, + "low": 1512.85, + "close": 1517.63 + }, + { + "open": 1517.63, + "high": 1517.85, + "low": 1507.66, + "close": 1514.06 + }, + { + "open": 1514.05, + "high": 1517.93, + "low": 1514.01, + "close": 1516.79 + }, + { + "open": 1516.8, + "high": 1516.8, + "low": 1512.55, + "close": 1514.02 + }, + { + "open": 1514.02, + "high": 1514.89, + "low": 1508.97, + "close": 1510.44 + }, + { + "open": 1510.44, + "high": 1512.24, + "low": 1506.68, + "close": 1510.1 + }, + { + "open": 1510.09, + "high": 1511.6, + "low": 1494.65, + "close": 1495.68 + }, + { + "open": 1495.69, + "high": 1498.59, + "low": 1492.45, + "close": 1496.12 + }, + { + "open": 1496.13, + "high": 1496.13, + "low": 1482.16, + "close": 1487 + }, + { + "open": 1487, + "high": 1488.44, + "low": 1469, + "close": 1471.65 + }, + { + "open": 1471.65, + "high": 1476.97, + "low": 1467.08, + "close": 1469.98 + }, + { + "open": 1469.98, + "high": 1475.2, + "low": 1469.78, + "close": 1472.26 + }, + { + "open": 1472.25, + "high": 1487.99, + "low": 1472.25, + "close": 1485.05 + }, + { + "open": 1485.05, + "high": 1494.72, + "low": 1473.92, + "close": 1485.16 + }, + { + "open": 1485.16, + "high": 1485.17, + "low": 1478.93, + "close": 1482.48 + }, + { + "open": 1482.48, + "high": 1489, + "low": 1479.21, + "close": 1482.03 + }, + { + "open": 1482.03, + "high": 1484.28, + "low": 1475.43, + "close": 1481.37 + }, + { + "open": 1481.38, + "high": 1482.3, + "low": 1469.55, + "close": 1470.66 + }, + { + "open": 1470.66, + "high": 1479.85, + "low": 1469, + "close": 1477.53 + }, + { + "open": 1477.54, + "high": 1483.67, + "low": 1476.45, + "close": 1482.88 + }, + { + "open": 1482.89, + "high": 1487.37, + "low": 1478.08, + "close": 1483.37 + }, + { + "open": 1483.37, + "high": 1484.36, + "low": 1472.36, + "close": 1474.97 + }, + { + "open": 1474.97, + "high": 1478.78, + "low": 1474.69, + "close": 1477.75 + }, + { + "open": 1477.75, + "high": 1478.84, + "low": 1470, + "close": 1470.96 + }, + { + "open": 1470.95, + "high": 1472.38, + "low": 1464, + "close": 1470 + }, + { + "open": 1470, + "high": 1471.07, + "low": 1436.02, + "close": 1443.03 + }, + { + "open": 1443.04, + "high": 1451.98, + "low": 1435.58, + "close": 1447.55 + }, + { + "open": 1447.55, + "high": 1448.1, + "low": 1437.01, + "close": 1440.79 + }, + { + "open": 1440.79, + "high": 1450, + "low": 1432.99, + "close": 1443.3 + }, + { + "open": 1443.29, + "high": 1446.49, + "low": 1441.15, + "close": 1443.34 + }, + { + "open": 1443.34, + "high": 1446.58, + "low": 1437.11, + "close": 1438.52 + }, + { + "open": 1438.51, + "high": 1441.99, + "low": 1429.68, + "close": 1439.43 + }, + { + "open": 1439.43, + "high": 1440.82, + "low": 1427.1, + "close": 1433.08 + }, + { + "open": 1433.08, + "high": 1434.82, + "low": 1428.65, + "close": 1429.91 + }, + { + "open": 1429.91, + "high": 1433.24, + "low": 1427.02, + "close": 1430.25 + }, + { + "open": 1430.19, + "high": 1430.2, + "low": 1415.6, + "close": 1423.09 + }, + { + "open": 1423.09, + "high": 1432, + "low": 1421.92, + "close": 1427.53 + }, + { + "open": 1427.52, + "high": 1428.64, + "low": 1421.55, + "close": 1427.64 + }, + { + "open": 1427.64, + "high": 1430.1, + "low": 1415, + "close": 1415.98 + }, + { + "open": 1415.99, + "high": 1421.48, + "low": 1412.15, + "close": 1415.38 + }, + { + "open": 1415.38, + "high": 1424, + "low": 1412.45, + "close": 1423.02 + }, + { + "open": 1423.03, + "high": 1426.99, + "low": 1422, + "close": 1425.62 + }, + { + "open": 1425.61, + "high": 1427.39, + "low": 1421.18, + "close": 1421.98 + }, + { + "open": 1421.98, + "high": 1426.36, + "low": 1418.04, + "close": 1423.96 + }, + { + "open": 1423.97, + "high": 1424.1, + "low": 1418.49, + "close": 1418.49 + }, + { + "open": 1418.49, + "high": 1421.16, + "low": 1414.03, + "close": 1421.15 + }, + { + "open": 1421.16, + "high": 1421.16, + "low": 1415, + "close": 1417.04 + }, + { + "open": 1417.04, + "high": 1418.37, + "low": 1413.06, + "close": 1413.36 + }, + { + "open": 1413.37, + "high": 1423.37, + "low": 1413.21, + "close": 1422.91 + }, + { + "open": 1422.9, + "high": 1425.51, + "low": 1421.11, + "close": 1423.99 + }, + { + "open": 1423.98, + "high": 1425.09, + "low": 1422.73, + "close": 1423.61 + }, + { + "open": 1423.62, + "high": 1424.96, + "low": 1420, + "close": 1420.23 + }, + { + "open": 1420.23, + "high": 1421.87, + "low": 1419.01, + "close": 1421.02 + }, + { + "open": 1421.01, + "high": 1426.03, + "low": 1420.88, + "close": 1425.12 + }, + { + "open": 1425.11, + "high": 1425.68, + "low": 1421.85, + "close": 1423.23 + }, + { + "open": 1423.23, + "high": 1424.73, + "low": 1421.37, + "close": 1423.76 + }, + { + "open": 1423.75, + "high": 1424, + "low": 1418.3, + "close": 1420 + }, + { + "open": 1419.99, + "high": 1420.19, + "low": 1415.15, + "close": 1419.8 + }, + { + "open": 1419.79, + "high": 1421.88, + "low": 1418.08, + "close": 1420.88 + }, + { + "open": 1420.89, + "high": 1424.26, + "low": 1420.87, + "close": 1422.07 + }, + { + "open": 1422.06, + "high": 1423.13, + "low": 1420.59, + "close": 1422.39 + }, + { + "open": 1422.39, + "high": 1423.95, + "low": 1421.75, + "close": 1421.9 + }, + { + "open": 1421.9, + "high": 1423.15, + "low": 1419.15, + "close": 1423.14 + }, + { + "open": 1423.15, + "high": 1423.15, + "low": 1417.79, + "close": 1419.37 + }, + { + "open": 1419.38, + "high": 1419.57, + "low": 1416.26, + "close": 1417.29 + }, + { + "open": 1417.28, + "high": 1419.21, + "low": 1416.5, + "close": 1418.07 + }, + { + "open": 1418.06, + "high": 1418.07, + "low": 1400.46, + "close": 1415.32 + }, + { + "open": 1415.31, + "high": 1424.48, + "low": 1414.74, + "close": 1423.85 + }, + { + "open": 1423.86, + "high": 1424.14, + "low": 1419.33, + "close": 1421.39 + }, + { + "open": 1421.38, + "high": 1430.92, + "low": 1420.53, + "close": 1429.17 + }, + { + "open": 1429.17, + "high": 1438.16, + "low": 1428.17, + "close": 1436.56 + }, + { + "open": 1436.56, + "high": 1436.57, + "low": 1427.07, + "close": 1429.86 + }, + { + "open": 1429.86, + "high": 1429.87, + "low": 1422.71, + "close": 1425.41 + }, + { + "open": 1425.4, + "high": 1428.97, + "low": 1423.88, + "close": 1427.36 + }, + { + "open": 1427.36, + "high": 1432, + "low": 1426.91, + "close": 1431.33 + }, + { + "open": 1431.33, + "high": 1434.92, + "low": 1431.32, + "close": 1433.05 + }, + { + "open": 1433.04, + "high": 1435, + "low": 1431.4, + "close": 1434.81 + }, + { + "open": 1434.81, + "high": 1435.37, + "low": 1430.27, + "close": 1430.44 + }, + { + "open": 1430.45, + "high": 1433.4, + "low": 1430.1, + "close": 1432.26 + }, + { + "open": 1432.27, + "high": 1433.74, + "low": 1428.86, + "close": 1429.56 + }, + { + "open": 1429.56, + "high": 1434.14, + "low": 1428.46, + "close": 1432.89 + }, + { + "open": 1432.89, + "high": 1438.3, + "low": 1431.03, + "close": 1432.86 + }, + { + "open": 1432.87, + "high": 1433.99, + "low": 1431, + "close": 1433.84 + }, + { + "open": 1433.85, + "high": 1434.14, + "low": 1431.21, + "close": 1431.54 + }, + { + "open": 1431.53, + "high": 1433.37, + "low": 1430.3, + "close": 1431.32 + }, + { + "open": 1431.32, + "high": 1434, + "low": 1431.05, + "close": 1432.62 + }, + { + "open": 1432.62, + "high": 1433.24, + "low": 1425.55, + "close": 1426.28 + }, + { + "open": 1426.28, + "high": 1429.8, + "low": 1424.16, + "close": 1427.33 + }, + { + "open": 1427.33, + "high": 1427.88, + "low": 1424.32, + "close": 1426.04 + }, + { + "open": 1426.03, + "high": 1426.33, + "low": 1422.21, + "close": 1423.93 + }, + { + "open": 1423.93, + "high": 1425.44, + "low": 1422.55, + "close": 1423.89 + }, + { + "open": 1423.88, + "high": 1424.55, + "low": 1420.83, + "close": 1421.27 + }, + { + "open": 1421.26, + "high": 1423.64, + "low": 1420.6, + "close": 1422.77 + }, + { + "open": 1422.77, + "high": 1426.46, + "low": 1422.77, + "close": 1425.91 + }, + { + "open": 1425.92, + "high": 1426.42, + "low": 1422.2, + "close": 1422.72 + }, + { + "open": 1422.73, + "high": 1427.27, + "low": 1422.14, + "close": 1427.22 + }, + { + "open": 1427.22, + "high": 1430.2, + "low": 1425.67, + "close": 1428.24 + }, + { + "open": 1428.25, + "high": 1432.24, + "low": 1427.87, + "close": 1429.9 + }, + { + "open": 1429.89, + "high": 1430.99, + "low": 1428, + "close": 1428.42 + }, + { + "open": 1428.43, + "high": 1429.92, + "low": 1424.61, + "close": 1426.78 + }, + { + "open": 1426.79, + "high": 1427.3, + "low": 1424.19, + "close": 1424.26 + }, + { + "open": 1424.27, + "high": 1425.86, + "low": 1423.42, + "close": 1424.38 + }, + { + "open": 1424.37, + "high": 1424.47, + "low": 1420.8, + "close": 1421.27 + }, + { + "open": 1421.27, + "high": 1423.26, + "low": 1421.01, + "close": 1422.37 + }, + { + "open": 1422.37, + "high": 1426, + "low": 1421.84, + "close": 1424.07 + }, + { + "open": 1424.07, + "high": 1424.35, + "low": 1421.43, + "close": 1423.56 + }, + { + "open": 1423.55, + "high": 1423.71, + "low": 1416.58, + "close": 1417.41 + }, + { + "open": 1417.4, + "high": 1420.22, + "low": 1413.72, + "close": 1416.05 + }, + { + "open": 1416.06, + "high": 1417.5, + "low": 1414.67, + "close": 1416.57 + }, + { + "open": 1416.57, + "high": 1422.13, + "low": 1415.8, + "close": 1417.42 + }, + { + "open": 1417.42, + "high": 1417.88, + "low": 1415, + "close": 1416 + }, + { + "open": 1416.01, + "high": 1419.27, + "low": 1415.19, + "close": 1417.59 + }, + { + "open": 1417.59, + "high": 1418.69, + "low": 1415.76, + "close": 1416.86 + }, + { + "open": 1416.86, + "high": 1419.7, + "low": 1414.22, + "close": 1419.51 + }, + { + "open": 1419.5, + "high": 1421.79, + "low": 1417.87, + "close": 1420.19 + }, + { + "open": 1420.19, + "high": 1423.7, + "low": 1413.12, + "close": 1422.48 + }, + { + "open": 1422.49, + "high": 1423.28, + "low": 1421.01, + "close": 1422.19 + }, + { + "open": 1422.19, + "high": 1423.55, + "low": 1421.32, + "close": 1423.15 + }, + { + "open": 1423.15, + "high": 1423.67, + "low": 1421.4, + "close": 1421.76 + }, + { + "open": 1421.77, + "high": 1421.77, + "low": 1419.22, + "close": 1420.17 + }, + { + "open": 1420.18, + "high": 1421.76, + "low": 1416.74, + "close": 1417.99 + }, + { + "open": 1418, + "high": 1418.82, + "low": 1414.58, + "close": 1415.97 + }, + { + "open": 1415.97, + "high": 1417.49, + "low": 1409.57, + "close": 1412.33 + }, + { + "open": 1412.29, + "high": 1414.74, + "low": 1406.68, + "close": 1409.46 + }, + { + "open": 1409.46, + "high": 1410.48, + "low": 1406.23, + "close": 1406.35 + }, + { + "open": 1406.36, + "high": 1413.17, + "low": 1406.34, + "close": 1408.12 + }, + { + "open": 1408.12, + "high": 1411.39, + "low": 1406.3, + "close": 1406.63 + }, + { + "open": 1406.64, + "high": 1410.32, + "low": 1403.94, + "close": 1409.74 + }, + { + "open": 1409.74, + "high": 1411.89, + "low": 1407.7, + "close": 1411.32 + }, + { + "open": 1411.33, + "high": 1413.91, + "low": 1410.85, + "close": 1411.65 + }, + { + "open": 1411.64, + "high": 1414.31, + "low": 1410.68, + "close": 1413 + }, + { + "open": 1413, + "high": 1414.29, + "low": 1410.84, + "close": 1413.58 + }, + { + "open": 1413.59, + "high": 1418.54, + "low": 1413.56, + "close": 1417.54 + }, + { + "open": 1417.54, + "high": 1418.17, + "low": 1415.47, + "close": 1416.05 + }, + { + "open": 1416.05, + "high": 1416.25, + "low": 1413.66, + "close": 1414.45 + }, + { + "open": 1414.46, + "high": 1416.15, + "low": 1411.4, + "close": 1412.57 + }, + { + "open": 1412.57, + "high": 1414.98, + "low": 1411.25, + "close": 1414.75 + }, + { + "open": 1414.75, + "high": 1416, + "low": 1411, + "close": 1415.8 + }, + { + "open": 1415.79, + "high": 1417, + "low": 1413.48, + "close": 1414.55 + }, + { + "open": 1414.56, + "high": 1415.39, + "low": 1412.53, + "close": 1413.86 + }, + { + "open": 1413.86, + "high": 1417.88, + "low": 1413.13, + "close": 1417.18 + }, + { + "open": 1417.17, + "high": 1418.7, + "low": 1415.3, + "close": 1417.9 + }, + { + "open": 1417.89, + "high": 1421.6, + "low": 1417.51, + "close": 1420.29 + }, + { + "open": 1420.3, + "high": 1421.17, + "low": 1419, + "close": 1420.01 + }, + { + "open": 1420, + "high": 1422.16, + "low": 1419.5, + "close": 1421.11 + }, + { + "open": 1421.12, + "high": 1423.97, + "low": 1420.9, + "close": 1423.25 + }, + { + "open": 1423.26, + "high": 1423.26, + "low": 1420.76, + "close": 1420.82 + }, + { + "open": 1420.82, + "high": 1421.01, + "low": 1418.92, + "close": 1419.11 + }, + { + "open": 1419.11, + "high": 1421.25, + "low": 1419, + "close": 1421 + }, + { + "open": 1421.01, + "high": 1422.98, + "low": 1417.66, + "close": 1418.53 + }, + { + "open": 1418.52, + "high": 1419.44, + "low": 1415.01, + "close": 1415.47 + }, + { + "open": 1415.47, + "high": 1417.15, + "low": 1413.71, + "close": 1414.16 + }, + { + "open": 1414.15, + "high": 1416.22, + "low": 1413.37, + "close": 1414.09 + }, + { + "open": 1414.1, + "high": 1416.01, + "low": 1413, + "close": 1415.83 + }, + { + "open": 1415.84, + "high": 1418.37, + "low": 1413.65, + "close": 1414.5 + }, + { + "open": 1414.46, + "high": 1414.96, + "low": 1408.49, + "close": 1411.92 + }, + { + "open": 1411.92, + "high": 1412.58, + "low": 1409.72, + "close": 1412.57 + }, + { + "open": 1412.57, + "high": 1413.05, + "low": 1411.23, + "close": 1411.32 + }, + { + "open": 1411.32, + "high": 1412.81, + "low": 1408.13, + "close": 1409.79 + }, + { + "open": 1409.79, + "high": 1412.35, + "low": 1409.34, + "close": 1410.44 + }, + { + "open": 1410.43, + "high": 1412.92, + "low": 1408.61, + "close": 1412.09 + }, + { + "open": 1412.1, + "high": 1412.84, + "low": 1410.56, + "close": 1410.57 + }, + { + "open": 1410.56, + "high": 1411.32, + "low": 1409.19, + "close": 1409.59 + }, + { + "open": 1409.6, + "high": 1412.09, + "low": 1407.68, + "close": 1410.42 + }, + { + "open": 1410.43, + "high": 1414.86, + "low": 1409.94, + "close": 1414.1 + }, + { + "open": 1414.11, + "high": 1414.47, + "low": 1412.17, + "close": 1412.65 + }, + { + "open": 1412.66, + "high": 1413.85, + "low": 1411.93, + "close": 1413.43 + }, + { + "open": 1413.44, + "high": 1415.79, + "low": 1412.99, + "close": 1413.51 + }, + { + "open": 1413.52, + "high": 1414.6, + "low": 1410.26, + "close": 1410.34 + }, + { + "open": 1410.33, + "high": 1413.83, + "low": 1410.33, + "close": 1413.39 + }, + { + "open": 1413.38, + "high": 1414.82, + "low": 1406.46, + "close": 1407.52 + }, + { + "open": 1407.52, + "high": 1413.46, + "low": 1385.08, + "close": 1406.33 + }, + { + "open": 1406.33, + "high": 1406.42, + "low": 1396.54, + "close": 1403.7 + }, + { + "open": 1403.69, + "high": 1413.95, + "low": 1403.69, + "close": 1411.91 + }, + { + "open": 1411.92, + "high": 1420.39, + "low": 1411.91, + "close": 1414.33 + }, + { + "open": 1414.34, + "high": 1414.52, + "low": 1405.66, + "close": 1405.79 + }, + { + "open": 1405.8, + "high": 1409.11, + "low": 1399, + "close": 1399.24 + }, + { + "open": 1399.23, + "high": 1405.73, + "low": 1399.23, + "close": 1400.56 + }, + { + "open": 1400.55, + "high": 1402, + "low": 1396.96, + "close": 1400.96 + }, + { + "open": 1400.96, + "high": 1401.35, + "low": 1390.31, + "close": 1394.24 + }, + { + "open": 1394.24, + "high": 1395.43, + "low": 1387.54, + "close": 1389.76 + }, + { + "open": 1389.87, + "high": 1390.95, + "low": 1381.54, + "close": 1384.88 + }, + { + "open": 1384.87, + "high": 1388.96, + "low": 1383.78, + "close": 1388.13 + }, + { + "open": 1388.13, + "high": 1393.11, + "low": 1387.84, + "close": 1391.98 + }, + { + "open": 1391.99, + "high": 1393.4, + "low": 1387.03, + "close": 1391.28 + }, + { + "open": 1391.38, + "high": 1391.68, + "low": 1384.04, + "close": 1384.51 + }, + { + "open": 1384.52, + "high": 1387.12, + "low": 1369.29, + "close": 1377.99 + }, + { + "open": 1377.99, + "high": 1384.82, + "low": 1371, + "close": 1383.01 + }, + { + "open": 1383.01, + "high": 1388, + "low": 1381.75, + "close": 1383.05 + }, + { + "open": 1383.06, + "high": 1388.47, + "low": 1383.05, + "close": 1387.82 + }, + { + "open": 1387.82, + "high": 1390, + "low": 1382.78, + "close": 1388.01 + }, + { + "open": 1388, + "high": 1392.96, + "low": 1386.5, + "close": 1391.27 + }, + { + "open": 1391.2, + "high": 1392.61, + "low": 1387.5, + "close": 1389.07 + }, + { + "open": 1389.07, + "high": 1389.07, + "low": 1382.39, + "close": 1385.3 + }, + { + "open": 1385.3, + "high": 1392, + "low": 1384.63, + "close": 1387.41 + }, + { + "open": 1387.41, + "high": 1390.71, + "low": 1386.5, + "close": 1389.12 + }, + { + "open": 1389.11, + "high": 1392.6, + "low": 1382.5, + "close": 1391.93 + }, + { + "open": 1391.93, + "high": 1396.92, + "low": 1390.04, + "close": 1391.11 + }, + { + "open": 1391.11, + "high": 1394.3, + "low": 1383.57, + "close": 1385.3 + }, + { + "open": 1385.31, + "high": 1386.85, + "low": 1379.45, + "close": 1382.74 + }, + { + "open": 1382.74, + "high": 1384.96, + "low": 1379.24, + "close": 1382.24 + }, + { + "open": 1382.24, + "high": 1384.6, + "low": 1380.2, + "close": 1380.79 + }, + { + "open": 1380.78, + "high": 1382.77, + "low": 1376, + "close": 1381.3 + }, + { + "open": 1381.29, + "high": 1384.6, + "low": 1379.59, + "close": 1382.74 + }, + { + "open": 1382.75, + "high": 1385.95, + "low": 1381.72, + "close": 1384.68 + }, + { + "open": 1384.67, + "high": 1392.47, + "low": 1384.26, + "close": 1389.48 + }, + { + "open": 1389.49, + "high": 1392, + "low": 1388.82, + "close": 1390.13 + }, + { + "open": 1390.13, + "high": 1392.46, + "low": 1389.7, + "close": 1391.94 + }, + { + "open": 1391.94, + "high": 1393.95, + "low": 1388.32, + "close": 1388.68 + }, + { + "open": 1388.68, + "high": 1389.35, + "low": 1379.59, + "close": 1381.72 + }, + { + "open": 1381.71, + "high": 1384.78, + "low": 1379.64, + "close": 1384.68 + }, + { + "open": 1384.67, + "high": 1384.92, + "low": 1381.13, + "close": 1383.07 + }, + { + "open": 1383.07, + "high": 1383.24, + "low": 1375.5, + "close": 1375.74 + }, + { + "open": 1375.73, + "high": 1378.38, + "low": 1372.01, + "close": 1374.42 + }, + { + "open": 1374.42, + "high": 1377.99, + "low": 1366, + "close": 1377.51 + }, + { + "open": 1377.51, + "high": 1378.73, + "low": 1372.87, + "close": 1375.18 + }, + { + "open": 1375.18, + "high": 1378.38, + "low": 1371.96, + "close": 1376.76 + }, + { + "open": 1376.77, + "high": 1377.59, + "low": 1370.81, + "close": 1370.95 + }, + { + "open": 1370.95, + "high": 1374.62, + "low": 1363.87, + "close": 1367.88 + }, + { + "open": 1367.88, + "high": 1372, + "low": 1365.03, + "close": 1368.68 + }, + { + "open": 1368.67, + "high": 1373.02, + "low": 1367, + "close": 1370.9 + } +]`) + +func Test_GHFilter(t *testing.T) { + type args struct { + allKLines []types.KLine + window int + } + var klines []types.KLine + if err := json.Unmarshal(testGHFilterDataEthusdt5m, &klines); err != nil { + panic(err) + } + tests := []struct { + name string + args args + want float64 + }{ + { + name: "ETHUSDT G-H Filter 7", + args: args{ + allKLines: klines, + window: 7, + }, + want: 1373.71, + }, + { + name: "ETHUSDT G-H Filter 25", + args: args{ + allKLines: klines, + window: 25, + }, + want: 1376.21, + }, + { + name: "ETHUSDT G-H Filter 99", + args: args{ + allKLines: klines, + window: 99, + }, + want: 1378.96, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + filter := &GHFilter{IntervalWindow: types.IntervalWindow{Window: tt.args.window}} + for _, k := range klines { + filter.PushK(k) + } + got := filter.Last() + got = math.Trunc(got*100.0) / 100.0 + if got != tt.want { + t.Errorf("GHFilter.Last() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_GHFilterEstimationAccurate(t *testing.T) { + type args struct { + allKLines []types.KLine + priceF KLineValueMapper + window int + } + var klines []types.KLine + if err := json.Unmarshal(testGHFilterDataEthusdt5m, &klines); err != nil { + panic(err) + } + tests := []struct { + name string + args args + want float64 + }{ + { + name: "ETHUSDT G-H Filter square error 7", + args: args{ + allKLines: klines, + window: 7, + }, + }, + { + name: "ETHUSDT G-H Filter square error 25", + args: args{ + allKLines: klines, + window: 25, + }, + }, + { + name: "ETHUSDT G-H Filter square error 99", + args: args{ + allKLines: klines, + window: 99, + }, + }, + } + klineSquareError := func(base float64, k types.KLine) float64 { + openDiff := math.Abs(k.Open.Float64() - base) + highDiff := math.Abs(k.High.Float64() - base) + lowDiff := math.Abs(k.Low.Float64() - base) + closeDiff := math.Abs(k.Close.Float64() - base) + return openDiff*openDiff + highDiff*highDiff + lowDiff*lowDiff + closeDiff*closeDiff + } + closeSquareError := func(base float64, k types.KLine) float64 { + closeDiff := math.Abs(k.Close.Float64() - base) + return closeDiff * closeDiff + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + filter := &GHFilter{IntervalWindow: types.IntervalWindow{Window: tt.args.window}} + ewma := &EWMA{IntervalWindow: types.IntervalWindow{Window: tt.args.window}} + + var filterDiff2Sum, ewmaDiff2Sum float64 + var filterCloseDiff2Sum, ewmaCloseDiff2Sum float64 + for i, k := range klines { + // square error between last estimated state and current actual state + if i > 0 { + filterDiff2Sum += klineSquareError(filter.Last(), k) + ewmaDiff2Sum += klineSquareError(ewma.Last(), k) + filterCloseDiff2Sum += closeSquareError(filter.Last(), k) + ewmaCloseDiff2Sum += closeSquareError(ewma.Last(), k) + } + + // update estimations + filter.PushK(k) + ewma.PushK(k) + } + numEstimations := len(klines) - 1 + filterSquareErr := math.Sqrt(filterDiff2Sum / float64(numEstimations*4)) + ewmaSquareErr := math.Sqrt(ewmaDiff2Sum / float64(numEstimations*4)) + if filterSquareErr > ewmaSquareErr { + t.Errorf("filter K-Line square error %f > EWMA K-Line square error %v", filterSquareErr, ewmaSquareErr) + } + filterCloseSquareErr := math.Sqrt(filterCloseDiff2Sum / float64(numEstimations)) + ewmaCloseSquareErr := math.Sqrt(ewmaCloseDiff2Sum / float64(numEstimations)) + if filterCloseSquareErr > ewmaCloseSquareErr { + t.Errorf("filter close price square error %f > EWMA close price square error %v", filterCloseSquareErr, ewmaCloseSquareErr) + } + }) + } +} diff --git a/pkg/indicator/gma.go b/pkg/indicator/gma.go new file mode 100644 index 0000000000..c1fb3aa117 --- /dev/null +++ b/pkg/indicator/gma.go @@ -0,0 +1,72 @@ +package indicator + +import ( + "math" + + "github.com/c9s/bbgo/pkg/types" +) + +// Geometric Moving Average +//go:generate callbackgen -type GMA +type GMA struct { + types.SeriesBase + types.IntervalWindow + SMA *SMA + UpdateCallbacks []func(value float64) +} + +func (inc *GMA) Last() float64 { + if inc.SMA == nil { + return 0.0 + } + return math.Exp(inc.SMA.Last()) +} + +func (inc *GMA) Index(i int) float64 { + if inc.SMA == nil { + return 0.0 + } + return math.Exp(inc.SMA.Index(i)) +} + +func (inc *GMA) Length() int { + return inc.SMA.Length() +} + +func (inc *GMA) Update(value float64) { + if inc.SMA == nil { + inc.SMA = &SMA{IntervalWindow: inc.IntervalWindow} + } + inc.SMA.Update(math.Log(value)) +} + +func (inc *GMA) Clone() (out *GMA) { + out = &GMA{ + IntervalWindow: inc.IntervalWindow, + SMA: inc.SMA.Clone().(*SMA), + } + out.SeriesBase.Series = out + return out +} + +func (inc *GMA) TestUpdate(value float64) *GMA { + out := inc.Clone() + out.Update(value) + return out +} + +var _ types.SeriesExtend = &GMA{} + +func (inc *GMA) PushK(k types.KLine) { + inc.Update(k.Close.Float64()) +} + +func (inc *GMA) LoadK(allKLines []types.KLine) { + for _, k := range allKLines { + inc.PushK(k) + } +} + +func (inc *GMA) BindK(target KLineClosedEmitter, symbol string, interval types.Interval) { + target.OnKLineClosed(types.KLineWith(symbol, interval, inc.PushK)) +} diff --git a/pkg/indicator/gma_callbacks.go b/pkg/indicator/gma_callbacks.go new file mode 100644 index 0000000000..28e0cd867e --- /dev/null +++ b/pkg/indicator/gma_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type GMA"; DO NOT EDIT. + +package indicator + +import () + +func (inc *GMA) OnUpdate(cb func(value float64)) { + inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) +} + +func (inc *GMA) EmitUpdate(value float64) { + for _, cb := range inc.UpdateCallbacks { + cb(value) + } +} diff --git a/pkg/indicator/gma_test.go b/pkg/indicator/gma_test.go new file mode 100644 index 0000000000..ab9740e094 --- /dev/null +++ b/pkg/indicator/gma_test.go @@ -0,0 +1,61 @@ +package indicator + +import ( + "encoding/json" + "testing" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" + "github.com/stretchr/testify/assert" +) + +/* +python: + +import pandas as pd +from scipy.stats.mstats import gmean + +data = pd.Series([1.1,1.2,1.3,1.4,1.5,1.6,1.7,1.8,1.9,1.1,1.2,1.3,1.4,1.5,1.6,1.7,1.8,1.9,1.1,1.2,1.3,1.4,1.5,1.6,1.7,1.8,1.9]) +gmean(data[-5:]) +gmean(data[-6:-1]) +gmean(pd.concat(data[-4:], pd.Series([1.3]))) +*/ +func Test_GMA(t *testing.T) { + var randomPrices = []byte(`[1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9]`) + var input []fixedpoint.Value + if err := json.Unmarshal(randomPrices, &input); err != nil { + panic(err) + } + tests := []struct { + name string + kLines []types.KLine + want float64 + next float64 + update float64 + updateResult float64 + all int + }{ + { + name: "test", + kLines: buildKLines(input), + want: 1.6940930229200213, + next: 1.5937204331251167, + update: 1.3, + updateResult: 1.6462950504034335, + all: 24, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gma := GMA{IntervalWindow: types.IntervalWindow{Window: 5}} + for _, k := range tt.kLines { + gma.PushK(k) + } + assert.InDelta(t, tt.want, gma.Last(), Delta) + assert.InDelta(t, tt.next, gma.Index(1), Delta) + gma.Update(tt.update) + assert.InDelta(t, tt.updateResult, gma.Last(), Delta) + assert.Equal(t, tt.all, gma.Length()) + }) + } +} diff --git a/pkg/indicator/hull.go b/pkg/indicator/hull.go index 0c8347f9b5..df3e8c5690 100644 --- a/pkg/indicator/hull.go +++ b/pkg/indicator/hull.go @@ -10,19 +10,23 @@ import ( // Refer URL: https://fidelity.com/learning-center/trading-investing/technical-analysis/technical-indicator-guide/hull-moving-average //go:generate callbackgen -type HULL type HULL struct { + types.SeriesBase types.IntervalWindow ma1 *EWMA ma2 *EWMA result *EWMA - UpdateCallbacks []func(value float64) + updateCallbacks []func(value float64) } +var _ types.SeriesExtend = &HULL{} + func (inc *HULL) Update(value float64) { if inc.result == nil { - inc.ma1 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window / 2}} - inc.ma2 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}} - inc.result = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, int(math.Sqrt(float64(inc.Window)))}} + inc.SeriesBase.Series = inc + inc.ma1 = &EWMA{IntervalWindow: types.IntervalWindow{Interval: inc.Interval, Window: inc.Window / 2}} + inc.ma2 = &EWMA{IntervalWindow: inc.IntervalWindow} + inc.result = &EWMA{IntervalWindow: types.IntervalWindow{Interval: inc.Interval, Window: int(math.Sqrt(float64(inc.Window)))}} } inc.ma1.Update(value) inc.ma2.Update(value) @@ -50,33 +54,11 @@ func (inc *HULL) Length() int { return inc.result.Length() } -var _ types.Series = &HULL{} - -// TODO: should we just ignore the possible overlapping? -func (inc *HULL) calculateAndUpdate(allKLines []types.KLine) { - doable := false - if inc.ma1 == nil || inc.ma1.Length() == 0 { - doable = true - } - for _, k := range allKLines { - if !doable && k.StartTime.After(inc.ma1.LastOpenTime) { - doable = true - } - if doable { - inc.Update(k.Close.Float64()) - inc.EmitUpdate(inc.Last()) - } - } -} - -func (inc *HULL) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { - if inc.Interval != interval { +func (inc *HULL) PushK(k types.KLine) { + if inc.ma1 != nil && inc.ma1.Length() > 0 && k.EndTime.Before(inc.ma1.EndTime) { return } - inc.calculateAndUpdate(window) -} - -func (inc *HULL) Bind(updater KLineWindowUpdater) { - updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) + inc.Update(k.Close.Float64()) + inc.EmitUpdate(inc.Last()) } diff --git a/pkg/indicator/hull_callbacks.go b/pkg/indicator/hull_callbacks.go index aa95c8dd96..5f6222f841 100644 --- a/pkg/indicator/hull_callbacks.go +++ b/pkg/indicator/hull_callbacks.go @@ -5,11 +5,11 @@ package indicator import () func (inc *HULL) OnUpdate(cb func(value float64)) { - inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) + inc.updateCallbacks = append(inc.updateCallbacks, cb) } func (inc *HULL) EmitUpdate(value float64) { - for _, cb := range inc.UpdateCallbacks { + for _, cb := range inc.updateCallbacks { cb(value) } } diff --git a/pkg/indicator/hull_test.go b/pkg/indicator/hull_test.go index 95f883cd8b..857c8d30da 100644 --- a/pkg/indicator/hull_test.go +++ b/pkg/indicator/hull_test.go @@ -4,9 +4,10 @@ import ( "encoding/json" "testing" + "github.com/stretchr/testify/assert" + "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" - "github.com/stretchr/testify/assert" ) /* @@ -26,6 +27,7 @@ func Test_HULL(t *testing.T) { if err := json.Unmarshal(randomPrices, &input); err != nil { panic(err) } + tests := []struct { name string kLines []types.KLine @@ -44,8 +46,11 @@ func Test_HULL(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - hull := HULL{IntervalWindow: types.IntervalWindow{Window: 16}} - hull.calculateAndUpdate(tt.kLines) + hull := &HULL{IntervalWindow: types.IntervalWindow{Window: 16}} + for _, k := range tt.kLines { + hull.PushK(k) + } + last := hull.Last() assert.InDelta(t, tt.want, last, Delta) assert.InDelta(t, tt.next, hull.Index(1), Delta) diff --git a/pkg/indicator/interface.go b/pkg/indicator/interface.go new file mode 100644 index 0000000000..d784f53087 --- /dev/null +++ b/pkg/indicator/interface.go @@ -0,0 +1,34 @@ +package indicator + +import "github.com/c9s/bbgo/pkg/types" + +type KLineWindowUpdater interface { + OnKLineWindowUpdate(func(interval types.Interval, window types.KLineWindow)) +} + +type KLineClosedBinder interface { + BindK(target KLineClosedEmitter, symbol string, interval types.Interval) +} + +// KLineClosedEmitter is currently applied to the market data stream +// the market data stream emits the KLine closed event to the listeners. +type KLineClosedEmitter interface { + OnKLineClosed(func(k types.KLine)) +} + +// KLinePusher provides an interface for API user to push kline value to the indicator. +// The indicator implements its own way to calculate the value from the given kline object. +type KLinePusher interface { + PushK(k types.KLine) +} + +// Simple is the simple indicator that only returns one float64 value +type Simple interface { + KLinePusher + Last() float64 + OnUpdate(f func(value float64)) +} + +type KLineCalculateUpdater interface { + CalculateAndUpdate(allKLines []types.KLine) +} diff --git a/pkg/indicator/kalmanfilter.go b/pkg/indicator/kalmanfilter.go new file mode 100644 index 0000000000..5d83457290 --- /dev/null +++ b/pkg/indicator/kalmanfilter.go @@ -0,0 +1,86 @@ +package indicator + +import ( + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/types" + "math" +) + +// Refer: https://www.kalmanfilter.net/kalman1d.html +// One-dimensional Kalman filter + +//go:generate callbackgen -type KalmanFilter +type KalmanFilter struct { + types.SeriesBase + types.IntervalWindow + AdditionalSmoothWindow uint + amp2 *types.Queue // measurement uncertainty + k float64 // Kalman gain + measurements *types.Queue + Values floats.Slice + + UpdateCallbacks []func(value float64) +} + +func (inc *KalmanFilter) Update(value float64) { + var measureMove = value + if inc.measurements != nil { + measureMove = value - inc.measurements.Last() + } + inc.update(value, math.Abs(measureMove)) +} + +func (inc *KalmanFilter) update(value, amp float64) { + if len(inc.Values) == 0 { + inc.amp2 = types.NewQueue(inc.Window) + inc.amp2.Update(amp * amp) + inc.measurements = types.NewQueue(inc.Window) + inc.measurements.Update(value) + inc.Values.Push(value) + return + } + + // measurement + inc.measurements.Update(value) + inc.amp2.Update(amp * amp) + q := math.Sqrt(types.Mean(inc.amp2)) * float64(1+inc.AdditionalSmoothWindow) + + // update + lastPredict := inc.Values.Last() + curState := value + (value - lastPredict) + estimated := lastPredict + inc.k*(curState-lastPredict) + + // predict + inc.Values.Push(estimated) + p := math.Abs(curState - estimated) + inc.k = p / (p + q) +} + +func (inc *KalmanFilter) Index(i int) float64 { + if inc.Values == nil { + return 0.0 + } + return inc.Values.Index(i) +} + +func (inc *KalmanFilter) Length() int { + if inc.Values == nil { + return 0 + } + return inc.Values.Length() +} + +func (inc *KalmanFilter) Last() float64 { + if inc.Values == nil { + return 0.0 + } + return inc.Values.Last() +} + +// interfaces implementation check +var _ Simple = &KalmanFilter{} +var _ types.SeriesExtend = &KalmanFilter{} + +func (inc *KalmanFilter) PushK(k types.KLine) { + inc.update(k.Close.Float64(), (k.High.Float64()-k.Low.Float64())/2) +} diff --git a/pkg/indicator/kalmanfilter_callbacks.go b/pkg/indicator/kalmanfilter_callbacks.go new file mode 100644 index 0000000000..3dff5fc498 --- /dev/null +++ b/pkg/indicator/kalmanfilter_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type KalmanFilter"; DO NOT EDIT. + +package indicator + +import () + +func (inc *KalmanFilter) OnUpdate(cb func(value float64)) { + inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) +} + +func (inc *KalmanFilter) EmitUpdate(value float64) { + for _, cb := range inc.UpdateCallbacks { + cb(value) + } +} diff --git a/pkg/indicator/kalmanfilter_test.go b/pkg/indicator/kalmanfilter_test.go new file mode 100644 index 0000000000..250656047e --- /dev/null +++ b/pkg/indicator/kalmanfilter_test.go @@ -0,0 +1,6191 @@ +package indicator + +import ( + "encoding/json" + "math" + "testing" + + "github.com/c9s/bbgo/pkg/types" +) + +// generated from Binance 2022/07/27 00:00 +// https://www.binance.com/api/v3/klines?symbol=ETHUSDT&interval=5m&endTime=1658851200000&limit=1000 +var testKalmanFilterDataEthusdt5m = []byte(`[ + { + "open": 1591.11, + "high": 1593.62, + "low": 1589.04, + "close": 1590.14 + }, + { + "open": 1590.14, + "high": 1596.51, + "low": 1590.13, + "close": 1592.06 + }, + { + "open": 1592.07, + "high": 1594.41, + "low": 1586.05, + "close": 1587.02 + }, + { + "open": 1587.02, + "high": 1588.38, + "low": 1583.86, + "close": 1585.33 + }, + { + "open": 1585.34, + "high": 1595.2, + "low": 1583.74, + "close": 1594.69 + }, + { + "open": 1594.69, + "high": 1594.75, + "low": 1589.89, + "close": 1591.36 + }, + { + "open": 1591.35, + "high": 1592.55, + "low": 1586.36, + "close": 1588.95 + }, + { + "open": 1588.95, + "high": 1589.75, + "low": 1588.39, + "close": 1589.38 + }, + { + "open": 1589.38, + "high": 1589.39, + "low": 1586.17, + "close": 1588.53 + }, + { + "open": 1588.52, + "high": 1588.62, + "low": 1581.95, + "close": 1583.4 + }, + { + "open": 1583.4, + "high": 1584.67, + "low": 1582.1, + "close": 1582.36 + }, + { + "open": 1582.35, + "high": 1584.29, + "low": 1577.82, + "close": 1578.14 + }, + { + "open": 1578.14, + "high": 1581.95, + "low": 1575.72, + "close": 1581.52 + }, + { + "open": 1581.52, + "high": 1584.86, + "low": 1578.51, + "close": 1580.88 + }, + { + "open": 1580.88, + "high": 1581.77, + "low": 1578.74, + "close": 1581.11 + }, + { + "open": 1581.1, + "high": 1582.72, + "low": 1579.07, + "close": 1579.4 + }, + { + "open": 1579.4, + "high": 1580.93, + "low": 1578, + "close": 1579.6 + }, + { + "open": 1579.59, + "high": 1583.81, + "low": 1579.59, + "close": 1582.7 + }, + { + "open": 1582.7, + "high": 1583, + "low": 1577.24, + "close": 1579.45 + }, + { + "open": 1579.46, + "high": 1581.59, + "low": 1577.44, + "close": 1579.59 + }, + { + "open": 1579.58, + "high": 1581.41, + "low": 1579.22, + "close": 1580.56 + }, + { + "open": 1580.57, + "high": 1586.23, + "low": 1579.86, + "close": 1584.23 + }, + { + "open": 1584.22, + "high": 1587.36, + "low": 1584.22, + "close": 1585.15 + }, + { + "open": 1585.15, + "high": 1585.15, + "low": 1579.83, + "close": 1583.75 + }, + { + "open": 1583.74, + "high": 1592.49, + "low": 1583.45, + "close": 1587.76 + }, + { + "open": 1587.76, + "high": 1590.7, + "low": 1585.62, + "close": 1587.5 + }, + { + "open": 1587.51, + "high": 1587.51, + "low": 1579.53, + "close": 1581.16 + }, + { + "open": 1581.15, + "high": 1585.71, + "low": 1581.15, + "close": 1582.47 + }, + { + "open": 1582.46, + "high": 1582.86, + "low": 1567.58, + "close": 1571.52 + }, + { + "open": 1571.53, + "high": 1577.8, + "low": 1571.03, + "close": 1575.16 + }, + { + "open": 1575.16, + "high": 1578.06, + "low": 1572.18, + "close": 1576.66 + }, + { + "open": 1576.66, + "high": 1578, + "low": 1574.62, + "close": 1577.21 + }, + { + "open": 1577.2, + "high": 1584.57, + "low": 1576.61, + "close": 1584.05 + }, + { + "open": 1584.06, + "high": 1585.61, + "low": 1580, + "close": 1582.08 + }, + { + "open": 1582.08, + "high": 1583.4, + "low": 1579.43, + "close": 1579.43 + }, + { + "open": 1579.43, + "high": 1579.98, + "low": 1574.53, + "close": 1575.06 + }, + { + "open": 1575.06, + "high": 1578.52, + "low": 1574.57, + "close": 1576.49 + }, + { + "open": 1576.5, + "high": 1577, + "low": 1572.5, + "close": 1573.26 + }, + { + "open": 1573.26, + "high": 1579.41, + "low": 1573.06, + "close": 1578.35 + }, + { + "open": 1578.35, + "high": 1585, + "low": 1577.16, + "close": 1584.32 + }, + { + "open": 1584.31, + "high": 1587.97, + "low": 1580.67, + "close": 1585.7 + }, + { + "open": 1585.7, + "high": 1588.35, + "low": 1584.37, + "close": 1585.95 + }, + { + "open": 1585.94, + "high": 1587.09, + "low": 1580.66, + "close": 1580.97 + }, + { + "open": 1580.97, + "high": 1583.38, + "low": 1577, + "close": 1581.64 + }, + { + "open": 1581.64, + "high": 1586.79, + "low": 1581.22, + "close": 1585.42 + }, + { + "open": 1585.42, + "high": 1585.42, + "low": 1581.67, + "close": 1582.37 + }, + { + "open": 1582.38, + "high": 1584.86, + "low": 1581.01, + "close": 1581.02 + }, + { + "open": 1581.03, + "high": 1582.05, + "low": 1578.99, + "close": 1579.46 + }, + { + "open": 1579.46, + "high": 1579.89, + "low": 1566.85, + "close": 1567.99 + }, + { + "open": 1567.99, + "high": 1567.99, + "low": 1553.2, + "close": 1554.87 + }, + { + "open": 1554.87, + "high": 1558, + "low": 1546.9, + "close": 1550.4 + }, + { + "open": 1550.4, + "high": 1554.98, + "low": 1546.27, + "close": 1549.67 + }, + { + "open": 1549.68, + "high": 1555, + "low": 1546.97, + "close": 1553.88 + }, + { + "open": 1553.89, + "high": 1557.86, + "low": 1553.6, + "close": 1557.85 + }, + { + "open": 1557.86, + "high": 1558.37, + "low": 1554.9, + "close": 1556.3 + }, + { + "open": 1556.31, + "high": 1557.4, + "low": 1552.81, + "close": 1557.18 + }, + { + "open": 1557.18, + "high": 1563.78, + "low": 1556.5, + "close": 1562.72 + }, + { + "open": 1562.72, + "high": 1564.11, + "low": 1558.76, + "close": 1560.64 + }, + { + "open": 1560.64, + "high": 1562.31, + "low": 1560.5, + "close": 1561.24 + }, + { + "open": 1561.25, + "high": 1565.69, + "low": 1561.23, + "close": 1564.79 + }, + { + "open": 1564.79, + "high": 1565.33, + "low": 1558.23, + "close": 1559.9 + }, + { + "open": 1559.89, + "high": 1561.77, + "low": 1555.87, + "close": 1560.79 + }, + { + "open": 1560.78, + "high": 1562.07, + "low": 1557.89, + "close": 1560.36 + }, + { + "open": 1560.36, + "high": 1561.2, + "low": 1556.13, + "close": 1558.26 + }, + { + "open": 1558.25, + "high": 1563.12, + "low": 1558.25, + "close": 1562.35 + }, + { + "open": 1562.36, + "high": 1564.02, + "low": 1561.76, + "close": 1563.32 + }, + { + "open": 1563.31, + "high": 1564.29, + "low": 1557.79, + "close": 1559.87 + }, + { + "open": 1559.86, + "high": 1562.71, + "low": 1558.77, + "close": 1559.8 + }, + { + "open": 1559.81, + "high": 1559.91, + "low": 1557.6, + "close": 1559.19 + }, + { + "open": 1559.2, + "high": 1559.95, + "low": 1554.3, + "close": 1557.16 + }, + { + "open": 1557.16, + "high": 1557.17, + "low": 1536.25, + "close": 1541.89 + }, + { + "open": 1541.89, + "high": 1544.39, + "low": 1538.55, + "close": 1539.33 + }, + { + "open": 1539.33, + "high": 1546.28, + "low": 1533.67, + "close": 1543.99 + }, + { + "open": 1543.99, + "high": 1544.5, + "low": 1538.21, + "close": 1539.17 + }, + { + "open": 1539.17, + "high": 1543.33, + "low": 1537.73, + "close": 1543 + }, + { + "open": 1543.2, + "high": 1544, + "low": 1535.81, + "close": 1541.12 + }, + { + "open": 1541.12, + "high": 1541.13, + "low": 1534.12, + "close": 1536.89 + }, + { + "open": 1536.9, + "high": 1539.09, + "low": 1528.25, + "close": 1531.02 + }, + { + "open": 1531.01, + "high": 1532.91, + "low": 1525.28, + "close": 1532.32 + }, + { + "open": 1532.33, + "high": 1537.58, + "low": 1532.32, + "close": 1535.07 + }, + { + "open": 1535.06, + "high": 1541.28, + "low": 1535.06, + "close": 1539.52 + }, + { + "open": 1539.53, + "high": 1539.85, + "low": 1533.37, + "close": 1536.22 + }, + { + "open": 1536.21, + "high": 1536.22, + "low": 1524.81, + "close": 1527.09 + }, + { + "open": 1527.1, + "high": 1529.2, + "low": 1520.62, + "close": 1525.04 + }, + { + "open": 1525.04, + "high": 1528.2, + "low": 1522.12, + "close": 1523.72 + }, + { + "open": 1523.71, + "high": 1525.54, + "low": 1519, + "close": 1522.82 + }, + { + "open": 1522.82, + "high": 1524.98, + "low": 1521, + "close": 1522.19 + }, + { + "open": 1522.19, + "high": 1524.27, + "low": 1512.68, + "close": 1513.27 + }, + { + "open": 1513.26, + "high": 1514.55, + "low": 1501.65, + "close": 1514.14 + }, + { + "open": 1514.14, + "high": 1524.43, + "low": 1513.03, + "close": 1520.08 + }, + { + "open": 1520.08, + "high": 1525.07, + "low": 1518.63, + "close": 1520.61 + }, + { + "open": 1520.61, + "high": 1525.68, + "low": 1517.43, + "close": 1524.86 + }, + { + "open": 1524.86, + "high": 1525.04, + "low": 1519.65, + "close": 1520.26 + }, + { + "open": 1520.27, + "high": 1521.19, + "low": 1517.91, + "close": 1518.46 + }, + { + "open": 1518.46, + "high": 1525.25, + "low": 1518.46, + "close": 1524.83 + }, + { + "open": 1524.84, + "high": 1526.94, + "low": 1521.69, + "close": 1521.9 + }, + { + "open": 1521.9, + "high": 1524.8, + "low": 1519.25, + "close": 1519.88 + }, + { + "open": 1519.88, + "high": 1520.5, + "low": 1517.33, + "close": 1518.76 + }, + { + "open": 1518.76, + "high": 1522.64, + "low": 1518.14, + "close": 1520.46 + }, + { + "open": 1520.46, + "high": 1522.96, + "low": 1518.63, + "close": 1522.52 + }, + { + "open": 1522.51, + "high": 1522.52, + "low": 1519.19, + "close": 1520.61 + }, + { + "open": 1520.61, + "high": 1526.26, + "low": 1519.52, + "close": 1526.1 + }, + { + "open": 1526.11, + "high": 1530.26, + "low": 1524.63, + "close": 1528.65 + }, + { + "open": 1528.65, + "high": 1529.64, + "low": 1521.24, + "close": 1523.18 + }, + { + "open": 1523.19, + "high": 1525.92, + "low": 1521.79, + "close": 1525.6 + }, + { + "open": 1525.61, + "high": 1525.79, + "low": 1522.78, + "close": 1525.31 + }, + { + "open": 1525.31, + "high": 1529.6, + "low": 1525.3, + "close": 1528.93 + }, + { + "open": 1528.93, + "high": 1530.33, + "low": 1527.02, + "close": 1527.61 + }, + { + "open": 1527.6, + "high": 1534, + "low": 1527.6, + "close": 1533.98 + }, + { + "open": 1533.98, + "high": 1537.3, + "low": 1532.24, + "close": 1536.49 + }, + { + "open": 1536.5, + "high": 1536.5, + "low": 1531.65, + "close": 1532.92 + }, + { + "open": 1532.92, + "high": 1532.92, + "low": 1529.1, + "close": 1529.58 + }, + { + "open": 1529.58, + "high": 1535.97, + "low": 1528.13, + "close": 1532.48 + }, + { + "open": 1532.49, + "high": 1533.45, + "low": 1530.29, + "close": 1531.01 + }, + { + "open": 1531.02, + "high": 1532.56, + "low": 1524.11, + "close": 1524.37 + }, + { + "open": 1524.36, + "high": 1534.58, + "low": 1524.28, + "close": 1529.52 + }, + { + "open": 1529.52, + "high": 1530.55, + "low": 1521.72, + "close": 1522.54 + }, + { + "open": 1522.55, + "high": 1526.64, + "low": 1522.45, + "close": 1526.49 + }, + { + "open": 1526.48, + "high": 1530.2, + "low": 1525.07, + "close": 1526.92 + }, + { + "open": 1526.92, + "high": 1527.65, + "low": 1525, + "close": 1526.44 + }, + { + "open": 1526.43, + "high": 1527.68, + "low": 1525.46, + "close": 1526.05 + }, + { + "open": 1526.05, + "high": 1526.27, + "low": 1516.23, + "close": 1516.52 + }, + { + "open": 1516.23, + "high": 1520.67, + "low": 1509.21, + "close": 1520.48 + }, + { + "open": 1520.48, + "high": 1530.83, + "low": 1519.77, + "close": 1529.59 + }, + { + "open": 1529.6, + "high": 1531.12, + "low": 1526.99, + "close": 1531.11 + }, + { + "open": 1531.12, + "high": 1533.79, + "low": 1529.25, + "close": 1531.72 + }, + { + "open": 1531.73, + "high": 1532.96, + "low": 1528.52, + "close": 1529.64 + }, + { + "open": 1529.64, + "high": 1530.49, + "low": 1523.16, + "close": 1524.37 + }, + { + "open": 1524.38, + "high": 1524.58, + "low": 1517.86, + "close": 1521.06 + }, + { + "open": 1521.07, + "high": 1530.49, + "low": 1515.75, + "close": 1526.14 + }, + { + "open": 1526.13, + "high": 1526.98, + "low": 1521.57, + "close": 1523.57 + }, + { + "open": 1523.56, + "high": 1523.68, + "low": 1520.39, + "close": 1521.17 + }, + { + "open": 1521.18, + "high": 1521.36, + "low": 1516.78, + "close": 1516.79 + }, + { + "open": 1516.79, + "high": 1521.81, + "low": 1516.48, + "close": 1520.2 + }, + { + "open": 1520.2, + "high": 1524.79, + "low": 1516.97, + "close": 1523.67 + }, + { + "open": 1523.67, + "high": 1527.82, + "low": 1522.79, + "close": 1525.77 + }, + { + "open": 1525.77, + "high": 1527.68, + "low": 1520.25, + "close": 1524.24 + }, + { + "open": 1524.23, + "high": 1530.88, + "low": 1523.28, + "close": 1529.87 + }, + { + "open": 1529.88, + "high": 1532.92, + "low": 1527.6, + "close": 1530.66 + }, + { + "open": 1530.65, + "high": 1531.32, + "low": 1528.31, + "close": 1530.85 + }, + { + "open": 1530.85, + "high": 1532.99, + "low": 1527.78, + "close": 1528.68 + }, + { + "open": 1528.69, + "high": 1529.92, + "low": 1527.13, + "close": 1527.14 + }, + { + "open": 1527.13, + "high": 1527.14, + "low": 1518.31, + "close": 1521.16 + }, + { + "open": 1521.16, + "high": 1530.26, + "low": 1521.15, + "close": 1526.67 + }, + { + "open": 1526.68, + "high": 1528.17, + "low": 1522.22, + "close": 1522.33 + }, + { + "open": 1522.33, + "high": 1526.23, + "low": 1521.09, + "close": 1523.59 + }, + { + "open": 1523.59, + "high": 1523.99, + "low": 1517.48, + "close": 1518.86 + }, + { + "open": 1518.85, + "high": 1523.43, + "low": 1513.25, + "close": 1521.57 + }, + { + "open": 1521.58, + "high": 1521.58, + "low": 1511.11, + "close": 1513.4 + }, + { + "open": 1513.4, + "high": 1515.26, + "low": 1507.7, + "close": 1508.31 + }, + { + "open": 1508.31, + "high": 1512.48, + "low": 1503.49, + "close": 1505.89 + }, + { + "open": 1505.88, + "high": 1509.76, + "low": 1494.63, + "close": 1500.13 + }, + { + "open": 1500.13, + "high": 1510.52, + "low": 1498.39, + "close": 1507.22 + }, + { + "open": 1507.21, + "high": 1508, + "low": 1495.51, + "close": 1501.06 + }, + { + "open": 1501.06, + "high": 1506.84, + "low": 1500.04, + "close": 1504.99 + }, + { + "open": 1505, + "high": 1507.4, + "low": 1497.16, + "close": 1498.46 + }, + { + "open": 1498.46, + "high": 1505.37, + "low": 1495, + "close": 1501.44 + }, + { + "open": 1501.36, + "high": 1504.4, + "low": 1500.27, + "close": 1500.55 + }, + { + "open": 1500.55, + "high": 1502.52, + "low": 1496.63, + "close": 1501.29 + }, + { + "open": 1501.29, + "high": 1501.88, + "low": 1496, + "close": 1496.37 + }, + { + "open": 1496.37, + "high": 1506.67, + "low": 1488, + "close": 1505.21 + }, + { + "open": 1505.21, + "high": 1508.6, + "low": 1502.24, + "close": 1508 + }, + { + "open": 1507.99, + "high": 1514.07, + "low": 1507.03, + "close": 1512.2 + }, + { + "open": 1512.2, + "high": 1513.73, + "low": 1510.89, + "close": 1512.64 + }, + { + "open": 1512.65, + "high": 1514.52, + "low": 1508.88, + "close": 1513.17 + }, + { + "open": 1513.17, + "high": 1513.95, + "low": 1511.68, + "close": 1511.94 + }, + { + "open": 1511.94, + "high": 1512.69, + "low": 1508, + "close": 1508.97 + }, + { + "open": 1508.97, + "high": 1511.92, + "low": 1508, + "close": 1511.71 + }, + { + "open": 1511.71, + "high": 1512.21, + "low": 1502.06, + "close": 1502.3 + }, + { + "open": 1502.29, + "high": 1505.5, + "low": 1499.75, + "close": 1503.8 + }, + { + "open": 1503.8, + "high": 1510.52, + "low": 1497.04, + "close": 1499.02 + }, + { + "open": 1499.03, + "high": 1500.56, + "low": 1497.35, + "close": 1499.88 + }, + { + "open": 1499.88, + "high": 1507.12, + "low": 1498.43, + "close": 1499 + }, + { + "open": 1498.99, + "high": 1501.4, + "low": 1489.93, + "close": 1493.7 + }, + { + "open": 1493.71, + "high": 1495.73, + "low": 1490.72, + "close": 1493.73 + }, + { + "open": 1493.72, + "high": 1495.82, + "low": 1492.44, + "close": 1493.23 + }, + { + "open": 1493.23, + "high": 1501.75, + "low": 1493.06, + "close": 1501.54 + }, + { + "open": 1501.54, + "high": 1506.81, + "low": 1500.45, + "close": 1506.61 + }, + { + "open": 1506.6, + "high": 1507.9, + "low": 1505.1, + "close": 1505.95 + }, + { + "open": 1505.95, + "high": 1509.42, + "low": 1505.69, + "close": 1508.9 + }, + { + "open": 1508.9, + "high": 1516.09, + "low": 1508.3, + "close": 1513.84 + }, + { + "open": 1513.83, + "high": 1516.35, + "low": 1510.74, + "close": 1512 + }, + { + "open": 1512, + "high": 1516.35, + "low": 1511.43, + "close": 1513.33 + }, + { + "open": 1513.25, + "high": 1518.68, + "low": 1511.56, + "close": 1517.19 + }, + { + "open": 1517.18, + "high": 1524.05, + "low": 1516.45, + "close": 1517.64 + }, + { + "open": 1517.64, + "high": 1519.51, + "low": 1514.37, + "close": 1518.03 + }, + { + "open": 1518.04, + "high": 1520.21, + "low": 1516.06, + "close": 1518.91 + }, + { + "open": 1518.91, + "high": 1519.8, + "low": 1516.06, + "close": 1518.1 + }, + { + "open": 1518.11, + "high": 1518.11, + "low": 1515.6, + "close": 1516.43 + }, + { + "open": 1516.43, + "high": 1521.28, + "low": 1515.79, + "close": 1519.82 + }, + { + "open": 1519.81, + "high": 1519.98, + "low": 1518.42, + "close": 1519.68 + }, + { + "open": 1519.69, + "high": 1521.68, + "low": 1518.67, + "close": 1520.28 + }, + { + "open": 1520.29, + "high": 1521.65, + "low": 1519.08, + "close": 1520.24 + }, + { + "open": 1520.25, + "high": 1527.76, + "low": 1520.24, + "close": 1526.28 + }, + { + "open": 1526.27, + "high": 1526.99, + "low": 1522.67, + "close": 1525.11 + }, + { + "open": 1525.1, + "high": 1529.05, + "low": 1523.62, + "close": 1525.51 + }, + { + "open": 1525.5, + "high": 1525.51, + "low": 1520.4, + "close": 1521.62 + }, + { + "open": 1521.62, + "high": 1525.97, + "low": 1521.59, + "close": 1523.41 + }, + { + "open": 1523.42, + "high": 1524.16, + "low": 1523, + "close": 1523.81 + }, + { + "open": 1523.8, + "high": 1523.99, + "low": 1522, + "close": 1523.43 + }, + { + "open": 1523.42, + "high": 1524.99, + "low": 1523.42, + "close": 1524.78 + }, + { + "open": 1524.79, + "high": 1525.35, + "low": 1523.06, + "close": 1523.24 + }, + { + "open": 1523.24, + "high": 1523.24, + "low": 1518.44, + "close": 1520.29 + }, + { + "open": 1520.28, + "high": 1521.95, + "low": 1518.01, + "close": 1521.07 + }, + { + "open": 1521.08, + "high": 1521.3, + "low": 1519.22, + "close": 1519.35 + }, + { + "open": 1519.35, + "high": 1519.63, + "low": 1516.3, + "close": 1517.68 + }, + { + "open": 1517.67, + "high": 1518.24, + "low": 1515.23, + "close": 1516.39 + }, + { + "open": 1516.39, + "high": 1520.22, + "low": 1515.31, + "close": 1519.56 + }, + { + "open": 1519.55, + "high": 1524.64, + "low": 1518, + "close": 1522.74 + }, + { + "open": 1522.74, + "high": 1523.93, + "low": 1520.21, + "close": 1520.29 + }, + { + "open": 1520.29, + "high": 1523.26, + "low": 1520.1, + "close": 1522.73 + }, + { + "open": 1522.74, + "high": 1541.63, + "low": 1522.73, + "close": 1539.67 + }, + { + "open": 1539.67, + "high": 1541.92, + "low": 1535.13, + "close": 1538.82 + }, + { + "open": 1538.82, + "high": 1547.2, + "low": 1538.27, + "close": 1545.55 + }, + { + "open": 1545.55, + "high": 1550, + "low": 1543.77, + "close": 1545.59 + }, + { + "open": 1545.6, + "high": 1546.69, + "low": 1539.57, + "close": 1539.68 + }, + { + "open": 1539.67, + "high": 1543.83, + "low": 1538.46, + "close": 1542.91 + }, + { + "open": 1542.91, + "high": 1545.89, + "low": 1542.34, + "close": 1543.44 + }, + { + "open": 1543.43, + "high": 1544.62, + "low": 1541.84, + "close": 1541.85 + }, + { + "open": 1541.85, + "high": 1554.35, + "low": 1539.93, + "close": 1545.74 + }, + { + "open": 1545.77, + "high": 1554.47, + "low": 1545, + "close": 1549.46 + }, + { + "open": 1549.46, + "high": 1552.24, + "low": 1549.45, + "close": 1551.24 + }, + { + "open": 1551.25, + "high": 1554.87, + "low": 1550.63, + "close": 1551.87 + }, + { + "open": 1551.86, + "high": 1553.53, + "low": 1545.58, + "close": 1546.72 + }, + { + "open": 1546.71, + "high": 1552.74, + "low": 1546.65, + "close": 1551.27 + }, + { + "open": 1551.26, + "high": 1555.26, + "low": 1549.71, + "close": 1551.45 + }, + { + "open": 1551.44, + "high": 1553.45, + "low": 1548.31, + "close": 1548.54 + }, + { + "open": 1548.54, + "high": 1549.16, + "low": 1546.57, + "close": 1547.39 + }, + { + "open": 1547.38, + "high": 1549.99, + "low": 1546.86, + "close": 1548.82 + }, + { + "open": 1548.82, + "high": 1554.04, + "low": 1544.92, + "close": 1552.11 + }, + { + "open": 1552.11, + "high": 1553.01, + "low": 1548.42, + "close": 1548.67 + }, + { + "open": 1548.66, + "high": 1577.24, + "low": 1548.66, + "close": 1568.11 + }, + { + "open": 1568.11, + "high": 1569.11, + "low": 1562, + "close": 1563.15 + }, + { + "open": 1563.16, + "high": 1572.49, + "low": 1562.7, + "close": 1566.75 + }, + { + "open": 1566.76, + "high": 1567.67, + "low": 1563.83, + "close": 1564.03 + }, + { + "open": 1564.03, + "high": 1566.14, + "low": 1561.79, + "close": 1563.28 + }, + { + "open": 1563.27, + "high": 1569.75, + "low": 1562.43, + "close": 1569.75 + }, + { + "open": 1569.75, + "high": 1571.84, + "low": 1566.17, + "close": 1569.27 + }, + { + "open": 1569.27, + "high": 1569.28, + "low": 1563.78, + "close": 1563.84 + }, + { + "open": 1563.84, + "high": 1565.98, + "low": 1563.84, + "close": 1564.01 + }, + { + "open": 1564.01, + "high": 1566, + "low": 1562.21, + "close": 1563.5 + }, + { + "open": 1563.51, + "high": 1566.46, + "low": 1562.51, + "close": 1564.33 + }, + { + "open": 1564.34, + "high": 1566.17, + "low": 1564.07, + "close": 1565.09 + }, + { + "open": 1565.09, + "high": 1570.37, + "low": 1565.09, + "close": 1567.3 + }, + { + "open": 1567.29, + "high": 1567.3, + "low": 1564.01, + "close": 1564.01 + }, + { + "open": 1564.01, + "high": 1564.08, + "low": 1560.55, + "close": 1560.71 + }, + { + "open": 1560.71, + "high": 1565.8, + "low": 1560.71, + "close": 1563.98 + }, + { + "open": 1563.97, + "high": 1566.38, + "low": 1563.86, + "close": 1564.27 + }, + { + "open": 1564.28, + "high": 1564.71, + "low": 1560.12, + "close": 1561.13 + }, + { + "open": 1561.13, + "high": 1561.66, + "low": 1551.32, + "close": 1556.13 + }, + { + "open": 1556.13, + "high": 1562.78, + "low": 1549.89, + "close": 1559.92 + }, + { + "open": 1559.92, + "high": 1559.97, + "low": 1545.67, + "close": 1552.36 + }, + { + "open": 1552.37, + "high": 1554.87, + "low": 1549.84, + "close": 1550.13 + }, + { + "open": 1550.12, + "high": 1555.85, + "low": 1549.76, + "close": 1553.41 + }, + { + "open": 1553.41, + "high": 1562.56, + "low": 1553.08, + "close": 1560.89 + }, + { + "open": 1560.9, + "high": 1561.67, + "low": 1556.7, + "close": 1557.88 + }, + { + "open": 1557.87, + "high": 1559.82, + "low": 1555.63, + "close": 1558.56 + }, + { + "open": 1558.56, + "high": 1558.91, + "low": 1555.59, + "close": 1557.08 + }, + { + "open": 1557.09, + "high": 1557.56, + "low": 1554.21, + "close": 1555.63 + }, + { + "open": 1555.62, + "high": 1556.44, + "low": 1553.5, + "close": 1556.34 + }, + { + "open": 1556.34, + "high": 1560.77, + "low": 1555.66, + "close": 1559.82 + }, + { + "open": 1559.83, + "high": 1567.93, + "low": 1559.82, + "close": 1561.87 + }, + { + "open": 1561.88, + "high": 1567.23, + "low": 1561.69, + "close": 1564.58 + }, + { + "open": 1564.58, + "high": 1565.55, + "low": 1561.26, + "close": 1561.58 + }, + { + "open": 1561.58, + "high": 1563.41, + "low": 1557.54, + "close": 1557.74 + }, + { + "open": 1557.73, + "high": 1559.22, + "low": 1556.8, + "close": 1557.69 + }, + { + "open": 1557.7, + "high": 1565.46, + "low": 1557.69, + "close": 1565.18 + }, + { + "open": 1565.18, + "high": 1566.39, + "low": 1563.34, + "close": 1564.35 + }, + { + "open": 1564.35, + "high": 1565.13, + "low": 1561.71, + "close": 1561.71 + }, + { + "open": 1561.72, + "high": 1561.85, + "low": 1557.41, + "close": 1558.38 + }, + { + "open": 1558.38, + "high": 1559.17, + "low": 1552.3, + "close": 1554.71 + }, + { + "open": 1554.7, + "high": 1555.89, + "low": 1552.21, + "close": 1553.36 + }, + { + "open": 1553.35, + "high": 1556.24, + "low": 1551.78, + "close": 1555.12 + }, + { + "open": 1555.12, + "high": 1557.49, + "low": 1553.78, + "close": 1554.54 + }, + { + "open": 1554.54, + "high": 1554.55, + "low": 1545.75, + "close": 1550.29 + }, + { + "open": 1550.29, + "high": 1554.52, + "low": 1549.21, + "close": 1552.37 + }, + { + "open": 1552.38, + "high": 1554.16, + "low": 1551.93, + "close": 1552.33 + }, + { + "open": 1552.33, + "high": 1553.41, + "low": 1551.41, + "close": 1551.65 + }, + { + "open": 1551.65, + "high": 1552.49, + "low": 1551, + "close": 1551.51 + }, + { + "open": 1551.51, + "high": 1556.79, + "low": 1550.86, + "close": 1553.86 + }, + { + "open": 1553.85, + "high": 1557.95, + "low": 1553.28, + "close": 1555.2 + }, + { + "open": 1555.19, + "high": 1555.45, + "low": 1546.9, + "close": 1553.41 + }, + { + "open": 1553.4, + "high": 1554.25, + "low": 1551.34, + "close": 1551.35 + }, + { + "open": 1551.35, + "high": 1553.57, + "low": 1551.1, + "close": 1551.67 + }, + { + "open": 1551.67, + "high": 1555.66, + "low": 1550.68, + "close": 1554.05 + }, + { + "open": 1554.09, + "high": 1560.4, + "low": 1554.09, + "close": 1559.55 + }, + { + "open": 1559.56, + "high": 1561.81, + "low": 1558.47, + "close": 1561.8 + }, + { + "open": 1561.81, + "high": 1561.81, + "low": 1558.59, + "close": 1559.39 + }, + { + "open": 1559.39, + "high": 1560.98, + "low": 1558.91, + "close": 1558.95 + }, + { + "open": 1558.96, + "high": 1563.63, + "low": 1557.85, + "close": 1558.69 + }, + { + "open": 1558.7, + "high": 1561.62, + "low": 1556.87, + "close": 1561.25 + }, + { + "open": 1561.25, + "high": 1572, + "low": 1560.1, + "close": 1564.23 + }, + { + "open": 1564.22, + "high": 1565.96, + "low": 1563.01, + "close": 1564.81 + }, + { + "open": 1564.81, + "high": 1579, + "low": 1563.25, + "close": 1577.63 + }, + { + "open": 1577.63, + "high": 1592.55, + "low": 1575.76, + "close": 1591.16 + }, + { + "open": 1591.17, + "high": 1603.78, + "low": 1590.69, + "close": 1594.31 + }, + { + "open": 1594.31, + "high": 1600.91, + "low": 1593.95, + "close": 1594.48 + }, + { + "open": 1594.47, + "high": 1599.53, + "low": 1589.52, + "close": 1590.67 + }, + { + "open": 1590.66, + "high": 1597.42, + "low": 1586.33, + "close": 1597.12 + }, + { + "open": 1597.12, + "high": 1608.5, + "low": 1596.08, + "close": 1607.22 + }, + { + "open": 1607.23, + "high": 1608.55, + "low": 1601.27, + "close": 1602.19 + }, + { + "open": 1602.19, + "high": 1604.2, + "low": 1599.23, + "close": 1601.92 + }, + { + "open": 1601.92, + "high": 1603.39, + "low": 1599.15, + "close": 1601.33 + }, + { + "open": 1601.33, + "high": 1604.92, + "low": 1600.37, + "close": 1604.71 + }, + { + "open": 1604.72, + "high": 1604.8, + "low": 1600.12, + "close": 1600.68 + }, + { + "open": 1600.69, + "high": 1605.89, + "low": 1600.23, + "close": 1604.66 + }, + { + "open": 1604.66, + "high": 1619.05, + "low": 1604.36, + "close": 1607.94 + }, + { + "open": 1607.95, + "high": 1613.84, + "low": 1605.04, + "close": 1612.56 + }, + { + "open": 1612.57, + "high": 1619.78, + "low": 1611.98, + "close": 1618.5 + }, + { + "open": 1618.49, + "high": 1619.35, + "low": 1612, + "close": 1614.67 + }, + { + "open": 1614.67, + "high": 1614.68, + "low": 1608.16, + "close": 1608.9 + }, + { + "open": 1608.89, + "high": 1612.96, + "low": 1608.89, + "close": 1610.35 + }, + { + "open": 1610.36, + "high": 1615.02, + "low": 1610.23, + "close": 1613.92 + }, + { + "open": 1613.91, + "high": 1614.82, + "low": 1611.6, + "close": 1612.52 + }, + { + "open": 1612.51, + "high": 1613.49, + "low": 1606.76, + "close": 1610.14 + }, + { + "open": 1610.14, + "high": 1615.15, + "low": 1608.17, + "close": 1613.23 + }, + { + "open": 1613.23, + "high": 1619.99, + "low": 1613.23, + "close": 1615.83 + }, + { + "open": 1615.84, + "high": 1617.05, + "low": 1610.28, + "close": 1610.39 + }, + { + "open": 1610.39, + "high": 1612.38, + "low": 1606.69, + "close": 1608.64 + }, + { + "open": 1608.64, + "high": 1611.02, + "low": 1608.16, + "close": 1610.05 + }, + { + "open": 1610.05, + "high": 1612, + "low": 1607.59, + "close": 1608.4 + }, + { + "open": 1608.39, + "high": 1609.76, + "low": 1603.32, + "close": 1603.65 + }, + { + "open": 1603.66, + "high": 1606.98, + "low": 1603.38, + "close": 1605.03 + }, + { + "open": 1605.03, + "high": 1611.78, + "low": 1605.03, + "close": 1610.29 + }, + { + "open": 1610.29, + "high": 1611.76, + "low": 1608.83, + "close": 1609.91 + }, + { + "open": 1609.9, + "high": 1609.93, + "low": 1604.31, + "close": 1605.01 + }, + { + "open": 1605.01, + "high": 1606.99, + "low": 1604.35, + "close": 1604.44 + }, + { + "open": 1604.44, + "high": 1607.45, + "low": 1599.66, + "close": 1601.09 + }, + { + "open": 1601.09, + "high": 1605, + "low": 1599.56, + "close": 1603.39 + }, + { + "open": 1603.39, + "high": 1604.36, + "low": 1601.89, + "close": 1604.17 + }, + { + "open": 1604.17, + "high": 1604.4, + "low": 1601.11, + "close": 1601.82 + }, + { + "open": 1601.82, + "high": 1602.58, + "low": 1597, + "close": 1598.54 + }, + { + "open": 1598.55, + "high": 1599.26, + "low": 1595, + "close": 1597.39 + }, + { + "open": 1597.4, + "high": 1599.58, + "low": 1595.34, + "close": 1595.5 + }, + { + "open": 1595.49, + "high": 1597.72, + "low": 1594, + "close": 1596.51 + }, + { + "open": 1596.5, + "high": 1608.06, + "low": 1596.03, + "close": 1605.83 + }, + { + "open": 1605.84, + "high": 1610.01, + "low": 1602.77, + "close": 1603.14 + }, + { + "open": 1603.13, + "high": 1605.96, + "low": 1602.02, + "close": 1605.76 + }, + { + "open": 1605.76, + "high": 1606.06, + "low": 1602.56, + "close": 1603.5 + }, + { + "open": 1603.5, + "high": 1608.17, + "low": 1603, + "close": 1606.46 + }, + { + "open": 1606.47, + "high": 1606.57, + "low": 1600.37, + "close": 1600.49 + }, + { + "open": 1600.49, + "high": 1603.15, + "low": 1599, + "close": 1602.38 + }, + { + "open": 1602.38, + "high": 1605.76, + "low": 1602.33, + "close": 1604.24 + }, + { + "open": 1604.24, + "high": 1613.6, + "low": 1603.63, + "close": 1608.86 + }, + { + "open": 1608.85, + "high": 1608.86, + "low": 1605.31, + "close": 1607.69 + }, + { + "open": 1607.69, + "high": 1611.81, + "low": 1606.26, + "close": 1606.85 + }, + { + "open": 1606.85, + "high": 1607.62, + "low": 1602.87, + "close": 1603.66 + }, + { + "open": 1603.67, + "high": 1603.9, + "low": 1600.29, + "close": 1602.19 + }, + { + "open": 1602.19, + "high": 1602.64, + "low": 1598.88, + "close": 1599.6 + }, + { + "open": 1599.6, + "high": 1602.21, + "low": 1599.59, + "close": 1601.28 + }, + { + "open": 1601.29, + "high": 1602.42, + "low": 1596.21, + "close": 1598 + }, + { + "open": 1598, + "high": 1600, + "low": 1597, + "close": 1599.99 + }, + { + "open": 1600, + "high": 1600.46, + "low": 1598.05, + "close": 1598.1 + }, + { + "open": 1598.11, + "high": 1600.46, + "low": 1597.24, + "close": 1598.62 + }, + { + "open": 1598.62, + "high": 1604.32, + "low": 1597.61, + "close": 1599.57 + }, + { + "open": 1599.58, + "high": 1603.67, + "low": 1599.05, + "close": 1602.84 + }, + { + "open": 1602.84, + "high": 1603.41, + "low": 1601.33, + "close": 1601.71 + }, + { + "open": 1601.72, + "high": 1604.38, + "low": 1600.84, + "close": 1601.06 + }, + { + "open": 1601.07, + "high": 1601.07, + "low": 1584.25, + "close": 1585.56 + }, + { + "open": 1585.57, + "high": 1590.35, + "low": 1583, + "close": 1583.3 + }, + { + "open": 1583.31, + "high": 1585.59, + "low": 1582, + "close": 1582.99 + }, + { + "open": 1582.99, + "high": 1587.47, + "low": 1580, + "close": 1585 + }, + { + "open": 1584.87, + "high": 1586.53, + "low": 1584.36, + "close": 1585.54 + }, + { + "open": 1585.53, + "high": 1592, + "low": 1583.94, + "close": 1590.4 + }, + { + "open": 1590.41, + "high": 1591.7, + "low": 1587.77, + "close": 1591.67 + }, + { + "open": 1591.67, + "high": 1627.93, + "low": 1591.67, + "close": 1619.34 + }, + { + "open": 1619.35, + "high": 1627.28, + "low": 1615.54, + "close": 1620.06 + }, + { + "open": 1620.07, + "high": 1627.91, + "low": 1616.57, + "close": 1618.04 + }, + { + "open": 1618.03, + "high": 1622.4, + "low": 1617.04, + "close": 1620.09 + }, + { + "open": 1620.09, + "high": 1628.86, + "low": 1615.37, + "close": 1615.63 + }, + { + "open": 1615.63, + "high": 1622.18, + "low": 1615.37, + "close": 1621.29 + }, + { + "open": 1621.3, + "high": 1622.8, + "low": 1620, + "close": 1620.51 + }, + { + "open": 1620.5, + "high": 1621.89, + "low": 1613.17, + "close": 1615.52 + }, + { + "open": 1615.53, + "high": 1617.4, + "low": 1614.06, + "close": 1615.32 + }, + { + "open": 1615.33, + "high": 1620.03, + "low": 1615.32, + "close": 1615.85 + }, + { + "open": 1615.84, + "high": 1619.22, + "low": 1603.38, + "close": 1606.41 + }, + { + "open": 1606.41, + "high": 1615.27, + "low": 1606.33, + "close": 1614.67 + }, + { + "open": 1614.68, + "high": 1618.39, + "low": 1614, + "close": 1617.37 + }, + { + "open": 1617.38, + "high": 1620.43, + "low": 1615.78, + "close": 1618.73 + }, + { + "open": 1618.72, + "high": 1618.73, + "low": 1610.77, + "close": 1611.04 + }, + { + "open": 1611.03, + "high": 1614.99, + "low": 1611.03, + "close": 1612.59 + }, + { + "open": 1612.59, + "high": 1613.22, + "low": 1605.77, + "close": 1606.25 + }, + { + "open": 1606.25, + "high": 1608.57, + "low": 1604.04, + "close": 1606.66 + }, + { + "open": 1606.66, + "high": 1609.22, + "low": 1594.74, + "close": 1596.49 + }, + { + "open": 1596.48, + "high": 1600.36, + "low": 1596.16, + "close": 1597.82 + }, + { + "open": 1597.82, + "high": 1598.27, + "low": 1586.09, + "close": 1587.01 + }, + { + "open": 1587.01, + "high": 1589.01, + "low": 1576.53, + "close": 1578.03 + }, + { + "open": 1578.03, + "high": 1583.88, + "low": 1575, + "close": 1579.57 + }, + { + "open": 1579.57, + "high": 1585.38, + "low": 1578.04, + "close": 1584.92 + }, + { + "open": 1584.92, + "high": 1585.58, + "low": 1579.7, + "close": 1584.64 + }, + { + "open": 1584.64, + "high": 1585.69, + "low": 1580.78, + "close": 1582.9 + }, + { + "open": 1582.91, + "high": 1582.91, + "low": 1576.04, + "close": 1578.85 + }, + { + "open": 1578.85, + "high": 1584.74, + "low": 1578.84, + "close": 1584.44 + }, + { + "open": 1584.43, + "high": 1584.94, + "low": 1575, + "close": 1579.18 + }, + { + "open": 1579.18, + "high": 1583.31, + "low": 1579.09, + "close": 1579.83 + }, + { + "open": 1579.83, + "high": 1582.08, + "low": 1573, + "close": 1577.81 + }, + { + "open": 1577.8, + "high": 1582.09, + "low": 1576.19, + "close": 1581.71 + }, + { + "open": 1581.71, + "high": 1582.97, + "low": 1579.21, + "close": 1579.75 + }, + { + "open": 1579.74, + "high": 1583.99, + "low": 1579.52, + "close": 1581.78 + }, + { + "open": 1581.79, + "high": 1583.99, + "low": 1580.27, + "close": 1580.67 + }, + { + "open": 1580.68, + "high": 1589.57, + "low": 1573.86, + "close": 1582.09 + }, + { + "open": 1582.09, + "high": 1586.32, + "low": 1578.54, + "close": 1581.81 + }, + { + "open": 1581.81, + "high": 1588.44, + "low": 1581.8, + "close": 1587.46 + }, + { + "open": 1587.46, + "high": 1618, + "low": 1587.45, + "close": 1611.24 + }, + { + "open": 1611.24, + "high": 1614.26, + "low": 1605.36, + "close": 1612.02 + }, + { + "open": 1612.02, + "high": 1616, + "low": 1608.85, + "close": 1610.58 + }, + { + "open": 1610.59, + "high": 1612.66, + "low": 1607.85, + "close": 1609.51 + }, + { + "open": 1609.51, + "high": 1611.59, + "low": 1607.13, + "close": 1610.04 + }, + { + "open": 1610.04, + "high": 1610.16, + "low": 1603.11, + "close": 1609.25 + }, + { + "open": 1609.25, + "high": 1617.77, + "low": 1605.15, + "close": 1611.78 + }, + { + "open": 1611.77, + "high": 1612.94, + "low": 1608.89, + "close": 1610.54 + }, + { + "open": 1610.53, + "high": 1610.79, + "low": 1607.76, + "close": 1609.46 + }, + { + "open": 1609.46, + "high": 1611.35, + "low": 1607.06, + "close": 1608.93 + }, + { + "open": 1608.92, + "high": 1621.42, + "low": 1608.92, + "close": 1615.58 + }, + { + "open": 1615.59, + "high": 1617.94, + "low": 1609.66, + "close": 1610.42 + }, + { + "open": 1610.41, + "high": 1613.09, + "low": 1607.94, + "close": 1610.04 + }, + { + "open": 1610.03, + "high": 1612.39, + "low": 1608.86, + "close": 1612.38 + }, + { + "open": 1612.39, + "high": 1612.39, + "low": 1606.68, + "close": 1607.45 + }, + { + "open": 1607.45, + "high": 1608.6, + "low": 1603.5, + "close": 1606.03 + }, + { + "open": 1606.03, + "high": 1608.42, + "low": 1605.16, + "close": 1606.46 + }, + { + "open": 1606.46, + "high": 1609.6, + "low": 1605.99, + "close": 1608.61 + }, + { + "open": 1608.61, + "high": 1611.82, + "low": 1608.6, + "close": 1609.35 + }, + { + "open": 1609.36, + "high": 1613.69, + "low": 1608.97, + "close": 1612.61 + }, + { + "open": 1612.61, + "high": 1615.38, + "low": 1600.05, + "close": 1605.29 + }, + { + "open": 1605.29, + "high": 1610.87, + "low": 1594.48, + "close": 1595.84 + }, + { + "open": 1595.85, + "high": 1600.32, + "low": 1593.78, + "close": 1595.82 + }, + { + "open": 1595.81, + "high": 1596.14, + "low": 1588.38, + "close": 1590.05 + }, + { + "open": 1590.05, + "high": 1595, + "low": 1587.23, + "close": 1590.98 + }, + { + "open": 1590.98, + "high": 1590.99, + "low": 1584.71, + "close": 1587.17 + }, + { + "open": 1587.16, + "high": 1591.85, + "low": 1583.51, + "close": 1590.73 + }, + { + "open": 1590.74, + "high": 1594.04, + "low": 1590, + "close": 1592.56 + }, + { + "open": 1592.55, + "high": 1596.8, + "low": 1591.81, + "close": 1593.82 + }, + { + "open": 1593.82, + "high": 1594.8, + "low": 1588.17, + "close": 1590.81 + }, + { + "open": 1590.81, + "high": 1593.02, + "low": 1590.45, + "close": 1592.96 + }, + { + "open": 1592.96, + "high": 1593.35, + "low": 1590, + "close": 1591.3 + }, + { + "open": 1591.31, + "high": 1594.96, + "low": 1590.04, + "close": 1593.73 + }, + { + "open": 1593.74, + "high": 1594.35, + "low": 1592.3, + "close": 1593.39 + }, + { + "open": 1593.4, + "high": 1607.22, + "low": 1593, + "close": 1602.44 + }, + { + "open": 1602.45, + "high": 1611.36, + "low": 1602.44, + "close": 1608.78 + }, + { + "open": 1608.79, + "high": 1613.99, + "low": 1608, + "close": 1610.29 + }, + { + "open": 1610.27, + "high": 1610.8, + "low": 1605.45, + "close": 1607.02 + }, + { + "open": 1607.01, + "high": 1607.17, + "low": 1591.77, + "close": 1594.46 + }, + { + "open": 1594.46, + "high": 1598.08, + "low": 1593.34, + "close": 1596.43 + }, + { + "open": 1596.43, + "high": 1606.97, + "low": 1596.42, + "close": 1606.51 + }, + { + "open": 1606.51, + "high": 1606.51, + "low": 1600, + "close": 1601.63 + }, + { + "open": 1601.63, + "high": 1604.92, + "low": 1600.43, + "close": 1603.69 + }, + { + "open": 1603.68, + "high": 1604.44, + "low": 1600.92, + "close": 1603.73 + }, + { + "open": 1603.73, + "high": 1604.5, + "low": 1596.63, + "close": 1599.85 + }, + { + "open": 1599.86, + "high": 1603.51, + "low": 1597.73, + "close": 1601.35 + }, + { + "open": 1601.35, + "high": 1603.01, + "low": 1598.18, + "close": 1599.56 + }, + { + "open": 1599.57, + "high": 1606, + "low": 1598.74, + "close": 1605.58 + }, + { + "open": 1605.57, + "high": 1605.59, + "low": 1600.41, + "close": 1600.46 + }, + { + "open": 1600.45, + "high": 1602.81, + "low": 1599.72, + "close": 1601.36 + }, + { + "open": 1601.36, + "high": 1602.24, + "low": 1595.8, + "close": 1596.99 + }, + { + "open": 1597, + "high": 1599, + "low": 1590.72, + "close": 1596.49 + }, + { + "open": 1596.49, + "high": 1597.31, + "low": 1593.48, + "close": 1593.61 + }, + { + "open": 1593.62, + "high": 1598.96, + "low": 1593, + "close": 1597.03 + }, + { + "open": 1597.02, + "high": 1598, + "low": 1594.17, + "close": 1596.97 + }, + { + "open": 1596.96, + "high": 1599.64, + "low": 1595.32, + "close": 1597.92 + }, + { + "open": 1597.91, + "high": 1600.45, + "low": 1597.28, + "close": 1597.64 + }, + { + "open": 1597.65, + "high": 1599.3, + "low": 1593.85, + "close": 1596.34 + }, + { + "open": 1596.34, + "high": 1607.62, + "low": 1595.76, + "close": 1603.49 + }, + { + "open": 1603.49, + "high": 1606.36, + "low": 1595, + "close": 1596.49 + }, + { + "open": 1596.48, + "high": 1604.91, + "low": 1596.1, + "close": 1604.91 + }, + { + "open": 1604.9, + "high": 1610, + "low": 1602.61, + "close": 1607.95 + }, + { + "open": 1607.94, + "high": 1614.95, + "low": 1607.59, + "close": 1613.14 + }, + { + "open": 1613.14, + "high": 1613.15, + "low": 1610.38, + "close": 1610.46 + }, + { + "open": 1610.46, + "high": 1611.95, + "low": 1607.42, + "close": 1607.63 + }, + { + "open": 1607.62, + "high": 1609.74, + "low": 1603.26, + "close": 1603.53 + }, + { + "open": 1603.53, + "high": 1604.18, + "low": 1599.56, + "close": 1600.77 + }, + { + "open": 1600.78, + "high": 1604.25, + "low": 1600.77, + "close": 1602.76 + }, + { + "open": 1602.75, + "high": 1604.05, + "low": 1601.46, + "close": 1601.89 + }, + { + "open": 1601.89, + "high": 1606.86, + "low": 1601.01, + "close": 1603.48 + }, + { + "open": 1603.49, + "high": 1605.52, + "low": 1600.24, + "close": 1600.55 + }, + { + "open": 1600.55, + "high": 1603.27, + "low": 1598.87, + "close": 1601.6 + }, + { + "open": 1601.6, + "high": 1605.65, + "low": 1600.97, + "close": 1605 + }, + { + "open": 1605, + "high": 1607.97, + "low": 1602.92, + "close": 1604.44 + }, + { + "open": 1604.44, + "high": 1606.44, + "low": 1602.58, + "close": 1605.59 + }, + { + "open": 1605.58, + "high": 1607.69, + "low": 1604.75, + "close": 1607.22 + }, + { + "open": 1607.23, + "high": 1620.12, + "low": 1607.17, + "close": 1618.72 + }, + { + "open": 1618.71, + "high": 1618.86, + "low": 1611.1, + "close": 1613.18 + }, + { + "open": 1613.19, + "high": 1613.72, + "low": 1610.16, + "close": 1612.13 + }, + { + "open": 1612.1, + "high": 1612.64, + "low": 1608.05, + "close": 1608.25 + }, + { + "open": 1608.25, + "high": 1617.11, + "low": 1608, + "close": 1614.17 + }, + { + "open": 1614.16, + "high": 1614.64, + "low": 1607.77, + "close": 1613.48 + }, + { + "open": 1613.48, + "high": 1625, + "low": 1612.54, + "close": 1618.6 + }, + { + "open": 1618.6, + "high": 1622.92, + "low": 1615.17, + "close": 1622.83 + }, + { + "open": 1622.84, + "high": 1626.94, + "low": 1620.14, + "close": 1621.06 + }, + { + "open": 1621.07, + "high": 1621.87, + "low": 1617.82, + "close": 1619.46 + }, + { + "open": 1619.45, + "high": 1634.2, + "low": 1618.5, + "close": 1627.23 + }, + { + "open": 1627.23, + "high": 1627.39, + "low": 1620.18, + "close": 1622.36 + }, + { + "open": 1622.36, + "high": 1626.93, + "low": 1616.66, + "close": 1620.82 + }, + { + "open": 1620.82, + "high": 1623.84, + "low": 1618.85, + "close": 1622.1 + }, + { + "open": 1622.1, + "high": 1622.15, + "low": 1618.52, + "close": 1620.43 + }, + { + "open": 1620.42, + "high": 1625, + "low": 1619.49, + "close": 1622 + }, + { + "open": 1622, + "high": 1625.99, + "low": 1615.31, + "close": 1617.81 + }, + { + "open": 1617.81, + "high": 1627, + "low": 1616.03, + "close": 1624.35 + }, + { + "open": 1624.36, + "high": 1629.49, + "low": 1624.35, + "close": 1626.81 + }, + { + "open": 1626.81, + "high": 1628.9, + "low": 1624.37, + "close": 1624.84 + }, + { + "open": 1624.85, + "high": 1628.28, + "low": 1622.99, + "close": 1623.1 + }, + { + "open": 1623.1, + "high": 1627.99, + "low": 1621.44, + "close": 1624.59 + }, + { + "open": 1624.6, + "high": 1626.35, + "low": 1622.57, + "close": 1623.59 + }, + { + "open": 1623.6, + "high": 1642.89, + "low": 1623.3, + "close": 1640.62 + }, + { + "open": 1640.63, + "high": 1661.98, + "low": 1634.91, + "close": 1660.48 + }, + { + "open": 1660.49, + "high": 1664.34, + "low": 1644.84, + "close": 1645.57 + }, + { + "open": 1645.58, + "high": 1646.67, + "low": 1595, + "close": 1608.22 + }, + { + "open": 1608.22, + "high": 1614.88, + "low": 1601.43, + "close": 1605.36 + }, + { + "open": 1605.36, + "high": 1609.8, + "low": 1591.92, + "close": 1592.38 + }, + { + "open": 1592.39, + "high": 1602.45, + "low": 1589.01, + "close": 1600 + }, + { + "open": 1600, + "high": 1607.26, + "low": 1599, + "close": 1606.64 + }, + { + "open": 1606.64, + "high": 1607.7, + "low": 1594.71, + "close": 1598.66 + }, + { + "open": 1598.66, + "high": 1604.57, + "low": 1595.3, + "close": 1602.48 + }, + { + "open": 1602.48, + "high": 1612.55, + "low": 1601.32, + "close": 1610.62 + }, + { + "open": 1610.63, + "high": 1615.92, + "low": 1608.01, + "close": 1610.8 + }, + { + "open": 1610.79, + "high": 1612.97, + "low": 1605.89, + "close": 1608.29 + }, + { + "open": 1608.3, + "high": 1609.65, + "low": 1605.11, + "close": 1605.12 + }, + { + "open": 1605.12, + "high": 1607.57, + "low": 1598.35, + "close": 1602.85 + }, + { + "open": 1602.85, + "high": 1602.86, + "low": 1598.44, + "close": 1599.51 + }, + { + "open": 1599.5, + "high": 1600.05, + "low": 1593.32, + "close": 1597.7 + }, + { + "open": 1597.7, + "high": 1603.62, + "low": 1596, + "close": 1598.76 + }, + { + "open": 1598.75, + "high": 1601.76, + "low": 1595.1, + "close": 1600.62 + }, + { + "open": 1600.53, + "high": 1604, + "low": 1596.81, + "close": 1602.64 + }, + { + "open": 1602.63, + "high": 1609.55, + "low": 1602, + "close": 1602.44 + }, + { + "open": 1602.45, + "high": 1603.9, + "low": 1592.51, + "close": 1593.89 + }, + { + "open": 1593.89, + "high": 1594.71, + "low": 1565.67, + "close": 1567.64 + }, + { + "open": 1567.63, + "high": 1570, + "low": 1560, + "close": 1561.39 + }, + { + "open": 1561.4, + "high": 1568.6, + "low": 1560, + "close": 1560.15 + }, + { + "open": 1560.14, + "high": 1567.72, + "low": 1558.34, + "close": 1562.48 + }, + { + "open": 1562.47, + "high": 1567.56, + "low": 1556.87, + "close": 1557.4 + }, + { + "open": 1557.39, + "high": 1562.84, + "low": 1555.8, + "close": 1559.22 + }, + { + "open": 1559.22, + "high": 1560.95, + "low": 1555.83, + "close": 1558.29 + }, + { + "open": 1558.3, + "high": 1558.71, + "low": 1552.58, + "close": 1557.29 + }, + { + "open": 1557.29, + "high": 1558.05, + "low": 1550.73, + "close": 1552.73 + }, + { + "open": 1552.73, + "high": 1556.73, + "low": 1551.93, + "close": 1555.62 + }, + { + "open": 1555.62, + "high": 1558.32, + "low": 1553.89, + "close": 1558.24 + }, + { + "open": 1558.23, + "high": 1560.73, + "low": 1555.85, + "close": 1556.41 + }, + { + "open": 1556.42, + "high": 1557.28, + "low": 1546.5, + "close": 1550.45 + }, + { + "open": 1550.44, + "high": 1553.2, + "low": 1548.64, + "close": 1551.76 + }, + { + "open": 1551.76, + "high": 1553.97, + "low": 1550.33, + "close": 1551.83 + }, + { + "open": 1551.83, + "high": 1551.84, + "low": 1536.1, + "close": 1545.23 + }, + { + "open": 1545.23, + "high": 1545.35, + "low": 1536.02, + "close": 1536.77 + }, + { + "open": 1536.78, + "high": 1545.98, + "low": 1535, + "close": 1545.85 + }, + { + "open": 1545.84, + "high": 1546.92, + "low": 1542.7, + "close": 1544.4 + }, + { + "open": 1544.41, + "high": 1544.41, + "low": 1539.46, + "close": 1542.9 + }, + { + "open": 1542.91, + "high": 1543.88, + "low": 1532.08, + "close": 1537.46 + }, + { + "open": 1537.46, + "high": 1539.81, + "low": 1528.5, + "close": 1539.6 + }, + { + "open": 1539.59, + "high": 1539.91, + "low": 1534.97, + "close": 1537.88 + }, + { + "open": 1537.89, + "high": 1538.41, + "low": 1519.26, + "close": 1529.17 + }, + { + "open": 1529.17, + "high": 1531.96, + "low": 1523.22, + "close": 1530.51 + }, + { + "open": 1530.51, + "high": 1530.53, + "low": 1516.53, + "close": 1516.54 + }, + { + "open": 1516.54, + "high": 1523, + "low": 1515.92, + "close": 1520.8 + }, + { + "open": 1520.8, + "high": 1523.87, + "low": 1519.23, + "close": 1520.13 + }, + { + "open": 1520.12, + "high": 1525.47, + "low": 1519.92, + "close": 1522.6 + }, + { + "open": 1522.59, + "high": 1523.37, + "low": 1518.67, + "close": 1520.02 + }, + { + "open": 1520.01, + "high": 1522.76, + "low": 1517.3, + "close": 1520.03 + }, + { + "open": 1520.03, + "high": 1522.47, + "low": 1514.84, + "close": 1522.2 + }, + { + "open": 1522.19, + "high": 1523.61, + "low": 1519.61, + "close": 1523.36 + }, + { + "open": 1523.36, + "high": 1523.74, + "low": 1520.13, + "close": 1523.27 + }, + { + "open": 1523.27, + "high": 1523.78, + "low": 1513.68, + "close": 1515.15 + }, + { + "open": 1515.14, + "high": 1516.51, + "low": 1508.06, + "close": 1512.14 + }, + { + "open": 1512.15, + "high": 1515.4, + "low": 1511.29, + "close": 1515.02 + }, + { + "open": 1515.02, + "high": 1516.74, + "low": 1512.8, + "close": 1515.19 + }, + { + "open": 1515.19, + "high": 1518.29, + "low": 1514.84, + "close": 1517.85 + }, + { + "open": 1517.85, + "high": 1518.5, + "low": 1509.02, + "close": 1511.14 + }, + { + "open": 1511.14, + "high": 1512.83, + "low": 1509.19, + "close": 1509.79 + }, + { + "open": 1509.78, + "high": 1510, + "low": 1502, + "close": 1504.63 + }, + { + "open": 1504.63, + "high": 1512, + "low": 1498.95, + "close": 1511.51 + }, + { + "open": 1511.51, + "high": 1512.42, + "low": 1506.71, + "close": 1506.71 + }, + { + "open": 1506.71, + "high": 1508.89, + "low": 1504.28, + "close": 1505.15 + }, + { + "open": 1505.15, + "high": 1509.41, + "low": 1499.45, + "close": 1509.01 + }, + { + "open": 1509.01, + "high": 1513.92, + "low": 1507.81, + "close": 1513.12 + }, + { + "open": 1513.12, + "high": 1517.5, + "low": 1512, + "close": 1515.83 + }, + { + "open": 1515.84, + "high": 1515.84, + "low": 1511.78, + "close": 1514.56 + }, + { + "open": 1514.56, + "high": 1514.59, + "low": 1509.96, + "close": 1512.45 + }, + { + "open": 1512.46, + "high": 1514, + "low": 1510.35, + "close": 1513.82 + }, + { + "open": 1513.82, + "high": 1516.09, + "low": 1510.83, + "close": 1513.39 + }, + { + "open": 1513.38, + "high": 1514.71, + "low": 1511.55, + "close": 1514.06 + }, + { + "open": 1514.07, + "high": 1518.42, + "low": 1514.06, + "close": 1517.45 + }, + { + "open": 1517.44, + "high": 1524.17, + "low": 1517.24, + "close": 1522.76 + }, + { + "open": 1522.76, + "high": 1522.77, + "low": 1516.74, + "close": 1519.27 + }, + { + "open": 1519.27, + "high": 1526.14, + "low": 1518.36, + "close": 1525.37 + }, + { + "open": 1525.36, + "high": 1526.16, + "low": 1523.05, + "close": 1524.78 + }, + { + "open": 1524.78, + "high": 1529.84, + "low": 1524.12, + "close": 1524.83 + }, + { + "open": 1524.83, + "high": 1527.97, + "low": 1523.69, + "close": 1525.07 + }, + { + "open": 1525.07, + "high": 1525.91, + "low": 1515.94, + "close": 1517.07 + }, + { + "open": 1517.08, + "high": 1521.93, + "low": 1514.76, + "close": 1519.75 + }, + { + "open": 1519.75, + "high": 1523.8, + "low": 1518.82, + "close": 1522.53 + }, + { + "open": 1522.52, + "high": 1527.71, + "low": 1521.24, + "close": 1527.26 + }, + { + "open": 1527.26, + "high": 1527.26, + "low": 1523.51, + "close": 1524.17 + }, + { + "open": 1524.18, + "high": 1526.33, + "low": 1522.5, + "close": 1526.02 + }, + { + "open": 1526.02, + "high": 1528.99, + "low": 1525.63, + "close": 1528.01 + }, + { + "open": 1528, + "high": 1528.98, + "low": 1524, + "close": 1527.85 + }, + { + "open": 1527.85, + "high": 1527.91, + "low": 1524.87, + "close": 1527.02 + }, + { + "open": 1527.01, + "high": 1527.77, + "low": 1525.08, + "close": 1527.53 + }, + { + "open": 1527.54, + "high": 1527.9, + "low": 1525.01, + "close": 1525.54 + }, + { + "open": 1525.54, + "high": 1526.36, + "low": 1517.7, + "close": 1521.32 + }, + { + "open": 1521.33, + "high": 1523.72, + "low": 1520.06, + "close": 1523.15 + }, + { + "open": 1523.16, + "high": 1526.22, + "low": 1521.86, + "close": 1523.7 + }, + { + "open": 1523.7, + "high": 1523.83, + "low": 1517.23, + "close": 1517.7 + }, + { + "open": 1517.69, + "high": 1519.99, + "low": 1514.45, + "close": 1515.5 + }, + { + "open": 1515.49, + "high": 1520.24, + "low": 1515.12, + "close": 1517.38 + }, + { + "open": 1517.38, + "high": 1519, + "low": 1516.54, + "close": 1517.4 + }, + { + "open": 1517.39, + "high": 1521.31, + "low": 1516.5, + "close": 1519.95 + }, + { + "open": 1519.96, + "high": 1521.47, + "low": 1514.7, + "close": 1519.12 + }, + { + "open": 1519.12, + "high": 1523.72, + "low": 1518.67, + "close": 1523.67 + }, + { + "open": 1523.66, + "high": 1528.82, + "low": 1522.18, + "close": 1528.71 + }, + { + "open": 1528.82, + "high": 1529.12, + "low": 1523.63, + "close": 1525.19 + }, + { + "open": 1525.2, + "high": 1525.2, + "low": 1518.09, + "close": 1523.17 + }, + { + "open": 1523.17, + "high": 1525.99, + "low": 1521.52, + "close": 1522.46 + }, + { + "open": 1522.47, + "high": 1522.9, + "low": 1519.17, + "close": 1521.6 + }, + { + "open": 1521.6, + "high": 1524.29, + "low": 1521.54, + "close": 1524.29 + }, + { + "open": 1524.29, + "high": 1526, + "low": 1523.3, + "close": 1524.49 + }, + { + "open": 1524.5, + "high": 1526, + "low": 1524.05, + "close": 1524.89 + }, + { + "open": 1524.88, + "high": 1526.85, + "low": 1524, + "close": 1524.81 + }, + { + "open": 1524.81, + "high": 1524.81, + "low": 1522.11, + "close": 1522.12 + }, + { + "open": 1522.12, + "high": 1525.61, + "low": 1521.27, + "close": 1521.65 + }, + { + "open": 1521.64, + "high": 1522.47, + "low": 1518.51, + "close": 1519.48 + }, + { + "open": 1519.48, + "high": 1519.48, + "low": 1516.15, + "close": 1517.9 + }, + { + "open": 1517.91, + "high": 1521.05, + "low": 1517.3, + "close": 1519.79 + }, + { + "open": 1519.79, + "high": 1519.8, + "low": 1510.7, + "close": 1513.91 + }, + { + "open": 1513.9, + "high": 1516.09, + "low": 1512.83, + "close": 1513.43 + }, + { + "open": 1513.43, + "high": 1518.21, + "low": 1513, + "close": 1516.58 + }, + { + "open": 1516.59, + "high": 1533.5, + "low": 1516.09, + "close": 1531.9 + }, + { + "open": 1531.89, + "high": 1537.27, + "low": 1530.14, + "close": 1534.55 + }, + { + "open": 1534.55, + "high": 1541.23, + "low": 1534.34, + "close": 1536.95 + }, + { + "open": 1536.96, + "high": 1542.82, + "low": 1536.28, + "close": 1539.96 + }, + { + "open": 1539.97, + "high": 1542.51, + "low": 1535.03, + "close": 1536.75 + }, + { + "open": 1536.76, + "high": 1542.68, + "low": 1533.45, + "close": 1541.74 + }, + { + "open": 1541.75, + "high": 1543.99, + "low": 1538.04, + "close": 1538.45 + }, + { + "open": 1538.44, + "high": 1539.6, + "low": 1536.86, + "close": 1537.48 + }, + { + "open": 1537.47, + "high": 1537.48, + "low": 1534.36, + "close": 1536.22 + }, + { + "open": 1536.22, + "high": 1537.19, + "low": 1534.16, + "close": 1536.47 + }, + { + "open": 1536.46, + "high": 1536.56, + "low": 1531.43, + "close": 1532.8 + }, + { + "open": 1532.81, + "high": 1536.41, + "low": 1532.68, + "close": 1536.21 + }, + { + "open": 1536.22, + "high": 1538.61, + "low": 1535.4, + "close": 1537.8 + }, + { + "open": 1537.8, + "high": 1538.88, + "low": 1535.72, + "close": 1535.99 + }, + { + "open": 1535.99, + "high": 1537.93, + "low": 1535.82, + "close": 1537.49 + }, + { + "open": 1537.48, + "high": 1537.49, + "low": 1532.8, + "close": 1533.82 + }, + { + "open": 1533.82, + "high": 1536.84, + "low": 1533.62, + "close": 1535.49 + }, + { + "open": 1535.49, + "high": 1535.76, + "low": 1534.01, + "close": 1535.42 + }, + { + "open": 1535.42, + "high": 1536, + "low": 1532.17, + "close": 1534.71 + }, + { + "open": 1534.72, + "high": 1538, + "low": 1534.71, + "close": 1537.86 + }, + { + "open": 1537.85, + "high": 1548.43, + "low": 1537.85, + "close": 1545.4 + }, + { + "open": 1545.39, + "high": 1546, + "low": 1542.09, + "close": 1544.54 + }, + { + "open": 1544.54, + "high": 1545.49, + "low": 1542.13, + "close": 1544.51 + }, + { + "open": 1544.51, + "high": 1545.82, + "low": 1543.21, + "close": 1545.81 + }, + { + "open": 1545.81, + "high": 1546.46, + "low": 1543.57, + "close": 1544.9 + }, + { + "open": 1544.9, + "high": 1548.69, + "low": 1540, + "close": 1547.55 + }, + { + "open": 1547.64, + "high": 1549.87, + "low": 1544.78, + "close": 1547.71 + }, + { + "open": 1547.7, + "high": 1549.65, + "low": 1546.11, + "close": 1547.1 + }, + { + "open": 1547.1, + "high": 1548.13, + "low": 1546.6, + "close": 1547.01 + }, + { + "open": 1547, + "high": 1548.88, + "low": 1542.95, + "close": 1548.88 + }, + { + "open": 1548.88, + "high": 1553.79, + "low": 1544.51, + "close": 1544.77 + }, + { + "open": 1544.77, + "high": 1546.59, + "low": 1543.06, + "close": 1543.48 + }, + { + "open": 1543.48, + "high": 1543.49, + "low": 1535.69, + "close": 1536.78 + }, + { + "open": 1536.78, + "high": 1542.5, + "low": 1535.31, + "close": 1541.57 + }, + { + "open": 1541.57, + "high": 1543.7, + "low": 1538.61, + "close": 1538.77 + }, + { + "open": 1538.77, + "high": 1541.47, + "low": 1537.85, + "close": 1538.86 + }, + { + "open": 1538.86, + "high": 1541.55, + "low": 1536.48, + "close": 1539.18 + }, + { + "open": 1539.19, + "high": 1541.52, + "low": 1538.97, + "close": 1539.38 + }, + { + "open": 1539.38, + "high": 1543, + "low": 1536.55, + "close": 1540.87 + }, + { + "open": 1540.86, + "high": 1543.23, + "low": 1539.09, + "close": 1539.58 + }, + { + "open": 1539.58, + "high": 1540.92, + "low": 1536.45, + "close": 1537.5 + }, + { + "open": 1537.49, + "high": 1538.34, + "low": 1533.5, + "close": 1534.83 + }, + { + "open": 1534.84, + "high": 1534.84, + "low": 1517.11, + "close": 1518.98 + }, + { + "open": 1518.98, + "high": 1525.5, + "low": 1518.64, + "close": 1523.83 + }, + { + "open": 1523.82, + "high": 1531.92, + "low": 1520.33, + "close": 1529.16 + }, + { + "open": 1529.16, + "high": 1531.5, + "low": 1524, + "close": 1530.11 + }, + { + "open": 1530.12, + "high": 1530.2, + "low": 1525.52, + "close": 1527.15 + }, + { + "open": 1527.15, + "high": 1530.94, + "low": 1526.55, + "close": 1530.8 + }, + { + "open": 1530.79, + "high": 1530.8, + "low": 1526.56, + "close": 1527.75 + }, + { + "open": 1527.75, + "high": 1530.84, + "low": 1526.98, + "close": 1527.33 + }, + { + "open": 1527.34, + "high": 1528.92, + "low": 1523.77, + "close": 1528.01 + }, + { + "open": 1528.01, + "high": 1531.35, + "low": 1527.46, + "close": 1530.32 + }, + { + "open": 1530.32, + "high": 1538.65, + "low": 1529.45, + "close": 1536.07 + }, + { + "open": 1536.08, + "high": 1536.51, + "low": 1533.75, + "close": 1535.46 + }, + { + "open": 1535.46, + "high": 1536.81, + "low": 1533.28, + "close": 1533.29 + }, + { + "open": 1533.28, + "high": 1533.97, + "low": 1522.46, + "close": 1523.87 + }, + { + "open": 1523.86, + "high": 1524.03, + "low": 1517.61, + "close": 1523.73 + }, + { + "open": 1523.72, + "high": 1537.06, + "low": 1523.21, + "close": 1532.35 + }, + { + "open": 1532.36, + "high": 1532.36, + "low": 1525.15, + "close": 1527.63 + }, + { + "open": 1527.63, + "high": 1531.98, + "low": 1521.21, + "close": 1522.82 + }, + { + "open": 1522.82, + "high": 1522.82, + "low": 1506.7, + "close": 1516.24 + }, + { + "open": 1516.24, + "high": 1527.52, + "low": 1514.14, + "close": 1523.97 + }, + { + "open": 1523.97, + "high": 1523.98, + "low": 1516.85, + "close": 1520.44 + }, + { + "open": 1520.45, + "high": 1525.98, + "low": 1517.98, + "close": 1521.25 + }, + { + "open": 1521.26, + "high": 1526.16, + "low": 1520, + "close": 1521.76 + }, + { + "open": 1521.77, + "high": 1525, + "low": 1518.05, + "close": 1518.17 + }, + { + "open": 1518.17, + "high": 1518.66, + "low": 1510.75, + "close": 1516.05 + }, + { + "open": 1516.05, + "high": 1518.01, + "low": 1509.24, + "close": 1509.65 + }, + { + "open": 1509.65, + "high": 1513.46, + "low": 1507.45, + "close": 1511.38 + }, + { + "open": 1511.37, + "high": 1517.77, + "low": 1510.69, + "close": 1513.23 + }, + { + "open": 1513.24, + "high": 1513.85, + "low": 1502.21, + "close": 1510.72 + }, + { + "open": 1510.72, + "high": 1516.44, + "low": 1508.36, + "close": 1512.4 + }, + { + "open": 1512.4, + "high": 1518.94, + "low": 1507.61, + "close": 1517.46 + }, + { + "open": 1517.45, + "high": 1523.41, + "low": 1515.15, + "close": 1522.78 + }, + { + "open": 1522.78, + "high": 1524, + "low": 1518.84, + "close": 1520.49 + }, + { + "open": 1520.49, + "high": 1523.13, + "low": 1519.01, + "close": 1521.68 + }, + { + "open": 1521.68, + "high": 1527.81, + "low": 1520.5, + "close": 1524.68 + }, + { + "open": 1524.68, + "high": 1532, + "low": 1523.05, + "close": 1527.05 + }, + { + "open": 1527.04, + "high": 1528.39, + "low": 1523, + "close": 1523.42 + }, + { + "open": 1523.41, + "high": 1523.64, + "low": 1514, + "close": 1514.71 + }, + { + "open": 1514.72, + "high": 1518, + "low": 1511.35, + "close": 1517.24 + }, + { + "open": 1517.23, + "high": 1518.75, + "low": 1514.48, + "close": 1515.82 + }, + { + "open": 1515.81, + "high": 1522.58, + "low": 1514.01, + "close": 1521.13 + }, + { + "open": 1521.13, + "high": 1530.48, + "low": 1521.11, + "close": 1527.23 + }, + { + "open": 1527.23, + "high": 1528.46, + "low": 1522.57, + "close": 1525.67 + }, + { + "open": 1525.67, + "high": 1528.64, + "low": 1525.67, + "close": 1528.35 + }, + { + "open": 1528.35, + "high": 1531.14, + "low": 1523.51, + "close": 1523.51 + }, + { + "open": 1523.51, + "high": 1528.76, + "low": 1523.48, + "close": 1527.56 + }, + { + "open": 1527.57, + "high": 1527.99, + "low": 1524.99, + "close": 1526.03 + }, + { + "open": 1526.02, + "high": 1534.93, + "low": 1524.67, + "close": 1532.6 + }, + { + "open": 1532.6, + "high": 1534.19, + "low": 1529.44, + "close": 1529.96 + }, + { + "open": 1529.96, + "high": 1531.24, + "low": 1524.14, + "close": 1525.3 + }, + { + "open": 1525.29, + "high": 1526.09, + "low": 1520.01, + "close": 1520.14 + }, + { + "open": 1520.15, + "high": 1520.51, + "low": 1517.04, + "close": 1519.66 + }, + { + "open": 1519.67, + "high": 1522.26, + "low": 1517.77, + "close": 1519.3 + }, + { + "open": 1519.31, + "high": 1523.43, + "low": 1518.31, + "close": 1520.11 + }, + { + "open": 1520.12, + "high": 1521.27, + "low": 1514.68, + "close": 1515.46 + }, + { + "open": 1515.46, + "high": 1523.5, + "low": 1515.46, + "close": 1523.21 + }, + { + "open": 1523.22, + "high": 1530.34, + "low": 1522.49, + "close": 1529.8 + }, + { + "open": 1529.8, + "high": 1530.43, + "low": 1526.66, + "close": 1527.1 + }, + { + "open": 1527.1, + "high": 1531.29, + "low": 1525.61, + "close": 1527.6 + }, + { + "open": 1527.61, + "high": 1530.92, + "low": 1527.6, + "close": 1530.26 + }, + { + "open": 1530.26, + "high": 1534.01, + "low": 1528.36, + "close": 1533 + }, + { + "open": 1532.93, + "high": 1534.94, + "low": 1529, + "close": 1529.91 + }, + { + "open": 1529.92, + "high": 1530.63, + "low": 1524.38, + "close": 1527.8 + }, + { + "open": 1527.8, + "high": 1529.94, + "low": 1524.87, + "close": 1526.36 + }, + { + "open": 1526.36, + "high": 1529.37, + "low": 1526.33, + "close": 1527.87 + }, + { + "open": 1527.87, + "high": 1529.29, + "low": 1526.79, + "close": 1527.17 + }, + { + "open": 1527.18, + "high": 1527.18, + "low": 1518.55, + "close": 1519.61 + }, + { + "open": 1519.6, + "high": 1521.47, + "low": 1517.39, + "close": 1520.78 + }, + { + "open": 1520.78, + "high": 1522.13, + "low": 1514.46, + "close": 1516.81 + }, + { + "open": 1516.8, + "high": 1517.6, + "low": 1512.82, + "close": 1513.66 + }, + { + "open": 1513.65, + "high": 1516.81, + "low": 1511.44, + "close": 1513.86 + }, + { + "open": 1513.86, + "high": 1515.47, + "low": 1507.11, + "close": 1509.29 + }, + { + "open": 1509.29, + "high": 1513.83, + "low": 1509.11, + "close": 1510.83 + }, + { + "open": 1510.83, + "high": 1518.12, + "low": 1510.83, + "close": 1515.33 + }, + { + "open": 1515.33, + "high": 1517.4, + "low": 1511.99, + "close": 1514.89 + }, + { + "open": 1514.88, + "high": 1518.67, + "low": 1513.78, + "close": 1516.93 + }, + { + "open": 1516.93, + "high": 1517.58, + "low": 1514.1, + "close": 1515.4 + }, + { + "open": 1515.4, + "high": 1518.96, + "low": 1514.65, + "close": 1518.43 + }, + { + "open": 1518.44, + "high": 1523.32, + "low": 1515.35, + "close": 1515.78 + }, + { + "open": 1515.78, + "high": 1521.43, + "low": 1515.46, + "close": 1519.21 + }, + { + "open": 1519.2, + "high": 1519.88, + "low": 1514.15, + "close": 1516.82 + }, + { + "open": 1516.82, + "high": 1517.28, + "low": 1510.85, + "close": 1513.86 + }, + { + "open": 1513.87, + "high": 1516.82, + "low": 1509.37, + "close": 1510.88 + }, + { + "open": 1510.88, + "high": 1512, + "low": 1505.9, + "close": 1505.93 + }, + { + "open": 1505.94, + "high": 1509.39, + "low": 1477.01, + "close": 1478.26 + }, + { + "open": 1478.26, + "high": 1484.64, + "low": 1474.16, + "close": 1482.69 + }, + { + "open": 1482.69, + "high": 1487.83, + "low": 1478.76, + "close": 1479.74 + }, + { + "open": 1479.74, + "high": 1481.79, + "low": 1470.8, + "close": 1474.95 + }, + { + "open": 1474.95, + "high": 1477.59, + "low": 1466.58, + "close": 1468.19 + }, + { + "open": 1468.2, + "high": 1474, + "low": 1455.17, + "close": 1466.85 + }, + { + "open": 1466.9, + "high": 1472.67, + "low": 1466.89, + "close": 1471.37 + }, + { + "open": 1471.37, + "high": 1478.04, + "low": 1467.14, + "close": 1470.07 + }, + { + "open": 1470.07, + "high": 1477.76, + "low": 1468.23, + "close": 1475.57 + }, + { + "open": 1475.56, + "high": 1491.72, + "low": 1474.88, + "close": 1488.58 + }, + { + "open": 1488.58, + "high": 1496.76, + "low": 1484.21, + "close": 1495 + }, + { + "open": 1495, + "high": 1523.99, + "low": 1492.01, + "close": 1514.98 + }, + { + "open": 1514.98, + "high": 1522.88, + "low": 1513.78, + "close": 1518.91 + }, + { + "open": 1518.91, + "high": 1522.64, + "low": 1517, + "close": 1520.67 + }, + { + "open": 1520.67, + "high": 1529.77, + "low": 1513.5, + "close": 1513.51 + }, + { + "open": 1513.51, + "high": 1520, + "low": 1513.28, + "close": 1517.2 + }, + { + "open": 1517.2, + "high": 1519.49, + "low": 1512.2, + "close": 1518.95 + }, + { + "open": 1518.95, + "high": 1519.49, + "low": 1513.26, + "close": 1515.54 + }, + { + "open": 1515.54, + "high": 1519.1, + "low": 1513.83, + "close": 1518.88 + }, + { + "open": 1518.89, + "high": 1538.51, + "low": 1518.88, + "close": 1531.01 + }, + { + "open": 1530.95, + "high": 1533.43, + "low": 1520.54, + "close": 1526.77 + }, + { + "open": 1526.77, + "high": 1530.58, + "low": 1524.92, + "close": 1527.44 + }, + { + "open": 1527.44, + "high": 1528.44, + "low": 1521.03, + "close": 1523.01 + }, + { + "open": 1523.01, + "high": 1524.56, + "low": 1517.95, + "close": 1521.65 + }, + { + "open": 1521.63, + "high": 1521.75, + "low": 1516.33, + "close": 1520.27 + }, + { + "open": 1520.28, + "high": 1523.83, + "low": 1518.53, + "close": 1523.21 + }, + { + "open": 1523.21, + "high": 1524, + "low": 1520.51, + "close": 1522.06 + }, + { + "open": 1522.05, + "high": 1524, + "low": 1519.38, + "close": 1520.38 + }, + { + "open": 1520.37, + "high": 1521.46, + "low": 1519.64, + "close": 1520.5 + }, + { + "open": 1520.5, + "high": 1525.95, + "low": 1520.49, + "close": 1523.01 + }, + { + "open": 1523, + "high": 1523.38, + "low": 1520.25, + "close": 1520.78 + }, + { + "open": 1520.77, + "high": 1521.39, + "low": 1512.85, + "close": 1517.63 + }, + { + "open": 1517.63, + "high": 1517.85, + "low": 1507.66, + "close": 1514.06 + }, + { + "open": 1514.05, + "high": 1517.93, + "low": 1514.01, + "close": 1516.79 + }, + { + "open": 1516.8, + "high": 1516.8, + "low": 1512.55, + "close": 1514.02 + }, + { + "open": 1514.02, + "high": 1514.89, + "low": 1508.97, + "close": 1510.44 + }, + { + "open": 1510.44, + "high": 1512.24, + "low": 1506.68, + "close": 1510.1 + }, + { + "open": 1510.09, + "high": 1511.6, + "low": 1494.65, + "close": 1495.68 + }, + { + "open": 1495.69, + "high": 1498.59, + "low": 1492.45, + "close": 1496.12 + }, + { + "open": 1496.13, + "high": 1496.13, + "low": 1482.16, + "close": 1487 + }, + { + "open": 1487, + "high": 1488.44, + "low": 1469, + "close": 1471.65 + }, + { + "open": 1471.65, + "high": 1476.97, + "low": 1467.08, + "close": 1469.98 + }, + { + "open": 1469.98, + "high": 1475.2, + "low": 1469.78, + "close": 1472.26 + }, + { + "open": 1472.25, + "high": 1487.99, + "low": 1472.25, + "close": 1485.05 + }, + { + "open": 1485.05, + "high": 1494.72, + "low": 1473.92, + "close": 1485.16 + }, + { + "open": 1485.16, + "high": 1485.17, + "low": 1478.93, + "close": 1482.48 + }, + { + "open": 1482.48, + "high": 1489, + "low": 1479.21, + "close": 1482.03 + }, + { + "open": 1482.03, + "high": 1484.28, + "low": 1475.43, + "close": 1481.37 + }, + { + "open": 1481.38, + "high": 1482.3, + "low": 1469.55, + "close": 1470.66 + }, + { + "open": 1470.66, + "high": 1479.85, + "low": 1469, + "close": 1477.53 + }, + { + "open": 1477.54, + "high": 1483.67, + "low": 1476.45, + "close": 1482.88 + }, + { + "open": 1482.89, + "high": 1487.37, + "low": 1478.08, + "close": 1483.37 + }, + { + "open": 1483.37, + "high": 1484.36, + "low": 1472.36, + "close": 1474.97 + }, + { + "open": 1474.97, + "high": 1478.78, + "low": 1474.69, + "close": 1477.75 + }, + { + "open": 1477.75, + "high": 1478.84, + "low": 1470, + "close": 1470.96 + }, + { + "open": 1470.95, + "high": 1472.38, + "low": 1464, + "close": 1470 + }, + { + "open": 1470, + "high": 1471.07, + "low": 1436.02, + "close": 1443.03 + }, + { + "open": 1443.04, + "high": 1451.98, + "low": 1435.58, + "close": 1447.55 + }, + { + "open": 1447.55, + "high": 1448.1, + "low": 1437.01, + "close": 1440.79 + }, + { + "open": 1440.79, + "high": 1450, + "low": 1432.99, + "close": 1443.3 + }, + { + "open": 1443.29, + "high": 1446.49, + "low": 1441.15, + "close": 1443.34 + }, + { + "open": 1443.34, + "high": 1446.58, + "low": 1437.11, + "close": 1438.52 + }, + { + "open": 1438.51, + "high": 1441.99, + "low": 1429.68, + "close": 1439.43 + }, + { + "open": 1439.43, + "high": 1440.82, + "low": 1427.1, + "close": 1433.08 + }, + { + "open": 1433.08, + "high": 1434.82, + "low": 1428.65, + "close": 1429.91 + }, + { + "open": 1429.91, + "high": 1433.24, + "low": 1427.02, + "close": 1430.25 + }, + { + "open": 1430.19, + "high": 1430.2, + "low": 1415.6, + "close": 1423.09 + }, + { + "open": 1423.09, + "high": 1432, + "low": 1421.92, + "close": 1427.53 + }, + { + "open": 1427.52, + "high": 1428.64, + "low": 1421.55, + "close": 1427.64 + }, + { + "open": 1427.64, + "high": 1430.1, + "low": 1415, + "close": 1415.98 + }, + { + "open": 1415.99, + "high": 1421.48, + "low": 1412.15, + "close": 1415.38 + }, + { + "open": 1415.38, + "high": 1424, + "low": 1412.45, + "close": 1423.02 + }, + { + "open": 1423.03, + "high": 1426.99, + "low": 1422, + "close": 1425.62 + }, + { + "open": 1425.61, + "high": 1427.39, + "low": 1421.18, + "close": 1421.98 + }, + { + "open": 1421.98, + "high": 1426.36, + "low": 1418.04, + "close": 1423.96 + }, + { + "open": 1423.97, + "high": 1424.1, + "low": 1418.49, + "close": 1418.49 + }, + { + "open": 1418.49, + "high": 1421.16, + "low": 1414.03, + "close": 1421.15 + }, + { + "open": 1421.16, + "high": 1421.16, + "low": 1415, + "close": 1417.04 + }, + { + "open": 1417.04, + "high": 1418.37, + "low": 1413.06, + "close": 1413.36 + }, + { + "open": 1413.37, + "high": 1423.37, + "low": 1413.21, + "close": 1422.91 + }, + { + "open": 1422.9, + "high": 1425.51, + "low": 1421.11, + "close": 1423.99 + }, + { + "open": 1423.98, + "high": 1425.09, + "low": 1422.73, + "close": 1423.61 + }, + { + "open": 1423.62, + "high": 1424.96, + "low": 1420, + "close": 1420.23 + }, + { + "open": 1420.23, + "high": 1421.87, + "low": 1419.01, + "close": 1421.02 + }, + { + "open": 1421.01, + "high": 1426.03, + "low": 1420.88, + "close": 1425.12 + }, + { + "open": 1425.11, + "high": 1425.68, + "low": 1421.85, + "close": 1423.23 + }, + { + "open": 1423.23, + "high": 1424.73, + "low": 1421.37, + "close": 1423.76 + }, + { + "open": 1423.75, + "high": 1424, + "low": 1418.3, + "close": 1420 + }, + { + "open": 1419.99, + "high": 1420.19, + "low": 1415.15, + "close": 1419.8 + }, + { + "open": 1419.79, + "high": 1421.88, + "low": 1418.08, + "close": 1420.88 + }, + { + "open": 1420.89, + "high": 1424.26, + "low": 1420.87, + "close": 1422.07 + }, + { + "open": 1422.06, + "high": 1423.13, + "low": 1420.59, + "close": 1422.39 + }, + { + "open": 1422.39, + "high": 1423.95, + "low": 1421.75, + "close": 1421.9 + }, + { + "open": 1421.9, + "high": 1423.15, + "low": 1419.15, + "close": 1423.14 + }, + { + "open": 1423.15, + "high": 1423.15, + "low": 1417.79, + "close": 1419.37 + }, + { + "open": 1419.38, + "high": 1419.57, + "low": 1416.26, + "close": 1417.29 + }, + { + "open": 1417.28, + "high": 1419.21, + "low": 1416.5, + "close": 1418.07 + }, + { + "open": 1418.06, + "high": 1418.07, + "low": 1400.46, + "close": 1415.32 + }, + { + "open": 1415.31, + "high": 1424.48, + "low": 1414.74, + "close": 1423.85 + }, + { + "open": 1423.86, + "high": 1424.14, + "low": 1419.33, + "close": 1421.39 + }, + { + "open": 1421.38, + "high": 1430.92, + "low": 1420.53, + "close": 1429.17 + }, + { + "open": 1429.17, + "high": 1438.16, + "low": 1428.17, + "close": 1436.56 + }, + { + "open": 1436.56, + "high": 1436.57, + "low": 1427.07, + "close": 1429.86 + }, + { + "open": 1429.86, + "high": 1429.87, + "low": 1422.71, + "close": 1425.41 + }, + { + "open": 1425.4, + "high": 1428.97, + "low": 1423.88, + "close": 1427.36 + }, + { + "open": 1427.36, + "high": 1432, + "low": 1426.91, + "close": 1431.33 + }, + { + "open": 1431.33, + "high": 1434.92, + "low": 1431.32, + "close": 1433.05 + }, + { + "open": 1433.04, + "high": 1435, + "low": 1431.4, + "close": 1434.81 + }, + { + "open": 1434.81, + "high": 1435.37, + "low": 1430.27, + "close": 1430.44 + }, + { + "open": 1430.45, + "high": 1433.4, + "low": 1430.1, + "close": 1432.26 + }, + { + "open": 1432.27, + "high": 1433.74, + "low": 1428.86, + "close": 1429.56 + }, + { + "open": 1429.56, + "high": 1434.14, + "low": 1428.46, + "close": 1432.89 + }, + { + "open": 1432.89, + "high": 1438.3, + "low": 1431.03, + "close": 1432.86 + }, + { + "open": 1432.87, + "high": 1433.99, + "low": 1431, + "close": 1433.84 + }, + { + "open": 1433.85, + "high": 1434.14, + "low": 1431.21, + "close": 1431.54 + }, + { + "open": 1431.53, + "high": 1433.37, + "low": 1430.3, + "close": 1431.32 + }, + { + "open": 1431.32, + "high": 1434, + "low": 1431.05, + "close": 1432.62 + }, + { + "open": 1432.62, + "high": 1433.24, + "low": 1425.55, + "close": 1426.28 + }, + { + "open": 1426.28, + "high": 1429.8, + "low": 1424.16, + "close": 1427.33 + }, + { + "open": 1427.33, + "high": 1427.88, + "low": 1424.32, + "close": 1426.04 + }, + { + "open": 1426.03, + "high": 1426.33, + "low": 1422.21, + "close": 1423.93 + }, + { + "open": 1423.93, + "high": 1425.44, + "low": 1422.55, + "close": 1423.89 + }, + { + "open": 1423.88, + "high": 1424.55, + "low": 1420.83, + "close": 1421.27 + }, + { + "open": 1421.26, + "high": 1423.64, + "low": 1420.6, + "close": 1422.77 + }, + { + "open": 1422.77, + "high": 1426.46, + "low": 1422.77, + "close": 1425.91 + }, + { + "open": 1425.92, + "high": 1426.42, + "low": 1422.2, + "close": 1422.72 + }, + { + "open": 1422.73, + "high": 1427.27, + "low": 1422.14, + "close": 1427.22 + }, + { + "open": 1427.22, + "high": 1430.2, + "low": 1425.67, + "close": 1428.24 + }, + { + "open": 1428.25, + "high": 1432.24, + "low": 1427.87, + "close": 1429.9 + }, + { + "open": 1429.89, + "high": 1430.99, + "low": 1428, + "close": 1428.42 + }, + { + "open": 1428.43, + "high": 1429.92, + "low": 1424.61, + "close": 1426.78 + }, + { + "open": 1426.79, + "high": 1427.3, + "low": 1424.19, + "close": 1424.26 + }, + { + "open": 1424.27, + "high": 1425.86, + "low": 1423.42, + "close": 1424.38 + }, + { + "open": 1424.37, + "high": 1424.47, + "low": 1420.8, + "close": 1421.27 + }, + { + "open": 1421.27, + "high": 1423.26, + "low": 1421.01, + "close": 1422.37 + }, + { + "open": 1422.37, + "high": 1426, + "low": 1421.84, + "close": 1424.07 + }, + { + "open": 1424.07, + "high": 1424.35, + "low": 1421.43, + "close": 1423.56 + }, + { + "open": 1423.55, + "high": 1423.71, + "low": 1416.58, + "close": 1417.41 + }, + { + "open": 1417.4, + "high": 1420.22, + "low": 1413.72, + "close": 1416.05 + }, + { + "open": 1416.06, + "high": 1417.5, + "low": 1414.67, + "close": 1416.57 + }, + { + "open": 1416.57, + "high": 1422.13, + "low": 1415.8, + "close": 1417.42 + }, + { + "open": 1417.42, + "high": 1417.88, + "low": 1415, + "close": 1416 + }, + { + "open": 1416.01, + "high": 1419.27, + "low": 1415.19, + "close": 1417.59 + }, + { + "open": 1417.59, + "high": 1418.69, + "low": 1415.76, + "close": 1416.86 + }, + { + "open": 1416.86, + "high": 1419.7, + "low": 1414.22, + "close": 1419.51 + }, + { + "open": 1419.5, + "high": 1421.79, + "low": 1417.87, + "close": 1420.19 + }, + { + "open": 1420.19, + "high": 1423.7, + "low": 1413.12, + "close": 1422.48 + }, + { + "open": 1422.49, + "high": 1423.28, + "low": 1421.01, + "close": 1422.19 + }, + { + "open": 1422.19, + "high": 1423.55, + "low": 1421.32, + "close": 1423.15 + }, + { + "open": 1423.15, + "high": 1423.67, + "low": 1421.4, + "close": 1421.76 + }, + { + "open": 1421.77, + "high": 1421.77, + "low": 1419.22, + "close": 1420.17 + }, + { + "open": 1420.18, + "high": 1421.76, + "low": 1416.74, + "close": 1417.99 + }, + { + "open": 1418, + "high": 1418.82, + "low": 1414.58, + "close": 1415.97 + }, + { + "open": 1415.97, + "high": 1417.49, + "low": 1409.57, + "close": 1412.33 + }, + { + "open": 1412.29, + "high": 1414.74, + "low": 1406.68, + "close": 1409.46 + }, + { + "open": 1409.46, + "high": 1410.48, + "low": 1406.23, + "close": 1406.35 + }, + { + "open": 1406.36, + "high": 1413.17, + "low": 1406.34, + "close": 1408.12 + }, + { + "open": 1408.12, + "high": 1411.39, + "low": 1406.3, + "close": 1406.63 + }, + { + "open": 1406.64, + "high": 1410.32, + "low": 1403.94, + "close": 1409.74 + }, + { + "open": 1409.74, + "high": 1411.89, + "low": 1407.7, + "close": 1411.32 + }, + { + "open": 1411.33, + "high": 1413.91, + "low": 1410.85, + "close": 1411.65 + }, + { + "open": 1411.64, + "high": 1414.31, + "low": 1410.68, + "close": 1413 + }, + { + "open": 1413, + "high": 1414.29, + "low": 1410.84, + "close": 1413.58 + }, + { + "open": 1413.59, + "high": 1418.54, + "low": 1413.56, + "close": 1417.54 + }, + { + "open": 1417.54, + "high": 1418.17, + "low": 1415.47, + "close": 1416.05 + }, + { + "open": 1416.05, + "high": 1416.25, + "low": 1413.66, + "close": 1414.45 + }, + { + "open": 1414.46, + "high": 1416.15, + "low": 1411.4, + "close": 1412.57 + }, + { + "open": 1412.57, + "high": 1414.98, + "low": 1411.25, + "close": 1414.75 + }, + { + "open": 1414.75, + "high": 1416, + "low": 1411, + "close": 1415.8 + }, + { + "open": 1415.79, + "high": 1417, + "low": 1413.48, + "close": 1414.55 + }, + { + "open": 1414.56, + "high": 1415.39, + "low": 1412.53, + "close": 1413.86 + }, + { + "open": 1413.86, + "high": 1417.88, + "low": 1413.13, + "close": 1417.18 + }, + { + "open": 1417.17, + "high": 1418.7, + "low": 1415.3, + "close": 1417.9 + }, + { + "open": 1417.89, + "high": 1421.6, + "low": 1417.51, + "close": 1420.29 + }, + { + "open": 1420.3, + "high": 1421.17, + "low": 1419, + "close": 1420.01 + }, + { + "open": 1420, + "high": 1422.16, + "low": 1419.5, + "close": 1421.11 + }, + { + "open": 1421.12, + "high": 1423.97, + "low": 1420.9, + "close": 1423.25 + }, + { + "open": 1423.26, + "high": 1423.26, + "low": 1420.76, + "close": 1420.82 + }, + { + "open": 1420.82, + "high": 1421.01, + "low": 1418.92, + "close": 1419.11 + }, + { + "open": 1419.11, + "high": 1421.25, + "low": 1419, + "close": 1421 + }, + { + "open": 1421.01, + "high": 1422.98, + "low": 1417.66, + "close": 1418.53 + }, + { + "open": 1418.52, + "high": 1419.44, + "low": 1415.01, + "close": 1415.47 + }, + { + "open": 1415.47, + "high": 1417.15, + "low": 1413.71, + "close": 1414.16 + }, + { + "open": 1414.15, + "high": 1416.22, + "low": 1413.37, + "close": 1414.09 + }, + { + "open": 1414.1, + "high": 1416.01, + "low": 1413, + "close": 1415.83 + }, + { + "open": 1415.84, + "high": 1418.37, + "low": 1413.65, + "close": 1414.5 + }, + { + "open": 1414.46, + "high": 1414.96, + "low": 1408.49, + "close": 1411.92 + }, + { + "open": 1411.92, + "high": 1412.58, + "low": 1409.72, + "close": 1412.57 + }, + { + "open": 1412.57, + "high": 1413.05, + "low": 1411.23, + "close": 1411.32 + }, + { + "open": 1411.32, + "high": 1412.81, + "low": 1408.13, + "close": 1409.79 + }, + { + "open": 1409.79, + "high": 1412.35, + "low": 1409.34, + "close": 1410.44 + }, + { + "open": 1410.43, + "high": 1412.92, + "low": 1408.61, + "close": 1412.09 + }, + { + "open": 1412.1, + "high": 1412.84, + "low": 1410.56, + "close": 1410.57 + }, + { + "open": 1410.56, + "high": 1411.32, + "low": 1409.19, + "close": 1409.59 + }, + { + "open": 1409.6, + "high": 1412.09, + "low": 1407.68, + "close": 1410.42 + }, + { + "open": 1410.43, + "high": 1414.86, + "low": 1409.94, + "close": 1414.1 + }, + { + "open": 1414.11, + "high": 1414.47, + "low": 1412.17, + "close": 1412.65 + }, + { + "open": 1412.66, + "high": 1413.85, + "low": 1411.93, + "close": 1413.43 + }, + { + "open": 1413.44, + "high": 1415.79, + "low": 1412.99, + "close": 1413.51 + }, + { + "open": 1413.52, + "high": 1414.6, + "low": 1410.26, + "close": 1410.34 + }, + { + "open": 1410.33, + "high": 1413.83, + "low": 1410.33, + "close": 1413.39 + }, + { + "open": 1413.38, + "high": 1414.82, + "low": 1406.46, + "close": 1407.52 + }, + { + "open": 1407.52, + "high": 1413.46, + "low": 1385.08, + "close": 1406.33 + }, + { + "open": 1406.33, + "high": 1406.42, + "low": 1396.54, + "close": 1403.7 + }, + { + "open": 1403.69, + "high": 1413.95, + "low": 1403.69, + "close": 1411.91 + }, + { + "open": 1411.92, + "high": 1420.39, + "low": 1411.91, + "close": 1414.33 + }, + { + "open": 1414.34, + "high": 1414.52, + "low": 1405.66, + "close": 1405.79 + }, + { + "open": 1405.8, + "high": 1409.11, + "low": 1399, + "close": 1399.24 + }, + { + "open": 1399.23, + "high": 1405.73, + "low": 1399.23, + "close": 1400.56 + }, + { + "open": 1400.55, + "high": 1402, + "low": 1396.96, + "close": 1400.96 + }, + { + "open": 1400.96, + "high": 1401.35, + "low": 1390.31, + "close": 1394.24 + }, + { + "open": 1394.24, + "high": 1395.43, + "low": 1387.54, + "close": 1389.76 + }, + { + "open": 1389.87, + "high": 1390.95, + "low": 1381.54, + "close": 1384.88 + }, + { + "open": 1384.87, + "high": 1388.96, + "low": 1383.78, + "close": 1388.13 + }, + { + "open": 1388.13, + "high": 1393.11, + "low": 1387.84, + "close": 1391.98 + }, + { + "open": 1391.99, + "high": 1393.4, + "low": 1387.03, + "close": 1391.28 + }, + { + "open": 1391.38, + "high": 1391.68, + "low": 1384.04, + "close": 1384.51 + }, + { + "open": 1384.52, + "high": 1387.12, + "low": 1369.29, + "close": 1377.99 + }, + { + "open": 1377.99, + "high": 1384.82, + "low": 1371, + "close": 1383.01 + }, + { + "open": 1383.01, + "high": 1388, + "low": 1381.75, + "close": 1383.05 + }, + { + "open": 1383.06, + "high": 1388.47, + "low": 1383.05, + "close": 1387.82 + }, + { + "open": 1387.82, + "high": 1390, + "low": 1382.78, + "close": 1388.01 + }, + { + "open": 1388, + "high": 1392.96, + "low": 1386.5, + "close": 1391.27 + }, + { + "open": 1391.2, + "high": 1392.61, + "low": 1387.5, + "close": 1389.07 + }, + { + "open": 1389.07, + "high": 1389.07, + "low": 1382.39, + "close": 1385.3 + }, + { + "open": 1385.3, + "high": 1392, + "low": 1384.63, + "close": 1387.41 + }, + { + "open": 1387.41, + "high": 1390.71, + "low": 1386.5, + "close": 1389.12 + }, + { + "open": 1389.11, + "high": 1392.6, + "low": 1382.5, + "close": 1391.93 + }, + { + "open": 1391.93, + "high": 1396.92, + "low": 1390.04, + "close": 1391.11 + }, + { + "open": 1391.11, + "high": 1394.3, + "low": 1383.57, + "close": 1385.3 + }, + { + "open": 1385.31, + "high": 1386.85, + "low": 1379.45, + "close": 1382.74 + }, + { + "open": 1382.74, + "high": 1384.96, + "low": 1379.24, + "close": 1382.24 + }, + { + "open": 1382.24, + "high": 1384.6, + "low": 1380.2, + "close": 1380.79 + }, + { + "open": 1380.78, + "high": 1382.77, + "low": 1376, + "close": 1381.3 + }, + { + "open": 1381.29, + "high": 1384.6, + "low": 1379.59, + "close": 1382.74 + }, + { + "open": 1382.75, + "high": 1385.95, + "low": 1381.72, + "close": 1384.68 + }, + { + "open": 1384.67, + "high": 1392.47, + "low": 1384.26, + "close": 1389.48 + }, + { + "open": 1389.49, + "high": 1392, + "low": 1388.82, + "close": 1390.13 + }, + { + "open": 1390.13, + "high": 1392.46, + "low": 1389.7, + "close": 1391.94 + }, + { + "open": 1391.94, + "high": 1393.95, + "low": 1388.32, + "close": 1388.68 + }, + { + "open": 1388.68, + "high": 1389.35, + "low": 1379.59, + "close": 1381.72 + }, + { + "open": 1381.71, + "high": 1384.78, + "low": 1379.64, + "close": 1384.68 + }, + { + "open": 1384.67, + "high": 1384.92, + "low": 1381.13, + "close": 1383.07 + }, + { + "open": 1383.07, + "high": 1383.24, + "low": 1375.5, + "close": 1375.74 + }, + { + "open": 1375.73, + "high": 1378.38, + "low": 1372.01, + "close": 1374.42 + }, + { + "open": 1374.42, + "high": 1377.99, + "low": 1366, + "close": 1377.51 + }, + { + "open": 1377.51, + "high": 1378.73, + "low": 1372.87, + "close": 1375.18 + }, + { + "open": 1375.18, + "high": 1378.38, + "low": 1371.96, + "close": 1376.76 + }, + { + "open": 1376.77, + "high": 1377.59, + "low": 1370.81, + "close": 1370.95 + }, + { + "open": 1370.95, + "high": 1374.62, + "low": 1363.87, + "close": 1367.88 + }, + { + "open": 1367.88, + "high": 1372, + "low": 1365.03, + "close": 1368.68 + }, + { + "open": 1368.67, + "high": 1373.02, + "low": 1367, + "close": 1370.9 + } +]`) + +func Test_KalmanFilter(t *testing.T) { + type args struct { + allKLines []types.KLine + window int + additionalSmoothWindow uint + } + var klines []types.KLine + if err := json.Unmarshal(testKalmanFilterDataEthusdt5m, &klines); err != nil { + panic(err) + } + tests := []struct { + name string + args args + want float64 + }{ + { + name: "ETHUSDT Kalman Filter 7", + args: args{ + allKLines: klines, + window: 7, + additionalSmoothWindow: 3, + }, + want: 1369.24, + }, + { + name: "ETHUSDT Kalman Filter 25", + args: args{ + allKLines: klines, + window: 25, + additionalSmoothWindow: 0, + }, + want: 1369.84, + }, + { + name: "ETHUSDT Kalman Filter 99", + args: args{ + allKLines: klines, + window: 99, + additionalSmoothWindow: 0, + }, + want: 1369.95, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + filter := &KalmanFilter{ + IntervalWindow: types.IntervalWindow{Window: tt.args.window}, + AdditionalSmoothWindow: tt.args.additionalSmoothWindow, + } + for _, k := range klines { + filter.PushK(k) + } + got := filter.Last() + got = math.Trunc(got*100.0) / 100.0 + if got != tt.want { + t.Errorf("KalmanFilter.Last() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_KalmanFilterEstimationAccurate(t *testing.T) { + type args struct { + allKLines []types.KLine + partialInfo bool + window int + } + var klines []types.KLine + if err := json.Unmarshal(testKalmanFilterDataEthusdt5m, &klines); err != nil { + panic(err) + } + tests := []struct { + name string + args args + want float64 + }{ + { + name: "ETHUSDT Kalman Filter full K-line square error 7", + args: args{ + allKLines: klines, + partialInfo: false, + window: 7, + }, + }, + { + name: "ETHUSDT Kalman Filter full K-line square error 25", + args: args{ + allKLines: klines, + partialInfo: false, + window: 25, + }, + }, + { + name: "ETHUSDT Kalman Filter full K-line square error 99", + args: args{ + allKLines: klines, + partialInfo: false, + window: 99, + }, + }, + { + name: "ETHUSDT Kalman Filter partial K-line square error 7", + args: args{ + allKLines: klines, + partialInfo: true, + window: 7, + }, + }, + { + name: "ETHUSDT Kalman Filter partial K-line square error 25", + args: args{ + allKLines: klines, + partialInfo: true, + window: 25, + }, + }, + { + name: "ETHUSDT Kalman Filter partial K-line square error 99", + args: args{ + allKLines: klines, + partialInfo: true, + window: 99, + }, + }, + } + klineSquareError := func(base float64, k types.KLine) float64 { + openDiff := math.Abs(k.Open.Float64() - base) + highDiff := math.Abs(k.High.Float64() - base) + lowDiff := math.Abs(k.Low.Float64() - base) + closeDiff := math.Abs(k.Close.Float64() - base) + return openDiff*openDiff + highDiff*highDiff + lowDiff*lowDiff + closeDiff*closeDiff + } + closeSquareError := func(base float64, k types.KLine) float64 { + closeDiff := math.Abs(k.Close.Float64() - base) + return closeDiff * closeDiff + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + filter := &KalmanFilter{IntervalWindow: types.IntervalWindow{Window: tt.args.window}} + ewma := &EWMA{IntervalWindow: types.IntervalWindow{Window: tt.args.window}} + + var filterDiff2Sum, ewmaDiff2Sum float64 + var filterCloseDiff2Sum, ewmaCloseDiff2Sum float64 + var numEstimations = 0 + for _, k := range klines { + // square error between last estimated state and current actual state + if ewma.Length() > 0 { + filterDiff2Sum += klineSquareError(filter.Last(), k) + ewmaDiff2Sum += klineSquareError(ewma.Last(), k) + filterCloseDiff2Sum += closeSquareError(filter.Last(), k) + ewmaCloseDiff2Sum += closeSquareError(ewma.Last(), k) + numEstimations++ + } + + // update estimations + if tt.args.partialInfo { + filter.Update(k.Close.Float64()) + ewma.Update(k.Close.Float64()) + } else { + filter.PushK(k) + ewma.PushK(k) + } + } + filterSquareErr := math.Sqrt(filterDiff2Sum / float64(numEstimations*4)) + ewmaSquareErr := math.Sqrt(ewmaDiff2Sum / float64(numEstimations*4)) + if filterSquareErr > ewmaSquareErr { + t.Errorf("filter K-Line square error %f > EWMA K-Line square error %v", filterSquareErr, ewmaSquareErr) + } + filterCloseSquareErr := math.Sqrt(filterCloseDiff2Sum / float64(numEstimations)) + ewmaCloseSquareErr := math.Sqrt(ewmaCloseDiff2Sum / float64(numEstimations)) + if filterCloseSquareErr > ewmaCloseSquareErr { + t.Errorf("filter close price square error %f > EWMA close price square error %v", filterCloseSquareErr, ewmaCloseSquareErr) + } + }) + } +} diff --git a/pkg/indicator/line.go b/pkg/indicator/line.go index 763d58f89e..edb8276f16 100644 --- a/pkg/indicator/line.go +++ b/pkg/indicator/line.go @@ -12,6 +12,7 @@ import ( // 3. resistance // of the market data, defined with series interface type Line struct { + types.SeriesBase types.IntervalWindow start float64 end float64 @@ -21,11 +22,12 @@ type Line struct { Interval types.Interval } -func (l *Line) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { +func (l *Line) handleKLineWindowUpdate(interval types.Interval, allKLines types.KLineWindow) { if interval != l.Interval { return } - newTime := window.Last().EndTime.Time() + + newTime := allKLines.Last().EndTime.Time() delta := int(newTime.Sub(l.currentTime).Minutes()) / l.Interval.Minutes() l.startIndex += delta l.endIndex += delta @@ -63,7 +65,7 @@ func (l *Line) SetXY2(index int, value float64) { } func NewLine(startIndex int, startValue float64, endIndex int, endValue float64, interval types.Interval) *Line { - return &Line{ + line := &Line{ start: startValue, end: endValue, startIndex: startIndex, @@ -71,6 +73,8 @@ func NewLine(startIndex int, startValue float64, endIndex int, endValue float64, currentTime: time.Time{}, Interval: interval, } + line.SeriesBase.Series = line + return line } -var _ types.Series = &Line{} +var _ types.SeriesExtend = &Line{} diff --git a/pkg/indicator/low.go b/pkg/indicator/low.go new file mode 100644 index 0000000000..77d0457ead --- /dev/null +++ b/pkg/indicator/low.go @@ -0,0 +1,37 @@ +package indicator + +import ( + "time" + + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/types" +) + +//go:generate callbackgen -type Low +type Low struct { + types.IntervalWindow + types.SeriesBase + + Values floats.Slice + EndTime time.Time + + updateCallbacks []func(value float64) +} + +func (inc *Low) Update(value float64) { + if len(inc.Values) == 0 { + inc.SeriesBase.Series = inc + } + + inc.Values.Push(value) +} + +func (inc *Low) PushK(k types.KLine) { + if k.EndTime.Before(inc.EndTime) { + return + } + + inc.Update(k.Low.Float64()) + inc.EndTime = k.EndTime.Time() + inc.EmitUpdate(inc.Last()) +} diff --git a/pkg/indicator/low_callbacks.go b/pkg/indicator/low_callbacks.go new file mode 100644 index 0000000000..bd261b79ae --- /dev/null +++ b/pkg/indicator/low_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type Low"; DO NOT EDIT. + +package indicator + +import () + +func (inc *Low) OnUpdate(cb func(value float64)) { + inc.updateCallbacks = append(inc.updateCallbacks, cb) +} + +func (inc *Low) EmitUpdate(value float64) { + for _, cb := range inc.updateCallbacks { + cb(value) + } +} diff --git a/pkg/indicator/macd.go b/pkg/indicator/macd.go index 3dfbd6d450..e8146bd173 100644 --- a/pkg/indicator/macd.go +++ b/pkg/indicator/macd.go @@ -3,6 +3,7 @@ package indicator import ( "time" + "github.com/c9s/bbgo/pkg/datatype/floats" "github.com/c9s/bbgo/pkg/types" ) @@ -11,82 +12,94 @@ macd implements moving average convergence divergence indicator Moving Average Convergence Divergence (MACD) - https://www.investopedia.com/terms/m/macd.asp +- https://school.stockcharts.com/doku.php?id=technical_indicators:macd-histogram */ +type MACDConfig struct { + types.IntervalWindow // 9 + + // ShortPeriod is the short term period EMA, usually 12 + ShortPeriod int `json:"short"` + // LongPeriod is the long term period EMA, usually 26 + LongPeriod int `json:"long"` +} //go:generate callbackgen -type MACD type MACD struct { - types.IntervalWindow // 9 - ShortPeriod int // 12 - LongPeriod int // 26 - Values types.Float64Slice - FastEWMA EWMA - SlowEWMA EWMA - SignalLine EWMA - Histogram types.Float64Slice + MACDConfig + + Values floats.Slice `json:"-"` + fastEWMA, slowEWMA, signalLine *EWMA + Histogram floats.Slice `json:"-"` EndTime time.Time - UpdateCallbacks []func(value float64) + updateCallbacks []func(macd, signal, histogram float64) } func (inc *MACD) Update(x float64) { if len(inc.Values) == 0 { - inc.FastEWMA = EWMA{IntervalWindow: types.IntervalWindow{Window: inc.ShortPeriod}} - inc.SlowEWMA = EWMA{IntervalWindow: types.IntervalWindow{Window: inc.LongPeriod}} - inc.SignalLine = EWMA{IntervalWindow: types.IntervalWindow{Window: inc.Window}} + // apply default values + inc.fastEWMA = &EWMA{IntervalWindow: types.IntervalWindow{Window: inc.ShortPeriod}} + inc.slowEWMA = &EWMA{IntervalWindow: types.IntervalWindow{Window: inc.LongPeriod}} + inc.signalLine = &EWMA{IntervalWindow: types.IntervalWindow{Window: inc.Window}} + if inc.ShortPeriod == 0 { + inc.ShortPeriod = 12 + } + + if inc.LongPeriod == 0 { + inc.LongPeriod = 26 + } } // update fast and slow ema - inc.FastEWMA.Update(x) - inc.SlowEWMA.Update(x) + inc.fastEWMA.Update(x) + inc.slowEWMA.Update(x) - // update macd - macd := inc.FastEWMA.Last() - inc.SlowEWMA.Last() + // update MACD value, it's also the signal line + fast := inc.fastEWMA.Last() + slow := inc.slowEWMA.Last() + macd := fast - slow inc.Values.Push(macd) // update signal line - inc.SignalLine.Update(macd) + inc.signalLine.Update(macd) + signal := inc.signalLine.Last() // update histogram - inc.Histogram.Push(macd - inc.SignalLine.Last()) -} + histogram := macd - signal + inc.Histogram.Push(histogram) -func (inc *MACD) calculateMACD(kLines []types.KLine, priceF KLinePriceMapper) float64 { - for _, kline := range kLines { - inc.Update(kline.Close.Float64()) - } - return inc.Values[len(inc.Values)-1] + inc.EmitUpdate(macd, signal, histogram) } -func (inc *MACD) calculateAndUpdate(kLines []types.KLine) { - if len(kLines) == 0 { - return +func (inc *MACD) Last() float64 { + if len(inc.Values) == 0 { + return 0.0 } - for _, k := range kLines { - if inc.EndTime != zeroTime && !k.EndTime.After(inc.EndTime) { - continue - } - inc.Update(k.Close.Float64()) - } + return inc.Values[len(inc.Values)-1] +} - inc.EmitUpdate(inc.Values[len(inc.Values)-1]) - inc.EndTime = kLines[len(kLines)-1].EndTime.Time() +func (inc *MACD) Length() int { + return len(inc.Values) } -func (inc *MACD) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { - if inc.Interval != interval { - return - } +func (inc *MACD) PushK(k types.KLine) { + inc.Update(k.Close.Float64()) +} - inc.calculateAndUpdate(window) +func (inc *MACD) MACD() types.SeriesExtend { + out := &MACDValues{MACD: inc} + out.SeriesBase.Series = out + return out } -func (inc *MACD) Bind(updater KLineWindowUpdater) { - updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) +func (inc *MACD) Singals() types.SeriesExtend { + return inc.signalLine } type MACDValues struct { + types.SeriesBase *MACD } @@ -94,6 +107,7 @@ func (inc *MACDValues) Last() float64 { if len(inc.Values) == 0 { return 0.0 } + return inc.Values[len(inc.Values)-1] } @@ -102,17 +116,10 @@ func (inc *MACDValues) Index(i int) float64 { if length == 0 || length-1-i < 0 { return 0.0 } + return inc.Values[length-1+i] } func (inc *MACDValues) Length() int { return len(inc.Values) } - -func (inc *MACD) MACD() types.Series { - return &MACDValues{inc} -} - -func (inc *MACD) Singals() types.Series { - return &inc.SignalLine -} diff --git a/pkg/indicator/macd_callbacks.go b/pkg/indicator/macd_callbacks.go index a368fa625d..93a1bc8c99 100644 --- a/pkg/indicator/macd_callbacks.go +++ b/pkg/indicator/macd_callbacks.go @@ -4,12 +4,12 @@ package indicator import () -func (inc *MACD) OnUpdate(cb func(value float64)) { - inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) +func (inc *MACD) OnUpdate(cb func(macd float64, signal float64, histogram float64)) { + inc.updateCallbacks = append(inc.updateCallbacks, cb) } -func (inc *MACD) EmitUpdate(value float64) { - for _, cb := range inc.UpdateCallbacks { - cb(value) +func (inc *MACD) EmitUpdate(macd float64, signal float64, histogram float64) { + for _, cb := range inc.updateCallbacks { + cb(macd, signal, histogram) } } diff --git a/pkg/indicator/macd_test.go b/pkg/indicator/macd_test.go index 6cf074fff6..e3f5075bb7 100644 --- a/pkg/indicator/macd_test.go +++ b/pkg/indicator/macd_test.go @@ -40,9 +40,12 @@ func Test_calculateMACD(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { iw := types.IntervalWindow{Window: 9} - macd := MACD{IntervalWindow: iw, ShortPeriod: 12, LongPeriod: 26} - priceF := KLineClosePriceMapper - got := macd.calculateMACD(tt.kLines, priceF) + macd := MACD{MACDConfig: MACDConfig{IntervalWindow: iw, ShortPeriod: 12, LongPeriod: 26}} + for _, k := range tt.kLines { + macd.PushK(k) + } + + got := macd.Last() diff := math.Trunc((got-tt.want)*100) / 100 if diff != 0 { t.Errorf("calculateMACD() = %v, want %v", got, tt.want) diff --git a/pkg/indicator/mapper.go b/pkg/indicator/mapper.go new file mode 100644 index 0000000000..e169bef168 --- /dev/null +++ b/pkg/indicator/mapper.go @@ -0,0 +1,33 @@ +package indicator + +import "github.com/c9s/bbgo/pkg/types" + +type KLineValueMapper func(k types.KLine) float64 + +func KLineOpenPriceMapper(k types.KLine) float64 { + return k.Open.Float64() +} + +func KLineClosePriceMapper(k types.KLine) float64 { + return k.Close.Float64() +} + +func KLineTypicalPriceMapper(k types.KLine) float64 { + return (k.High.Float64() + k.Low.Float64() + k.Close.Float64()) / 3. +} + +func KLinePriceVolumeMapper(k types.KLine) float64 { + return k.Close.Mul(k.Volume).Float64() +} + +func KLineVolumeMapper(k types.KLine) float64 { + return k.Volume.Float64() +} + +func MapKLinePrice(kLines []types.KLine, f KLineValueMapper) (prices []float64) { + for _, k := range kLines { + prices = append(prices, f(k)) + } + + return prices +} diff --git a/pkg/indicator/obv.go b/pkg/indicator/obv.go index 3ea11772da..4d13fbfa21 100644 --- a/pkg/indicator/obv.go +++ b/pkg/indicator/obv.go @@ -3,6 +3,7 @@ package indicator import ( "time" + "github.com/c9s/bbgo/pkg/datatype/floats" "github.com/c9s/bbgo/pkg/types" ) @@ -14,16 +15,18 @@ On-Balance Volume (OBV) Definition */ //go:generate callbackgen -type OBV type OBV struct { + types.SeriesBase types.IntervalWindow - Values types.Float64Slice + Values floats.Slice PrePrice float64 + EndTime time.Time - EndTime time.Time - UpdateCallbacks []func(value float64) + updateCallbacks []func(value float64) } func (inc *OBV) Update(price, volume float64) { if len(inc.Values) == 0 { + inc.SeriesBase.Series = inc inc.PrePrice = price inc.Values.Push(volume) return @@ -43,13 +46,28 @@ func (inc *OBV) Last() float64 { return inc.Values[len(inc.Values)-1] } -func (inc *OBV) calculateAndUpdate(kLines []types.KLine) { +func (inc *OBV) Index(i int) float64 { + if len(inc.Values)-i <= 0 { + return 0.0 + } + return inc.Values[len(inc.Values)-i-1] +} + +var _ types.SeriesExtend = &OBV{} + +func (inc *OBV) PushK(k types.KLine) { + inc.Update(k.Close.Float64(), k.Volume.Float64()) +} + +func (inc *OBV) CalculateAndUpdate(kLines []types.KLine) { for _, k := range kLines { if inc.EndTime != zeroTime && !k.EndTime.After(inc.EndTime) { continue } - inc.Update(k.Close.Float64(), k.Volume.Float64()) + + inc.PushK(k) } + inc.EmitUpdate(inc.Last()) inc.EndTime = kLines[len(kLines)-1].EndTime.Time() } @@ -59,7 +77,7 @@ func (inc *OBV) handleKLineWindowUpdate(interval types.Interval, window types.KL return } - inc.calculateAndUpdate(window) + inc.CalculateAndUpdate(window) } func (inc *OBV) Bind(updater KLineWindowUpdater) { diff --git a/pkg/indicator/obv_callbacks.go b/pkg/indicator/obv_callbacks.go index b0897152c8..2b1ce69b15 100644 --- a/pkg/indicator/obv_callbacks.go +++ b/pkg/indicator/obv_callbacks.go @@ -5,11 +5,11 @@ package indicator import () func (inc *OBV) OnUpdate(cb func(value float64)) { - inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) + inc.updateCallbacks = append(inc.updateCallbacks, cb) } func (inc *OBV) EmitUpdate(value float64) { - for _, cb := range inc.UpdateCallbacks { + for _, cb := range inc.updateCallbacks { cb(value) } } diff --git a/pkg/indicator/obv_test.go b/pkg/indicator/obv_test.go index 66d951a29d..a4bc0ad216 100644 --- a/pkg/indicator/obv_test.go +++ b/pkg/indicator/obv_test.go @@ -6,6 +6,7 @@ import ( "github.com/stretchr/testify/assert" + "github.com/c9s/bbgo/pkg/datatype/floats" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) @@ -30,7 +31,7 @@ func Test_calculateOBV(t *testing.T) { name string kLines []types.KLine window int - want types.Float64Slice + want floats.Slice }{ { name: "trivial_case", @@ -38,20 +39,20 @@ func Test_calculateOBV(t *testing.T) { []fixedpoint.Value{fixedpoint.Zero}, []fixedpoint.Value{fixedpoint.One}, ), window: 0, - want: types.Float64Slice{1.0}, + want: floats.Slice{1.0}, }, { name: "easy_case", kLines: buildKLines(input1, input2), window: 0, - want: types.Float64Slice{3, 1, -1, 5}, + want: floats.Slice{3, 1, -1, 5}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { obv := OBV{IntervalWindow: types.IntervalWindow{Window: tt.window}} - obv.calculateAndUpdate(tt.kLines) + obv.CalculateAndUpdate(tt.kLines) assert.Equal(t, len(obv.Values), len(tt.want)) for i, v := range obv.Values { assert.InDelta(t, v, tt.want[i], Delta) diff --git a/pkg/indicator/pivot.go b/pkg/indicator/pivot.go new file mode 100644 index 0000000000..8e027e4109 --- /dev/null +++ b/pkg/indicator/pivot.go @@ -0,0 +1,126 @@ +package indicator + +import ( + "fmt" + "time" + + log "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/types" +) + + +//go:generate callbackgen -type Pivot +type Pivot struct { + types.IntervalWindow + + // Values + Lows floats.Slice // higher low + Highs floats.Slice // lower high + + EndTime time.Time + + updateCallbacks []func(valueLow, valueHigh float64) +} + +func (inc *Pivot) LastLow() float64 { + if len(inc.Lows) == 0 { + return 0.0 + } + return inc.Lows[len(inc.Lows)-1] +} + +func (inc *Pivot) LastHigh() float64 { + if len(inc.Highs) == 0 { + return 0.0 + } + return inc.Highs[len(inc.Highs)-1] +} + +func (inc *Pivot) CalculateAndUpdate(klines []types.KLine) { + if len(klines) < inc.Window { + return + } + + var end = len(klines) - 1 + var lastKLine = klines[end] + + // skip old data + if inc.EndTime != zeroTime && lastKLine.GetEndTime().Before(inc.EndTime) { + return + } + + recentT := klines[end-(inc.Window-1) : end+1] + + l, h, err := calculatePivot(recentT, inc.Window, KLineLowPriceMapper, KLineHighPriceMapper) + if err != nil { + log.WithError(err).Error("can not calculate pivots") + return + } + + if l > 0.0 { + inc.Lows.Push(l) + } + if h > 0.0 { + inc.Highs.Push(h) + } + + if len(inc.Lows) > MaxNumOfVOL { + inc.Lows = inc.Lows[MaxNumOfVOLTruncateSize-1:] + } + if len(inc.Highs) > MaxNumOfVOL { + inc.Highs = inc.Highs[MaxNumOfVOLTruncateSize-1:] + } + + inc.EndTime = klines[end].GetEndTime().Time() + + inc.EmitUpdate(l, h) + +} + +func (inc *Pivot) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { + if inc.Interval != interval { + return + } + + inc.CalculateAndUpdate(window) +} + +func (inc *Pivot) Bind(updater KLineWindowUpdater) { + updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) +} + +func calculatePivot(klines []types.KLine, window int, valLow KLineValueMapper, valHigh KLineValueMapper) (float64, float64, error) { + length := len(klines) + if length == 0 || length < window { + return 0., 0., fmt.Errorf("insufficient elements for calculating with window = %d", window) + } + + var lows floats.Slice + var highs floats.Slice + for _, k := range klines { + lows.Push(valLow(k)) + highs.Push(valHigh(k)) + } + + pl := 0. + if lows.Min() == lows.Index(int(window/2.)-1) { + pl = lows.Min() + } + + ph := 0. + if highs.Max() == highs.Index(int(window/2.)-1) { + ph = highs.Max() + } + + return pl, ph, nil +} + +func KLineLowPriceMapper(k types.KLine) float64 { + return k.Low.Float64() +} + +func KLineHighPriceMapper(k types.KLine) float64 { + return k.High.Float64() +} diff --git a/pkg/indicator/pivot_callbacks.go b/pkg/indicator/pivot_callbacks.go new file mode 100644 index 0000000000..4c3a90ccf0 --- /dev/null +++ b/pkg/indicator/pivot_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type Pivot"; DO NOT EDIT. + +package indicator + +import () + +func (inc *Pivot) OnUpdate(cb func(valueLow float64, valueHigh float64)) { + inc.updateCallbacks = append(inc.updateCallbacks, cb) +} + +func (inc *Pivot) EmitUpdate(valueLow float64, valueHigh float64) { + for _, cb := range inc.updateCallbacks { + cb(valueLow, valueHigh) + } +} diff --git a/pkg/indicator/pivothigh.go b/pkg/indicator/pivothigh.go new file mode 100644 index 0000000000..8414c826cd --- /dev/null +++ b/pkg/indicator/pivothigh.go @@ -0,0 +1,64 @@ +package indicator + +import ( + "time" + + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/types" +) + +//go:generate callbackgen -type PivotHigh +type PivotHigh struct { + types.SeriesBase + + types.IntervalWindow + + Highs floats.Slice + Values floats.Slice + EndTime time.Time + + updateCallbacks []func(value float64) +} + +func (inc *PivotHigh) Length() int { + return inc.Values.Length() +} + +func (inc *PivotHigh) Last() float64 { + if len(inc.Values) == 0 { + return 0.0 + } + + return inc.Values.Last() +} + +func (inc *PivotHigh) Update(value float64) { + if len(inc.Highs) == 0 { + inc.SeriesBase.Series = inc + } + + inc.Highs.Push(value) + + if len(inc.Highs) < inc.Window { + return + } + + high, ok := calculatePivotHigh(inc.Highs, inc.Window, inc.RightWindow) + if !ok { + return + } + + if high > 0.0 { + inc.Values.Push(high) + } +} + +func (inc *PivotHigh) PushK(k types.KLine) { + if k.EndTime.Before(inc.EndTime) { + return + } + + inc.Update(k.High.Float64()) + inc.EndTime = k.EndTime.Time() + inc.EmitUpdate(inc.Last()) +} diff --git a/pkg/indicator/pivothigh_callbacks.go b/pkg/indicator/pivothigh_callbacks.go new file mode 100644 index 0000000000..64891ada03 --- /dev/null +++ b/pkg/indicator/pivothigh_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type PivotHigh"; DO NOT EDIT. + +package indicator + +import () + +func (inc *PivotHigh) OnUpdate(cb func(value float64)) { + inc.updateCallbacks = append(inc.updateCallbacks, cb) +} + +func (inc *PivotHigh) EmitUpdate(value float64) { + for _, cb := range inc.updateCallbacks { + cb(value) + } +} diff --git a/pkg/indicator/pivotlow.go b/pkg/indicator/pivotlow.go new file mode 100644 index 0000000000..135b61868a --- /dev/null +++ b/pkg/indicator/pivotlow.go @@ -0,0 +1,76 @@ +package indicator + +import ( + "time" + + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/types" +) + +//go:generate callbackgen -type PivotLow +type PivotLow struct { + types.SeriesBase + + types.IntervalWindow + + Lows floats.Slice + Values floats.Slice + EndTime time.Time + + updateCallbacks []func(value float64) +} + +func (inc *PivotLow) Length() int { + return inc.Values.Length() +} + +func (inc *PivotLow) Last() float64 { + if len(inc.Values) == 0 { + return 0.0 + } + + return inc.Values.Last() +} + +func (inc *PivotLow) Update(value float64) { + if len(inc.Lows) == 0 { + inc.SeriesBase.Series = inc + } + + inc.Lows.Push(value) + + if len(inc.Lows) < inc.Window { + return + } + + low, ok := calculatePivotLow(inc.Lows, inc.Window, inc.RightWindow) + if !ok { + return + } + + if low > 0.0 { + inc.Values.Push(low) + } +} + +func (inc *PivotLow) PushK(k types.KLine) { + if k.EndTime.Before(inc.EndTime) { + return + } + + inc.Update(k.Low.Float64()) + inc.EndTime = k.EndTime.Time() + inc.EmitUpdate(inc.Last()) +} + +func calculatePivotHigh(highs floats.Slice, left, right int) (float64, bool) { + return floats.CalculatePivot(highs, left, right, func(a, pivot float64) bool { + return a < pivot + }) +} + +func calculatePivotLow(lows floats.Slice, left, right int) (float64, bool) { + return floats.CalculatePivot(lows, left, right, func(a, pivot float64) bool { + return a > pivot + }) +} diff --git a/pkg/indicator/pivotlow_callbacks.go b/pkg/indicator/pivotlow_callbacks.go new file mode 100644 index 0000000000..5ea139caf4 --- /dev/null +++ b/pkg/indicator/pivotlow_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type PivotLow"; DO NOT EDIT. + +package indicator + +import () + +func (inc *PivotLow) OnUpdate(cb func(value float64)) { + inc.updateCallbacks = append(inc.updateCallbacks, cb) +} + +func (inc *PivotLow) EmitUpdate(value float64) { + for _, cb := range inc.updateCallbacks { + cb(value) + } +} diff --git a/pkg/indicator/pivotlow_test.go b/pkg/indicator/pivotlow_test.go new file mode 100644 index 0000000000..318df37a7c --- /dev/null +++ b/pkg/indicator/pivotlow_test.go @@ -0,0 +1,51 @@ +package indicator + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_calculatePivotLow(t *testing.T) { + t.Run("normal", func(t *testing.T) { + low, ok := calculatePivotLow([]float64{15.0, 13.0, 12.0, 10.0, 14.0, 15.0}, 2, 2) + // ^left ----- ^pivot ---- ^right + assert.True(t, ok) + assert.Equal(t, 10.0, low) + + low, ok = calculatePivotLow([]float64{15.0, 13.0, 12.0, 10.0, 14.0, 9.0}, 2, 2) + // ^left ----- ^pivot ---- ^right + assert.False(t, ok) + + low, ok = calculatePivotLow([]float64{15.0, 9.0, 12.0, 10.0, 14.0, 15.0}, 2, 2) + // ^left ----- ^pivot ---- ^right + assert.False(t, ok) + }) + + t.Run("different left and right", func(t *testing.T) { + low, ok := calculatePivotLow([]float64{11.0, 12.0, 16.0, 15.0, 13.0, 12.0, 10.0, 14.0, 15.0}, 5, 2) + // ^left ---------------------- ^pivot ---- ^right + + assert.True(t, ok) + assert.Equal(t, 10.0, low) + + low, ok = calculatePivotLow([]float64{9.0, 8.0, 16.0, 15.0, 13.0, 12.0, 10.0, 14.0, 15.0}, 5, 2) + // ^left ---------------------- ^pivot ---- ^right + // 8.0 < 10.0 + assert.False(t, ok) + assert.Equal(t, 0.0, low) + }) + + t.Run("right window 0", func(t *testing.T) { + low, ok := calculatePivotLow([]float64{15.0, 13.0, 12.0, 10.0, 14.0, 15.0}, 2, 0) + assert.True(t, ok) + assert.Equal(t, 10.0, low) + }) + + t.Run("insufficient length", func(t *testing.T) { + low, ok := calculatePivotLow([]float64{15.0, 13.0, 12.0, 10.0, 14.0, 15.0}, 3, 3) + assert.False(t, ok) + assert.Equal(t, 0.0, low) + }) + +} diff --git a/pkg/indicator/rma.go b/pkg/indicator/rma.go index 4418ab54a9..fa47d94d98 100644 --- a/pkg/indicator/rma.go +++ b/pkg/indicator/rma.go @@ -3,36 +3,65 @@ package indicator import ( "time" + "github.com/c9s/bbgo/pkg/datatype/floats" "github.com/c9s/bbgo/pkg/types" ) -// Refer: Running Moving Average +// Running Moving Average +// Refer: https://github.com/twopirllc/pandas-ta/blob/main/pandas_ta/overlap/rma.py#L5 +// Refer: https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.ewm.html#pandas-dataframe-ewm //go:generate callbackgen -type RMA type RMA struct { + types.SeriesBase types.IntervalWindow - Values types.Float64Slice - Sources types.Float64Slice - EndTime time.Time - UpdateCallbacks []func(value float64) + Values floats.Slice + EndTime time.Time + + counter int + Adjust bool + tmp float64 + sum float64 + + updateCallbacks []func(value float64) } -func (inc *RMA) Update(x float64) { - inc.Sources.Push(x) +func (inc *RMA) Clone() types.UpdatableSeriesExtend { + out := &RMA{ + IntervalWindow: inc.IntervalWindow, + Values: inc.Values[:], + counter: inc.counter, + Adjust: inc.Adjust, + tmp: inc.tmp, + sum: inc.sum, + EndTime: inc.EndTime, + } + out.SeriesBase.Series = out + return out +} - if len(inc.Sources) < inc.Window { - inc.Values.Push(0) - return +func (inc *RMA) Update(x float64) { + lambda := 1 / float64(inc.Window) + if inc.counter == 0 { + inc.SeriesBase.Series = inc + inc.sum = 1 + inc.tmp = x + } else { + if inc.Adjust { + inc.sum = inc.sum*(1-lambda) + 1 + inc.tmp = inc.tmp + (x-inc.tmp)/inc.sum + } else { + inc.tmp = inc.tmp*(1-lambda) + x*lambda + } } + inc.counter++ - if len(inc.Sources) == inc.Window { - inc.Values.Push(inc.Sources.Mean()) + if inc.counter < inc.Window { + inc.Values.Push(0) return } - lambda := 1 / float64(inc.Window) - rma := (1-lambda)*inc.Values.Last() + lambda*x - inc.Values.Push(rma) + inc.Values.Push(inc.tmp) } func (inc *RMA) Last() float64 { @@ -51,25 +80,37 @@ func (inc *RMA) Length() int { return len(inc.Values) } -var _ types.Series = &RMA{} +var _ types.SeriesExtend = &RMA{} + +func (inc *RMA) PushK(k types.KLine) { + inc.Update(k.Close.Float64()) + inc.EndTime = k.EndTime.Time() +} + +func (inc *RMA) CalculateAndUpdate(kLines []types.KLine) { + last := kLines[len(kLines)-1] -func (inc *RMA) calculateAndUpdate(kLines []types.KLine) { - for _, k := range kLines { - if inc.EndTime != zeroTime && !k.EndTime.After(inc.EndTime) { - continue + if len(inc.Values) == 0 { + for _, k := range kLines { + if inc.EndTime != zeroTime && !k.EndTime.After(inc.EndTime) { + continue + } + + inc.PushK(k) } - inc.Update(k.Close.Float64()) + } else { + inc.PushK(last) } inc.EmitUpdate(inc.Last()) - inc.EndTime = kLines[len(kLines)-1].EndTime.Time() } + func (inc *RMA) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { if inc.Interval != interval { return } - inc.calculateAndUpdate(window) + inc.CalculateAndUpdate(window) } func (inc *RMA) Bind(updater KLineWindowUpdater) { diff --git a/pkg/indicator/rma_callbacks.go b/pkg/indicator/rma_callbacks.go index f5a40ca5ea..e08b306682 100644 --- a/pkg/indicator/rma_callbacks.go +++ b/pkg/indicator/rma_callbacks.go @@ -5,11 +5,11 @@ package indicator import () func (inc *RMA) OnUpdate(cb func(value float64)) { - inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) + inc.updateCallbacks = append(inc.updateCallbacks, cb) } func (inc *RMA) EmitUpdate(value float64) { - for _, cb := range inc.UpdateCallbacks { + for _, cb := range inc.updateCallbacks { cb(value) } } diff --git a/pkg/indicator/rsi.go b/pkg/indicator/rsi.go index b9eabd6f48..92060f455a 100644 --- a/pkg/indicator/rsi.go +++ b/pkg/indicator/rsi.go @@ -4,6 +4,7 @@ import ( "math" "time" + "github.com/c9s/bbgo/pkg/datatype/floats" "github.com/c9s/bbgo/pkg/types" ) @@ -14,17 +15,21 @@ https://www.investopedia.com/terms/r/rsi.asp */ //go:generate callbackgen -type RSI type RSI struct { + types.SeriesBase types.IntervalWindow - Values types.Float64Slice - Prices types.Float64Slice + Values floats.Slice + Prices floats.Slice PreviousAvgLoss float64 PreviousAvgGain float64 EndTime time.Time - UpdateCallbacks []func(value float64) + updateCallbacks []func(value float64) } func (inc *RSI) Update(price float64) { + if len(inc.Prices) == 0 { + inc.SeriesBase.Series = inc + } inc.Prices.Push(price) if len(inc.Prices) < inc.Window+1 { @@ -74,14 +79,19 @@ func (inc *RSI) Length() int { return len(inc.Values) } -var _ types.Series = &RSI{} +var _ types.SeriesExtend = &RSI{} + +func (inc *RSI) PushK(k types.KLine) { + inc.Update(k.Close.Float64()) +} -func (inc *RSI) calculateAndUpdate(kLines []types.KLine) { +func (inc *RSI) CalculateAndUpdate(kLines []types.KLine) { for _, k := range kLines { if inc.EndTime != zeroTime && !k.EndTime.After(inc.EndTime) { continue } - inc.Update(k.Close.Float64()) + + inc.PushK(k) } inc.EmitUpdate(inc.Last()) @@ -93,7 +103,7 @@ func (inc *RSI) handleKLineWindowUpdate(interval types.Interval, window types.KL return } - inc.calculateAndUpdate(window) + inc.CalculateAndUpdate(window) } func (inc *RSI) Bind(updater KLineWindowUpdater) { diff --git a/pkg/indicator/rsi_callbacks.go b/pkg/indicator/rsi_callbacks.go index 2c1a11f661..ea795930e7 100644 --- a/pkg/indicator/rsi_callbacks.go +++ b/pkg/indicator/rsi_callbacks.go @@ -5,11 +5,11 @@ package indicator import () func (inc *RSI) OnUpdate(cb func(value float64)) { - inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) + inc.updateCallbacks = append(inc.updateCallbacks, cb) } func (inc *RSI) EmitUpdate(value float64) { - for _, cb := range inc.UpdateCallbacks { + for _, cb := range inc.updateCallbacks { cb(value) } } diff --git a/pkg/indicator/rsi_test.go b/pkg/indicator/rsi_test.go index 80e4c91870..36322c9e13 100644 --- a/pkg/indicator/rsi_test.go +++ b/pkg/indicator/rsi_test.go @@ -6,6 +6,7 @@ import ( "github.com/stretchr/testify/assert" + "github.com/c9s/bbgo/pkg/datatype/floats" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) @@ -26,13 +27,13 @@ func Test_calculateRSI(t *testing.T) { name string kLines []types.KLine window int - want types.Float64Slice + want floats.Slice }{ { name: "RSI", kLines: buildKLines(values), window: 14, - want: types.Float64Slice{ + want: floats.Slice{ 70.46413502109704, 66.24961855355505, 66.48094183471265, @@ -59,7 +60,7 @@ func Test_calculateRSI(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { rsi := RSI{IntervalWindow: types.IntervalWindow{Window: tt.window}} - rsi.calculateAndUpdate(tt.kLines) + rsi.CalculateAndUpdate(tt.kLines) assert.Equal(t, len(rsi.Values), len(tt.want)) for i, v := range rsi.Values { assert.InDelta(t, v, tt.want[i], Delta) diff --git a/pkg/indicator/sma.go b/pkg/indicator/sma.go index d726cd2093..279e112806 100644 --- a/pkg/indicator/sma.go +++ b/pkg/indicator/sma.go @@ -4,104 +4,97 @@ import ( "fmt" "time" - log "github.com/sirupsen/logrus" - + "github.com/c9s/bbgo/pkg/datatype/floats" "github.com/c9s/bbgo/pkg/types" ) const MaxNumOfSMA = 5_000 const MaxNumOfSMATruncateSize = 100 -var zeroTime time.Time - //go:generate callbackgen -type SMA type SMA struct { + types.SeriesBase types.IntervalWindow - Values types.Float64Slice - EndTime time.Time + Values floats.Slice + rawValues *types.Queue + EndTime time.Time UpdateCallbacks []func(value float64) } func (inc *SMA) Last() float64 { - if len(inc.Values) == 0 { + if inc.Values.Length() == 0 { return 0.0 } - return inc.Values[len(inc.Values)-1] + return inc.Values.Last() } func (inc *SMA) Index(i int) float64 { - length := len(inc.Values) - if length == 0 || length-i-1 < 0 { + if i >= inc.Values.Length() { return 0.0 } - return inc.Values[length-i-1] + return inc.Values.Index(i) } func (inc *SMA) Length() int { - return len(inc.Values) + return inc.Values.Length() } -var _ types.Series = &SMA{} - -func (inc *SMA) Update(value float64) { - length := len(inc.Values) - if length == 0 { - inc.Values = append(inc.Values, value) - return +func (inc *SMA) Clone() types.UpdatableSeriesExtend { + out := &SMA{ + Values: inc.Values[:], + rawValues: inc.rawValues.Clone(), + EndTime: inc.EndTime, } - newVal := (inc.Values[length-1]*float64(inc.Window-1) + value) / float64(inc.Window) - inc.Values = append(inc.Values, newVal) + out.SeriesBase.Series = out + return out } -func (inc *SMA) calculateAndUpdate(kLines []types.KLine) { - if len(kLines) < inc.Window { - return - } - - var index = len(kLines) - 1 - var kline = kLines[index] +var _ types.SeriesExtend = &SMA{} - if inc.EndTime != zeroTime && kline.EndTime.Before(inc.EndTime) { - return +func (inc *SMA) Update(value float64) { + if inc.rawValues == nil { + inc.rawValues = types.NewQueue(inc.Window) + inc.SeriesBase.Series = inc } - var recentK = kLines[index-(inc.Window-1) : index+1] - - sma, err := calculateSMA(recentK, inc.Window, KLineClosePriceMapper) - if err != nil { - log.WithError(err).Error("SMA error") + inc.rawValues.Update(value) + if inc.rawValues.Length() < inc.Window { return } - inc.Values.Push(sma) - - if len(inc.Values) > MaxNumOfSMA { - inc.Values = inc.Values[MaxNumOfSMATruncateSize-1:] - } - inc.EndTime = kLines[index].EndTime.Time() + inc.Values.Push(types.Mean(inc.rawValues)) +} - inc.EmitUpdate(sma) +func (inc *SMA) BindK(target KLineClosedEmitter, symbol string, interval types.Interval) { + target.OnKLineClosed(types.KLineWith(symbol, interval, inc.PushK)) } -func (inc *SMA) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { - if inc.Interval != interval { +func (inc *SMA) PushK(k types.KLine) { + if inc.EndTime != zeroTime && k.EndTime.Before(inc.EndTime) { return } - inc.calculateAndUpdate(window) + inc.Update(k.Close.Float64()) + inc.EndTime = k.EndTime.Time() + inc.EmitUpdate(inc.Values.Last()) } -func (inc *SMA) Bind(updater KLineWindowUpdater) { - updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) +func (inc *SMA) LoadK(allKLines []types.KLine) { + for _, k := range allKLines { + inc.PushK(k) + } } -func calculateSMA(kLines []types.KLine, window int, priceF KLinePriceMapper) (float64, error) { +func calculateSMA(kLines []types.KLine, window int, priceF KLineValueMapper) (float64, error) { length := len(kLines) if length == 0 || length < window { return 0.0, fmt.Errorf("insufficient elements for calculating SMA with window = %d", window) } + if length != window { + return 0.0, fmt.Errorf("too much klines passed in, requires only %d klines", window) + } sum := 0.0 for _, k := range kLines { diff --git a/pkg/indicator/sma_test.go b/pkg/indicator/sma_test.go new file mode 100644 index 0000000000..a6ecc13241 --- /dev/null +++ b/pkg/indicator/sma_test.go @@ -0,0 +1,68 @@ +package indicator + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +/* +python: + +import pandas as pd +import pandas_ta as ta + +data = pd.Series([0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0]) +size = 5 + +result = ta.sma(data, size) +print(result) +*/ +func Test_SMA(t *testing.T) { + Delta := 0.001 + var randomPrices = []byte(`[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]`) + var input []fixedpoint.Value + if err := json.Unmarshal(randomPrices, &input); err != nil { + panic(err) + } + tests := []struct { + name string + kLines []types.KLine + want float64 + next float64 + update float64 + updateResult float64 + all int + }{ + { + name: "test", + kLines: buildKLines(input), + want: 7.0, + next: 6.0, + update: 0, + updateResult: 6.0, + all: 27, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sma := SMA{ + IntervalWindow: types.IntervalWindow{Window: 5}, + } + + for _, k := range tt.kLines { + sma.PushK(k) + } + + assert.InDelta(t, tt.want, sma.Last(), Delta) + assert.InDelta(t, tt.next, sma.Index(1), Delta) + sma.Update(tt.update) + assert.InDelta(t, tt.updateResult, sma.Last(), Delta) + assert.Equal(t, tt.all, sma.Length()) + }) + } +} diff --git a/pkg/indicator/ssf.go b/pkg/indicator/ssf.go new file mode 100644 index 0000000000..9458e16e86 --- /dev/null +++ b/pkg/indicator/ssf.go @@ -0,0 +1,123 @@ +package indicator + +import ( + "math" + + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/types" +) + +// Refer: https://easylanguagemastery.com/indicators/predictive-indicators/ +// Refer: https://github.com/twopirllc/pandas-ta/blob/main/pandas_ta/overlap/ssf.py +// Ehler's Super Smoother Filter +// +// John F. Ehlers's solution to reduce lag and remove aliasing noise with his +// research in aerospace analog filter design. This indicator comes with two +// versions determined by the keyword poles. By default, it uses two poles but +// there is an option for three poles. Since SSF is a (Resursive) Digital Filter, +// the number of poles determine how many prior recursive SSF bars to include in +// the design of the filter. So two poles uses two prior SSF bars and three poles +// uses three prior SSF bars for their filter calculations. +// +//go:generate callbackgen -type SSF +type SSF struct { + types.SeriesBase + types.IntervalWindow + Poles int + c1 float64 + c2 float64 + c3 float64 + c4 float64 + Values floats.Slice + + UpdateCallbacks []func(value float64) +} + +func (inc *SSF) Update(value float64) { + if inc.Poles == 3 { + if inc.Values == nil { + inc.SeriesBase.Series = inc + x := math.Pi / float64(inc.Window) + a0 := math.Exp(-x) + b0 := 2. * a0 * math.Cos(math.Sqrt(3.)*x) + c0 := a0 * a0 + + inc.c4 = c0 * c0 + inc.c3 = -c0 * (1. + b0) + inc.c2 = c0 + b0 + inc.c1 = 1. - inc.c2 - inc.c3 - inc.c4 + inc.Values = floats.Slice{} + } + + result := inc.c1*value + + inc.c2*inc.Values.Index(0) + + inc.c3*inc.Values.Index(1) + + inc.c4*inc.Values.Index(2) + inc.Values.Push(result) + } else { // poles == 2 + if inc.Values == nil { + inc.SeriesBase.Series = inc + x := math.Pi * math.Sqrt(2.) / float64(inc.Window) + a0 := math.Exp(-x) + inc.c3 = -a0 * a0 + inc.c2 = 2. * a0 * math.Cos(x) + inc.c1 = 1. - inc.c2 - inc.c3 + inc.Values = floats.Slice{} + } + result := inc.c1*value + + inc.c2*inc.Values.Index(0) + + inc.c3*inc.Values.Index(1) + inc.Values.Push(result) + } +} + +func (inc *SSF) Index(i int) float64 { + if inc.Values == nil { + return 0.0 + } + return inc.Values.Index(i) +} + +func (inc *SSF) Length() int { + if inc.Values == nil { + return 0 + } + return inc.Values.Length() +} + +func (inc *SSF) Last() float64 { + if inc.Values == nil { + return 0.0 + } + return inc.Values.Last() +} + +var _ types.SeriesExtend = &SSF{} + +func (inc *SSF) PushK(k types.KLine) { + inc.Update(k.Close.Float64()) +} + +func (inc *SSF) CalculateAndUpdate(allKLines []types.KLine) { + if inc.Values != nil { + k := allKLines[len(allKLines)-1] + inc.PushK(k) + inc.EmitUpdate(inc.Last()) + return + } + for _, k := range allKLines { + inc.PushK(k) + inc.EmitUpdate(inc.Last()) + } +} + +func (inc *SSF) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { + if inc.Interval != interval { + return + } + inc.CalculateAndUpdate(window) +} + +func (inc *SSF) Bind(updater KLineWindowUpdater) { + updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) +} diff --git a/pkg/indicator/ssf_callbacks.go b/pkg/indicator/ssf_callbacks.go new file mode 100644 index 0000000000..cdd2e8acaa --- /dev/null +++ b/pkg/indicator/ssf_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type SSF"; DO NOT EDIT. + +package indicator + +import () + +func (inc *SSF) OnUpdate(cb func(value float64)) { + inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) +} + +func (inc *SSF) EmitUpdate(value float64) { + for _, cb := range inc.UpdateCallbacks { + cb(value) + } +} diff --git a/pkg/indicator/ssf_test.go b/pkg/indicator/ssf_test.go new file mode 100644 index 0000000000..253d722204 --- /dev/null +++ b/pkg/indicator/ssf_test.go @@ -0,0 +1,71 @@ +package indicator + +import ( + "encoding/json" + "testing" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" + "github.com/stretchr/testify/assert" +) + +/* +python: + +import pandas as pd +import pandas_ta as ta + +data = pd.Series([0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9]) +size = 5 + +result = ta.ssf(data, size, 2) +print(result) + +result = ta.ssf(data, size, 3) +print(result) +*/ +func Test_SSF(t *testing.T) { + var Delta = 0.00001 + var randomPrices = []byte(`[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]`) + var input []fixedpoint.Value + if err := json.Unmarshal(randomPrices, &input); err != nil { + panic(err) + } + tests := []struct { + name string + kLines []types.KLine + poles int + want float64 + next float64 + all int + }{ + { + name: "pole2", + kLines: buildKLines(input), + poles: 2, + want: 8.721776, + next: 7.723223, + all: 30, + }, + { + name: "pole3", + kLines: buildKLines(input), + poles: 3, + want: 8.687588, + next: 7.668013, + all: 30, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ssf := SSF{ + IntervalWindow: types.IntervalWindow{Window: 5}, + Poles: tt.poles, + } + ssf.CalculateAndUpdate(tt.kLines) + assert.InDelta(t, tt.want, ssf.Last(), Delta) + assert.InDelta(t, tt.next, ssf.Index(1), Delta) + assert.Equal(t, tt.all, ssf.Length()) + }) + } +} diff --git a/pkg/indicator/stddev.go b/pkg/indicator/stddev.go new file mode 100644 index 0000000000..8811fb208e --- /dev/null +++ b/pkg/indicator/stddev.go @@ -0,0 +1,86 @@ +package indicator + +import ( + "time" + + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/types" +) + +//go:generate callbackgen -type StdDev +type StdDev struct { + types.SeriesBase + types.IntervalWindow + Values floats.Slice + rawValues *types.Queue + + EndTime time.Time + updateCallbacks []func(value float64) +} + +func (inc *StdDev) Last() float64 { + if inc.Values.Length() == 0 { + return 0.0 + } + return inc.Values.Last() +} + +func (inc *StdDev) Index(i int) float64 { + if i >= inc.Values.Length() { + return 0.0 + } + + return inc.Values.Index(i) +} + +func (inc *StdDev) Length() int { + return inc.Values.Length() +} + +var _ types.SeriesExtend = &StdDev{} + +func (inc *StdDev) Update(value float64) { + if inc.rawValues == nil { + inc.rawValues = types.NewQueue(inc.Window) + inc.SeriesBase.Series = inc + } + + inc.rawValues.Update(value) + + var std = inc.rawValues.Stdev() + inc.Values.Push(std) +} + +func (inc *StdDev) PushK(k types.KLine) { + inc.Update(k.Close.Float64()) + inc.EndTime = k.EndTime.Time() +} + +func (inc *StdDev) CalculateAndUpdate(allKLines []types.KLine) { + var last = allKLines[len(allKLines)-1] + + if inc.rawValues == nil { + for _, k := range allKLines { + if inc.EndTime != zeroTime && k.EndTime.Before(inc.EndTime) { + continue + } + inc.PushK(k) + } + } else { + inc.PushK(last) + } + + inc.EmitUpdate(inc.Values.Last()) +} + +func (inc *StdDev) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { + if inc.Interval != interval { + return + } + + inc.CalculateAndUpdate(window) +} + +func (inc *StdDev) Bind(updater KLineWindowUpdater) { + updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) +} diff --git a/pkg/indicator/stddev_callbacks.go b/pkg/indicator/stddev_callbacks.go new file mode 100644 index 0000000000..745f006eeb --- /dev/null +++ b/pkg/indicator/stddev_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type StdDev"; DO NOT EDIT. + +package indicator + +import () + +func (inc *StdDev) OnUpdate(cb func(value float64)) { + inc.updateCallbacks = append(inc.updateCallbacks, cb) +} + +func (inc *StdDev) EmitUpdate(value float64) { + for _, cb := range inc.updateCallbacks { + cb(value) + } +} diff --git a/pkg/indicator/stoch.go b/pkg/indicator/stoch.go index a5581fc40e..aa86fea8b6 100644 --- a/pkg/indicator/stoch.go +++ b/pkg/indicator/stoch.go @@ -3,6 +3,7 @@ package indicator import ( "time" + "github.com/c9s/bbgo/pkg/datatype/floats" "github.com/c9s/bbgo/pkg/types" ) @@ -17,11 +18,11 @@ Stochastic Oscillator //go:generate callbackgen -type STOCH type STOCH struct { types.IntervalWindow - K types.Float64Slice - D types.Float64Slice + K floats.Slice + D floats.Slice - HighValues types.Float64Slice - LowValues types.Float64Slice + HighValues floats.Slice + LowValues floats.Slice EndTime time.Time UpdateCallbacks []func(k float64, d float64) @@ -34,8 +35,12 @@ func (inc *STOCH) Update(high, low, cloze float64) { lowest := inc.LowValues.Tail(inc.Window).Min() highest := inc.HighValues.Tail(inc.Window).Max() - k := 100.0 * (cloze - lowest) / (highest - lowest) - inc.K.Push(k) + if highest == lowest { + inc.K.Push(50.0) + } else { + k := 100.0 * (cloze - lowest) / (highest - lowest) + inc.K.Push(k) + } d := inc.K.Tail(DPeriod).Mean() inc.D.Push(d) @@ -55,32 +60,14 @@ func (inc *STOCH) LastD() float64 { return inc.D[len(inc.D)-1] } -func (inc *STOCH) calculateAndUpdate(kLines []types.KLine) { - if len(kLines) < inc.Window || len(kLines) < DPeriod { +func (inc *STOCH) PushK(k types.KLine) { + if inc.EndTime != zeroTime && !k.EndTime.After(inc.EndTime) { return } - for _, k := range kLines { - if inc.EndTime != zeroTime && !k.EndTime.After(inc.EndTime) { - continue - } - inc.Update(k.High.Float64(), k.Low.Float64(), k.Close.Float64()) - } - + inc.Update(k.High.Float64(), k.Low.Float64(), k.Close.Float64()) + inc.EndTime = k.EndTime.Time() inc.EmitUpdate(inc.LastK(), inc.LastD()) - inc.EndTime = kLines[len(kLines)-1].EndTime.Time() -} - -func (inc *STOCH) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { - if inc.Interval != interval { - return - } - - inc.calculateAndUpdate(window) -} - -func (inc *STOCH) Bind(updater KLineWindowUpdater) { - updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) } func (inc *STOCH) GetD() types.Series { diff --git a/pkg/indicator/stoch_test.go b/pkg/indicator/stoch_test.go index f8a90bfee8..eb3d0206c9 100644 --- a/pkg/indicator/stoch_test.go +++ b/pkg/indicator/stoch_test.go @@ -56,7 +56,10 @@ func TestSTOCH_update(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { kd := STOCH{IntervalWindow: types.IntervalWindow{Window: tt.window}} - kd.calculateAndUpdate(tt.kLines) + + for _, k := range tt.kLines { + kd.PushK(k) + } got_k := kd.LastK() diff_k := math.Trunc((got_k-tt.want_k)*100) / 100 diff --git a/pkg/indicator/supertrend.go b/pkg/indicator/supertrend.go new file mode 100644 index 0000000000..6d15e19a2c --- /dev/null +++ b/pkg/indicator/supertrend.go @@ -0,0 +1,177 @@ +package indicator + +import ( + "math" + "time" + + "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/types" +) + +var logst = logrus.WithField("indicator", "supertrend") + +//go:generate callbackgen -type Supertrend +type Supertrend struct { + types.SeriesBase + types.IntervalWindow + ATRMultiplier float64 `json:"atrMultiplier"` + + AverageTrueRange *ATR + + trendPrices floats.Slice + + closePrice float64 + previousClosePrice float64 + uptrendPrice float64 + previousUptrendPrice float64 + downtrendPrice float64 + previousDowntrendPrice float64 + + trend types.Direction + previousTrend types.Direction + tradeSignal types.Direction + + EndTime time.Time + UpdateCallbacks []func(value float64) +} + +func (inc *Supertrend) Last() float64 { + return inc.trendPrices.Last() +} + +func (inc *Supertrend) Index(i int) float64 { + length := inc.Length() + if length == 0 || length-i-1 < 0 { + return 0 + } + return inc.trendPrices[length-i-1] +} + +func (inc *Supertrend) Length() int { + return len(inc.trendPrices) +} + +func (inc *Supertrend) Update(highPrice, lowPrice, closePrice float64) { + if inc.Window <= 0 { + panic("window must be greater than 0") + } + + if inc.AverageTrueRange == nil { + inc.SeriesBase.Series = inc + } + + // Start with DirectionUp + if inc.trend != types.DirectionUp && inc.trend != types.DirectionDown { + inc.trend = types.DirectionUp + } + + // Update ATR + inc.AverageTrueRange.Update(highPrice, lowPrice, closePrice) + + // Update last prices + inc.previousUptrendPrice = inc.uptrendPrice + inc.previousDowntrendPrice = inc.downtrendPrice + inc.previousClosePrice = inc.closePrice + inc.previousTrend = inc.trend + + inc.closePrice = closePrice + + src := (highPrice + lowPrice) / 2 + + // Update uptrend + inc.uptrendPrice = src - inc.AverageTrueRange.Last()*inc.ATRMultiplier + if inc.previousClosePrice > inc.previousUptrendPrice { + inc.uptrendPrice = math.Max(inc.uptrendPrice, inc.previousUptrendPrice) + } + + // Update downtrend + inc.downtrendPrice = src + inc.AverageTrueRange.Last()*inc.ATRMultiplier + if inc.previousClosePrice < inc.previousDowntrendPrice { + inc.downtrendPrice = math.Min(inc.downtrendPrice, inc.previousDowntrendPrice) + } + + // Update trend + if inc.previousTrend == types.DirectionUp && inc.closePrice < inc.previousUptrendPrice { + inc.trend = types.DirectionDown + } else if inc.previousTrend == types.DirectionDown && inc.closePrice > inc.previousDowntrendPrice { + inc.trend = types.DirectionUp + } else { + inc.trend = inc.previousTrend + } + + // Update signal + if inc.AverageTrueRange.Last() <= 0 { + inc.tradeSignal = types.DirectionNone + } else if inc.trend == types.DirectionUp && inc.previousTrend == types.DirectionDown { + inc.tradeSignal = types.DirectionUp + } else if inc.trend == types.DirectionDown && inc.previousTrend == types.DirectionUp { + inc.tradeSignal = types.DirectionDown + } else { + inc.tradeSignal = types.DirectionNone + } + + // Update trend price + if inc.trend == types.DirectionDown { + inc.trendPrices.Push(inc.downtrendPrice) + } else { + inc.trendPrices.Push(inc.uptrendPrice) + } + + logst.Debugf("Update supertrend result: closePrice: %v, uptrendPrice: %v, downtrendPrice: %v, trend: %v,"+ + " tradeSignal: %v, AverageTrueRange.Last(): %v", inc.closePrice, inc.uptrendPrice, inc.downtrendPrice, + inc.trend, inc.tradeSignal, inc.AverageTrueRange.Last()) +} + +func (inc *Supertrend) GetSignal() types.Direction { + return inc.tradeSignal +} + +var _ types.SeriesExtend = &Supertrend{} + +func (inc *Supertrend) PushK(k types.KLine) { + if inc.EndTime != zeroTime && k.EndTime.Before(inc.EndTime) { + return + } + + inc.Update(k.GetHigh().Float64(), k.GetLow().Float64(), k.GetClose().Float64()) + inc.EndTime = k.EndTime.Time() + inc.EmitUpdate(inc.Last()) + +} + +func (inc *Supertrend) BindK(target KLineClosedEmitter, symbol string, interval types.Interval) { + target.OnKLineClosed(types.KLineWith(symbol, interval, inc.PushK)) +} + +func (inc *Supertrend) LoadK(allKLines []types.KLine) { + for _, k := range allKLines { + inc.PushK(k) + } +} + +func (inc *Supertrend) CalculateAndUpdate(kLines []types.KLine) { + for _, k := range kLines { + if inc.EndTime != zeroTime && !k.EndTime.After(inc.EndTime) { + continue + } + + inc.PushK(k) + } + + inc.EmitUpdate(inc.Last()) + inc.EndTime = kLines[len(kLines)-1].EndTime.Time() +} + +func (inc *Supertrend) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { + if inc.Interval != interval { + return + } + + inc.CalculateAndUpdate(window) +} + +func (inc *Supertrend) Bind(updater KLineWindowUpdater) { + updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) +} diff --git a/pkg/indicator/supertrend_callbacks.go b/pkg/indicator/supertrend_callbacks.go new file mode 100644 index 0000000000..d02345798e --- /dev/null +++ b/pkg/indicator/supertrend_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type Supertrend"; DO NOT EDIT. + +package indicator + +import () + +func (inc *Supertrend) OnUpdate(cb func(value float64)) { + inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) +} + +func (inc *Supertrend) EmitUpdate(value float64) { + for _, cb := range inc.UpdateCallbacks { + cb(value) + } +} diff --git a/pkg/indicator/tema.go b/pkg/indicator/tema.go index 91d53a63d0..3481775249 100644 --- a/pkg/indicator/tema.go +++ b/pkg/indicator/tema.go @@ -1,6 +1,7 @@ package indicator import ( + "github.com/c9s/bbgo/pkg/datatype/floats" "github.com/c9s/bbgo/pkg/types" ) @@ -9,8 +10,9 @@ import ( //go:generate callbackgen -type TEMA type TEMA struct { + types.SeriesBase types.IntervalWindow - Values types.Float64Slice + Values floats.Slice A1 *EWMA A2 *EWMA A3 *EWMA @@ -20,9 +22,10 @@ type TEMA struct { func (inc *TEMA) Update(value float64) { if len(inc.Values) == 0 { - inc.A1 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}} - inc.A2 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}} - inc.A3 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}} + inc.SeriesBase.Series = inc + inc.A1 = &EWMA{IntervalWindow: inc.IntervalWindow} + inc.A2 = &EWMA{IntervalWindow: inc.IntervalWindow} + inc.A3 = &EWMA{IntervalWindow: inc.IntervalWindow} } inc.A1.Update(value) a1 := inc.A1.Last() @@ -51,16 +54,21 @@ func (inc *TEMA) Length() int { return len(inc.Values) } -var _ types.Series = &TEMA{} +var _ types.SeriesExtend = &TEMA{} -func (inc *TEMA) calculateAndUpdate(allKLines []types.KLine) { +func (inc *TEMA) PushK(k types.KLine) { + inc.Update(k.Close.Float64()) +} + +func (inc *TEMA) CalculateAndUpdate(allKLines []types.KLine) { if inc.A1 == nil { for _, k := range allKLines { - inc.Update(k.Close.Float64()) + inc.PushK(k) inc.EmitUpdate(inc.Last()) } } else { - inc.Update(allKLines[len(allKLines)-1].Close.Float64()) + k := allKLines[len(allKLines)-1] + inc.PushK(k) inc.EmitUpdate(inc.Last()) } } @@ -70,7 +78,7 @@ func (inc *TEMA) handleKLineWindowUpdate(interval types.Interval, window types.K return } - inc.calculateAndUpdate(window) + inc.CalculateAndUpdate(window) } func (inc *TEMA) Bind(updater KLineWindowUpdater) { diff --git a/pkg/indicator/tema_test.go b/pkg/indicator/tema_test.go index 641153f402..b50c72ca75 100644 --- a/pkg/indicator/tema_test.go +++ b/pkg/indicator/tema_test.go @@ -46,7 +46,7 @@ func Test_TEMA(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tema := TEMA{IntervalWindow: types.IntervalWindow{Window: 16}} - tema.calculateAndUpdate(tt.kLines) + tema.CalculateAndUpdate(tt.kLines) last := tema.Last() assert.InDelta(t, tt.want, last, Delta) assert.InDelta(t, tt.next, tema.Index(1), Delta) diff --git a/pkg/indicator/till.go b/pkg/indicator/till.go index 73f97ead50..6c1860b2e3 100644 --- a/pkg/indicator/till.go +++ b/pkg/indicator/till.go @@ -10,19 +10,21 @@ const defaultVolumeFactor = 0.7 // Refer URL: https://tradingpedia.com/forex-trading-indicator/t3-moving-average-indicator/ //go:generate callbackgen -type TILL type TILL struct { + types.SeriesBase types.IntervalWindow - VolumeFactor float64 - e1 *EWMA - e2 *EWMA - e3 *EWMA - e4 *EWMA - e5 *EWMA - e6 *EWMA - c1 float64 - c2 float64 - c3 float64 - c4 float64 - UpdateCallbacks []func(value float64) + VolumeFactor float64 + e1 *EWMA + e2 *EWMA + e3 *EWMA + e4 *EWMA + e5 *EWMA + e6 *EWMA + c1 float64 + c2 float64 + c3 float64 + c4 float64 + + updateCallbacks []func(value float64) } func (inc *TILL) Update(value float64) { @@ -30,12 +32,13 @@ func (inc *TILL) Update(value float64) { if inc.VolumeFactor == 0 { inc.VolumeFactor = defaultVolumeFactor } - inc.e1 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}} - inc.e2 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}} - inc.e3 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}} - inc.e4 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}} - inc.e5 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}} - inc.e6 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}} + inc.SeriesBase.Series = inc + inc.e1 = &EWMA{IntervalWindow: inc.IntervalWindow} + inc.e2 = &EWMA{IntervalWindow: inc.IntervalWindow} + inc.e3 = &EWMA{IntervalWindow: inc.IntervalWindow} + inc.e4 = &EWMA{IntervalWindow: inc.IntervalWindow} + inc.e5 = &EWMA{IntervalWindow: inc.IntervalWindow} + inc.e6 = &EWMA{IntervalWindow: inc.IntervalWindow} square := inc.VolumeFactor * inc.VolumeFactor cube := inc.VolumeFactor * square inc.c1 = -cube @@ -83,20 +86,36 @@ func (inc *TILL) Length() int { var _ types.Series = &TILL{} -func (inc *TILL) calculateAndUpdate(allKLines []types.KLine) { - doable := false - if inc.e1 == nil { - doable = true +func (inc *TILL) PushK(k types.KLine) { + if inc.e1 != nil && inc.e1.EndTime != zeroTime && k.EndTime.Before(inc.e1.EndTime) { + return } + + inc.Update(k.Close.Float64()) + inc.EmitUpdate(inc.Last()) +} + +func (inc *TILL) LoadK(allKLines []types.KLine) { for _, k := range allKLines { - if !doable && k.StartTime.After(inc.e1.LastOpenTime) { - doable = true - } - if doable { - inc.Update(k.Close.Float64()) - inc.EmitUpdate(inc.Last()) + inc.PushK(k) + } +} + +func (inc *TILL) BindK(target KLineClosedEmitter, symbol string, interval types.Interval) { + target.OnKLineClosed(types.KLineWith(symbol, interval, inc.PushK)) +} + +func (inc *TILL) CalculateAndUpdate(allKLines []types.KLine) { + if inc.e1 == nil { + for _, k := range allKLines { + inc.PushK(k) } + } else { + end := len(allKLines) + last := allKLines[end-1] + inc.PushK(last) } + } func (inc *TILL) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { @@ -104,7 +123,7 @@ func (inc *TILL) handleKLineWindowUpdate(interval types.Interval, window types.K return } - inc.calculateAndUpdate(window) + inc.CalculateAndUpdate(window) } func (inc *TILL) Bind(updater KLineWindowUpdater) { diff --git a/pkg/indicator/till_callbacks.go b/pkg/indicator/till_callbacks.go index 53d89cb8d8..d17a8dcd96 100644 --- a/pkg/indicator/till_callbacks.go +++ b/pkg/indicator/till_callbacks.go @@ -5,11 +5,11 @@ package indicator import () func (inc *TILL) OnUpdate(cb func(value float64)) { - inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) + inc.updateCallbacks = append(inc.updateCallbacks, cb) } func (inc *TILL) EmitUpdate(value float64) { - for _, cb := range inc.UpdateCallbacks { + for _, cb := range inc.updateCallbacks { cb(value) } } diff --git a/pkg/indicator/till_test.go b/pkg/indicator/till_test.go index 4615a5dbe0..1b03017b80 100644 --- a/pkg/indicator/till_test.go +++ b/pkg/indicator/till_test.go @@ -4,9 +4,10 @@ import ( "encoding/json" "testing" + "github.com/stretchr/testify/assert" + "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" - "github.com/stretchr/testify/assert" ) /* @@ -55,7 +56,7 @@ func Test_TILL(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { till := TILL{IntervalWindow: types.IntervalWindow{Window: 16}} - till.calculateAndUpdate(tt.kLines) + till.CalculateAndUpdate(tt.kLines) last := till.Last() assert.InDelta(t, tt.want, last, Delta) assert.InDelta(t, tt.next, till.Index(1), Delta) diff --git a/pkg/indicator/tma.go b/pkg/indicator/tma.go index 482f3936cd..97c5997d57 100644 --- a/pkg/indicator/tma.go +++ b/pkg/indicator/tma.go @@ -8,6 +8,7 @@ import ( // Refer URL: https://ja.wikipedia.org/wiki/移拕ćčłć‡ //go:generate callbackgen -type TMA type TMA struct { + types.SeriesBase types.IntervalWindow s1 *SMA s2 *SMA @@ -16,9 +17,10 @@ type TMA struct { func (inc *TMA) Update(value float64) { if inc.s1 == nil { + inc.SeriesBase.Series = inc w := (inc.Window + 1) / 2 - inc.s1 = &SMA{IntervalWindow: types.IntervalWindow{inc.Interval, w}} - inc.s2 = &SMA{IntervalWindow: types.IntervalWindow{inc.Interval, w}} + inc.s1 = &SMA{IntervalWindow: types.IntervalWindow{Interval: inc.Interval, Window: w}} + inc.s2 = &SMA{IntervalWindow: types.IntervalWindow{Interval: inc.Interval, Window: w}} } inc.s1.Update(value) @@ -46,16 +48,21 @@ func (inc *TMA) Length() int { return inc.s2.Length() } -var _ types.Series = &TMA{} +var _ types.SeriesExtend = &TMA{} -func (inc *TMA) calculateAndUpdate(allKLines []types.KLine) { +func (inc *TMA) PushK(k types.KLine) { + inc.Update(k.Close.Float64()) +} + +func (inc *TMA) CalculateAndUpdate(allKLines []types.KLine) { if inc.s1 == nil { for _, k := range allKLines { - inc.Update(k.Close.Float64()) + inc.PushK(k) inc.EmitUpdate(inc.Last()) } } else { - inc.Update(allKLines[len(allKLines)-1].Close.Float64()) + k := allKLines[len(allKLines)-1] + inc.PushK(k) inc.EmitUpdate(inc.Last()) } } @@ -65,7 +72,7 @@ func (inc *TMA) handleKLineWindowUpdate(interval types.Interval, window types.KL return } - inc.calculateAndUpdate(window) + inc.CalculateAndUpdate(window) } func (inc *TMA) Bind(updater KLineWindowUpdater) { diff --git a/pkg/indicator/util.go b/pkg/indicator/util.go index 05f4c6a690..722d45a367 100644 --- a/pkg/indicator/util.go +++ b/pkg/indicator/util.go @@ -1,29 +1 @@ package indicator - -import "github.com/c9s/bbgo/pkg/types" - -type KLinePriceMapper func(k types.KLine) float64 - -func KLineOpenPriceMapper(k types.KLine) float64 { - return k.Open.Float64() -} - -func KLineClosePriceMapper(k types.KLine) float64 { - return k.Close.Float64() -} - -func KLineTypicalPriceMapper(k types.KLine) float64 { - return (k.High.Float64() + k.Low.Float64() + k.Close.Float64()) / 3. -} - -func MapKLinePrice(kLines []types.KLine, f KLinePriceMapper) (prices []float64) { - for _, k := range kLines { - prices = append(prices, f(k)) - } - - return prices -} - -type KLineWindowUpdater interface { - OnKLineWindowUpdate(func(interval types.Interval, window types.KLineWindow)) -} diff --git a/pkg/indicator/vidya.go b/pkg/indicator/vidya.go index 658e89ac11..023e39a666 100644 --- a/pkg/indicator/vidya.go +++ b/pkg/indicator/vidya.go @@ -3,6 +3,7 @@ package indicator import ( "math" + "github.com/c9s/bbgo/pkg/datatype/floats" "github.com/c9s/bbgo/pkg/types" ) @@ -10,15 +11,17 @@ import ( // Refer URL: https://metatrader5.com/en/terminal/help/indicators/trend_indicators/vida //go:generate callbackgen -type VIDYA type VIDYA struct { + types.SeriesBase types.IntervalWindow - Values types.Float64Slice - input types.Float64Slice + Values floats.Slice + input floats.Slice - UpdateCallbacks []func(value float64) + updateCallbacks []func(value float64) } func (inc *VIDYA) Update(value float64) { if inc.Values.Length() == 0 { + inc.SeriesBase.Series = inc inc.Values.Push(value) inc.input.Push(value) return @@ -66,16 +69,21 @@ func (inc *VIDYA) Length() int { return inc.Values.Length() } -var _ types.Series = &VIDYA{} +var _ types.SeriesExtend = &VIDYA{} -func (inc *VIDYA) calculateAndUpdate(allKLines []types.KLine) { +func (inc *VIDYA) PushK(k types.KLine) { + inc.Update(k.Close.Float64()) +} + +func (inc *VIDYA) CalculateAndUpdate(allKLines []types.KLine) { if inc.input.Length() == 0 { for _, k := range allKLines { - inc.Update(k.Close.Float64()) + inc.PushK(k) inc.EmitUpdate(inc.Last()) } } else { - inc.Update(allKLines[len(allKLines)-1].Close.Float64()) + k := allKLines[len(allKLines)-1] + inc.PushK(k) inc.EmitUpdate(inc.Last()) } } @@ -85,7 +93,7 @@ func (inc *VIDYA) handleKLineWindowUpdate(interval types.Interval, window types. return } - inc.calculateAndUpdate(window) + inc.CalculateAndUpdate(window) } func (inc *VIDYA) Bind(updater KLineWindowUpdater) { diff --git a/pkg/indicator/vidya_callbacks.go b/pkg/indicator/vidya_callbacks.go index b78e797c45..c05d0a20b3 100644 --- a/pkg/indicator/vidya_callbacks.go +++ b/pkg/indicator/vidya_callbacks.go @@ -5,11 +5,11 @@ package indicator import () func (inc *VIDYA) OnUpdate(cb func(value float64)) { - inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) + inc.updateCallbacks = append(inc.updateCallbacks, cb) } func (inc *VIDYA) EmitUpdate(value float64) { - for _, cb := range inc.UpdateCallbacks { + for _, cb := range inc.updateCallbacks { cb(value) } } diff --git a/pkg/indicator/volatility.go b/pkg/indicator/volatility.go index aae62e2836..6b3dcd7cb1 100644 --- a/pkg/indicator/volatility.go +++ b/pkg/indicator/volatility.go @@ -7,43 +7,62 @@ import ( log "github.com/sirupsen/logrus" + "github.com/c9s/bbgo/pkg/datatype/floats" "github.com/c9s/bbgo/pkg/types" ) const MaxNumOfVOL = 5_000 const MaxNumOfVOLTruncateSize = 100 -//var zeroTime time.Time +// var zeroTime time.Time -//go:generate callbackgen -type VOLATILITY -type VOLATILITY struct { +//go:generate callbackgen -type Volatility +type Volatility struct { + types.SeriesBase types.IntervalWindow - Values types.Float64Slice + Values floats.Slice EndTime time.Time UpdateCallbacks []func(value float64) } -func (inc *VOLATILITY) Last() float64 { +func (inc *Volatility) Last() float64 { if len(inc.Values) == 0 { return 0.0 } return inc.Values[len(inc.Values)-1] } -func (inc *VOLATILITY) calculateAndUpdate(klines []types.KLine) { - if len(klines) < inc.Window { +func (inc *Volatility) Index(i int) float64 { + if len(inc.Values)-i <= 0 { + return 0.0 + } + return inc.Values[len(inc.Values)-i-1] +} + +func (inc *Volatility) Length() int { + return len(inc.Values) +} + +var _ types.SeriesExtend = &Volatility{} + +func (inc *Volatility) CalculateAndUpdate(allKLines []types.KLine) { + if len(allKLines) < inc.Window { return } - var end = len(klines) - 1 - var lastKLine = klines[end] + var end = len(allKLines) - 1 + var lastKLine = allKLines[end] if inc.EndTime != zeroTime && lastKLine.GetEndTime().Before(inc.EndTime) { return } - var recentT = klines[end-(inc.Window-1) : end+1] + if len(inc.Values) == 0 { + inc.SeriesBase.Series = inc + } + + var recentT = allKLines[end-(inc.Window-1) : end+1] volatility, err := calculateVOLATILITY(recentT, inc.Window, KLineClosePriceMapper) if err != nil { @@ -56,24 +75,24 @@ func (inc *VOLATILITY) calculateAndUpdate(klines []types.KLine) { inc.Values = inc.Values[MaxNumOfVOLTruncateSize-1:] } - inc.EndTime = klines[end].GetEndTime().Time() + inc.EndTime = allKLines[end].GetEndTime().Time() inc.EmitUpdate(volatility) } -func (inc *VOLATILITY) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { +func (inc *Volatility) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { if inc.Interval != interval { return } - inc.calculateAndUpdate(window) + inc.CalculateAndUpdate(window) } -func (inc *VOLATILITY) Bind(updater KLineWindowUpdater) { +func (inc *Volatility) Bind(updater KLineWindowUpdater) { updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) } -func calculateVOLATILITY(klines []types.KLine, window int, priceF KLinePriceMapper) (float64, error) { +func calculateVOLATILITY(klines []types.KLine, window int, priceF KLineValueMapper) (float64, error) { length := len(klines) if length == 0 || length < window { return 0.0, fmt.Errorf("insufficient elements for calculating VOL with window = %d", window) diff --git a/pkg/indicator/volatility_callbacks.go b/pkg/indicator/volatility_callbacks.go index 9f5311d757..c04211a083 100644 --- a/pkg/indicator/volatility_callbacks.go +++ b/pkg/indicator/volatility_callbacks.go @@ -1,14 +1,14 @@ -// Code generated by "callbackgen -type VOLATILITY"; DO NOT EDIT. +// Code generated by "callbackgen -type Volatility"; DO NOT EDIT. package indicator import () -func (inc *VOLATILITY) OnUpdate(cb func(value float64)) { +func (inc *Volatility) OnUpdate(cb func(value float64)) { inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) } -func (inc *VOLATILITY) EmitUpdate(value float64) { +func (inc *Volatility) EmitUpdate(value float64) { for _, cb := range inc.UpdateCallbacks { cb(value) } diff --git a/pkg/indicator/vwap.go b/pkg/indicator/vwap.go index 7fcac717aa..5cb36c847f 100644 --- a/pkg/indicator/vwap.go +++ b/pkg/indicator/vwap.go @@ -3,6 +3,7 @@ package indicator import ( "time" + "github.com/c9s/bbgo/pkg/datatype/floats" "github.com/c9s/bbgo/pkg/types" ) @@ -17,10 +18,11 @@ Volume-Weighted Average Price (VWAP) Explained */ //go:generate callbackgen -type VWAP type VWAP struct { + types.SeriesBase types.IntervalWindow - Values types.Float64Slice - Prices types.Float64Slice - Volumes types.Float64Slice + Values floats.Slice + Prices floats.Slice + Volumes floats.Slice WeightedSum float64 VolumeSum float64 @@ -29,6 +31,9 @@ type VWAP struct { } func (inc *VWAP) Update(price, volume float64) { + if len(inc.Prices) == 0 { + inc.SeriesBase.Series = inc + } inc.Prices.Push(price) inc.Volumes.Push(volume) @@ -65,20 +70,23 @@ func (inc *VWAP) Length() int { return len(inc.Values) } -var _ types.Series = &VWAP{} +var _ types.SeriesExtend = &VWAP{} -func (inc *VWAP) calculateAndUpdate(kLines []types.KLine) { - var priceF = KLineTypicalPriceMapper +func (inc *VWAP) PushK(k types.KLine) { + inc.Update(KLineTypicalPriceMapper(k), k.Volume.Float64()) +} - for _, k := range kLines { +func (inc *VWAP) CalculateAndUpdate(allKLines []types.KLine) { + for _, k := range allKLines { if inc.EndTime != zeroTime && !k.EndTime.After(inc.EndTime) { continue } - inc.Update(priceF(k), k.Volume.Float64()) + + inc.PushK(k) } inc.EmitUpdate(inc.Last()) - inc.EndTime = kLines[len(kLines)-1].EndTime.Time() + inc.EndTime = allKLines[len(allKLines)-1].EndTime.Time() } func (inc *VWAP) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { @@ -86,14 +94,14 @@ func (inc *VWAP) handleKLineWindowUpdate(interval types.Interval, window types.K return } - inc.calculateAndUpdate(window) + inc.CalculateAndUpdate(window) } func (inc *VWAP) Bind(updater KLineWindowUpdater) { updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) } -func CalculateVWAP(klines []types.KLine, priceF KLinePriceMapper, window int) float64 { +func calculateVWAP(klines []types.KLine, priceF KLineValueMapper, window int) float64 { vwap := VWAP{IntervalWindow: types.IntervalWindow{Window: window}} for _, k := range klines { vwap.Update(priceF(k), k.Volume.Float64()) diff --git a/pkg/indicator/vwap_callbacks.go b/pkg/indicator/vwap_callbacks.go index 9a235d17ae..918ddcf506 100644 --- a/pkg/indicator/vwap_callbacks.go +++ b/pkg/indicator/vwap_callbacks.go @@ -4,12 +4,12 @@ package indicator import () -func (V *VWAP) OnUpdate(cb func(value float64)) { - V.UpdateCallbacks = append(V.UpdateCallbacks, cb) +func (inc *VWAP) OnUpdate(cb func(value float64)) { + inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) } -func (V *VWAP) EmitUpdate(value float64) { - for _, cb := range V.UpdateCallbacks { +func (inc *VWAP) EmitUpdate(value float64) { + for _, cb := range inc.UpdateCallbacks { cb(value) } } diff --git a/pkg/indicator/vwap_test.go b/pkg/indicator/vwap_test.go index d168bb938f..7929b4bbae 100644 --- a/pkg/indicator/vwap_test.go +++ b/pkg/indicator/vwap_test.go @@ -64,7 +64,7 @@ func Test_calculateVWAP(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { priceF := KLineTypicalPriceMapper - got := CalculateVWAP(tt.kLines, priceF, tt.window) + got := calculateVWAP(tt.kLines, priceF, tt.window) diff := math.Trunc((got-tt.want)*100) / 100 if diff != 0 { t.Errorf("calculateVWAP() = %v, want %v", got, tt.want) diff --git a/pkg/indicator/vwma.go b/pkg/indicator/vwma.go index 131e2f5df0..9998bbc49b 100644 --- a/pkg/indicator/vwma.go +++ b/pkg/indicator/vwma.go @@ -3,8 +3,7 @@ package indicator import ( "time" - log "github.com/sirupsen/logrus" - + "github.com/c9s/bbgo/pkg/datatype/floats" "github.com/c9s/bbgo/pkg/types" ) @@ -20,11 +19,16 @@ Volume Weighted Moving Average */ //go:generate callbackgen -type VWMA type VWMA struct { + types.SeriesBase types.IntervalWindow - Values types.Float64Slice + + Values floats.Slice + PriceVolumeSMA *SMA + VolumeSMA *SMA + EndTime time.Time - UpdateCallbacks []func(value float64) + updateCallbacks []func(value float64) } func (inc *VWMA) Last() float64 { @@ -46,51 +50,57 @@ func (inc *VWMA) Length() int { return len(inc.Values) } -var _ types.Series = &VWMA{} - -func KLinePriceVolumeMapper(k types.KLine) float64 { - return k.Close.Mul(k.Volume).Float64() -} +var _ types.SeriesExtend = &VWMA{} -func KLineVolumeMapper(k types.KLine) float64 { - return k.Volume.Float64() -} +func (inc *VWMA) Update(price, volume float64) { + if inc.PriceVolumeSMA == nil { + inc.PriceVolumeSMA = &SMA{IntervalWindow: inc.IntervalWindow} + inc.SeriesBase.Series = inc + } -func (inc *VWMA) calculateAndUpdate(kLines []types.KLine) { - if len(kLines) < inc.Window { - return + if inc.VolumeSMA == nil { + inc.VolumeSMA = &SMA{IntervalWindow: inc.IntervalWindow} } - var index = len(kLines) - 1 - var kline = kLines[index] + inc.PriceVolumeSMA.Update(price * volume) + inc.VolumeSMA.Update(volume) + + pv := inc.PriceVolumeSMA.Last() + v := inc.VolumeSMA.Last() + vwma := pv / v + inc.Values.Push(vwma) +} - if inc.EndTime != zeroTime && kline.EndTime.Before(inc.EndTime) { +func (inc *VWMA) PushK(k types.KLine) { + if inc.EndTime != zeroTime && k.EndTime.Before(inc.EndTime) { return } - var recentK = kLines[index-(inc.Window-1) : index+1] + inc.Update(k.Close.Float64(), k.Volume.Float64()) +} - pv, err := calculateSMA(recentK, inc.Window, KLinePriceVolumeMapper) - if err != nil { - log.WithError(err).Error("price x volume SMA error") - return - } - v, err := calculateSMA(recentK, inc.Window, KLineVolumeMapper) - if err != nil { - log.WithError(err).Error("volume SMA error") + +func (inc *VWMA) CalculateAndUpdate(allKLines []types.KLine) { + if len(allKLines) < inc.Window { return } - vwma := pv / v - inc.Values.Push(vwma) + var last = allKLines[len(allKLines)-1] - if len(inc.Values) > MaxNumOfSMA { - inc.Values = inc.Values[MaxNumOfSMATruncateSize-1:] - } + if inc.VolumeSMA == nil { + for _, k := range allKLines { + if inc.EndTime != zeroTime && k.EndTime.Before(inc.EndTime) { + return + } - inc.EndTime = kLines[index].EndTime.Time() + inc.Update(k.Close.Float64(), k.Volume.Float64()) + } + } else { + inc.Update(last.Close.Float64(), last.Volume.Float64()) + } - inc.EmitUpdate(vwma) + inc.EndTime = last.EndTime.Time() + inc.EmitUpdate(inc.Values.Last()) } func (inc *VWMA) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { @@ -98,7 +108,7 @@ func (inc *VWMA) handleKLineWindowUpdate(interval types.Interval, window types.K return } - inc.calculateAndUpdate(window) + inc.CalculateAndUpdate(window) } func (inc *VWMA) Bind(updater KLineWindowUpdater) { diff --git a/pkg/indicator/vwma_callbacks.go b/pkg/indicator/vwma_callbacks.go index 5be9f70f01..375aee111e 100644 --- a/pkg/indicator/vwma_callbacks.go +++ b/pkg/indicator/vwma_callbacks.go @@ -5,11 +5,11 @@ package indicator import () func (inc *VWMA) OnUpdate(cb func(value float64)) { - inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) + inc.updateCallbacks = append(inc.updateCallbacks, cb) } func (inc *VWMA) EmitUpdate(value float64) { - for _, cb := range inc.UpdateCallbacks { + for _, cb := range inc.updateCallbacks { cb(value) } } diff --git a/pkg/indicator/wdrift.go b/pkg/indicator/wdrift.go new file mode 100644 index 0000000000..e6150ee40a --- /dev/null +++ b/pkg/indicator/wdrift.go @@ -0,0 +1,152 @@ +package indicator + +import ( + "math" + + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/types" +) + +// Refer: https://tradingview.com/script/aDymGrFx-Drift-Study-Inspired-by-Monte-Carlo-Simulations-with-BM-KL/ +// Brownian Motion's drift factor +// could be used in Monte Carlo Simulations +//go:generate callbackgen -type WeightedDrift +type WeightedDrift struct { + types.SeriesBase + types.IntervalWindow + chng *types.Queue + Values floats.Slice + MA types.UpdatableSeriesExtend + Weight *types.Queue + LastValue float64 + UpdateCallbacks []func(value float64) +} + +func (inc *WeightedDrift) Update(value float64, weight float64) { + win := 10 + if inc.Window > win { + win = inc.Window + } + if inc.chng == nil { + inc.SeriesBase.Series = inc + if inc.MA == nil { + inc.MA = &SMA{IntervalWindow: types.IntervalWindow{Interval: inc.Interval, Window: inc.Window}} + } + inc.Weight = types.NewQueue(win) + inc.chng = types.NewQueue(inc.Window) + inc.LastValue = value + inc.Weight.Update(weight) + return + } + inc.Weight.Update(weight) + base := inc.Weight.Lowest(win) + multiplier := int(weight / base) + var chng float64 + if value == 0 { + chng = 0 + } else { + chng = math.Log(value/inc.LastValue) / weight * base + inc.LastValue = value + } + for i := 0; i < multiplier; i++ { + inc.MA.Update(chng) + inc.chng.Update(chng) + } + if inc.chng.Length() >= inc.Window { + stdev := types.Stdev(inc.chng, inc.Window) + drift := inc.MA.Last() - stdev*stdev*0.5 + inc.Values.Push(drift) + } +} + +// Assume that MA is SMA +func (inc *WeightedDrift) ZeroPoint() float64 { + window := float64(inc.Window) + stdev := types.Stdev(inc.chng, inc.Window) + chng := inc.chng.Index(inc.Window - 1) + /*b := -2 * inc.MA.Last() - 2 + c := window * stdev * stdev - chng * chng + 2 * chng * (inc.MA.Last() + 1) - 2 * inc.MA.Last() * window + + root := math.Sqrt(b*b - 4*c) + K1 := (-b + root)/2 + K2 := (-b - root)/2 + N1 := math.Exp(K1) * inc.LastValue + N2 := math.Exp(K2) * inc.LastValue + if math.Abs(inc.LastValue-N1) < math.Abs(inc.LastValue-N2) { + return N1 + } else { + return N2 + }*/ + return inc.LastValue * math.Exp(window*(0.5*stdev*stdev)+chng-inc.MA.Last()*window) +} + +func (inc *WeightedDrift) Clone() (out *WeightedDrift) { + out = &WeightedDrift{ + IntervalWindow: inc.IntervalWindow, + chng: inc.chng.Clone(), + Values: inc.Values[:], + MA: types.Clone(inc.MA), + Weight: inc.Weight.Clone(), + LastValue: inc.LastValue, + } + out.SeriesBase.Series = out + return out +} + +func (inc *WeightedDrift) TestUpdate(value float64, weight float64) *WeightedDrift { + out := inc.Clone() + out.Update(value, weight) + return out +} + +func (inc *WeightedDrift) Index(i int) float64 { + if inc.Values == nil { + return 0 + } + return inc.Values.Index(i) +} + +func (inc *WeightedDrift) Last() float64 { + if inc.Values.Length() == 0 { + return 0 + } + return inc.Values.Last() +} + +func (inc *WeightedDrift) Length() int { + if inc.Values == nil { + return 0 + } + return inc.Values.Length() +} + +var _ types.SeriesExtend = &Drift{} + +func (inc *WeightedDrift) PushK(k types.KLine) { + inc.Update(k.Close.Float64(), k.Volume.Abs().Float64()) +} + +func (inc *WeightedDrift) CalculateAndUpdate(allKLines []types.KLine) { + if inc.chng == nil { + for _, k := range allKLines { + inc.PushK(k) + inc.EmitUpdate(inc.Last()) + } + } else { + k := allKLines[len(allKLines)-1] + inc.PushK(k) + inc.EmitUpdate(inc.Last()) + } +} + +func (inc *WeightedDrift) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { + if inc.Interval != interval { + return + } + + inc.CalculateAndUpdate(window) +} + +func (inc *WeightedDrift) Bind(updater KLineWindowUpdater) { + updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) +} diff --git a/pkg/indicator/wdrift_test.go b/pkg/indicator/wdrift_test.go new file mode 100644 index 0000000000..1cf6c41206 --- /dev/null +++ b/pkg/indicator/wdrift_test.go @@ -0,0 +1,47 @@ +package indicator + +import ( + "encoding/json" + "testing" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" + "github.com/stretchr/testify/assert" +) + +func Test_WDrift(t *testing.T) { + var randomPrices = []byte(`[1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 4, 1, 2, 3, 4, 5, 6, 7, 8, 9, 4, 1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 4, 1, 2, 3, 4, 5, 6, 7, 8, 9]`) + var input []fixedpoint.Value + if err := json.Unmarshal(randomPrices, &input); err != nil { + panic(err) + } + buildKLines := func(prices []fixedpoint.Value) (klines []types.KLine) { + for _, p := range prices { + klines = append(klines, types.KLine{Close: p, Volume: fixedpoint.One}) + } + + return klines + } + tests := []struct { + name string + kLines []types.KLine + all int + }{ + { + name: "random_case", + kLines: buildKLines(input), + all: 47, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + drift := WeightedDrift{IntervalWindow: types.IntervalWindow{Window: 3}} + drift.CalculateAndUpdate(tt.kLines) + assert.Equal(t, drift.Length(), tt.all) + for _, v := range drift.Values { + assert.LessOrEqual(t, v, 1.0) + } + }) + } +} diff --git a/pkg/indicator/weighteddrift_callbacks.go b/pkg/indicator/weighteddrift_callbacks.go new file mode 100644 index 0000000000..476e61506e --- /dev/null +++ b/pkg/indicator/weighteddrift_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type WeightedDrift"; DO NOT EDIT. + +package indicator + +import () + +func (inc *WeightedDrift) OnUpdate(cb func(value float64)) { + inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) +} + +func (inc *WeightedDrift) EmitUpdate(value float64) { + for _, cb := range inc.UpdateCallbacks { + cb(value) + } +} diff --git a/pkg/indicator/wwma.go b/pkg/indicator/wwma.go index 13fd1b8d19..0dbc67ca1e 100644 --- a/pkg/indicator/wwma.go +++ b/pkg/indicator/wwma.go @@ -1,8 +1,10 @@ package indicator import ( - "github.com/c9s/bbgo/pkg/types" "time" + + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/types" ) // Refer: Welles Wilder's Moving Average @@ -14,8 +16,9 @@ const MaxNumOfWWMATruncateSize = 100 //go:generate callbackgen -type WWMA type WWMA struct { + types.SeriesBase types.IntervalWindow - Values types.Float64Slice + Values floats.Slice LastOpenTime time.Time UpdateCallbacks []func(value float64) @@ -23,6 +26,7 @@ type WWMA struct { func (inc *WWMA) Update(value float64) { if len(inc.Values) == 0 { + inc.SeriesBase.Series = inc inc.Values.Push(value) return } else if len(inc.Values) > MaxNumOfWWMA { @@ -54,7 +58,11 @@ func (inc *WWMA) Length() int { return len(inc.Values) } -func (inc *WWMA) calculateAndUpdate(allKLines []types.KLine) { +func (inc *WWMA) PushK(k types.KLine) { + inc.Update(k.Close.Float64()) +} + +func (inc *WWMA) CalculateAndUpdate(allKLines []types.KLine) { if len(allKLines) < inc.Window { // we can't calculate return @@ -66,7 +74,7 @@ func (inc *WWMA) calculateAndUpdate(allKLines []types.KLine) { doable = true } if doable { - inc.Update(k.Close.Float64()) + inc.PushK(k) inc.LastOpenTime = k.StartTime.Time() inc.EmitUpdate(inc.Last()) } @@ -78,11 +86,11 @@ func (inc *WWMA) handleKLineWindowUpdate(interval types.Interval, window types.K return } - inc.calculateAndUpdate(window) + inc.CalculateAndUpdate(window) } func (inc *WWMA) Bind(updater KLineWindowUpdater) { updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) } -var _ types.Series = &WWMA{} +var _ types.SeriesExtend = &WWMA{} diff --git a/pkg/indicator/zlema.go b/pkg/indicator/zlema.go index 4ed97d84ac..3f6f4b08fd 100644 --- a/pkg/indicator/zlema.go +++ b/pkg/indicator/zlema.go @@ -1,6 +1,7 @@ package indicator import ( + "github.com/c9s/bbgo/pkg/datatype/floats" "github.com/c9s/bbgo/pkg/types" ) @@ -9,13 +10,14 @@ import ( //go:generate callbackgen -type ZLEMA type ZLEMA struct { + types.SeriesBase types.IntervalWindow - data types.Float64Slice + data floats.Slice zlema *EWMA lag int - UpdateCallbacks []func(value float64) + updateCallbacks []func(value float64) } func (inc *ZLEMA) Index(i int) float64 { @@ -41,7 +43,8 @@ func (inc *ZLEMA) Length() int { func (inc *ZLEMA) Update(value float64) { if inc.lag == 0 || inc.zlema == nil { - inc.zlema = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}} + inc.SeriesBase.Series = inc + inc.zlema = &EWMA{IntervalWindow: inc.IntervalWindow} inc.lag = int((float64(inc.Window)-1.)/2. + 0.5) } inc.data.Push(value) @@ -55,16 +58,21 @@ func (inc *ZLEMA) Update(value float64) { inc.zlema.Update(emaData) } -var _ types.Series = &ZLEMA{} +var _ types.SeriesExtend = &ZLEMA{} -func (inc *ZLEMA) calculateAndUpdate(allKLines []types.KLine) { +func (inc *ZLEMA) PushK(k types.KLine) { + inc.Update(k.Close.Float64()) +} + +func (inc *ZLEMA) CalculateAndUpdate(allKLines []types.KLine) { if inc.zlema == nil { for _, k := range allKLines { - inc.Update(k.Close.Float64()) + inc.PushK(k) inc.EmitUpdate(inc.Last()) } } else { - inc.Update(allKLines[len(allKLines)-1].Close.Float64()) + k := allKLines[len(allKLines)-1] + inc.PushK(k) inc.EmitUpdate(inc.Last()) } } @@ -74,7 +82,7 @@ func (inc *ZLEMA) handleKLineWindowUpdate(interval types.Interval, window types. return } - inc.calculateAndUpdate(window) + inc.CalculateAndUpdate(window) } func (inc *ZLEMA) Bind(updater KLineWindowUpdater) { diff --git a/pkg/indicator/zlema_callbacks.go b/pkg/indicator/zlema_callbacks.go index d70147699f..98a84c6597 100644 --- a/pkg/indicator/zlema_callbacks.go +++ b/pkg/indicator/zlema_callbacks.go @@ -5,11 +5,11 @@ package indicator import () func (inc *ZLEMA) OnUpdate(cb func(value float64)) { - inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) + inc.updateCallbacks = append(inc.updateCallbacks, cb) } func (inc *ZLEMA) EmitUpdate(value float64) { - for _, cb := range inc.UpdateCallbacks { + for _, cb := range inc.updateCallbacks { cb(value) } } diff --git a/pkg/indicator/zlema_test.go b/pkg/indicator/zlema_test.go index 4b0e546ab2..4560f4276b 100644 --- a/pkg/indicator/zlema_test.go +++ b/pkg/indicator/zlema_test.go @@ -45,7 +45,7 @@ func Test_ZLEMA(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { zlema := ZLEMA{IntervalWindow: types.IntervalWindow{Window: 16}} - zlema.calculateAndUpdate(tt.kLines) + zlema.CalculateAndUpdate(tt.kLines) last := zlema.Last() assert.InDelta(t, tt.want, last, Delta) assert.InDelta(t, tt.next, zlema.Index(1), Delta) diff --git a/pkg/interact/auth.go b/pkg/interact/auth.go index 71212df765..343ff382a4 100644 --- a/pkg/interact/auth.go +++ b/pkg/interact/auth.go @@ -37,6 +37,7 @@ type AuthInteract struct { func (it *AuthInteract) Commands(interact *Interact) { if it.Strict { // generate a one-time-use otp + // pragma: allowlist nextline secret if it.OneTimePasswordKey == nil { opts := totp.GenerateOpts{ Issuer: "interact", @@ -48,7 +49,7 @@ func (it *AuthInteract) Commands(interact *Interact) { if err != nil { panic(err) } - + // pragma: allowlist nextline secret it.OneTimePasswordKey = key } interact.Command("/auth", "authorize", func(reply Reply, session Session) error { diff --git a/pkg/interact/interact.go b/pkg/interact/interact.go index a4c4501ee6..820979cfe1 100644 --- a/pkg/interact/interact.go +++ b/pkg/interact/interact.go @@ -46,8 +46,6 @@ type Interact struct { states map[State]State statesFunc map[State]interface{} - authenticatedSessions map[string]Session - customInteractions []CustomInteraction messengers []Messenger @@ -97,10 +95,6 @@ func (it *Interact) getNextState(session Session, currentState State) (nextState return session.GetOriginState(), final } -func (it *Interact) handleCallback(session Session, payload string) error { - return nil -} - func (it *Interact) handleResponse(session Session, text string, ctxObjects ...interface{}) error { // We only need response when executing a command switch session.GetState() { @@ -118,7 +112,7 @@ func (it *Interact) handleResponse(session Session, text string, ctxObjects ...i } ctxObjects = append(ctxObjects, session) - _, err := parseFuncArgsAndCall(f, args, ctxObjects...) + _, err := ParseFuncArgsAndCall(f, args, ctxObjects...) if err != nil { return err } @@ -160,7 +154,7 @@ func (it *Interact) runCommand(session Session, command string, args []string, c ctxObjects = append(ctxObjects, session) session.SetState(cmd.initState) - if _, err := parseFuncArgsAndCall(cmd.F, args, ctxObjects...); err != nil { + if _, err := ParseFuncArgsAndCall(cmd.F, args, ctxObjects...); err != nil { return err } diff --git a/pkg/interact/interact_test.go b/pkg/interact/interact_test.go index bd08282405..8402ba1c8c 100644 --- a/pkg/interact/interact_test.go +++ b/pkg/interact/interact_test.go @@ -18,7 +18,7 @@ func Test_parseFuncArgsAndCall_NoErrorFunction(t *testing.T) { return nil } - _, err := parseFuncArgsAndCall(noErrorFunc, []string{"BTCUSDT", "0.123", "true"}) + _, err := ParseFuncArgsAndCall(noErrorFunc, []string{"BTCUSDT", "0.123", "true"}) assert.NoError(t, err) } @@ -27,7 +27,7 @@ func Test_parseFuncArgsAndCall_ErrorFunction(t *testing.T) { return errors.New("error") } - _, err := parseFuncArgsAndCall(errorFunc, []string{"BTCUSDT", "0.123"}) + _, err := ParseFuncArgsAndCall(errorFunc, []string{"BTCUSDT", "0.123"}) assert.Error(t, err) } @@ -38,7 +38,7 @@ func Test_parseFuncArgsAndCall_InterfaceInjection(t *testing.T) { } buf := bytes.NewBuffer(nil) - _, err := parseFuncArgsAndCall(f, []string{"BTCUSDT", "0.123"}, buf) + _, err := ParseFuncArgsAndCall(f, []string{"BTCUSDT", "0.123"}, buf) assert.NoError(t, err) assert.Equal(t, "123", buf.String()) } diff --git a/pkg/interact/parse.go b/pkg/interact/parse.go index db4f3d1fd1..64f55871be 100644 --- a/pkg/interact/parse.go +++ b/pkg/interact/parse.go @@ -10,21 +10,20 @@ import ( log "github.com/sirupsen/logrus" ) -func parseFuncArgsAndCall(f interface{}, args []string, objects ...interface{}) (State, error) { +func ParseFuncArgsAndCall(f interface{}, args []string, objects ...interface{}) (State, error) { fv := reflect.ValueOf(f) ft := reflect.TypeOf(f) - argIndex := 0 var rArgs []reflect.Value for i := 0; i < ft.NumIn(); i++ { at := ft.In(i) + // get the kind of argument switch k := at.Kind(); k { case reflect.Interface: found := false - for oi := 0; oi < len(objects); oi++ { obj := objects[oi] objT := reflect.TypeOf(obj) @@ -90,8 +89,8 @@ func parseFuncArgsAndCall(f interface{}, args []string, objects ...interface{}) } // try to get the error object from the return value - var state State var err error + var state State for i := 0; i < ft.NumOut(); i++ { outType := ft.Out(i) switch outType.Kind() { @@ -107,7 +106,6 @@ func parseFuncArgsAndCall(f interface{}, args []string, objects ...interface{}) err = ov } - } } return state, err diff --git a/pkg/interact/slack.go b/pkg/interact/slack.go index a6ef167c88..b9bb3c625c 100644 --- a/pkg/interact/slack.go +++ b/pkg/interact/slack.go @@ -242,7 +242,7 @@ func (s *Slack) listen(ctx context.Context) { innerEvent := eventsAPIEvent.InnerEvent switch ev := innerEvent.Data.(type) { case *slackevents.MessageEvent: - log.Infof("message event: text=%+v", ev.Text) + log.Debugf("message event: text=%+v", ev.Text) if len(ev.BotID) > 0 { log.Debug("skip bot message") diff --git a/pkg/interact/telegram.go b/pkg/interact/telegram.go index 4b1bb13fe0..c2a45a0025 100644 --- a/pkg/interact/telegram.go +++ b/pkg/interact/telegram.go @@ -6,7 +6,9 @@ import ( "strings" "time" + "github.com/c9s/bbgo/pkg/util" log "github.com/sirupsen/logrus" + "golang.org/x/time/rate" "gopkg.in/tucnak/telebot.v2" ) @@ -15,6 +17,10 @@ func init() { _ = Reply(&TelegramReply{}) } +var sendLimiter = rate.NewLimiter(10, 2) + +const maxMessageSize int = 3000 + type TelegramSessionMap map[int64]*TelegramSession type TelegramSession struct { @@ -62,7 +68,14 @@ type TelegramReply struct { } func (r *TelegramReply) Send(message string) { - checkSendErr(r.bot.Send(r.session.Chat, message)) + ctx := context.Background() + splits := util.StringSplitByLength(message, maxMessageSize) + for _, split := range splits { + if err := sendLimiter.Wait(ctx); err != nil { + log.WithError(err).Errorf("telegram send limit exceeded") + } + checkSendErr(r.bot.Send(r.session.Chat, split)) + } } func (r *TelegramReply) Message(message string) { @@ -104,8 +117,6 @@ type Telegram struct { // Private is used to protect the telegram bot, users not authenticated can not see messages or sending commands Private bool `json:"private,omitempty"` - authorizing bool - sessions TelegramSessionMap // textMessageResponder is used for interact to register its message handler @@ -134,7 +145,7 @@ func (tm *Telegram) SetTextMessageResponder(responder Responder) { tm.textMessageResponder = responder } -func (tm *Telegram) Start(context.Context) { +func (tm *Telegram) Start(ctx context.Context) { tm.Bot.Handle(telebot.OnCallback, func(c *telebot.Callback) { log.Infof("[telegram] onCallback: %+v", c) }) @@ -159,7 +170,20 @@ func (tm *Telegram) Start(context.Context) { if reply.set { reply.build() - checkSendErr(tm.Bot.Send(m.Chat, reply.message, reply.menu)) + if len(reply.message) > 0 || reply.menu != nil { + splits := util.StringSplitByLength(reply.message, maxMessageSize) + for i, split := range splits { + if err := sendLimiter.Wait(ctx); err != nil { + log.WithError(err).Errorf("telegram send limit exceeded") + } + if i == len(splits)-1 { + // only set menu on the last message + checkSendErr(tm.Bot.Send(m.Chat, split, reply.menu)) + } else { + checkSendErr(tm.Bot.Send(m.Chat, split)) + } + } + } } }) @@ -237,7 +261,7 @@ func (tm *Telegram) Sessions() TelegramSessionMap { } func (tm *Telegram) RestoreSessions(sessions TelegramSessionMap) { - if sessions == nil || len(sessions) == 0 { + if len(sessions) == 0 { return } diff --git a/pkg/migrations/mysql/20211211034819_add_nav_history_details.go b/pkg/migrations/mysql/20211211034819_add_nav_history_details.go index f077ce8102..f98de36fdf 100644 --- a/pkg/migrations/mysql/20211211034819_add_nav_history_details.go +++ b/pkg/migrations/mysql/20211211034819_add_nav_history_details.go @@ -14,12 +14,12 @@ func init() { func upAddNavHistoryDetails(ctx context.Context, tx rockhopper.SQLExecutor) (err error) { // This code is executed when the migration is applied. - _, err = tx.ExecContext(ctx, "CREATE TABLE nav_history_details\n(\n gid bigint unsigned auto_increment PRIMARY KEY,\n exchange VARCHAR(30) NOT NULL,\n subaccount VARCHAR(30) NOT NULL,\n time DATETIME(3) NOT NULL,\n currency VARCHAR(12) NOT NULL,\n balance_in_usd DECIMAL(32, 8) UNSIGNED DEFAULT 0.00000000 NOT NULL,\n balance_in_btc DECIMAL(32, 8) UNSIGNED DEFAULT 0.00000000 NOT NULL,\n balance DECIMAL(32, 8) UNSIGNED DEFAULT 0.00000000 NOT NULL,\n available DECIMAL(32, 8) UNSIGNED DEFAULT 0.00000000 NOT NULL,\n locked DECIMAL(32, 8) UNSIGNED DEFAULT 0.00000000 NOT NULL\n);") + _, err = tx.ExecContext(ctx, "CREATE TABLE nav_history_details\n(\n gid bigint unsigned auto_increment PRIMARY KEY,\n exchange VARCHAR(30) NOT NULL,\n subaccount VARCHAR(30) NOT NULL,\n time DATETIME(3) NOT NULL,\n currency VARCHAR(12) NOT NULL,\n balance_in_usd DECIMAL(32, 8) UNSIGNED DEFAULT 0.00000000 NOT NULL,\n balance_in_btc DECIMAL(32, 8) UNSIGNED DEFAULT 0.00000000 NOT NULL,\n balance DECIMAL(32, 8) UNSIGNED DEFAULT 0.00000000 NOT NULL,\n available DECIMAL(32, 8) UNSIGNED DEFAULT 0.00000000 NOT NULL,\n locked DECIMAL(32, 8) UNSIGNED DEFAULT 0.00000000 NOT NULL\n);") if err != nil { return err } - _, err = tx.ExecContext(ctx, "CREATE INDEX idx_nav_history_details\n on nav_history_details(time, currency, exchange);") + _, err = tx.ExecContext(ctx, "CREATE INDEX idx_nav_history_details\n on nav_history_details (time, currency, exchange);") if err != nil { return err } diff --git a/pkg/migrations/mysql/20220503144849_add_margin_info_to_nav.go b/pkg/migrations/mysql/20220503144849_add_margin_info_to_nav.go new file mode 100644 index 0000000000..98b25028aa --- /dev/null +++ b/pkg/migrations/mysql/20220503144849_add_margin_info_to_nav.go @@ -0,0 +1,34 @@ +package mysql + +import ( + "context" + + "github.com/c9s/rockhopper" +) + +func init() { + AddMigration(upAddMarginInfoToNav, downAddMarginInfoToNav) + +} + +func upAddMarginInfoToNav(ctx context.Context, tx rockhopper.SQLExecutor) (err error) { + // This code is executed when the migration is applied. + + _, err = tx.ExecContext(ctx, "ALTER TABLE `nav_history_details`\n ADD COLUMN `session` VARCHAR(30) NOT NULL,\n ADD COLUMN `is_margin` BOOLEAN NOT NULL DEFAULT FALSE,\n ADD COLUMN `is_isolated` BOOLEAN NOT NULL DEFAULT FALSE,\n ADD COLUMN `isolated_symbol` VARCHAR(30) NOT NULL DEFAULT '',\n ADD COLUMN `net_asset` DECIMAL(32, 8) UNSIGNED DEFAULT 0.00000000 NOT NULL,\n ADD COLUMN `borrowed` DECIMAL(32, 8) UNSIGNED DEFAULT 0.00000000 NOT NULL,\n ADD COLUMN `price_in_usd` DECIMAL(32, 8) UNSIGNED DEFAULT 0.00000000 NOT NULL\n;") + if err != nil { + return err + } + + return err +} + +func downAddMarginInfoToNav(ctx context.Context, tx rockhopper.SQLExecutor) (err error) { + // This code is executed when the migration is rolled back. + + _, err = tx.ExecContext(ctx, "ALTER TABLE `nav_history_details`\n DROP COLUMN `session`,\n DROP COLUMN `net_asset`,\n DROP COLUMN `borrowed`,\n DROP COLUMN `price_in_usd`,\n DROP COLUMN `is_margin`,\n DROP COLUMN `is_isolated`,\n DROP COLUMN `isolated_symbol`\n;") + if err != nil { + return err + } + + return err +} diff --git a/pkg/migrations/mysql/20220504184155_fix_net_asset_column.go b/pkg/migrations/mysql/20220504184155_fix_net_asset_column.go new file mode 100644 index 0000000000..c986d66fe9 --- /dev/null +++ b/pkg/migrations/mysql/20220504184155_fix_net_asset_column.go @@ -0,0 +1,39 @@ +package mysql + +import ( + "context" + + "github.com/c9s/rockhopper" +) + +func init() { + AddMigration(upFixNetAssetColumn, downFixNetAssetColumn) + +} + +func upFixNetAssetColumn(ctx context.Context, tx rockhopper.SQLExecutor) (err error) { + // This code is executed when the migration is applied. + + _, err = tx.ExecContext(ctx, "ALTER TABLE `nav_history_details`\n MODIFY COLUMN `net_asset` DECIMAL(32, 8) DEFAULT 0.00000000 NOT NULL,\n CHANGE COLUMN `balance_in_usd` `net_asset_in_usd` DECIMAL(32, 2) DEFAULT 0.00000000 NOT NULL,\n CHANGE COLUMN `balance_in_btc` `net_asset_in_btc` DECIMAL(32, 20) DEFAULT 0.00000000 NOT NULL;") + if err != nil { + return err + } + + _, err = tx.ExecContext(ctx, "ALTER TABLE `nav_history_details`\n ADD COLUMN `interest` DECIMAL(32, 20) UNSIGNED DEFAULT 0.00000000 NOT NULL;") + if err != nil { + return err + } + + return err +} + +func downFixNetAssetColumn(ctx context.Context, tx rockhopper.SQLExecutor) (err error) { + // This code is executed when the migration is rolled back. + + _, err = tx.ExecContext(ctx, "ALTER TABLE `nav_history_details`\n DROP COLUMN `interest`;") + if err != nil { + return err + } + + return err +} diff --git a/pkg/migrations/mysql/20220512170322_fix_profit_symbol_length.go b/pkg/migrations/mysql/20220512170322_fix_profit_symbol_length.go new file mode 100644 index 0000000000..1196c4d06e --- /dev/null +++ b/pkg/migrations/mysql/20220512170322_fix_profit_symbol_length.go @@ -0,0 +1,34 @@ +package mysql + +import ( + "context" + + "github.com/c9s/rockhopper" +) + +func init() { + AddMigration(upFixProfitSymbolLength, downFixProfitSymbolLength) + +} + +func upFixProfitSymbolLength(ctx context.Context, tx rockhopper.SQLExecutor) (err error) { + // This code is executed when the migration is applied. + + _, err = tx.ExecContext(ctx, "ALTER TABLE profits\n CHANGE symbol symbol VARCHAR(20) NOT NULL;") + if err != nil { + return err + } + + return err +} + +func downFixProfitSymbolLength(ctx context.Context, tx rockhopper.SQLExecutor) (err error) { + // This code is executed when the migration is rolled back. + + _, err = tx.ExecContext(ctx, "SELECT 1;") + if err != nil { + return err + } + + return err +} diff --git a/pkg/migrations/mysql/20220520140707_kline_unique_idx.go b/pkg/migrations/mysql/20220520140707_kline_unique_idx.go new file mode 100644 index 0000000000..84dfb50303 --- /dev/null +++ b/pkg/migrations/mysql/20220520140707_kline_unique_idx.go @@ -0,0 +1,74 @@ +package mysql + +import ( + "context" + + "github.com/c9s/rockhopper" +) + +func init() { + AddMigration(upKlineUniqueIdx, downKlineUniqueIdx) + +} + +func upKlineUniqueIdx(ctx context.Context, tx rockhopper.SQLExecutor) (err error) { + // This code is executed when the migration is applied. + + _, err = tx.ExecContext(ctx, "CREATE UNIQUE INDEX idx_kline_binance_unique\n ON binance_klines (`symbol`, `interval`, `start_time`);") + if err != nil { + return err + } + + _, err = tx.ExecContext(ctx, "CREATE UNIQUE INDEX idx_kline_max_unique\n ON max_klines (`symbol`, `interval`, `start_time`);") + if err != nil { + return err + } + + _, err = tx.ExecContext(ctx, "CREATE UNIQUE INDEX `idx_kline_ftx_unique`\n ON ftx_klines (`symbol`, `interval`, `start_time`);") + if err != nil { + return err + } + + _, err = tx.ExecContext(ctx, "CREATE UNIQUE INDEX `idx_kline_kucoin_unique`\n ON kucoin_klines (`symbol`, `interval`, `start_time`);") + if err != nil { + return err + } + + _, err = tx.ExecContext(ctx, "CREATE UNIQUE INDEX `idx_kline_okex_unique`\n ON okex_klines (`symbol`, `interval`, `start_time`);") + if err != nil { + return err + } + + return err +} + +func downKlineUniqueIdx(ctx context.Context, tx rockhopper.SQLExecutor) (err error) { + // This code is executed when the migration is rolled back. + + _, err = tx.ExecContext(ctx, "DROP INDEX `idx_kline_ftx_unique` ON `ftx_klines`;") + if err != nil { + return err + } + + _, err = tx.ExecContext(ctx, "DROP INDEX `idx_kline_max_unique` ON `max_klines`;") + if err != nil { + return err + } + + _, err = tx.ExecContext(ctx, "DROP INDEX `idx_kline_binance_unique` ON `binance_klines`;") + if err != nil { + return err + } + + _, err = tx.ExecContext(ctx, "DROP INDEX `idx_kline_kucoin_unique` ON `kucoin_klines`;") + if err != nil { + return err + } + + _, err = tx.ExecContext(ctx, "DROP INDEX `idx_kline_okex_unique` ON `okex_klines`;") + if err != nil { + return err + } + + return err +} diff --git a/pkg/migrations/mysql/20220531012226_margin_loans.go b/pkg/migrations/mysql/20220531012226_margin_loans.go new file mode 100644 index 0000000000..1857a18ee4 --- /dev/null +++ b/pkg/migrations/mysql/20220531012226_margin_loans.go @@ -0,0 +1,34 @@ +package mysql + +import ( + "context" + + "github.com/c9s/rockhopper" +) + +func init() { + AddMigration(upMarginLoans, downMarginLoans) + +} + +func upMarginLoans(ctx context.Context, tx rockhopper.SQLExecutor) (err error) { + // This code is executed when the migration is applied. + + _, err = tx.ExecContext(ctx, "CREATE TABLE `margin_loans`\n(\n `gid` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,\n `transaction_id` BIGINT UNSIGNED NOT NULL,\n `exchange` VARCHAR(24) NOT NULL DEFAULT '',\n `asset` VARCHAR(24) NOT NULL DEFAULT '',\n `isolated_symbol` VARCHAR(24) NOT NULL DEFAULT '',\n -- quantity is the quantity of the trade that makes profit\n `principle` DECIMAL(16, 8) UNSIGNED NOT NULL,\n `time` DATETIME(3) NOT NULL,\n PRIMARY KEY (`gid`),\n UNIQUE KEY (`transaction_id`)\n);") + if err != nil { + return err + } + + return err +} + +func downMarginLoans(ctx context.Context, tx rockhopper.SQLExecutor) (err error) { + // This code is executed when the migration is rolled back. + + _, err = tx.ExecContext(ctx, "DROP TABLE IF EXISTS `margin_loans`;") + if err != nil { + return err + } + + return err +} diff --git a/pkg/migrations/mysql/20220531013327_margin_repays.go b/pkg/migrations/mysql/20220531013327_margin_repays.go new file mode 100644 index 0000000000..66582d9770 --- /dev/null +++ b/pkg/migrations/mysql/20220531013327_margin_repays.go @@ -0,0 +1,34 @@ +package mysql + +import ( + "context" + + "github.com/c9s/rockhopper" +) + +func init() { + AddMigration(upMarginRepays, downMarginRepays) + +} + +func upMarginRepays(ctx context.Context, tx rockhopper.SQLExecutor) (err error) { + // This code is executed when the migration is applied. + + _, err = tx.ExecContext(ctx, "CREATE TABLE `margin_repays`\n(\n `gid` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,\n `transaction_id` BIGINT UNSIGNED NOT NULL,\n `exchange` VARCHAR(24) NOT NULL DEFAULT '',\n `asset` VARCHAR(24) NOT NULL DEFAULT '',\n `isolated_symbol` VARCHAR(24) NOT NULL DEFAULT '',\n -- quantity is the quantity of the trade that makes profit\n `principle` DECIMAL(16, 8) UNSIGNED NOT NULL,\n `time` DATETIME(3) NOT NULL,\n PRIMARY KEY (`gid`),\n UNIQUE KEY (`transaction_id`)\n);") + if err != nil { + return err + } + + return err +} + +func downMarginRepays(ctx context.Context, tx rockhopper.SQLExecutor) (err error) { + // This code is executed when the migration is rolled back. + + _, err = tx.ExecContext(ctx, "DROP TABLE IF EXISTS `margin_repays`;") + if err != nil { + return err + } + + return err +} diff --git a/pkg/migrations/mysql/20220531013542_margin_interests.go b/pkg/migrations/mysql/20220531013542_margin_interests.go new file mode 100644 index 0000000000..b6f3be1519 --- /dev/null +++ b/pkg/migrations/mysql/20220531013542_margin_interests.go @@ -0,0 +1,34 @@ +package mysql + +import ( + "context" + + "github.com/c9s/rockhopper" +) + +func init() { + AddMigration(upMarginInterests, downMarginInterests) + +} + +func upMarginInterests(ctx context.Context, tx rockhopper.SQLExecutor) (err error) { + // This code is executed when the migration is applied. + + _, err = tx.ExecContext(ctx, "CREATE TABLE `margin_interests`\n(\n `gid` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,\n `exchange` VARCHAR(24) NOT NULL DEFAULT '',\n `asset` VARCHAR(24) NOT NULL DEFAULT '',\n `isolated_symbol` VARCHAR(24) NOT NULL DEFAULT '',\n `principle` DECIMAL(16, 8) UNSIGNED NOT NULL,\n `interest` DECIMAL(20, 16) UNSIGNED NOT NULL,\n `interest_rate` DECIMAL(20, 16) UNSIGNED NOT NULL,\n `time` DATETIME(3) NOT NULL,\n PRIMARY KEY (`gid`)\n);") + if err != nil { + return err + } + + return err +} + +func downMarginInterests(ctx context.Context, tx rockhopper.SQLExecutor) (err error) { + // This code is executed when the migration is rolled back. + + _, err = tx.ExecContext(ctx, "DROP TABLE IF EXISTS `margin_interests`;") + if err != nil { + return err + } + + return err +} diff --git a/pkg/migrations/mysql/20220531015005_margin_liquidations.go b/pkg/migrations/mysql/20220531015005_margin_liquidations.go new file mode 100644 index 0000000000..194c0a67fd --- /dev/null +++ b/pkg/migrations/mysql/20220531015005_margin_liquidations.go @@ -0,0 +1,34 @@ +package mysql + +import ( + "context" + + "github.com/c9s/rockhopper" +) + +func init() { + AddMigration(upMarginLiquidations, downMarginLiquidations) + +} + +func upMarginLiquidations(ctx context.Context, tx rockhopper.SQLExecutor) (err error) { + // This code is executed when the migration is applied. + + _, err = tx.ExecContext(ctx, "CREATE TABLE `margin_liquidations`\n(\n `gid` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,\n `exchange` VARCHAR(24) NOT NULL DEFAULT '',\n `symbol` VARCHAR(24) NOT NULL DEFAULT '',\n `order_id` BIGINT UNSIGNED NOT NULL,\n `is_isolated` BOOL NOT NULL DEFAULT false,\n `average_price` DECIMAL(16, 8) UNSIGNED NOT NULL,\n `price` DECIMAL(16, 8) UNSIGNED NOT NULL,\n `quantity` DECIMAL(16, 8) UNSIGNED NOT NULL,\n `executed_quantity` DECIMAL(16, 8) UNSIGNED NOT NULL,\n `side` VARCHAR(5) NOT NULL DEFAULT '',\n `time_in_force` VARCHAR(5) NOT NULL DEFAULT '',\n `time` DATETIME(3) NOT NULL,\n PRIMARY KEY (`gid`),\n UNIQUE KEY (`order_id`, `exchange`)\n);") + if err != nil { + return err + } + + return err +} + +func downMarginLiquidations(ctx context.Context, tx rockhopper.SQLExecutor) (err error) { + // This code is executed when the migration is rolled back. + + _, err = tx.ExecContext(ctx, "DROP TABLE IF EXISTS `margin_liquidations`;") + if err != nil { + return err + } + + return err +} diff --git a/pkg/migrations/sqlite3/20211211034818_add_nav_history_details.go b/pkg/migrations/sqlite3/20211211034818_add_nav_history_details.go index 63ee752546..4dc6eda9ea 100644 --- a/pkg/migrations/sqlite3/20211211034818_add_nav_history_details.go +++ b/pkg/migrations/sqlite3/20211211034818_add_nav_history_details.go @@ -14,7 +14,7 @@ func init() { func upAddNavHistoryDetails(ctx context.Context, tx rockhopper.SQLExecutor) (err error) { // This code is executed when the migration is applied. - _, err = tx.ExecContext(ctx, "CREATE TABLE `nav_history_details`\n(\n gid bigint unsigned auto_increment PRIMARY KEY,\n `exchange` VARCHAR NOT NULL DEFAULT '',\n `subaccount` VARCHAR NOT NULL DEFAULT '',\n time DATETIME(3) NOT NULL DEFAULT (strftime('%s','now')),\n currency VARCHAR NOT NULL,\n balance_in_usd DECIMAL DEFAULT 0.00000000 NOT NULL,\n balance_in_btc DECIMAL DEFAULT 0.00000000 NOT NULL,\n balance DECIMAL DEFAULT 0.00000000 NOT NULL,\n available DECIMAL DEFAULT 0.00000000 NOT NULL,\n locked DECIMAL DEFAULT 0.00000000 NOT NULL\n);") + _, err = tx.ExecContext(ctx, "CREATE TABLE `nav_history_details`\n(\n `gid` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,\n `exchange` VARCHAR(30) NOT NULL DEFAULT '',\n `subaccount` VARCHAR(30) NOT NULL DEFAULT '',\n `time` DATETIME(3) NOT NULL DEFAULT (strftime('%s', 'now')),\n `currency` VARCHAR(30) NOT NULL,\n `net_asset_in_usd` DECIMAL DEFAULT 0.00000000 NOT NULL,\n `net_asset_in_btc` DECIMAL DEFAULT 0.00000000 NOT NULL,\n `balance` DECIMAL DEFAULT 0.00000000 NOT NULL,\n `available` DECIMAL DEFAULT 0.00000000 NOT NULL,\n `locked` DECIMAL DEFAULT 0.00000000 NOT NULL\n);") if err != nil { return err } diff --git a/pkg/migrations/sqlite3/20220503144849_add_margin_info_to_nav.go b/pkg/migrations/sqlite3/20220503144849_add_margin_info_to_nav.go new file mode 100644 index 0000000000..849e7e19ac --- /dev/null +++ b/pkg/migrations/sqlite3/20220503144849_add_margin_info_to_nav.go @@ -0,0 +1,64 @@ +package sqlite3 + +import ( + "context" + + "github.com/c9s/rockhopper" +) + +func init() { + AddMigration(upAddMarginInfoToNav, downAddMarginInfoToNav) + +} + +func upAddMarginInfoToNav(ctx context.Context, tx rockhopper.SQLExecutor) (err error) { + // This code is executed when the migration is applied. + + _, err = tx.ExecContext(ctx, "ALTER TABLE `nav_history_details` ADD COLUMN `session` VARCHAR(50) NOT NULL;") + if err != nil { + return err + } + + _, err = tx.ExecContext(ctx, "ALTER TABLE `nav_history_details` ADD COLUMN `borrowed` DECIMAL DEFAULT 0.00000000 NOT NULL;") + if err != nil { + return err + } + + _, err = tx.ExecContext(ctx, "ALTER TABLE `nav_history_details` ADD COLUMN `net_asset` DECIMAL DEFAULT 0.00000000 NOT NULL;") + if err != nil { + return err + } + + _, err = tx.ExecContext(ctx, "ALTER TABLE `nav_history_details` ADD COLUMN `price_in_usd` DECIMAL DEFAULT 0.00000000 NOT NULL;") + if err != nil { + return err + } + + _, err = tx.ExecContext(ctx, "ALTER TABLE `nav_history_details` ADD COLUMN `is_margin` BOOL DEFAULT FALSE NOT NULL;") + if err != nil { + return err + } + + _, err = tx.ExecContext(ctx, "ALTER TABLE `nav_history_details` ADD COLUMN `is_isolated` BOOL DEFAULT FALSE NOT NULL;") + if err != nil { + return err + } + + _, err = tx.ExecContext(ctx, "ALTER TABLE `nav_history_details` ADD COLUMN `isolated_symbol` VARCHAR(30) DEFAULT '' NOT NULL;") + if err != nil { + return err + } + + return err +} + +func downAddMarginInfoToNav(ctx context.Context, tx rockhopper.SQLExecutor) (err error) { + // This code is executed when the migration is rolled back. + + _, err = tx.ExecContext(ctx, "SELECT 1;") + if err != nil { + return err + } + + return err +} diff --git a/pkg/migrations/sqlite3/20220504184155_fix_net_asset_column.go b/pkg/migrations/sqlite3/20220504184155_fix_net_asset_column.go new file mode 100644 index 0000000000..d398ad5272 --- /dev/null +++ b/pkg/migrations/sqlite3/20220504184155_fix_net_asset_column.go @@ -0,0 +1,34 @@ +package sqlite3 + +import ( + "context" + + "github.com/c9s/rockhopper" +) + +func init() { + AddMigration(upFixNetAssetColumn, downFixNetAssetColumn) + +} + +func upFixNetAssetColumn(ctx context.Context, tx rockhopper.SQLExecutor) (err error) { + // This code is executed when the migration is applied. + + _, err = tx.ExecContext(ctx, "ALTER TABLE `nav_history_details` ADD COLUMN `interest` DECIMAL DEFAULT 0.00000000 NOT NULL;") + if err != nil { + return err + } + + return err +} + +func downFixNetAssetColumn(ctx context.Context, tx rockhopper.SQLExecutor) (err error) { + // This code is executed when the migration is rolled back. + + _, err = tx.ExecContext(ctx, "SELECT 1;") + if err != nil { + return err + } + + return err +} diff --git a/pkg/migrations/sqlite3/20220512170330_fix_profit_symbol_length.go b/pkg/migrations/sqlite3/20220512170330_fix_profit_symbol_length.go new file mode 100644 index 0000000000..31b3783607 --- /dev/null +++ b/pkg/migrations/sqlite3/20220512170330_fix_profit_symbol_length.go @@ -0,0 +1,34 @@ +package sqlite3 + +import ( + "context" + + "github.com/c9s/rockhopper" +) + +func init() { + AddMigration(upFixProfitSymbolLength, downFixProfitSymbolLength) + +} + +func upFixProfitSymbolLength(ctx context.Context, tx rockhopper.SQLExecutor) (err error) { + // This code is executed when the migration is applied. + + _, err = tx.ExecContext(ctx, "SELECT 1;") + if err != nil { + return err + } + + return err +} + +func downFixProfitSymbolLength(ctx context.Context, tx rockhopper.SQLExecutor) (err error) { + // This code is executed when the migration is rolled back. + + _, err = tx.ExecContext(ctx, "SELECT 1;") + if err != nil { + return err + } + + return err +} diff --git a/pkg/migrations/sqlite3/20220520140707_kline_unique_idx.go b/pkg/migrations/sqlite3/20220520140707_kline_unique_idx.go new file mode 100644 index 0000000000..605187154f --- /dev/null +++ b/pkg/migrations/sqlite3/20220520140707_kline_unique_idx.go @@ -0,0 +1,74 @@ +package sqlite3 + +import ( + "context" + + "github.com/c9s/rockhopper" +) + +func init() { + AddMigration(upKlineUniqueIdx, downKlineUniqueIdx) + +} + +func upKlineUniqueIdx(ctx context.Context, tx rockhopper.SQLExecutor) (err error) { + // This code is executed when the migration is applied. + + _, err = tx.ExecContext(ctx, "CREATE UNIQUE INDEX idx_kline_binance_unique\n ON binance_klines (`symbol`, `interval`, `start_time`);") + if err != nil { + return err + } + + _, err = tx.ExecContext(ctx, "CREATE UNIQUE INDEX idx_kline_max_unique\n ON max_klines (`symbol`, `interval`, `start_time`);") + if err != nil { + return err + } + + _, err = tx.ExecContext(ctx, "CREATE UNIQUE INDEX `idx_kline_ftx_unique`\n ON ftx_klines (`symbol`, `interval`, `start_time`);") + if err != nil { + return err + } + + _, err = tx.ExecContext(ctx, "CREATE UNIQUE INDEX `idx_kline_kucoin_unique`\n ON kucoin_klines (`symbol`, `interval`, `start_time`);") + if err != nil { + return err + } + + _, err = tx.ExecContext(ctx, "CREATE UNIQUE INDEX `idx_kline_okex_unique`\n ON okex_klines (`symbol`, `interval`, `start_time`);") + if err != nil { + return err + } + + return err +} + +func downKlineUniqueIdx(ctx context.Context, tx rockhopper.SQLExecutor) (err error) { + // This code is executed when the migration is rolled back. + + _, err = tx.ExecContext(ctx, "DROP INDEX `idx_kline_ftx_unique` ON `ftx_klines`;") + if err != nil { + return err + } + + _, err = tx.ExecContext(ctx, "DROP INDEX `idx_kline_max_unique` ON `max_klines`;") + if err != nil { + return err + } + + _, err = tx.ExecContext(ctx, "DROP INDEX `idx_kline_binance_unique` ON `binance_klines`;") + if err != nil { + return err + } + + _, err = tx.ExecContext(ctx, "DROP INDEX `idx_kline_kucoin_unique` ON `kucoin_klines`;") + if err != nil { + return err + } + + _, err = tx.ExecContext(ctx, "DROP INDEX `idx_kline_okex_unique` ON `okex_klines`;") + if err != nil { + return err + } + + return err +} diff --git a/pkg/migrations/sqlite3/20220531012226_margin_loans.go b/pkg/migrations/sqlite3/20220531012226_margin_loans.go new file mode 100644 index 0000000000..25bfc68b98 --- /dev/null +++ b/pkg/migrations/sqlite3/20220531012226_margin_loans.go @@ -0,0 +1,34 @@ +package sqlite3 + +import ( + "context" + + "github.com/c9s/rockhopper" +) + +func init() { + AddMigration(upMarginLoans, downMarginLoans) + +} + +func upMarginLoans(ctx context.Context, tx rockhopper.SQLExecutor) (err error) { + // This code is executed when the migration is applied. + + _, err = tx.ExecContext(ctx, "CREATE TABLE `margin_loans`\n(\n `gid` INTEGER PRIMARY KEY AUTOINCREMENT,\n `transaction_id` INTEGER NOT NULL,\n `exchange` VARCHAR(24) NOT NULL DEFAULT '',\n `asset` VARCHAR(24) NOT NULL DEFAULT '',\n `isolated_symbol` VARCHAR(24) NOT NULL DEFAULT '',\n -- quantity is the quantity of the trade that makes profit\n `principle` DECIMAL(16, 8) NOT NULL,\n `time` DATETIME(3) NOT NULL\n);") + if err != nil { + return err + } + + return err +} + +func downMarginLoans(ctx context.Context, tx rockhopper.SQLExecutor) (err error) { + // This code is executed when the migration is rolled back. + + _, err = tx.ExecContext(ctx, "DROP TABLE IF EXISTS `margin_loans`;") + if err != nil { + return err + } + + return err +} diff --git a/pkg/migrations/sqlite3/20220531013327_margin_repays.go b/pkg/migrations/sqlite3/20220531013327_margin_repays.go new file mode 100644 index 0000000000..d915643220 --- /dev/null +++ b/pkg/migrations/sqlite3/20220531013327_margin_repays.go @@ -0,0 +1,34 @@ +package sqlite3 + +import ( + "context" + + "github.com/c9s/rockhopper" +) + +func init() { + AddMigration(upMarginRepays, downMarginRepays) + +} + +func upMarginRepays(ctx context.Context, tx rockhopper.SQLExecutor) (err error) { + // This code is executed when the migration is applied. + + _, err = tx.ExecContext(ctx, "CREATE TABLE `margin_repays`\n(\n `gid` INTEGER PRIMARY KEY AUTOINCREMENT,\n `transaction_id` INTEGER NOT NULL,\n `exchange` VARCHAR(24) NOT NULL DEFAULT '',\n `asset` VARCHAR(24) NOT NULL DEFAULT '',\n `isolated_symbol` VARCHAR(24) NOT NULL DEFAULT '',\n -- quantity is the quantity of the trade that makes profit\n `principle` DECIMAL(16, 8) NOT NULL,\n `time` DATETIME(3) NOT NULL\n);") + if err != nil { + return err + } + + return err +} + +func downMarginRepays(ctx context.Context, tx rockhopper.SQLExecutor) (err error) { + // This code is executed when the migration is rolled back. + + _, err = tx.ExecContext(ctx, "DROP TABLE IF EXISTS `margin_repays`;") + if err != nil { + return err + } + + return err +} diff --git a/pkg/migrations/sqlite3/20220531013541_margin_interests.go b/pkg/migrations/sqlite3/20220531013541_margin_interests.go new file mode 100644 index 0000000000..0c06a2ce62 --- /dev/null +++ b/pkg/migrations/sqlite3/20220531013541_margin_interests.go @@ -0,0 +1,34 @@ +package sqlite3 + +import ( + "context" + + "github.com/c9s/rockhopper" +) + +func init() { + AddMigration(upMarginInterests, downMarginInterests) + +} + +func upMarginInterests(ctx context.Context, tx rockhopper.SQLExecutor) (err error) { + // This code is executed when the migration is applied. + + _, err = tx.ExecContext(ctx, "CREATE TABLE `margin_interests`\n(\n `gid` INTEGER PRIMARY KEY AUTOINCREMENT,\n `exchange` VARCHAR(24) NOT NULL DEFAULT '',\n `asset` VARCHAR(24) NOT NULL DEFAULT '',\n `isolated_symbol` VARCHAR(24) NOT NULL DEFAULT '',\n `principle` DECIMAL(16, 8) NOT NULL,\n `interest` DECIMAL(20, 16) NOT NULL,\n `interest_rate` DECIMAL(20, 16) NOT NULL,\n `time` DATETIME(3) NOT NULL\n);") + if err != nil { + return err + } + + return err +} + +func downMarginInterests(ctx context.Context, tx rockhopper.SQLExecutor) (err error) { + // This code is executed when the migration is rolled back. + + _, err = tx.ExecContext(ctx, "DROP TABLE IF EXISTS `margin_interests`;") + if err != nil { + return err + } + + return err +} diff --git a/pkg/migrations/sqlite3/20220531015005_margin_liquidations.go b/pkg/migrations/sqlite3/20220531015005_margin_liquidations.go new file mode 100644 index 0000000000..5f1e07f76e --- /dev/null +++ b/pkg/migrations/sqlite3/20220531015005_margin_liquidations.go @@ -0,0 +1,34 @@ +package sqlite3 + +import ( + "context" + + "github.com/c9s/rockhopper" +) + +func init() { + AddMigration(upMarginLiquidations, downMarginLiquidations) + +} + +func upMarginLiquidations(ctx context.Context, tx rockhopper.SQLExecutor) (err error) { + // This code is executed when the migration is applied. + + _, err = tx.ExecContext(ctx, "CREATE TABLE `margin_liquidations`\n(\n `gid` INTEGER PRIMARY KEY AUTOINCREMENT,\n `exchange` VARCHAR(24) NOT NULL DEFAULT '',\n `symbol` VARCHAR(24) NOT NULL DEFAULT '',\n `order_id` INTEGER NOT NULL,\n `is_isolated` BOOL NOT NULL DEFAULT false,\n `average_price` DECIMAL(16, 8) NOT NULL,\n `price` DECIMAL(16, 8) NOT NULL,\n `quantity` DECIMAL(16, 8) NOT NULL,\n `executed_quantity` DECIMAL(16, 8) NOT NULL,\n `side` VARCHAR(5) NOT NULL DEFAULT '',\n `time_in_force` VARCHAR(5) NOT NULL DEFAULT '',\n `time` DATETIME(3) NOT NULL\n);") + if err != nil { + return err + } + + return err +} + +func downMarginLiquidations(ctx context.Context, tx rockhopper.SQLExecutor) (err error) { + // This code is executed when the migration is rolled back. + + _, err = tx.ExecContext(ctx, "DROP TABLE IF EXISTS `margin_liquidations`;") + if err != nil { + return err + } + + return err +} diff --git a/pkg/service/websocket.go b/pkg/net/websocketbase/client.go similarity index 93% rename from pkg/service/websocket.go rename to pkg/net/websocketbase/client.go index 45cbfde91a..0754777f15 100644 --- a/pkg/service/websocket.go +++ b/pkg/net/websocketbase/client.go @@ -1,4 +1,4 @@ -package service +package websocketbase import ( "context" @@ -8,6 +8,8 @@ import ( "github.com/gorilla/websocket" ) +// WebsocketClientBase is a legacy base client +// Deprecated: please use standard stream instead. //go:generate callbackgen -type WebsocketClientBase type WebsocketClientBase struct { baseURL string diff --git a/pkg/service/websocketclientbase_callbacks.go b/pkg/net/websocketbase/websocketclientbase_callbacks.go similarity index 98% rename from pkg/service/websocketclientbase_callbacks.go rename to pkg/net/websocketbase/websocketclientbase_callbacks.go index b7afc5a0d6..4445357854 100644 --- a/pkg/service/websocketclientbase_callbacks.go +++ b/pkg/net/websocketbase/websocketclientbase_callbacks.go @@ -1,6 +1,6 @@ // Code generated by "callbackgen -type WebsocketClientBase"; DO NOT EDIT. -package service +package websocketbase import ( "github.com/gorilla/websocket" diff --git a/pkg/notifier/slacknotifier/slack.go b/pkg/notifier/slacknotifier/slack.go index 69e2292574..4b775532f6 100644 --- a/pkg/notifier/slacknotifier/slack.go +++ b/pkg/notifier/slacknotifier/slack.go @@ -1,6 +1,7 @@ package slacknotifier import ( + "bytes" "context" "fmt" "time" @@ -150,33 +151,10 @@ func (n *Notifier) NotifyTo(channel string, obj interface{}, args ...interface{} } } -/* -func (n *Notifier) NotifyTrade(trade *types.Trade) { - _, _, err := n.client.PostMessageContext(context.Background(), n.TradeChannel, - slack.MsgOptionText(util.Render(`:handshake: {{ .Symbol }} {{ .Side }} Trade Execution @ {{ .Price }}`, trade), true), - slack.MsgOptionAttachments(trade.SlackAttachment())) - - if err != nil { - logrus.WithError(err).Error("slack send error") - } +func (n *Notifier) SendPhoto(buffer *bytes.Buffer) { + n.SendPhotoTo(n.channel, buffer) } -*/ - -/* -func (n *Notifier) NotifyPnL(report *pnl.AverageCostPnlReport) { - attachment := report.SlackAttachment() - - _, _, err := n.client.PostMessageContext(context.Background(), n.PnlChannel, - slack.MsgOptionText(util.Render( - `:heavy_dollar_sign: Here is your *{{ .symbol }}* PnL report collected since *{{ .startTime }}*`, - map[string]interface{}{ - "symbol": report.Symbol, - "startTime": report.StartTime.Format(time.RFC822), - }), true), - slack.MsgOptionAttachments(attachment)) - - if err != nil { - logrus.WithError(err).Errorf("slack send error") - } + +func (n *Notifier) SendPhotoTo(channel string, buffer *bytes.Buffer) { + // TODO } -*/ diff --git a/pkg/notifier/telegramnotifier/logrus_look.go b/pkg/notifier/telegramnotifier/logrus_look.go new file mode 100644 index 0000000000..dfe92479cb --- /dev/null +++ b/pkg/notifier/telegramnotifier/logrus_look.go @@ -0,0 +1,45 @@ +package telegramnotifier + +import ( + "fmt" + "time" + + "github.com/sirupsen/logrus" + "golang.org/x/time/rate" +) + +var limiter = rate.NewLimiter(rate.Every(time.Minute), 3) + +type LogHook struct { + notifier *Notifier +} + +func NewLogHook(notifier *Notifier) *LogHook { + return &LogHook{ + notifier: notifier, + } +} + +func (t *LogHook) Levels() []logrus.Level { + return []logrus.Level{ + logrus.ErrorLevel, + logrus.FatalLevel, + logrus.PanicLevel, + } +} + +func (t *LogHook) Fire(e *logrus.Entry) error { + if !limiter.Allow() { + return nil + } + + var message = fmt.Sprintf("[%s] %s", e.Level.String(), e.Message) + if errData, ok := e.Data[logrus.ErrorKey]; ok && errData != nil { + if err, isErr := errData.(error); isErr { + message += " Error: " + err.Error() + } + } + + t.notifier.Notify(message) + return nil +} diff --git a/pkg/notifier/telegramnotifier/telegram.go b/pkg/notifier/telegramnotifier/telegram.go index 07a11c97de..e2018b4ca2 100644 --- a/pkg/notifier/telegramnotifier/telegram.go +++ b/pkg/notifier/telegramnotifier/telegram.go @@ -1,18 +1,30 @@ package telegramnotifier import ( + "bytes" + "context" "fmt" + "reflect" "strconv" "time" "github.com/sirupsen/logrus" + "golang.org/x/time/rate" "gopkg.in/tucnak/telebot.v2" "github.com/c9s/bbgo/pkg/types" ) +var apiLimiter = rate.NewLimiter(rate.Every(1*time.Second), 1) + var log = logrus.WithField("service", "telegram") +type notifyTask struct { + message string + texts []string + photoBuffer *bytes.Buffer +} + type Notifier struct { bot *telebot.Bot @@ -23,6 +35,8 @@ type Notifier struct { Chats map[int64]*telebot.Chat `json:"chats"` broadcast bool + + taskC chan notifyTask } type Option func(notifier *Notifier) @@ -33,21 +47,90 @@ func UseBroadcast() Option { } } -// New +// New returns a telegram notifier instance func New(bot *telebot.Bot, options ...Option) *Notifier { notifier := &Notifier{ bot: bot, Chats: make(map[int64]*telebot.Chat), Subscribers: make(map[int64]time.Time), + taskC: make(chan notifyTask, 100), } for _, o := range options { o(notifier) } + go notifier.worker() + return notifier } +func (n *Notifier) worker() { + ctx := context.Background() + for { + select { + case <-ctx.Done(): + return + case task := <-n.taskC: + apiLimiter.Wait(ctx) + n.consume(task) + } + } +} + +func (n *Notifier) consume(task notifyTask) { + if n.broadcast { + if n.Subscribers == nil { + return + } + if task.message != "" { + n.Broadcast(task.message) + } + for _, text := range task.texts { + n.Broadcast(text) + } + if task.photoBuffer == nil { + return + } + + for chatID := range n.Subscribers { + chat, err := n.bot.ChatByID(strconv.FormatInt(chatID, 10)) + if err != nil { + log.WithError(err).Error("can not get chat by ID") + continue + } + album := telebot.Album{ + photoFromBuffer(task.photoBuffer), + } + if _, err := n.bot.SendAlbum(chat, album); err != nil { + log.WithError(err).Error("failed to send message") + } + } + } else if n.Chats != nil { + for _, chat := range n.Chats { + if task.message != "" { + if _, err := n.bot.Send(chat, task.message); err != nil { + log.WithError(err).Error("telegram send error") + } + } + + for _, text := range task.texts { + if _, err := n.bot.Send(chat, text); err != nil { + log.WithError(err).Error("telegram send error") + } + } + if task.photoBuffer != nil { + album := telebot.Album{ + photoFromBuffer(task.photoBuffer), + } + if _, err := n.bot.SendAlbum(chat, album); err != nil { + log.WithError(err).Error("telegram send error") + } + } + } + } +} + func (n *Notifier) Notify(obj interface{}, args ...interface{}) { n.NotifyTo("", obj, args...) } @@ -55,18 +138,27 @@ func (n *Notifier) Notify(obj interface{}, args ...interface{}) { func filterPlaintextMessages(args []interface{}) (texts []string, pureArgs []interface{}) { var firstObjectOffset = -1 for idx, arg := range args { - switch a := arg.(type) { + rt := reflect.TypeOf(arg) + if rt.Kind() == reflect.Ptr { + switch a := arg.(type) { + + case nil: + texts = append(texts, "nil") + if firstObjectOffset == -1 { + firstObjectOffset = idx + } - case types.PlainText: - texts = append(texts, a.PlainText()) - if firstObjectOffset == -1 { - firstObjectOffset = idx - } + case types.PlainText: + texts = append(texts, a.PlainText()) + if firstObjectOffset == -1 { + firstObjectOffset = idx + } - case types.Stringer: - texts = append(texts, a.String()) - if firstObjectOffset == -1 { - firstObjectOffset = idx + case types.Stringer: + texts = append(texts, a.String()) + if firstObjectOffset == -1 { + firstObjectOffset = idx + } } } } @@ -99,23 +191,34 @@ func (n *Notifier) NotifyTo(channel string, obj interface{}, args ...interface{} } - if n.broadcast { - n.Broadcast(message) - for _, text := range texts { - n.Broadcast(text) - } - } else if n.Chats != nil { - for _, chat := range n.Chats { - if _, err := n.bot.Send(chat, message); err != nil { - log.WithError(err).Error("telegram send error") - } + select { + case n.taskC <- notifyTask{ + texts: texts, + message: message, + }: + default: + log.Error("[telegram] cannot send task to notify") + } +} - for _, text := range texts { - if _, err := n.bot.Send(chat, text); err != nil { - log.WithError(err).Error("telegram send error") - } - } - } +func (n *Notifier) SendPhoto(buffer *bytes.Buffer) { + n.SendPhotoTo("", buffer) +} + +func photoFromBuffer(buffer *bytes.Buffer) telebot.InputMedia { + reader := bytes.NewReader(buffer.Bytes()) + return &telebot.Photo{ + File: telebot.FromReader(reader), + } +} + +func (n *Notifier) SendPhotoTo(channel string, buffer *bytes.Buffer) { + select { + case n.taskC <- notifyTask{ + photoBuffer: buffer, + }: + case <-time.After(50 * time.Millisecond): + return } } diff --git a/pkg/optimizer/config.go b/pkg/optimizer/config.go new file mode 100644 index 0000000000..a9e03b3786 --- /dev/null +++ b/pkg/optimizer/config.go @@ -0,0 +1,105 @@ +package optimizer + +import ( + "fmt" + "io/ioutil" + "strings" + + "gopkg.in/yaml.v3" + + "github.com/c9s/bbgo/pkg/fixedpoint" +) + +const ( + selectorTypeRange = "range" // deprecated: replaced by selectorTypeRangeFloat + selectorTypeRangeFloat = "rangeFloat" + selectorTypeRangeInt = "rangeInt" + selectorTypeIterate = "iterate" // deprecated: replaced by selectorTypeString + selectorTypeString = "string" + selectorTypeBool = "bool" +) + +type SelectorConfig struct { + Type string `json:"type" yaml:"type"` + Label string `json:"label,omitempty" yaml:"label,omitempty"` + Path string `json:"path" yaml:"path"` + Values []string `json:"values,omitempty" yaml:"values,omitempty"` + Min fixedpoint.Value `json:"min,omitempty" yaml:"min,omitempty"` + Max fixedpoint.Value `json:"max,omitempty" yaml:"max,omitempty"` + Step fixedpoint.Value `json:"step,omitempty" yaml:"step,omitempty"` +} + +type LocalExecutorConfig struct { + MaxNumberOfProcesses int `json:"maxNumberOfProcesses" yaml:"maxNumberOfProcesses"` +} + +type ExecutorConfig struct { + Type string `json:"type" yaml:"type"` + LocalExecutorConfig *LocalExecutorConfig `json:"local" yaml:"local"` +} + +type Config struct { + Executor *ExecutorConfig `json:"executor" yaml:"executor"` + MaxThread int `yaml:"maxThread,omitempty"` + Matrix []SelectorConfig `yaml:"matrix"` + Algorithm string `yaml:"algorithm,omitempty"` + Objective string `yaml:"objectiveBy,omitempty"` + MaxEvaluation int `yaml:"maxEvaluation"` +} + +var defaultExecutorConfig = &ExecutorConfig{ + Type: "local", + LocalExecutorConfig: defaultLocalExecutorConfig, +} + +var defaultLocalExecutorConfig = &LocalExecutorConfig{ + MaxNumberOfProcesses: 10, +} + +func LoadConfig(yamlConfigFileName string) (*Config, error) { + configYaml, err := ioutil.ReadFile(yamlConfigFileName) + if err != nil { + return nil, err + } + + var optConfig Config + if err := yaml.Unmarshal(configYaml, &optConfig); err != nil { + return nil, err + } + + switch alg := strings.ToLower(optConfig.Algorithm); alg { + case "", "default": + optConfig.Algorithm = HpOptimizerAlgorithmTPE + case HpOptimizerAlgorithmTPE, HpOptimizerAlgorithmCMAES, HpOptimizerAlgorithmSOBOL, HpOptimizerAlgorithmRandom: + optConfig.Algorithm = alg + default: + return nil, fmt.Errorf(`unknown algorithm "%s"`, optConfig.Algorithm) + } + + switch objective := strings.ToLower(optConfig.Objective); objective { + case "", "default": + optConfig.Objective = HpOptimizerObjectiveEquity + case HpOptimizerObjectiveEquity, HpOptimizerObjectiveProfit, HpOptimizerObjectiveVolume: + optConfig.Objective = objective + default: + return nil, fmt.Errorf(`unknown objective "%s"`, optConfig.Objective) + } + + if optConfig.MaxEvaluation <= 0 { + optConfig.MaxEvaluation = 100 + } + + if optConfig.Executor == nil { + optConfig.Executor = defaultExecutorConfig + } + + if optConfig.Executor.Type == "" { + optConfig.Executor.Type = "local" + } + + if optConfig.Executor.Type == "local" && optConfig.Executor.LocalExecutorConfig == nil { + optConfig.Executor.LocalExecutorConfig = defaultLocalExecutorConfig + } + + return &optConfig, nil +} diff --git a/pkg/optimizer/format.go b/pkg/optimizer/format.go new file mode 100644 index 0000000000..3822c0f307 --- /dev/null +++ b/pkg/optimizer/format.go @@ -0,0 +1,142 @@ +package optimizer + +import ( + "fmt" + "github.com/c9s/bbgo/pkg/data/tsv" + "github.com/c9s/bbgo/pkg/fixedpoint" + "io" + "strconv" +) + +func FormatResultsTsv(writer io.WriteCloser, labelPaths map[string]string, results []*HyperparameterOptimizeTrialResult) error { + headerLen := len(labelPaths) + headers := make([]string, 0, headerLen) + for label := range labelPaths { + headers = append(headers, label) + } + + rows := make([][]interface{}, len(labelPaths)) + for ri, result := range results { + row := make([]interface{}, headerLen) + for ci, columnKey := range headers { + var ok bool + if row[ci], ok = result.Parameters[columnKey]; !ok { + return fmt.Errorf(`missing parameter "%s" from trial result (%v)`, columnKey, result.Parameters) + } + } + rows[ri] = row + } + + w := tsv.NewWriter(writer) + if err := w.Write(headers); err != nil { + return err + } + + for _, row := range rows { + var cells []string + for _, o := range row { + cell, err := castCellValue(o) + if err != nil { + return err + } + cells = append(cells, cell) + } + + if err := w.Write(cells); err != nil { + return err + } + } + return w.Close() +} + +func FormatMetricsTsv(writer io.WriteCloser, metrics map[string][]Metric) error { + headers, rows := transformMetricsToRows(metrics) + w := tsv.NewWriter(writer) + if err := w.Write(headers); err != nil { + return err + } + + for _, row := range rows { + var cells []string + for _, o := range row { + cell, err := castCellValue(o) + if err != nil { + return err + } + cells = append(cells, cell) + } + + if err := w.Write(cells); err != nil { + return err + } + } + return w.Close() +} + +func transformMetricsToRows(metrics map[string][]Metric) (headers []string, rows [][]interface{}) { + var metricsKeys []string + for k := range metrics { + metricsKeys = append(metricsKeys, k) + } + + var numEntries int + var paramLabels []string + for _, ms := range metrics { + for _, m := range ms { + paramLabels = m.Labels + break + } + + numEntries = len(ms) + break + } + + headers = append(paramLabels, metricsKeys...) + rows = make([][]interface{}, numEntries) + + var metricsRows = make([][]interface{}, numEntries) + + // build params into the rows + for i, m := range metrics[metricsKeys[0]] { + rows[i] = m.Params + } + + for _, metricKey := range metricsKeys { + for i, ms := range metrics[metricKey] { + if len(metricsRows[i]) == 0 { + metricsRows[i] = make([]interface{}, 0, len(metricsKeys)) + } + metricsRows[i] = append(metricsRows[i], ms.Value) + } + } + + // merge rows + for i := range rows { + rows[i] = append(rows[i], metricsRows[i]...) + } + + return headers, rows +} + +func castCellValue(a interface{}) (string, error) { + switch tv := a.(type) { + case fixedpoint.Value: + return tv.String(), nil + case float64: + return strconv.FormatFloat(tv, 'f', -1, 64), nil + case int64: + return strconv.FormatInt(tv, 10), nil + case int32: + return strconv.FormatInt(int64(tv), 10), nil + case int: + return strconv.Itoa(tv), nil + case bool: + return strconv.FormatBool(tv), nil + case string: + return tv, nil + case []byte: + return string(tv), nil + default: + return "", fmt.Errorf("unsupported object type: %T value: %v", tv, tv) + } +} diff --git a/pkg/optimizer/grid.go b/pkg/optimizer/grid.go new file mode 100644 index 0000000000..eab8352f7a --- /dev/null +++ b/pkg/optimizer/grid.go @@ -0,0 +1,310 @@ +package optimizer + +import ( + "context" + "encoding/json" + "fmt" + "sort" + + "github.com/cheggaaa/pb/v3" + + jsonpatch "github.com/evanphx/json-patch/v5" + + "github.com/c9s/bbgo/pkg/backtest" + "github.com/c9s/bbgo/pkg/fixedpoint" +) + +type MetricValueFunc func(summaryReport *backtest.SummaryReport) fixedpoint.Value + +var TotalProfitMetricValueFunc = func(summaryReport *backtest.SummaryReport) fixedpoint.Value { + return summaryReport.TotalProfit +} + +var TotalVolume = func(summaryReport *backtest.SummaryReport) fixedpoint.Value { + if len(summaryReport.SymbolReports) == 0 { + return fixedpoint.Zero + } + + buyVolume := summaryReport.SymbolReports[0].PnL.BuyVolume + sellVolume := summaryReport.SymbolReports[0].PnL.SellVolume + return buyVolume.Add(sellVolume) +} + +var TotalEquityDiff = func(summaryReport *backtest.SummaryReport) fixedpoint.Value { + if len(summaryReport.SymbolReports) == 0 { + return fixedpoint.Zero + } + + initEquity := summaryReport.InitialEquityValue + finalEquity := summaryReport.FinalEquityValue + return finalEquity.Sub(initEquity) +} + +type Metric struct { + // Labels is the labels of the given parameters + Labels []string `json:"labels,omitempty"` + + // Params is the parameters used to output the metrics result + Params []interface{} `json:"params,omitempty"` + + // Key is the metric name + Key string `json:"key"` + + // Value is the metric value of the metric + Value fixedpoint.Value `json:"value,omitempty"` +} + +func copyParams(params []interface{}) []interface{} { + var c = make([]interface{}, len(params)) + copy(c, params) + return c +} + +func copyLabels(labels []string) []string { + var c = make([]string, len(labels)) + copy(c, labels) + return c +} + +type GridOptimizer struct { + Config *Config + + ParamLabels []string + CurrentParams []interface{} +} + +func (o *GridOptimizer) buildOps() []OpFunc { + var ops []OpFunc + + o.CurrentParams = make([]interface{}, len(o.Config.Matrix)) + o.ParamLabels = make([]string, len(o.Config.Matrix)) + + for i, selector := range o.Config.Matrix { + var path = selector.Path + var ii = i // copy variable because we need to use them in the closure + + if selector.Label != "" { + o.ParamLabels[ii] = selector.Label + } else { + o.ParamLabels[ii] = selector.Path + } + + switch selector.Type { + case selectorTypeRange, selectorTypeRangeFloat, selectorTypeRangeInt: + min := selector.Min + max := selector.Max + step := selector.Step + if step.IsZero() { + step = fixedpoint.One + } + + var values []fixedpoint.Value + for val := min; val.Compare(max) <= 0; val = val.Add(step) { + values = append(values, val) + } + + f := func(configJson []byte, next func(configJson []byte) error) error { + for _, val := range values { + jsonOp := []byte(reformatJson(fmt.Sprintf(`[{"op": "replace", "path": "%s", "value": %v }]`, path, val))) + patch, err := jsonpatch.DecodePatch(jsonOp) + if err != nil { + return err + } + + log.Debugf("json op: %s", jsonOp) + + patchedJson, err := patch.ApplyIndent(configJson, " ") + if err != nil { + return err + } + + valCopy := val + o.CurrentParams[ii] = valCopy + if err := next(patchedJson); err != nil { + return err + } + } + + return nil + } + ops = append(ops, f) + + case selectorTypeIterate, selectorTypeString: + values := selector.Values + f := func(configJson []byte, next func(configJson []byte) error) error { + for _, val := range values { + log.Debugf("%d %s: %v of %v", ii, path, val, values) + + jsonOp := []byte(reformatJson(fmt.Sprintf(`[{"op": "replace", "path": "%s", "value": "%s"}]`, path, val))) + patch, err := jsonpatch.DecodePatch(jsonOp) + if err != nil { + return err + } + + log.Debugf("json op: %s", jsonOp) + + patchedJson, err := patch.ApplyIndent(configJson, " ") + if err != nil { + return err + } + + valCopy := val + o.CurrentParams[ii] = valCopy + if err := next(patchedJson); err != nil { + return err + } + } + + return nil + } + ops = append(ops, f) + case selectorTypeBool: + values := []bool{true, false} + f := func(configJson []byte, next func(configJson []byte) error) error { + for _, val := range values { + log.Debugf("%d %s: %v of %v", ii, path, val, values) + + jsonOp := []byte(reformatJson(fmt.Sprintf(`[{"op": "replace", "path": "%s", "value": %v}]`, path, val))) + patch, err := jsonpatch.DecodePatch(jsonOp) + if err != nil { + return err + } + + log.Debugf("json op: %s", jsonOp) + + patchedJson, err := patch.ApplyIndent(configJson, " ") + if err != nil { + return err + } + + valCopy := val + o.CurrentParams[ii] = valCopy + if err := next(patchedJson); err != nil { + return err + } + } + + return nil + } + ops = append(ops, f) + } + } + return ops +} + +func (o *GridOptimizer) Run(executor Executor, configJson []byte) (map[string][]Metric, error) { + o.CurrentParams = make([]interface{}, len(o.Config.Matrix)) + + var valueFunctions = map[string]MetricValueFunc{ + "totalProfit": TotalProfitMetricValueFunc, + "totalVolume": TotalVolume, + "totalEquityDiff": TotalEquityDiff, + } + var metrics = map[string][]Metric{} + + var ops = o.buildOps() + + var taskC = make(chan BacktestTask, 10000) + + var taskCnt = 0 + var app = func(configJson []byte, next func(configJson []byte) error) error { + var labels = copyLabels(o.ParamLabels) + var params = copyParams(o.CurrentParams) + taskC <- BacktestTask{ + ConfigJson: configJson, + Params: params, + Labels: labels, + } + return nil + } + var appCnt = func(configJson []byte, next func(configJson []byte) error) error { + taskCnt++ + return nil + } + + log.Debugf("build %d ops", len(ops)) + + var wrapper = func(configJson []byte) error { + return app(configJson, nil) + } + var wrapperCnt = func(configJson []byte) error { + return appCnt(configJson, nil) + } + + for i := len(ops) - 1; i >= 0; i-- { + cur := ops[i] + inner := wrapper + innerCnt := wrapperCnt + wrapper = func(configJson []byte) error { + return cur(configJson, inner) + } + wrapperCnt = func(configJson []byte) error { + return cur(configJson, innerCnt) + } + } + + if err := wrapperCnt(configJson); err != nil { + return nil, err + } + var bar = pb.Full.New(taskCnt) + bar.SetTemplateString(`{{ string . "log" | green}} | {{counters . }} {{bar . }} {{percent . }} {{etime . }} {{rtime . "ETA %s"}}`) + + ctx := context.Background() + var taskGenErr error + go func() { + taskGenErr = wrapper(configJson) + close(taskC) // this will shut down the executor + }() + + resultsC, err := executor.Run(ctx, taskC, bar) + if err != nil { + return nil, err + } + + for result := range resultsC { + bar.Increment() + + if result.Report == nil { + log.Errorf("no summaryReport found for params: %+v", result.Params) + continue + } + + for metricKey, metricFunc := range valueFunctions { + var metricValue = metricFunc(result.Report) + bar.Set("log", fmt.Sprintf("params: %+v => %s %+v", result.Params, metricKey, metricValue)) + + metrics[metricKey] = append(metrics[metricKey], Metric{ + Params: result.Params, + Labels: result.Labels, + Key: metricKey, + Value: metricValue, + }) + } + } + bar.Finish() + + for n := range metrics { + sort.Slice(metrics[n], func(i, j int) bool { + a := metrics[n][i].Value + b := metrics[n][j].Value + return a.Compare(b) > 0 + }) + } + + if taskGenErr != nil { + return metrics, taskGenErr + } else { + return metrics, err + } +} + +func reformatJson(text string) string { + var a interface{} + var err = json.Unmarshal([]byte(text), &a) + if err != nil { + return "{invalid json}" + } + + out, _ := json.MarshalIndent(a, "", " ") + return string(out) +} diff --git a/pkg/optimizer/hpoptimizer.go b/pkg/optimizer/hpoptimizer.go new file mode 100644 index 0000000000..ecf806ac43 --- /dev/null +++ b/pkg/optimizer/hpoptimizer.go @@ -0,0 +1,297 @@ +package optimizer + +import ( + "context" + "fmt" + "github.com/c-bata/goptuna" + goptunaCMAES "github.com/c-bata/goptuna/cmaes" + goptunaSOBOL "github.com/c-bata/goptuna/sobol" + goptunaTPE "github.com/c-bata/goptuna/tpe" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/cheggaaa/pb/v3" + "github.com/sirupsen/logrus" + "golang.org/x/sync/errgroup" + "math" + "sync" +) + +const ( + // HpOptimizerObjectiveEquity optimize the parameters to maximize equity gain + HpOptimizerObjectiveEquity = "equity" + // HpOptimizerObjectiveProfit optimize the parameters to maximize trading profit + HpOptimizerObjectiveProfit = "profit" + // HpOptimizerObjectiveVolume optimize the parameters to maximize trading volume + HpOptimizerObjectiveVolume = "volume" +) + +const ( + // HpOptimizerAlgorithmTPE is the implementation of Tree-structured Parzen Estimators + HpOptimizerAlgorithmTPE = "tpe" + // HpOptimizerAlgorithmCMAES is the implementation Covariance Matrix Adaptation Evolution Strategy + HpOptimizerAlgorithmCMAES = "cmaes" + // HpOptimizerAlgorithmSOBOL is the implementation Quasi-monte carlo sampling based on Sobol sequence + HpOptimizerAlgorithmSOBOL = "sobol" + // HpOptimizerAlgorithmRandom is the implementation random search + HpOptimizerAlgorithmRandom = "random" +) + +type HyperparameterOptimizeTrialResult struct { + Value fixedpoint.Value `json:"value"` + Parameters map[string]interface{} `json:"parameters"` + ID *int `json:"id,omitempty"` + State string `json:"state,omitempty"` +} + +type HyperparameterOptimizeReport struct { + Name string `json:"studyName"` + Objective string `json:"objective"` + Parameters map[string]string `json:"domains"` + Best *HyperparameterOptimizeTrialResult `json:"best"` + Trials []*HyperparameterOptimizeTrialResult `json:"trials,omitempty"` +} + +func buildBestHyperparameterOptimizeResult(study *goptuna.Study) *HyperparameterOptimizeTrialResult { + val, _ := study.GetBestValue() + params, _ := study.GetBestParams() + return &HyperparameterOptimizeTrialResult{ + Value: fixedpoint.NewFromFloat(val), + Parameters: params, + } +} + +func buildHyperparameterOptimizeTrialResults(study *goptuna.Study) []*HyperparameterOptimizeTrialResult { + trials, _ := study.GetTrials() + results := make([]*HyperparameterOptimizeTrialResult, len(trials)) + for i, trial := range trials { + trialId := trial.ID + trialResult := &HyperparameterOptimizeTrialResult{ + ID: &trialId, + Value: fixedpoint.NewFromFloat(trial.Value), + Parameters: trial.Params, + } + results[i] = trialResult + } + return results +} + +type HyperparameterOptimizer struct { + SessionName string + Config *Config + + // Workaround for goptuna/tpe parameter suggestion. Remove this after fixed. + // ref: https://github.com/c-bata/goptuna/issues/236 + paramSuggestionLock sync.Mutex +} + +func (o *HyperparameterOptimizer) buildStudy(trialFinishChan chan goptuna.FrozenTrial) (*goptuna.Study, error) { + var studyOpts = make([]goptuna.StudyOption, 0, 2) + + // maximum the profit, volume, equity gain, ...etc + studyOpts = append(studyOpts, goptuna.StudyOptionDirection(goptuna.StudyDirectionMaximize)) + + // disable search log and collect trial progress + studyOpts = append(studyOpts, goptuna.StudyOptionLogger(nil)) + studyOpts = append(studyOpts, goptuna.StudyOptionTrialNotifyChannel(trialFinishChan)) + + // the search algorithm + var sampler goptuna.Sampler = nil + var relativeSampler goptuna.RelativeSampler = nil + switch o.Config.Algorithm { + case HpOptimizerAlgorithmRandom: + sampler = goptuna.NewRandomSampler() + case HpOptimizerAlgorithmTPE: + sampler = goptunaTPE.NewSampler() + case HpOptimizerAlgorithmCMAES: + relativeSampler = goptunaCMAES.NewSampler(goptunaCMAES.SamplerOptionNStartupTrials(5)) + case HpOptimizerAlgorithmSOBOL: + relativeSampler = goptunaSOBOL.NewSampler() + } + if sampler != nil { + studyOpts = append(studyOpts, goptuna.StudyOptionSampler(sampler)) + } else { + studyOpts = append(studyOpts, goptuna.StudyOptionRelativeSampler(relativeSampler)) + } + + return goptuna.CreateStudy(o.SessionName, studyOpts...) +} + +func (o *HyperparameterOptimizer) buildParamDomains() (map[string]string, []paramDomain) { + labelPaths := make(map[string]string) + domains := make([]paramDomain, 0, len(o.Config.Matrix)) + + for _, selector := range o.Config.Matrix { + var domain paramDomain + switch selector.Type { + case selectorTypeRange, selectorTypeRangeFloat: + if selector.Step.IsZero() { + domain = &floatRangeDomain{ + paramDomainBase: paramDomainBase{ + label: selector.Label, + path: selector.Path, + }, + min: selector.Min.Float64(), + max: selector.Max.Float64(), + } + } else { + domain = &floatDiscreteRangeDomain{ + paramDomainBase: paramDomainBase{ + label: selector.Label, + path: selector.Path, + }, + min: selector.Min.Float64(), + max: selector.Max.Float64(), + step: selector.Step.Float64(), + } + } + case selectorTypeRangeInt: + if selector.Step.IsZero() { + domain = &intRangeDomain{ + paramDomainBase: paramDomainBase{ + label: selector.Label, + path: selector.Path, + }, + min: selector.Min.Int(), + max: selector.Max.Int(), + } + } else { + domain = &intStepRangeDomain{ + paramDomainBase: paramDomainBase{ + label: selector.Label, + path: selector.Path, + }, + min: selector.Min.Int(), + max: selector.Max.Int(), + step: selector.Step.Int(), + } + } + case selectorTypeIterate, selectorTypeString: + domain = &stringDomain{ + paramDomainBase: paramDomainBase{ + label: selector.Label, + path: selector.Path, + }, + options: selector.Values, + } + case selectorTypeBool: + domain = &boolDomain{ + paramDomainBase: paramDomainBase{ + label: selector.Label, + path: selector.Path, + }, + } + default: + // unknown parameter type, skip + continue + } + labelPaths[selector.Label] = selector.Path + domains = append(domains, domain) + } + return labelPaths, domains +} + +func (o *HyperparameterOptimizer) buildObjective(executor Executor, configJson []byte, paramDomains []paramDomain) goptuna.FuncObjective { + var metricValueFunc MetricValueFunc + switch o.Config.Objective { + case HpOptimizerObjectiveProfit: + metricValueFunc = TotalProfitMetricValueFunc + case HpOptimizerObjectiveVolume: + metricValueFunc = TotalVolume + case HpOptimizerObjectiveEquity: + metricValueFunc = TotalEquityDiff + } + + return func(trial goptuna.Trial) (float64, error) { + trialConfig, err := func(trialConfig []byte) ([]byte, error) { + o.paramSuggestionLock.Lock() + defer o.paramSuggestionLock.Unlock() + + for _, domain := range paramDomains { + if patch, err := domain.buildPatch(&trial); err != nil { + return nil, err + } else if patchedConfig, err := patch.ApplyIndent(trialConfig, " "); err != nil { + return nil, err + } else { + trialConfig = patchedConfig + } + } + return trialConfig, nil + }(configJson) + if err != nil { + return 0.0, err + } + + summary, err := executor.Execute(trialConfig) + if err != nil { + return 0.0, err + } + // By config, the Goptuna optimize the parameters by maximize the objective output. + return metricValueFunc(summary).Float64(), nil + } +} + +func (o *HyperparameterOptimizer) Run(ctx context.Context, executor Executor, configJson []byte) (*HyperparameterOptimizeReport, error) { + labelPaths, paramDomains := o.buildParamDomains() + objective := o.buildObjective(executor, configJson, paramDomains) + + maxEvaluation := o.Config.MaxEvaluation + numOfProcesses := o.Config.Executor.LocalExecutorConfig.MaxNumberOfProcesses + if numOfProcesses > maxEvaluation { + numOfProcesses = maxEvaluation + } + maxEvaluationPerProcess := maxEvaluation / numOfProcesses + if maxEvaluation%numOfProcesses > 0 { + maxEvaluationPerProcess++ + } + + trialFinishChan := make(chan goptuna.FrozenTrial, 128) + allTrailFinishChan := make(chan struct{}) + bar := pb.Full.Start(maxEvaluation) + bar.SetTemplateString(`{{ string . "log" | green}} | {{counters . }} {{bar . }} {{percent . }} {{etime . }} {{rtime . "ETA %s"}}`) + + go func() { + defer close(allTrailFinishChan) + var bestVal = math.Inf(-1) + for result := range trialFinishChan { + log.WithFields(logrus.Fields{"ID": result.ID, "evaluation": result.Value, "state": result.State}).Debug("trial finished") + if result.State == goptuna.TrialStateFail { + log.WithFields(result.Params).Errorf("failed at trial #%d", result.ID) + } + if result.Value > bestVal { + bestVal = result.Value + } + bar.Set("log", fmt.Sprintf("best value: %v", bestVal)) + bar.Increment() + } + }() + + study, err := o.buildStudy(trialFinishChan) + if err != nil { + return nil, err + } + eg, studyCtx := errgroup.WithContext(ctx) + study.WithContext(studyCtx) + for i := 0; i < numOfProcesses; i++ { + processEvaluations := maxEvaluationPerProcess + if processEvaluations > maxEvaluation { + processEvaluations = maxEvaluation + } + eg.Go(func() error { + return study.Optimize(objective, processEvaluations) + }) + maxEvaluation -= processEvaluations + } + if err := eg.Wait(); err != nil && ctx.Err() != context.Canceled { + return nil, err + } + close(trialFinishChan) + <-allTrailFinishChan + bar.Finish() + + return &HyperparameterOptimizeReport{ + Name: o.SessionName, + Objective: o.Config.Objective, + Parameters: labelPaths, + Best: buildBestHyperparameterOptimizeResult(study), + Trials: buildHyperparameterOptimizeTrialResults(study), + }, nil +} diff --git a/pkg/optimizer/hpoptimizer_test.go b/pkg/optimizer/hpoptimizer_test.go new file mode 100644 index 0000000000..2107946b3a --- /dev/null +++ b/pkg/optimizer/hpoptimizer_test.go @@ -0,0 +1,201 @@ +package optimizer + +import ( + "github.com/c9s/bbgo/pkg/fixedpoint" + "reflect" + "testing" +) + +func TestBuildParamDomains(t *testing.T) { + var floatRangeDomainVerifier = func(domain paramDomain, expect SelectorConfig) bool { + concrete := domain.(*floatRangeDomain) + return *concrete == floatRangeDomain{ + paramDomainBase: paramDomainBase{label: expect.Label, path: expect.Path}, + min: expect.Min.Float64(), + max: expect.Max.Float64(), + } + } + var floatDiscreteRangeDomainVerifier = func(domain paramDomain, expect SelectorConfig) bool { + concrete := domain.(*floatDiscreteRangeDomain) + return *concrete == floatDiscreteRangeDomain{ + paramDomainBase: paramDomainBase{label: expect.Label, path: expect.Path}, + min: expect.Min.Float64(), + max: expect.Max.Float64(), + step: expect.Step.Float64(), + } + } + var intRangeDomainVerifier = func(domain paramDomain, expect SelectorConfig) bool { + concrete := domain.(*intRangeDomain) + return *concrete == intRangeDomain{ + paramDomainBase: paramDomainBase{label: expect.Label, path: expect.Path}, + min: expect.Min.Int(), + max: expect.Max.Int(), + } + } + var intStepRangeDomainVerifier = func(domain paramDomain, expect SelectorConfig) bool { + concrete := domain.(*intStepRangeDomain) + return *concrete == intStepRangeDomain{ + paramDomainBase: paramDomainBase{label: expect.Label, path: expect.Path}, + min: expect.Min.Int(), + max: expect.Max.Int(), + step: expect.Step.Int(), + } + } + var stringDomainVerifier = func(domain paramDomain, expect SelectorConfig) bool { + concrete := domain.(*stringDomain) + expectBase := paramDomainBase{label: expect.Label, path: expect.Path} + if concrete.paramDomainBase != expectBase { + return false + } + if len(concrete.options) != len(expect.Values) { + return false + } + for i, item := range concrete.options { + if item != expect.Values[i] { + return false + } + } + return true + } + var boolDomainVerifier = func(domain paramDomain, expect SelectorConfig) bool { + concrete := domain.(*boolDomain) + return *concrete == boolDomain{ + paramDomainBase: paramDomainBase{label: expect.Label, path: expect.Path}, + } + } + + tests := []struct { + config SelectorConfig + verify func(domain paramDomain, expect SelectorConfig) bool + }{ + { + config: SelectorConfig{ + Type: selectorTypeRange, + Label: "range label", + Path: "range path", + Values: []string{"ignore", "ignore"}, + Min: fixedpoint.NewFromFloat(7.0), + Max: fixedpoint.NewFromFloat(80.0), + Step: fixedpoint.NewFromFloat(0.0), + }, + verify: floatRangeDomainVerifier, + }, { + config: SelectorConfig{ + Type: selectorTypeRangeFloat, + Label: "rangeFloat label", + Path: "rangeFloat path", + Values: []string{"ignore", "ignore"}, + Min: fixedpoint.NewFromFloat(6.0), + Max: fixedpoint.NewFromFloat(10.0), + Step: fixedpoint.NewFromFloat(0.0), + }, + verify: floatRangeDomainVerifier, + }, { + config: SelectorConfig{ + Type: selectorTypeRangeFloat, + Label: "rangeDiscreteFloat label", + Path: "rangeDiscreteFloat path", + Values: []string{"ignore", "ignore"}, + Min: fixedpoint.NewFromFloat(6.0), + Max: fixedpoint.NewFromFloat(10.0), + Step: fixedpoint.NewFromFloat(2.0), + }, + verify: floatDiscreteRangeDomainVerifier, + }, { + config: SelectorConfig{ + Type: selectorTypeRangeInt, + Label: "rangeInt label", + Path: "rangeInt path", + Values: []string{"ignore", "ignore"}, + Min: fixedpoint.NewFromInt(3), + Max: fixedpoint.NewFromInt(100), + Step: fixedpoint.NewFromInt(0), + }, + verify: intRangeDomainVerifier, + }, { + config: SelectorConfig{ + Type: selectorTypeRangeInt, + Label: "rangeInt label", + Path: "rangeInt path", + Values: []string{"ignore", "ignore"}, + Min: fixedpoint.NewFromInt(3), + Max: fixedpoint.NewFromInt(100), + Step: fixedpoint.NewFromInt(7), + }, + verify: intStepRangeDomainVerifier, + }, { + config: SelectorConfig{ + Type: selectorTypeIterate, + Label: "iterate label", + Path: "iterate path", + Values: nil, + Min: fixedpoint.NewFromInt(0), + Max: fixedpoint.NewFromInt(-8), + Step: fixedpoint.NewFromInt(-1), + }, + verify: stringDomainVerifier, + }, { + config: SelectorConfig{ + Type: selectorTypeString, + Label: "string label", + Path: "string path", + Values: []string{"option1", "option2", "option3"}, + Min: fixedpoint.NewFromInt(0), + Max: fixedpoint.NewFromInt(-8), + Step: fixedpoint.NewFromInt(-1), + }, + verify: stringDomainVerifier, + }, { + config: SelectorConfig{ + Type: selectorTypeBool, + Label: "bool label", + Path: "bool path", + Values: []string{"ignore"}, + Min: fixedpoint.NewFromInt(99), + Max: fixedpoint.NewFromInt(1064), + Step: fixedpoint.NewFromInt(-89), + }, + verify: boolDomainVerifier, + }, { + config: SelectorConfig{ + Type: "unknown type", + Label: "unknown label", + Path: "unknown path", + Values: []string{"unknown option"}, + Min: fixedpoint.NewFromInt(99), + Max: fixedpoint.NewFromFloat(1064), + Step: fixedpoint.NewFromInt(0), + }, + verify: nil, + }, + } + + selectors := make([]SelectorConfig, len(tests)) + expectLabelPaths := make(map[string]string) + verifiers := make([]func(domain paramDomain) bool, 0, len(tests)) + for i, testItem := range tests { + itemConfig, itemVerify := testItem.config, testItem.verify + selectors[i] = itemConfig + if itemVerify != nil { + expectLabelPaths[testItem.config.Label] = testItem.config.Path + verifiers = append(verifiers, func(domain paramDomain) bool { + return itemVerify(domain, itemConfig) + }) + } + } + optimizer := &HyperparameterOptimizer{Config: &Config{Matrix: selectors}} + exactLabelPaths, exactParamDomains := optimizer.buildParamDomains() + + if !reflect.DeepEqual(exactLabelPaths, expectLabelPaths) { + t.Errorf("expectLabelPaths=%v, exactLabelPaths=%v", expectLabelPaths, exactLabelPaths) + } + if len(exactParamDomains) != len(verifiers) { + t.Errorf("expect %d param domains, got %d", len(verifiers), len(exactParamDomains)) + } + for i, verifier := range verifiers { + pd := exactParamDomains[i] + if !verifier(pd) { + t.Errorf("unexpect param domain at #%d: %#v", i, pd) + } + } +} diff --git a/pkg/optimizer/hyperparam.go b/pkg/optimizer/hyperparam.go new file mode 100644 index 0000000000..ae62228949 --- /dev/null +++ b/pkg/optimizer/hyperparam.go @@ -0,0 +1,105 @@ +package optimizer + +import ( + "fmt" + "github.com/c-bata/goptuna" + jsonpatch "github.com/evanphx/json-patch/v5" +) + +type paramDomain interface { + buildPatch(trail *goptuna.Trial) (jsonpatch.Patch, error) +} + +type paramDomainBase struct { + label string + path string +} + +type intRangeDomain struct { + paramDomainBase + min int + max int +} + +func (d *intRangeDomain) buildPatch(trial *goptuna.Trial) (jsonpatch.Patch, error) { + val, err := trial.SuggestInt(d.label, d.min, d.max) + if err != nil { + return nil, err + } + jsonOp := []byte(reformatJson(fmt.Sprintf(`[{"op": "replace", "path": "%s", "value": %v }]`, d.path, val))) + return jsonpatch.DecodePatch(jsonOp) +} + +type intStepRangeDomain struct { + paramDomainBase + min int + max int + step int +} + +func (d *intStepRangeDomain) buildPatch(trial *goptuna.Trial) (jsonpatch.Patch, error) { + val, err := trial.SuggestStepInt(d.label, d.min, d.max, d.step) + if err != nil { + return nil, err + } + jsonOp := []byte(reformatJson(fmt.Sprintf(`[{"op": "replace", "path": "%s", "value": %v }]`, d.path, val))) + return jsonpatch.DecodePatch(jsonOp) +} + +type floatRangeDomain struct { + paramDomainBase + min float64 + max float64 +} + +func (d *floatRangeDomain) buildPatch(trial *goptuna.Trial) (jsonpatch.Patch, error) { + val, err := trial.SuggestFloat(d.label, d.min, d.max) + if err != nil { + return nil, err + } + jsonOp := []byte(reformatJson(fmt.Sprintf(`[{"op": "replace", "path": "%s", "value": %v }]`, d.path, val))) + return jsonpatch.DecodePatch(jsonOp) +} + +type floatDiscreteRangeDomain struct { + paramDomainBase + min float64 + max float64 + step float64 +} + +func (d *floatDiscreteRangeDomain) buildPatch(trial *goptuna.Trial) (jsonpatch.Patch, error) { + val, err := trial.SuggestDiscreteFloat(d.label, d.min, d.max, d.step) + if err != nil { + return nil, err + } + jsonOp := []byte(reformatJson(fmt.Sprintf(`[{"op": "replace", "path": "%s", "value": %v }]`, d.path, val))) + return jsonpatch.DecodePatch(jsonOp) +} + +type stringDomain struct { + paramDomainBase + options []string +} + +func (d *stringDomain) buildPatch(trial *goptuna.Trial) (jsonpatch.Patch, error) { + val, err := trial.SuggestCategorical(d.label, d.options) + if err != nil { + return nil, err + } + jsonOp := []byte(reformatJson(fmt.Sprintf(`[{"op": "replace", "path": "%s", "value": "%v" }]`, d.path, val))) + return jsonpatch.DecodePatch(jsonOp) +} + +type boolDomain struct { + paramDomainBase +} + +func (d *boolDomain) buildPatch(trial *goptuna.Trial) (jsonpatch.Patch, error) { + valStr, err := trial.SuggestCategorical(d.label, []string{"false", "true"}) + if err != nil { + return nil, err + } + jsonOp := []byte(reformatJson(fmt.Sprintf(`[{"op": "replace", "path": "%s", "value": %s }]`, d.path, valStr))) + return jsonpatch.DecodePatch(jsonOp) +} diff --git a/pkg/optimizer/local.go b/pkg/optimizer/local.go new file mode 100644 index 0000000000..799bc9f5e5 --- /dev/null +++ b/pkg/optimizer/local.go @@ -0,0 +1,204 @@ +package optimizer + +import ( + "bufio" + "bytes" + "context" + "encoding/json" + "fmt" + "os" + "os/exec" + "strings" + "sync" + + "github.com/cheggaaa/pb/v3" + "github.com/pkg/errors" + + "github.com/sirupsen/logrus" + "gopkg.in/yaml.v3" + + "github.com/c9s/bbgo/pkg/backtest" +) + +var log = logrus.WithField("component", "optimizer") + +type BacktestTask struct { + ConfigJson []byte + Params []interface{} + Labels []string + Report *backtest.SummaryReport + Error error +} + +type Executor interface { + Execute(configJson []byte) (*backtest.SummaryReport, error) + Run(ctx context.Context, taskC chan BacktestTask, bar *pb.ProgressBar) (chan BacktestTask, error) +} + +type AsyncHandle struct { + Error error + Report *backtest.SummaryReport + Done chan struct{} +} + +type LocalProcessExecutor struct { + Config *LocalExecutorConfig + Bin string + WorkDir string + ConfigDir string + OutputDir string +} + +func (e *LocalProcessExecutor) ExecuteAsync(configJson []byte) *AsyncHandle { + handle := &AsyncHandle{ + Done: make(chan struct{}), + } + + go func() { + defer close(handle.Done) + report, err := e.Execute(configJson) + handle.Error = err + handle.Report = report + }() + + return handle +} + +func (e *LocalProcessExecutor) readReport(reportPath string) (*backtest.SummaryReport, error) { + summaryReportFilepath := strings.TrimSpace(reportPath) + _, err := os.Stat(summaryReportFilepath) + if os.IsNotExist(err) { + return nil, err + } + + summaryReport, err := backtest.ReadSummaryReport(summaryReportFilepath) + if err != nil { + return nil, err + } + + return summaryReport, nil +} + +// Prepare prepares the environment for the following back tests +// this is a blocking operation +func (e *LocalProcessExecutor) Prepare(configJson []byte) error { + log.Debugln("syncing backtest data before starting backtests...") + tf, err := jsonToYamlConfig(e.ConfigDir, configJson) + if err != nil { + return err + } + + c := exec.Command(e.Bin, "backtest", "--sync", "--sync-only", "--config", tf.Name()) + output, err := c.Output() + if err != nil { + return errors.Wrapf(err, "failed to sync backtest data: %s", string(output)) + } + + return nil +} + +func (e *LocalProcessExecutor) Run(ctx context.Context, taskC chan BacktestTask, bar *pb.ProgressBar) (chan BacktestTask, error) { + var maxNumOfProcess = e.Config.MaxNumberOfProcesses + var resultsC = make(chan BacktestTask, maxNumOfProcess*2) + + wg := sync.WaitGroup{} + wg.Add(maxNumOfProcess) + + go func() { + wg.Wait() + close(resultsC) + }() + + for i := 0; i < maxNumOfProcess; i++ { + // fork workers + go func(id int, taskC chan BacktestTask) { + taskCnt := 0 + bar.Set("log", fmt.Sprintf("starting local worker #%d", id)) + bar.Write() + defer wg.Done() + for { + select { + case <-ctx.Done(): + return + + case task, ok := <-taskC: + if !ok { + return + } + + taskCnt++ + bar.Set("log", fmt.Sprintf("local worker #%d received param task: %v", id, task.Params)) + bar.Write() + + report, err := e.Execute(task.ConfigJson) + if err != nil { + if err2, ok := err.(*exec.ExitError); ok { + log.WithError(err).Errorf("execute error: %s", err2.Stderr) + } else { + log.WithError(err).Errorf("execute error") + } + } + + task.Error = err + task.Report = report + + resultsC <- task + } + } + }(i+1, taskC) + } + + return resultsC, nil +} + +// Execute runs the config json and returns the summary report. This is a blocking operation. +func (e *LocalProcessExecutor) Execute(configJson []byte) (*backtest.SummaryReport, error) { + tf, err := jsonToYamlConfig(e.ConfigDir, configJson) + if err != nil { + return nil, err + } + + c := exec.Command(e.Bin, "backtest", "--config", tf.Name(), "--output", e.OutputDir, "--subdir") + output, err := c.Output() + if err != nil { + log.WithError(err).WithField("command", []string{e.Bin, "backtest", "--config", tf.Name(), "--output", e.OutputDir, "--subdir"}).Errorf("failed to execute backtest") + return nil, err + } + + // the last line is the report path + scanner := bufio.NewScanner(bytes.NewBuffer(output)) + var reportFilePath string + for scanner.Scan() { + reportFilePath = scanner.Text() + } + return e.readReport(reportFilePath) +} + +// jsonToYamlConfig translate json format config into a YAML format config file +// The generated file is a temp file +func jsonToYamlConfig(dir string, configJson []byte) (*os.File, error) { + var o map[string]interface{} + if err := json.Unmarshal(configJson, &o); err != nil { + return nil, err + } + + yamlConfig, err := yaml.Marshal(o) + if err != nil { + return nil, err + } + + tf, err := os.CreateTemp(dir, "bbgo-*.yaml") + if err != nil { + return nil, err + } + + if _, err = tf.Write(yamlConfig); err != nil { + return nil, err + } + + if err := tf.Close(); err != nil { + return nil, err + } + + return tf, nil +} diff --git a/pkg/optimizer/local_test.go b/pkg/optimizer/local_test.go new file mode 100644 index 0000000000..1c0298fb71 --- /dev/null +++ b/pkg/optimizer/local_test.go @@ -0,0 +1,21 @@ +package optimizer + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_jsonToYamlConfig(t *testing.T) { + err := os.Mkdir(".tmpconfig", 0755) + assert.NoError(t, err) + + tf, err := jsonToYamlConfig(".tmpconfig", []byte(`{ + }`)) + assert.NoError(t, err) + assert.NotNil(t, tf) + assert.NotEmpty(t, tf.Name()) + + _ = os.RemoveAll(".tmpconfig") +} diff --git a/pkg/optimizer/operator.go b/pkg/optimizer/operator.go new file mode 100644 index 0000000000..c4ac89cf49 --- /dev/null +++ b/pkg/optimizer/operator.go @@ -0,0 +1,3 @@ +package optimizer + +type OpFunc func(configJson []byte, next func(configJson []byte) error) error diff --git a/pkg/risk/leverage.go b/pkg/risk/leverage.go new file mode 100644 index 0000000000..f9550073d1 --- /dev/null +++ b/pkg/risk/leverage.go @@ -0,0 +1,51 @@ +package risk + +import ( + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +// How to Calculate Cost Required to Open a Position in Perpetual Futures Contracts +// +// See +// +// For Long Position: +// = Number of Contract * Absolute Value {min[0, direction of order x (mark price - order price)]} +// +// For short position: +// = Number of Contract * Absolute Value {min[0, direction of order x (mark price - order price)]} +func CalculateOpenLoss(numContract, markPrice, orderPrice fixedpoint.Value, side types.SideType) fixedpoint.Value { + var d = fixedpoint.One + if side == types.SideTypeSell { + d = fixedpoint.NegOne + } + + var openLoss = numContract.Mul(fixedpoint.Min(fixedpoint.Zero, d.Mul(markPrice.Sub(orderPrice))).Abs()) + return openLoss +} + +// CalculateMarginCost calculate the margin cost of the given notional position by price * quantity +func CalculateMarginCost(price, quantity, leverage fixedpoint.Value) fixedpoint.Value { + var notionalValue = price.Mul(quantity) + var cost = notionalValue.Div(leverage) + return cost +} + +func CalculatePositionCost(markPrice, orderPrice, quantity, leverage fixedpoint.Value, side types.SideType) fixedpoint.Value { + var marginCost = CalculateMarginCost(orderPrice, quantity, leverage) + var openLoss = CalculateOpenLoss(quantity, markPrice, orderPrice, side) + return marginCost.Add(openLoss) +} + +// CalculateMaxPosition calculates the maximum notional value of the position and return the max quantity you can use. +func CalculateMaxPosition(price, availableMargin, leverage fixedpoint.Value) fixedpoint.Value { + var maxNotionalValue = availableMargin.Mul(leverage) + var maxQuantity = maxNotionalValue.Div(price) + return maxQuantity +} + +// CalculateMinRequiredLeverage calculates the leverage of the given position (price and quantity) +func CalculateMinRequiredLeverage(price, quantity, availableMargin fixedpoint.Value) fixedpoint.Value { + var notional = price.Mul(quantity) + return notional.Div(availableMargin) +} diff --git a/pkg/risk/leverage_test.go b/pkg/risk/leverage_test.go new file mode 100644 index 0000000000..b59dd22a23 --- /dev/null +++ b/pkg/risk/leverage_test.go @@ -0,0 +1,145 @@ +package risk + +import ( + "testing" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +func TestCalculateMarginCost(t *testing.T) { + type args struct { + price fixedpoint.Value + quantity fixedpoint.Value + leverage fixedpoint.Value + } + tests := []struct { + name string + args args + want fixedpoint.Value + }{ + { + name: "simple", + args: args{ + price: fixedpoint.NewFromFloat(9000.0), + quantity: fixedpoint.NewFromFloat(2.0), + leverage: fixedpoint.NewFromFloat(3.0), + }, + want: fixedpoint.NewFromFloat(9000.0 * 2.0 / 3.0), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := CalculateMarginCost(tt.args.price, tt.args.quantity, tt.args.leverage); got.String() != tt.want.String() { + t.Errorf("CalculateMarginCost() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestCalculatePositionCost(t *testing.T) { + type args struct { + markPrice fixedpoint.Value + orderPrice fixedpoint.Value + quantity fixedpoint.Value + leverage fixedpoint.Value + side types.SideType + } + tests := []struct { + name string + args args + want fixedpoint.Value + }{ + { + // long position does not have openLoss + name: "long", + args: args{ + markPrice: fixedpoint.NewFromFloat(9050.0), + orderPrice: fixedpoint.NewFromFloat(9000.0), + quantity: fixedpoint.NewFromFloat(2.0), + leverage: fixedpoint.NewFromFloat(3.0), + side: types.SideTypeBuy, + }, + want: fixedpoint.NewFromFloat(6000.0), + }, + { + // long position does not have openLoss + name: "short", + args: args{ + markPrice: fixedpoint.NewFromFloat(9050.0), + orderPrice: fixedpoint.NewFromFloat(9000.0), + quantity: fixedpoint.NewFromFloat(2.0), + leverage: fixedpoint.NewFromFloat(3.0), + side: types.SideTypeSell, + }, + want: fixedpoint.NewFromFloat(6100.0), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := CalculatePositionCost(tt.args.markPrice, tt.args.orderPrice, tt.args.quantity, tt.args.leverage, tt.args.side); got.String() != tt.want.String() { + t.Errorf("CalculatePositionCost() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestCalculateMaxPosition(t *testing.T) { + type args struct { + price fixedpoint.Value + availableMargin fixedpoint.Value + leverage fixedpoint.Value + } + tests := []struct { + name string + args args + want fixedpoint.Value + }{ + { + name: "3x", + args: args{ + price: fixedpoint.NewFromFloat(9000.0), + availableMargin: fixedpoint.NewFromFloat(300.0), + leverage: fixedpoint.NewFromFloat(3.0), + }, + want: fixedpoint.NewFromFloat(0.1), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := CalculateMaxPosition(tt.args.price, tt.args.availableMargin, tt.args.leverage); got.String() != tt.want.String() { + t.Errorf("CalculateMaxPosition() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestCalculateMinRequiredLeverage(t *testing.T) { + type args struct { + price fixedpoint.Value + quantity fixedpoint.Value + availableMargin fixedpoint.Value + } + tests := []struct { + name string + args args + want fixedpoint.Value + }{ + { + name: "30x", + args: args{ + price: fixedpoint.NewFromFloat(9000.0), + quantity: fixedpoint.NewFromFloat(10.0), + availableMargin: fixedpoint.NewFromFloat(3000.0), + }, + want: fixedpoint.NewFromFloat(30.0), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := CalculateMinRequiredLeverage(tt.args.price, tt.args.quantity, tt.args.availableMargin); got.String() != tt.want.String() { + t.Errorf("CalculateMinRequiredLeverage() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/server/envvars.go b/pkg/server/envvars.go index 82f4fbbf9c..f11a418f93 100644 --- a/pkg/server/envvars.go +++ b/pkg/server/envvars.go @@ -17,11 +17,15 @@ func collectSessionEnvVars(sessions map[string]*bbgo.ExchangeSession) (envVars m } if len(session.EnvVarPrefix) > 0 { + // pragma: allowlist nextline secret envVars[session.EnvVarPrefix+"_API_KEY"] = session.Key + // pragma: allowlist nextline secret envVars[session.EnvVarPrefix+"_API_SECRET"] = session.Secret } else if len(session.Name) > 0 { sn := strings.ToUpper(session.Name) + // pragma: allowlist nextline secret envVars[sn+"_API_KEY"] = session.Key + // pragma: allowlist nextline secret envVars[sn+"_API_SECRET"] = session.Secret } else { err = fmt.Errorf("session %s name or env var prefix is not defined", session.Name) diff --git a/pkg/server/routes.go b/pkg/server/routes.go index 68a67cdcea..6502c17e1c 100644 --- a/pkg/server/routes.go +++ b/pkg/server/routes.go @@ -48,7 +48,7 @@ type Server struct { srv *http.Server } -func (s *Server) newEngine() *gin.Engine { +func (s *Server) newEngine(ctx context.Context) *gin.Engine { r := gin.Default() r.Use(cors.New(cors.Config{ AllowOrigins: []string{"*"}, @@ -77,11 +77,15 @@ func (s *Server) newEngine() *gin.Engine { }) r.POST("/api/environment/sync", func(c *gin.Context) { - go func() { - if err := s.Environ.Sync(context.Background()); err != nil { - logrus.WithError(err).Error("sync error") - } - }() + if s.Environ.IsSyncing() != bbgo.Syncing { + go func() { + // We use the root context here because the syncing operation is a background goroutine. + // It should not be terminated if the request is disconnected. + if err := s.Environ.Sync(ctx); err != nil { + logrus.WithError(err).Error("sync error") + } + }() + } c.JSON(http.StatusOK, gin.H{ "success": true, @@ -249,7 +253,7 @@ func (s *Server) newEngine() *gin.Engine { } func (s *Server) RunWithListener(ctx context.Context, l net.Listener) error { - r := s.newEngine() + r := s.newEngine(ctx) bind := l.Addr().String() if s.OpenInBrowser { @@ -261,7 +265,7 @@ func (s *Server) RunWithListener(ctx context.Context, l net.Listener) error { } func (s *Server) Run(ctx context.Context, bindArgs ...string) error { - r := s.newEngine() + r := s.newEngine(ctx) bind := resolveBind(bindArgs) if s.OpenInBrowser { openBrowser(ctx, bind) @@ -442,7 +446,7 @@ func genFakeAssets() types.AssetMap { "DOTUSDT": fixedpoint.NewFromFloat(20.0), "SANDUSDT": fixedpoint.NewFromFloat(0.13), "MAXUSDT": fixedpoint.NewFromFloat(0.122), - }) + }, time.Now()) for currency, asset := range assets { totalAssets[currency] = asset } @@ -460,13 +464,13 @@ func (s *Server) listAssets(c *gin.Context) { for _, session := range s.Environ.Sessions() { balances := session.GetAccount().Balances() - if err := session.UpdatePrices(c); err != nil { + if err := session.UpdatePrices(c, balances.Currencies(), "USDT"); err != nil { logrus.WithError(err).Error("price update failed") c.Status(http.StatusInternalServerError) return } - assets := balances.Assets(session.LastPrices()) + assets := balances.Assets(session.LastPrices(), time.Now()) for currency, asset := range assets { totalAssets[currency] = asset @@ -593,7 +597,6 @@ func (s *Server) tradingVolume(c *gin.Context) { } c.JSON(http.StatusOK, gin.H{"tradingVolumes": rows}) - return } func newServer(r http.Handler, bind string) *http.Server { diff --git a/pkg/service/account.go b/pkg/service/account.go index 0bc22062d4..d924932660 100644 --- a/pkg/service/account.go +++ b/pkg/service/account.go @@ -15,20 +15,48 @@ func NewAccountService(db *sqlx.DB) *AccountService { return &AccountService{DB: db} } -func (s *AccountService) InsertAsset(time time.Time, name types.ExchangeName, account string, assets types.AssetMap) error { - +// TODO: should pass bbgo.ExchangeSession to this function, but that might cause cyclic import +func (s *AccountService) InsertAsset(time time.Time, session string, name types.ExchangeName, account string, isMargin bool, isIsolatedMargin bool, isolatedMarginSymbol string, assets types.AssetMap) error { if s.DB == nil { - //skip db insert when no db connection setting. + // skip db insert when no db connection setting. return nil } var err error for _, v := range assets { _, _err := s.DB.Exec(` - insert into nav_history_details ( exchange, subaccount, time, currency, balance_in_usd, balance_in_btc, - balance,available,locked) - values (?,?,?,?,?,?,?,?,?); - `, name, account, time, v.Currency, v.InUSD, v.InBTC, v.Total, v.Available, v.Locked) + INSERT INTO nav_history_details ( + session, + exchange, + subaccount, + time, + currency, + net_asset_in_usd, + net_asset_in_btc, + balance, + available, + locked, + borrowed, + net_asset, + price_in_usd, + is_margin, is_isolated, isolated_symbol) + values (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);`, + session, + name, + account, + time, + v.Currency, + v.InUSD, + v.InBTC, + v.Total, + v.Available, + v.Locked, + v.Borrowed, + v.NetAsset, + v.PriceInUSD, + isMargin, + isIsolatedMargin, + isolatedMarginSymbol) err = multierr.Append(err, _err) // successful request diff --git a/pkg/service/account_test.go b/pkg/service/account_test.go new file mode 100644 index 0000000000..89c0fa98cf --- /dev/null +++ b/pkg/service/account_test.go @@ -0,0 +1,41 @@ +package service + +import ( + "testing" + "time" + + "github.com/jmoiron/sqlx" + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +func TestAccountService(t *testing.T) { + db, err := prepareDB(t) + if err != nil { + t.Fatal(err) + } + + defer db.Close() + + xdb := sqlx.NewDb(db.DB, "sqlite3") + service := &AccountService{DB: xdb} + + t1 := time.Now() + err = service.InsertAsset(t1, "binance", types.ExchangeBinance, "main", false, false, "", types.AssetMap{ + "BTC": types.Asset{ + Currency: "BTC", + Total: fixedpoint.MustNewFromString("1.0"), + InUSD: fixedpoint.MustNewFromString("10.0"), + InBTC: fixedpoint.MustNewFromString("0.0001"), + Time: t1, + Locked: fixedpoint.MustNewFromString("0"), + Available: fixedpoint.MustNewFromString("1.0"), + Borrowed: fixedpoint.MustNewFromString("0"), + NetAsset: fixedpoint.MustNewFromString("1"), + PriceInUSD: fixedpoint.MustNewFromString("44870"), + }, + }) + assert.NoError(t, err) +} diff --git a/pkg/service/backtest.go b/pkg/service/backtest.go index 791402b9f7..5a609ff8f0 100644 --- a/pkg/service/backtest.go +++ b/pkg/service/backtest.go @@ -2,17 +2,19 @@ package service import ( "context" + "database/sql" "fmt" - "os" "strconv" "strings" "time" + sq "github.com/Masterminds/squirrel" "github.com/jmoiron/sqlx" "github.com/pkg/errors" log "github.com/sirupsen/logrus" - batch2 "github.com/c9s/bbgo/pkg/exchange/batch" + exchange2 "github.com/c9s/bbgo/pkg/exchange" + "github.com/c9s/bbgo/pkg/exchange/batch" "github.com/c9s/bbgo/pkg/types" ) @@ -21,68 +23,92 @@ type BacktestService struct { } func (s *BacktestService) SyncKLineByInterval(ctx context.Context, exchange types.Exchange, symbol string, interval types.Interval, startTime, endTime time.Time) error { - log.Infof("synchronizing lastKLine for interval %s from exchange %s", interval, exchange.Name()) + log.Infof("synchronizing %s klines with interval %s: %s <=> %s", exchange.Name(), interval, startTime, endTime) - batch := &batch2.KLineBatchQuery{Exchange: exchange} + // TODO: use isFutures here + _, _, isIsolated, isolatedSymbol := exchange2.GetSessionAttributes(exchange) + // override symbol if isolatedSymbol is not empty + if isIsolated && len(isolatedSymbol) > 0 { + symbol = isolatedSymbol + } - // should use channel here - klineC, errC := batch.Query(ctx, symbol, interval, startTime, endTime) + if s.DB.DriverName() == "sqlite3" { + _, _ = s.DB.Exec("PRAGMA journal_mode = WAL") + _, _ = s.DB.Exec("PRAGMA synchronous = NORMAL") + } - // var previousKLine types.KLine - count := 0 - for klines := range klineC { - if err := s.BatchInsert(klines); err != nil { - return err - } - count += len(klines) + now := time.Now() + tasks := []SyncTask{ + { + Type: types.KLine{}, + Select: SelectLastKLines(exchange.Name(), symbol, interval, startTime, endTime, 100), + Time: func(obj interface{}) time.Time { + return obj.(types.KLine).StartTime.Time() + }, + Filter: func(obj interface{}) bool { + k := obj.(types.KLine) + if k.EndTime.Before(k.StartTime.Time().Add(k.Interval.Duration() - time.Second)) { + return false + } + + // Filter klines that has the endTime closed in the future + if k.EndTime.After(now) { + return false + } + + return true + }, + ID: func(obj interface{}) string { + kline := obj.(types.KLine) + return strconv.FormatInt(kline.StartTime.UnixMilli(), 10) + // return kline.Symbol + kline.Interval.String() + strconv.FormatInt(kline.StartTime.UnixMilli(), 10) + }, + BatchQuery: func(ctx context.Context, startTime, endTime time.Time) (interface{}, chan error) { + q := &batch.KLineBatchQuery{Exchange: exchange} + return q.Query(ctx, symbol, interval, startTime, endTime) + }, + BatchInsertBuffer: 1000, + BatchInsert: func(obj interface{}) error { + kLines := obj.([]types.KLine) + return s.BatchInsert(kLines) + }, + Insert: func(obj interface{}) error { + kline := obj.(types.KLine) + return s.Insert(kline) + }, + LogInsert: log.GetLevel() == log.DebugLevel, + }, } - log.Infof("found %s kline %s data count: %d", symbol, interval.String(), count) - if err := <-errC; err != nil { - return err + for _, sel := range tasks { + if err := sel.execute(ctx, s.DB, startTime, endTime); err != nil { + return err + } } return nil } -func (s *BacktestService) Verify(symbols []string, startTime time.Time, endTime time.Time, sourceExchange types.Exchange, verboseCnt int) (error, bool) { +func (s *BacktestService) Verify(sourceExchange types.Exchange, symbols []string, startTime time.Time, endTime time.Time) error { var corruptCnt = 0 for _, symbol := range symbols { - log.Infof("verifying backtesting data...") - for interval := range types.SupportedIntervals { - log.Infof("verifying %s %s kline data...", symbol, interval) - - klineC, errC := s.QueryKLinesCh(startTime, time.Now(), sourceExchange, []string{symbol}, []types.Interval{interval}) - var emptyKLine types.KLine - var prevKLine types.KLine - for k := range klineC { - if verboseCnt > 1 { - fmt.Fprint(os.Stderr, ".") - } - - if prevKLine != emptyKLine { - if prevKLine.StartTime.Unix() == k.StartTime.Unix() { - s._deleteDuplicatedKLine(k) - log.Errorf("found kline data duplicated at time: %s kline: %+v , deleted it", k.StartTime, k) - } else if prevKLine.StartTime.Time().Add(interval.Duration()).Unix() != k.StartTime.Time().Unix() { - corruptCnt++ - log.Errorf("found kline data corrupted at time: %s kline: %+v", k.StartTime, k) - log.Errorf("between %d and %d", - prevKLine.StartTime.Unix(), - k.StartTime.Unix()) - } - } + log.Infof("verifying %s %s backtesting data: %s to %s...", symbol, interval, startTime, endTime) - prevKLine = k + timeRanges, err := s.FindMissingTimeRanges(context.Background(), sourceExchange, symbol, interval, + startTime, endTime) + if err != nil { + return err } - if verboseCnt > 1 { - fmt.Fprintln(os.Stderr) + if len(timeRanges) == 0 { + continue } - if err := <-errC; err != nil { - return err, true + log.Warnf("%s %s found missing time ranges:", symbol, interval) + corruptCnt += len(timeRanges) + for _, timeRange := range timeRanges { + log.Warnf("- %s", timeRange.String()) } } } @@ -93,38 +119,30 @@ func (s *BacktestService) Verify(symbols []string, startTime time.Time, endTime } else { log.Infof("found %d corruptions", corruptCnt) } - return nil, false -} - -func (s *BacktestService) Sync(ctx context.Context, exchange types.Exchange, symbol string, - startTime time.Time, endTime time.Time, interval types.Interval) error { - - return s.SyncKLineByInterval(ctx, exchange, symbol, interval, startTime, endTime) - -} -func (s *BacktestService) QueryFirstKLine(ex types.ExchangeName, symbol string, interval types.Interval) (*types.KLine, error) { - return s.QueryKLine(ex, symbol, interval, "ASC", 1) + return nil } -// QueryLastKLine queries the last kline from the database -func (s *BacktestService) QueryLastKLine(ex types.ExchangeName, symbol string, interval types.Interval) (*types.KLine, error) { - return s.QueryKLine(ex, symbol, interval, "DESC", 1) +func (s *BacktestService) SyncFresh(ctx context.Context, exchange types.Exchange, symbol string, interval types.Interval, startTime, endTime time.Time) error { + log.Infof("starting fresh sync %s %s %s: %s <=> %s", exchange.Name(), symbol, interval, startTime, endTime) + startTime = startTime.Truncate(time.Minute).Add(-2 * time.Second) + endTime = endTime.Truncate(time.Minute).Add(2 * time.Second) + return s.SyncKLineByInterval(ctx, exchange, symbol, interval, startTime, endTime) } // QueryKLine queries the klines from the database func (s *BacktestService) QueryKLine(ex types.ExchangeName, symbol string, interval types.Interval, orderBy string, limit int) (*types.KLine, error) { log.Infof("querying last kline exchange = %s AND symbol = %s AND interval = %s", ex, symbol, interval) - tableName := s._targetKlineTable(ex) + tableName := targetKlineTable(ex) // make the SQL syntax IDE friendly, so that it can analyze it. - sql := fmt.Sprintf("SELECT * FROM `%s` WHERE `symbol` = :symbol AND `interval` = :interval and exchange = :exchange ORDER BY end_time "+orderBy+" LIMIT "+strconv.Itoa(limit), tableName) + sql := fmt.Sprintf("SELECT * FROM `%s` WHERE `symbol` = :symbol AND `interval` = :interval ORDER BY end_time "+orderBy+" LIMIT "+strconv.Itoa(limit), tableName) rows, err := s.DB.NamedQuery(sql, map[string]interface{}{ - "exchange": ex.String(), "interval": interval, "symbol": symbol, }) + defer rows.Close() if err != nil { return nil, errors.Wrap(err, "query kline error") @@ -134,8 +152,6 @@ func (s *BacktestService) QueryKLine(ex types.ExchangeName, symbol string, inter return nil, rows.Err() } - defer rows.Close() - if rows.Next() { var kline types.KLine err = rows.StructScan(&kline) @@ -145,8 +161,9 @@ func (s *BacktestService) QueryKLine(ex types.ExchangeName, symbol string, inter return nil, rows.Err() } +// QueryKLinesForward is used for querying klines to back-testing func (s *BacktestService) QueryKLinesForward(exchange types.ExchangeName, symbol string, interval types.Interval, startTime time.Time, limit int) ([]types.KLine, error) { - tableName := s._targetKlineTable(exchange) + tableName := targetKlineTable(exchange) sql := "SELECT * FROM `binance_klines` WHERE `end_time` >= :start_time AND `symbol` = :symbol AND `interval` = :interval and exchange = :exchange ORDER BY end_time ASC LIMIT :limit" sql = strings.ReplaceAll(sql, "binance_klines", tableName) @@ -165,7 +182,7 @@ func (s *BacktestService) QueryKLinesForward(exchange types.ExchangeName, symbol } func (s *BacktestService) QueryKLinesBackward(exchange types.ExchangeName, symbol string, interval types.Interval, endTime time.Time, limit int) ([]types.KLine, error) { - tableName := s._targetKlineTable(exchange) + tableName := targetKlineTable(exchange) sql := "SELECT * FROM `binance_klines` WHERE `end_time` <= :end_time and exchange = :exchange AND `symbol` = :symbol AND `interval` = :interval ORDER BY end_time DESC LIMIT :limit" sql = strings.ReplaceAll(sql, "binance_klines", tableName) @@ -186,21 +203,29 @@ func (s *BacktestService) QueryKLinesBackward(exchange types.ExchangeName, symbo } func (s *BacktestService) QueryKLinesCh(since, until time.Time, exchange types.Exchange, symbols []string, intervals []types.Interval) (chan types.KLine, chan error) { - if len(symbols) == 0 { return returnError(errors.Errorf("symbols is empty when querying kline, plesae check your strategy setting. ")) } - tableName := s._targetKlineTable(exchange.Name()) - sql := "SELECT * FROM `binance_klines` WHERE `end_time` BETWEEN :since AND :until AND `symbol` IN (:symbols) AND `interval` IN (:intervals) and exchange = :exchange ORDER BY end_time ASC" - sql = strings.ReplaceAll(sql, "binance_klines", tableName) + tableName := targetKlineTable(exchange.Name()) + var query string - sql, args, err := sqlx.Named(sql, map[string]interface{}{ + // need to sort by start_time desc in order to let matching engine process 1m first + // otherwise any other close event could peek on the final close price + if len(symbols) == 1 { + query = "SELECT * FROM `binance_klines` WHERE `end_time` BETWEEN :since AND :until AND `symbol` = :symbols AND `interval` IN (:intervals) ORDER BY end_time ASC, start_time DESC" + } else { + query = "SELECT * FROM `binance_klines` WHERE `end_time` BETWEEN :since AND :until AND `symbol` IN (:symbols) AND `interval` IN (:intervals) ORDER BY end_time ASC, start_time DESC" + } + + query = strings.ReplaceAll(query, "binance_klines", tableName) + + sql, args, err := sqlx.Named(query, map[string]interface{}{ "since": since, "until": until, + "symbol": symbols[0], "symbols": symbols, "intervals": types.IntervalSlice(intervals), - "exchange": exchange.Name().String(), }) sql, args, err = sqlx.In(sql, args...) @@ -218,7 +243,7 @@ func (s *BacktestService) QueryKLinesCh(since, until time.Time, exchange types.E } func returnError(err error) (chan types.KLine, chan error) { - ch := make(chan types.KLine, 0) + ch := make(chan types.KLine) close(ch) log.WithError(err).Error("backtest query error") @@ -262,6 +287,7 @@ func (s *BacktestService) scanRowsCh(rows *sqlx.Rows) (chan types.KLine, chan er } func (s *BacktestService) scanRows(rows *sqlx.Rows) (klines []types.KLine, err error) { + defer rows.Close() for rows.Next() { var kline types.KLine if err := rows.StructScan(&kline); err != nil { @@ -274,29 +300,18 @@ func (s *BacktestService) scanRows(rows *sqlx.Rows) (klines []types.KLine, err e return klines, rows.Err() } -func (s *BacktestService) _targetKlineTable(exchangeName types.ExchangeName) string { - switch exchangeName { - case types.ExchangeBinance: - return "binance_klines" - case types.ExchangeFTX: - return "ftx_klines" - case types.ExchangeMax: - return "max_klines" - case types.ExchangeOKEx: - return "okex_klines" - case types.ExchangeKucoin: - return "kucoin_klines" - default: - return "klines" - } +func targetKlineTable(exchangeName types.ExchangeName) string { + return strings.ToLower(exchangeName.String()) + "_klines" } +var errExchangeFieldIsUnset = errors.New("kline.Exchange field should not be empty") + func (s *BacktestService) Insert(kline types.KLine) error { if len(kline.Exchange) == 0 { - return errors.New("kline.Exchange field should not be empty") + return errExchangeFieldIsUnset } - tableName := s._targetKlineTable(kline.Exchange) + tableName := targetKlineTable(kline.Exchange) sql := fmt.Sprintf("INSERT INTO `%s` (`exchange`, `start_time`, `end_time`, `symbol`, `interval`, `open`, `high`, `low`, `close`, `closed`, `volume`, `quote_volume`, `taker_buy_base_volume`, `taker_buy_quote_volume`)"+ "VALUES (:exchange, :start_time, :end_time, :symbol, :interval, :open, :high, :low, :close, :closed, :volume, :quote_volume, :taker_buy_base_volume, :taker_buy_quote_volume)", tableName) @@ -310,50 +325,223 @@ func (s *BacktestService) BatchInsert(kline []types.KLine) error { if len(kline) == 0 { return nil } - if len(kline[0].Exchange) == 0 { - return errors.New("kline.Exchange field should not be empty") - } - tableName := s._targetKlineTable(kline[0].Exchange) + tableName := targetKlineTable(kline[0].Exchange) sql := fmt.Sprintf("INSERT INTO `%s` (`exchange`, `start_time`, `end_time`, `symbol`, `interval`, `open`, `high`, `low`, `close`, `closed`, `volume`, `quote_volume`, `taker_buy_base_volume`, `taker_buy_quote_volume`)"+ - " values (:exchange, :start_time, :end_time, :symbol, :interval, :open, :high, :low, :close, :closed, :volume, :quote_volume, :taker_buy_base_volume, :taker_buy_quote_volume); ", tableName) + " VALUES (:exchange, :start_time, :end_time, :symbol, :interval, :open, :high, :low, :close, :closed, :volume, :quote_volume, :taker_buy_base_volume, :taker_buy_quote_volume); ", tableName) - _, err := s.DB.NamedExec(sql, kline) - return err + tx := s.DB.MustBegin() + if _, err := tx.NamedExec(sql, kline); err != nil { + if e := tx.Rollback(); e != nil { + log.WithError(e).Fatalf("cannot rollback insertion %v", err) + } + return err + } + return tx.Commit() +} + +type TimeRange struct { + Start time.Time + End time.Time } -func (s *BacktestService) _deleteDuplicatedKLine(k types.KLine) error { +func (t *TimeRange) String() string { + return t.Start.String() + " ~ " + t.End.String() +} - if len(k.Exchange) == 0 { - return errors.New("kline.Exchange field should not be empty") +func (s *BacktestService) Sync(ctx context.Context, ex types.Exchange, symbol string, interval types.Interval, since, until time.Time) error { + t1, t2, err := s.QueryExistingDataRange(ctx, ex, symbol, interval, since, until) + if err != nil && err != sql.ErrNoRows { + return err } - tableName := s._targetKlineTable(k.Exchange) - sql := fmt.Sprintf("delete from `%s` where gid = :gid ", tableName) - _, err := s.DB.NamedExec(sql, k) - return err + if err == sql.ErrNoRows || t1 == nil || t2 == nil { + // fallback to fresh sync + return s.SyncFresh(ctx, ex, symbol, interval, since, until) + } + + return s.SyncPartial(ctx, ex, symbol, interval, since, until) } -func (s *BacktestService) SyncExist(ctx context.Context, exchange types.Exchange, symbol string, - fromTime time.Time, endTime time.Time, interval types.Interval) error { - klineC, errC := s.QueryKLinesCh(fromTime, endTime, exchange, []string{symbol}, []types.Interval{interval}) +// SyncPartial +// find the existing data time range (t1, t2) +// scan if there is a missing part +// create a time range slice []TimeRange +// iterate the []TimeRange slice to sync data. +func (s *BacktestService) SyncPartial(ctx context.Context, ex types.Exchange, symbol string, interval types.Interval, since, until time.Time) error { + log.Infof("starting partial sync %s %s %s: %s <=> %s", ex.Name(), symbol, interval, since, until) - nowStartTime := fromTime - for k := range klineC { - if nowStartTime.Unix() < k.StartTime.Unix() { - log.Infof("syncing %s interval %s syncing %s ~ %s ", symbol, interval, nowStartTime, k.EndTime) - s.Sync(ctx, exchange, symbol, nowStartTime, k.EndTime.Time().Add(-1*interval.Duration()), interval) - } - nowStartTime = k.StartTime.Time().Add(interval.Duration()) + t1, t2, err := s.QueryExistingDataRange(ctx, ex, symbol, interval, since, until) + if err != nil && err != sql.ErrNoRows { + return err } - if nowStartTime.Unix() < endTime.Unix() && nowStartTime.Unix() < time.Now().Unix() { - s.Sync(ctx, exchange, symbol, nowStartTime, endTime, interval) + if err == sql.ErrNoRows || t1 == nil || t2 == nil { + // fallback to fresh sync + return s.SyncFresh(ctx, ex, symbol, interval, since, until) } - if err := <-errC; err != nil { + timeRanges, err := s.FindMissingTimeRanges(ctx, ex, symbol, interval, t1.Time(), t2.Time()) + if err != nil { return err } + + if len(timeRanges) > 0 { + log.Infof("found missing data time ranges: %v", timeRanges) + } + + // there are few cases: + // t1 == since && t2 == until + // [since] ------- [t1] data [t2] ------ [until] + if since.Before(t1.Time()) && t1.Time().Sub(since) > interval.Duration() { + // shift slice + timeRanges = append([]TimeRange{ + {Start: since.Add(-2 * time.Second), End: t1.Time()}, // we should include since + }, timeRanges...) + } + + if t2.Time().Before(until) && until.Sub(t2.Time()) > interval.Duration() { + timeRanges = append(timeRanges, TimeRange{ + Start: t2.Time(), + End: until.Add(-interval.Duration()), // include until + }) + } + + for _, timeRange := range timeRanges { + err = s.SyncKLineByInterval(ctx, ex, symbol, interval, timeRange.Start.Add(time.Second), timeRange.End.Add(-time.Second)) + if err != nil { + return err + } + } + return nil } + +// FindMissingTimeRanges returns the missing time ranges, the start/end time represents the existing data time points. +// So when sending kline query to the exchange API, we need to add one second to the start time and minus one second to the end time. +func (s *BacktestService) FindMissingTimeRanges(ctx context.Context, ex types.Exchange, symbol string, interval types.Interval, since, until time.Time) ([]TimeRange, error) { + query := SelectKLineTimePoints(ex.Name(), symbol, interval, since, until) + sql, args, err := query.ToSql() + if err != nil { + return nil, err + } + + rows, err := s.DB.QueryContext(ctx, sql, args...) + defer rows.Close() + if err != nil { + return nil, err + } + + var timeRanges []TimeRange + var lastTime = since + var intervalDuration = interval.Duration() + for rows.Next() { + var tt types.Time + if err := rows.Scan(&tt); err != nil { + return nil, err + } + + var t = time.Time(tt) + if t.Sub(lastTime) > intervalDuration { + timeRanges = append(timeRanges, TimeRange{ + Start: lastTime, + End: t, + }) + } + + lastTime = t + } + + if lastTime.Before(until) && until.Sub(lastTime) > intervalDuration { + timeRanges = append(timeRanges, TimeRange{ + Start: lastTime, + End: until, + }) + } + + return timeRanges, nil +} + +func (s *BacktestService) QueryExistingDataRange(ctx context.Context, ex types.Exchange, symbol string, interval types.Interval, tArgs ...time.Time) (start, end *types.Time, err error) { + sel := SelectKLineTimeRange(ex.Name(), symbol, interval, tArgs...) + sql, args, err := sel.ToSql() + if err != nil { + return nil, nil, err + } + + var t1, t2 types.Time + + row := s.DB.QueryRowContext(ctx, sql, args...) + + if err := row.Scan(&t1, &t2); err != nil { + return nil, nil, err + } + + if err := row.Err(); err != nil { + return nil, nil, err + } + + if t1 == (types.Time{}) || t2 == (types.Time{}) { + return nil, nil, nil + } + + return &t1, &t2, nil +} + +func SelectKLineTimePoints(ex types.ExchangeName, symbol string, interval types.Interval, args ...time.Time) sq.SelectBuilder { + conditions := sq.And{ + sq.Eq{"symbol": symbol}, + sq.Eq{"`interval`": interval.String()}, + } + + if len(args) == 2 { + since := args[0] + until := args[1] + conditions = append(conditions, sq.Expr("`start_time` BETWEEN ? AND ?", since, until)) + } + + tableName := targetKlineTable(ex) + + return sq.Select("start_time"). + From(tableName). + Where(conditions). + OrderBy("start_time ASC") +} + +// SelectKLineTimeRange returns the existing klines time range (since < kline.start_time < until) +func SelectKLineTimeRange(ex types.ExchangeName, symbol string, interval types.Interval, args ...time.Time) sq.SelectBuilder { + conditions := sq.And{ + sq.Eq{"symbol": symbol}, + sq.Eq{"`interval`": interval.String()}, + } + + if len(args) == 2 { + // NOTE + // sqlite does not support timezone format, so we are converting to local timezone + // mysql works in this case, so this is a workaround + since := args[0] + until := args[1] + conditions = append(conditions, sq.Expr("`start_time` BETWEEN ? AND ?", since, until)) + } + + tableName := targetKlineTable(ex) + + return sq.Select("MIN(start_time) AS t1, MAX(start_time) AS t2"). + From(tableName). + Where(conditions) +} + +// TODO: add is_futures column since the klines data is different +func SelectLastKLines(ex types.ExchangeName, symbol string, interval types.Interval, startTime, endTime time.Time, limit uint64) sq.SelectBuilder { + tableName := targetKlineTable(ex) + return sq.Select("*"). + From(tableName). + Where(sq.And{ + sq.Eq{"symbol": symbol}, + sq.Eq{"`interval`": interval.String()}, + sq.Expr("start_time BETWEEN ? AND ?", startTime, endTime), + }). + OrderBy("start_time DESC"). + Limit(limit) +} diff --git a/pkg/service/backtest_test.go b/pkg/service/backtest_test.go new file mode 100644 index 0000000000..ca863e2871 --- /dev/null +++ b/pkg/service/backtest_test.go @@ -0,0 +1,169 @@ +package service + +import ( + "context" + "database/sql" + "testing" + "time" + + "github.com/jmoiron/sqlx" + log "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/exchange" + "github.com/c9s/bbgo/pkg/types" +) + +func TestBacktestService_FindMissingTimeRanges_EmptyData(t *testing.T) { + db, err := prepareDB(t) + if err != nil { + t.Fatal(err) + } + + defer db.Close() + + ctx := context.Background() + dbx := sqlx.NewDb(db.DB, "sqlite3") + + ex, err := exchange.NewPublic(types.ExchangeBinance) + assert.NoError(t, err) + + service := &BacktestService{DB: dbx} + + symbol := "BTCUSDT" + now := time.Now() + startTime1 := now.AddDate(0, 0, -7).Truncate(time.Hour) + endTime1 := now.AddDate(0, 0, -6).Truncate(time.Hour) + timeRanges, err := service.FindMissingTimeRanges(ctx, ex, symbol, types.Interval1h, startTime1, endTime1) + assert.NoError(t, err) + assert.NotEmpty(t, timeRanges) +} + +func TestBacktestService_QueryExistingDataRange(t *testing.T) { + db, err := prepareDB(t) + if err != nil { + t.Fatal(err) + } + + defer db.Close() + + ctx := context.Background() + dbx := sqlx.NewDb(db.DB, "sqlite3") + + ex, err := exchange.NewPublic(types.ExchangeBinance) + assert.NoError(t, err) + + service := &BacktestService{DB: dbx} + + symbol := "BTCUSDT" + now := time.Now() + startTime1 := now.AddDate(0, 0, -7).Truncate(time.Hour) + endTime1 := now.AddDate(0, 0, -6).Truncate(time.Hour) + // empty range + t1, t2, err := service.QueryExistingDataRange(ctx, ex, symbol, types.Interval1h, startTime1, endTime1) + assert.Error(t, sql.ErrNoRows, err) + assert.Nil(t, t1) + assert.Nil(t, t2) +} + +func TestBacktestService_SyncPartial(t *testing.T) { + db, err := prepareDB(t) + if err != nil { + t.Fatal(err) + } + + defer db.Close() + + ctx := context.Background() + dbx := sqlx.NewDb(db.DB, "sqlite3") + + ex, err := exchange.NewPublic(types.ExchangeBinance) + assert.NoError(t, err) + + service := &BacktestService{DB: dbx} + + symbol := "BTCUSDT" + now := time.Now() + startTime1 := now.AddDate(0, 0, -7).Truncate(time.Hour) + endTime1 := now.AddDate(0, 0, -6).Truncate(time.Hour) + + startTime2 := now.AddDate(0, 0, -5).Truncate(time.Hour) + endTime2 := now.AddDate(0, 0, -4).Truncate(time.Hour) + + // kline query is exclusive + err = service.SyncKLineByInterval(ctx, ex, symbol, types.Interval1h, startTime1.Add(-time.Second), endTime1.Add(time.Second)) + assert.NoError(t, err) + + err = service.SyncKLineByInterval(ctx, ex, symbol, types.Interval1h, startTime2.Add(-time.Second), endTime2.Add(time.Second)) + assert.NoError(t, err) + + timeRanges, err := service.FindMissingTimeRanges(ctx, ex, symbol, types.Interval1h, startTime1, endTime2) + assert.NoError(t, err) + assert.NotEmpty(t, timeRanges) + assert.Len(t, timeRanges, 1) + + t.Run("fill missing time ranges", func(t *testing.T) { + err = service.SyncPartial(ctx, ex, symbol, types.Interval1h, startTime1, endTime2) + assert.NoError(t, err, "sync partial should not return error") + + timeRanges2, err := service.FindMissingTimeRanges(ctx, ex, symbol, types.Interval1h, startTime1, endTime2) + assert.NoError(t, err) + assert.Empty(t, timeRanges2) + }) +} + +func TestBacktestService_FindMissingTimeRanges(t *testing.T) { + db, err := prepareDB(t) + if err != nil { + t.Fatal(err) + } + + defer db.Close() + + ctx := context.Background() + dbx := sqlx.NewDb(db.DB, "sqlite3") + + ex, err := exchange.NewPublic(types.ExchangeBinance) + assert.NoError(t, err) + + service := &BacktestService{DB: dbx} + + symbol := "BTCUSDT" + now := time.Now() + startTime1 := now.AddDate(0, 0, -6).Truncate(time.Hour) + endTime1 := now.AddDate(0, 0, -5).Truncate(time.Hour) + + startTime2 := now.AddDate(0, 0, -4).Truncate(time.Hour) + endTime2 := now.AddDate(0, 0, -3).Truncate(time.Hour) + + // kline query is exclusive + err = service.SyncKLineByInterval(ctx, ex, symbol, types.Interval1h, startTime1.Add(-time.Second), endTime1.Add(time.Second)) + assert.NoError(t, err) + + err = service.SyncKLineByInterval(ctx, ex, symbol, types.Interval1h, startTime2.Add(-time.Second), endTime2.Add(time.Second)) + assert.NoError(t, err) + + t1, t2, err := service.QueryExistingDataRange(ctx, ex, symbol, types.Interval1h) + if assert.NoError(t, err) { + assert.Equal(t, startTime1, t1.Time(), "start time point should match") + assert.Equal(t, endTime2, t2.Time(), "end time point should match") + } + + timeRanges, err := service.FindMissingTimeRanges(ctx, ex, symbol, types.Interval1h, startTime1, endTime2) + if assert.NoError(t, err) { + assert.NotEmpty(t, timeRanges) + assert.Len(t, timeRanges, 1, "should find one missing time range") + t.Logf("found timeRanges: %+v", timeRanges) + + log.SetLevel(log.DebugLevel) + + for _, timeRange := range timeRanges { + err = service.SyncKLineByInterval(ctx, ex, symbol, types.Interval1h, timeRange.Start.Add(time.Second), timeRange.End.Add(-time.Second)) + assert.NoError(t, err) + } + + timeRanges, err = service.FindMissingTimeRanges(ctx, ex, symbol, types.Interval1h, startTime1, endTime2) + assert.NoError(t, err) + assert.Empty(t, timeRanges, "after partial sync, missing time ranges should be back-filled") + } +} diff --git a/pkg/service/database.go b/pkg/service/database.go index 0a71b7a91f..3719b82311 100644 --- a/pkg/service/database.go +++ b/pkg/service/database.go @@ -11,6 +11,9 @@ import ( sqlite3Migrations "github.com/c9s/bbgo/pkg/migrations/sqlite3" ) +// reflect cache for database +var dbCache = NewReflectCache() + type DatabaseService struct { Driver string DSN string @@ -40,6 +43,12 @@ func (s *DatabaseService) Connect() error { return err } +func (s *DatabaseService) Insert(record interface{}) error { + sql := dbCache.InsertSqlOf(record) + _, err := s.DB.NamedExec(sql, record) + return err +} + func (s *DatabaseService) Close() error { return s.DB.Close() } diff --git a/pkg/service/db_test.go b/pkg/service/db_test.go index 34029a799e..3093f7dd1b 100644 --- a/pkg/service/db_test.go +++ b/pkg/service/db_test.go @@ -38,7 +38,7 @@ func prepareDB(t *testing.T) (*rockhopper.DB, error) { ctx := context.Background() err = rockhopper.Up(ctx, db, migrations, 0, 0) - assert.NoError(t, err) + assert.NoError(t, err, "should migrate successfully") return db, err } diff --git a/pkg/service/deposit.go b/pkg/service/deposit.go index aff2bf77df..6b49cce136 100644 --- a/pkg/service/deposit.go +++ b/pkg/service/deposit.go @@ -4,8 +4,11 @@ import ( "context" "time" + sq "github.com/Masterminds/squirrel" "github.com/jmoiron/sqlx" + "github.com/c9s/bbgo/pkg/exchange" + "github.com/c9s/bbgo/pkg/exchange/batch" "github.com/c9s/bbgo/pkg/types" ) @@ -14,41 +17,45 @@ type DepositService struct { } // Sync syncs the withdraw records into db -func (s *DepositService) Sync(ctx context.Context, ex types.Exchange) error { - txnIDs := map[string]struct{}{} - - // query descending - records, err := s.QueryLast(ex.Name(), 10) - if err != nil { - return err - } - - for _, record := range records { - txnIDs[record.TransactionID] = struct{}{} +func (s *DepositService) Sync(ctx context.Context, ex types.Exchange, startTime time.Time) error { + isMargin, isFutures, isIsolated, _ := exchange.GetSessionAttributes(ex) + if isMargin || isFutures || isIsolated { + // only works in spot + return nil } transferApi, ok := ex.(types.ExchangeTransferService) if !ok { - return ErrNotImplemented + return nil } - since := time.Time{} - if len(records) > 0 { - since = records[len(records)-1].Time.Time() + tasks := []SyncTask{ + { + Type: types.Deposit{}, + Select: SelectLastDeposits(ex.Name(), 100), + BatchQuery: func(ctx context.Context, startTime, endTime time.Time) (interface{}, chan error) { + query := &batch.DepositBatchQuery{ + ExchangeTransferService: transferApi, + } + return query.Query(ctx, "", startTime, endTime) + }, + Time: func(obj interface{}) time.Time { + return obj.(types.Deposit).Time.Time() + }, + ID: func(obj interface{}) string { + deposit := obj.(types.Deposit) + return deposit.TransactionID + }, + Filter: func(obj interface{}) bool { + deposit := obj.(types.Deposit) + return len(deposit.TransactionID) != 0 + }, + LogInsert: true, + }, } - // asset "" means all assets - deposits, err := transferApi.QueryDepositHistory(ctx, "", since, time.Now()) - if err != nil { - return err - } - - for _, deposit := range deposits { - if _, exists := txnIDs[deposit.TransactionID]; exists { - continue - } - - if err := s.Insert(deposit); err != nil { + for _, sel := range tasks { + if err := sel.execute(ctx, s.DB, startTime); err != nil { return err } } @@ -56,20 +63,6 @@ func (s *DepositService) Sync(ctx context.Context, ex types.Exchange) error { return nil } -func (s *DepositService) QueryLast(ex types.ExchangeName, limit int) ([]types.Deposit, error) { - sql := "SELECT * FROM `deposits` WHERE `exchange` = :exchange ORDER BY `time` DESC LIMIT :limit" - rows, err := s.DB.NamedQuery(sql, map[string]interface{}{ - "exchange": ex, - "limit": limit, - }) - if err != nil { - return nil, err - } - - defer rows.Close() - return s.scanRows(rows) -} - func (s *DepositService) Query(exchangeName types.ExchangeName) ([]types.Deposit, error) { args := map[string]interface{}{ "exchange": exchangeName, @@ -98,9 +91,12 @@ func (s *DepositService) scanRows(rows *sqlx.Rows) (deposits []types.Deposit, er return deposits, rows.Err() } -func (s *DepositService) Insert(deposit types.Deposit) error { - sql := `INSERT INTO deposits (exchange, asset, address, amount, txn_id, time) - VALUES (:exchange, :asset, :address, :amount, :txn_id, :time)` - _, err := s.DB.NamedExec(sql, deposit) - return err +func SelectLastDeposits(ex types.ExchangeName, limit uint64) sq.SelectBuilder { + return sq.Select("*"). + From("deposits"). + Where(sq.And{ + sq.Eq{"exchange": ex}, + }). + OrderBy("time DESC"). + Limit(limit) } diff --git a/pkg/service/deposit_test.go b/pkg/service/deposit_test.go index d3d60114f6..6d43c3366c 100644 --- a/pkg/service/deposit_test.go +++ b/pkg/service/deposit_test.go @@ -1,39 +1 @@ package service - -import ( - "testing" - "time" - - "github.com/jmoiron/sqlx" - "github.com/stretchr/testify/assert" - - "github.com/c9s/bbgo/pkg/types" - "github.com/c9s/bbgo/pkg/fixedpoint" -) - -func TestDepositService(t *testing.T) { - db, err := prepareDB(t) - if err != nil { - t.Fatal(err) - } - - defer db.Close() - - xdb := sqlx.NewDb(db.DB, "sqlite3") - service := &DepositService{DB: xdb} - - err = service.Insert(types.Deposit{ - Exchange: types.ExchangeMax, - Time: types.Time(time.Now()), - Amount: fixedpoint.NewFromFloat(0.001), - Asset: "BTC", - Address: "test", - TransactionID: "02", - Status: types.DepositSuccess, - }) - assert.NoError(t, err) - - deposits, err := service.Query(types.ExchangeMax) - assert.NoError(t, err) - assert.NotEmpty(t, deposits) -} diff --git a/pkg/service/margin.go b/pkg/service/margin.go new file mode 100644 index 0000000000..1794712f4e --- /dev/null +++ b/pkg/service/margin.go @@ -0,0 +1,147 @@ +package service + +import ( + "context" + "strconv" + "time" + + sq "github.com/Masterminds/squirrel" + "github.com/jmoiron/sqlx" + + "github.com/c9s/bbgo/pkg/exchange/batch" + "github.com/c9s/bbgo/pkg/types" +) + +type MarginService struct { + DB *sqlx.DB +} + +func (s *MarginService) Sync(ctx context.Context, ex types.Exchange, asset string, startTime time.Time) error { + api, ok := ex.(types.MarginHistory) + if !ok { + return nil + } + + marginExchange, ok := ex.(types.MarginExchange) + if !ok { + return nil + } + + marginSettings := marginExchange.GetMarginSettings() + if !marginSettings.IsMargin { + return nil + } + + tasks := []SyncTask{ + { + Select: SelectLastMarginLoans(ex.Name(), 100), + Type: types.MarginLoan{}, + BatchQuery: func(ctx context.Context, startTime, endTime time.Time) (interface{}, chan error) { + query := &batch.MarginLoanBatchQuery{ + MarginHistory: api, + } + return query.Query(ctx, asset, startTime, endTime) + }, + Time: func(obj interface{}) time.Time { + return obj.(types.MarginLoan).Time.Time() + }, + ID: func(obj interface{}) string { + return strconv.FormatUint(obj.(types.MarginLoan).TransactionID, 10) + }, + LogInsert: true, + }, + { + Select: SelectLastMarginRepays(ex.Name(), 100), + Type: types.MarginRepay{}, + BatchQuery: func(ctx context.Context, startTime, endTime time.Time) (interface{}, chan error) { + query := &batch.MarginRepayBatchQuery{ + MarginHistory: api, + } + return query.Query(ctx, asset, startTime, endTime) + }, + Time: func(obj interface{}) time.Time { + return obj.(types.MarginRepay).Time.Time() + }, + ID: func(obj interface{}) string { + return strconv.FormatUint(obj.(types.MarginRepay).TransactionID, 10) + }, + LogInsert: true, + }, + { + Select: SelectLastMarginInterests(ex.Name(), 100), + Type: types.MarginInterest{}, + BatchQuery: func(ctx context.Context, startTime, endTime time.Time) (interface{}, chan error) { + query := &batch.MarginInterestBatchQuery{ + MarginHistory: api, + } + return query.Query(ctx, asset, startTime, endTime) + }, + Time: func(obj interface{}) time.Time { + return obj.(types.MarginInterest).Time.Time() + }, + ID: func(obj interface{}) string { + m := obj.(types.MarginInterest) + return m.Asset + m.IsolatedSymbol + strconv.FormatInt(m.Time.UnixMilli(), 10) + }, + LogInsert: true, + }, + { + Select: SelectLastMarginLiquidations(ex.Name(), 100), + Type: types.MarginLiquidation{}, + BatchQuery: func(ctx context.Context, startTime, endTime time.Time) (interface{}, chan error) { + query := &batch.MarginLiquidationBatchQuery{ + MarginHistory: api, + } + return query.Query(ctx, startTime, endTime) + }, + Time: func(obj interface{}) time.Time { + return obj.(types.MarginLiquidation).UpdatedTime.Time() + }, + ID: func(obj interface{}) string { + m := obj.(types.MarginLiquidation) + return strconv.FormatUint(m.OrderID, 10) + }, + LogInsert: true, + }, + } + + for _, sel := range tasks { + if err := sel.execute(ctx, s.DB, startTime); err != nil { + return err + } + } + + return nil +} + +func SelectLastMarginLoans(ex types.ExchangeName, limit uint64) sq.SelectBuilder { + return sq.Select("*"). + From("margin_loans"). + Where(sq.Eq{"exchange": ex}). + OrderBy("time DESC"). + Limit(limit) +} + +func SelectLastMarginRepays(ex types.ExchangeName, limit uint64) sq.SelectBuilder { + return sq.Select("*"). + From("margin_repays"). + Where(sq.Eq{"exchange": ex}). + OrderBy("time DESC"). + Limit(limit) +} + +func SelectLastMarginInterests(ex types.ExchangeName, limit uint64) sq.SelectBuilder { + return sq.Select("*"). + From("margin_interests"). + Where(sq.Eq{"exchange": ex}). + OrderBy("time DESC"). + Limit(limit) +} + +func SelectLastMarginLiquidations(ex types.ExchangeName, limit uint64) sq.SelectBuilder { + return sq.Select("*"). + From("margin_liquidations"). + Where(sq.Eq{"exchange": ex}). + OrderBy("time DESC"). + Limit(limit) +} diff --git a/pkg/service/margin_test.go b/pkg/service/margin_test.go new file mode 100644 index 0000000000..5fa85265d2 --- /dev/null +++ b/pkg/service/margin_test.go @@ -0,0 +1,52 @@ +package service + +import ( + "context" + "testing" + "time" + + "github.com/jmoiron/sqlx" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/exchange/binance" + "github.com/c9s/bbgo/pkg/testutil" +) + +func TestMarginService(t *testing.T) { + key, secret, ok := testutil.IntegrationTestConfigured(t, "BINANCE") + if !ok { + t.SkipNow() + return + } + + ex := binance.New(key, secret) + ex.MarginSettings.IsMargin = true + ex.MarginSettings.IsIsolatedMargin = true + ex.MarginSettings.IsolatedMarginSymbol = "DOTUSDT" + + logrus.SetLevel(logrus.ErrorLevel) + db, err := prepareDB(t) + + assert.NoError(t, err) + + if err != nil { + t.Fail() + return + } + + defer db.Close() + + ctx := context.Background() + + dbx := sqlx.NewDb(db.DB, "sqlite3") + service := &MarginService{DB: dbx} + + logrus.SetLevel(logrus.DebugLevel) + err = service.Sync(ctx, ex, "USDT", time.Date(2022, time.February, 1, 0, 0, 0, 0, time.UTC)) + assert.NoError(t, err) + + // sync second time to ensure that we can query records + err = service.Sync(ctx, ex, "USDT", time.Date(2022, time.February, 1, 0, 0, 0, 0, time.UTC)) + assert.NoError(t, err) +} diff --git a/pkg/service/memory.go b/pkg/service/memory.go index a23500aca0..92ee9f6cdc 100644 --- a/pkg/service/memory.go +++ b/pkg/service/memory.go @@ -36,7 +36,8 @@ func (store *MemoryStore) Save(val interface{}) error { func (store *MemoryStore) Load(val interface{}) error { v := reflect.ValueOf(val) if data, ok := store.memory.Slots[store.Key]; ok { - v.Elem().Set(reflect.ValueOf(data).Elem()) + dataRV := reflect.ValueOf(data) + v.Elem().Set(dataRV) } else { return ErrPersistenceNotExists } diff --git a/pkg/service/memory_test.go b/pkg/service/memory_test.go index 3accd9608e..e6106d78b0 100644 --- a/pkg/service/memory_test.go +++ b/pkg/service/memory_test.go @@ -21,7 +21,7 @@ func TestMemoryService(t *testing.T) { store := service.NewStore("test") i := 3 - err := store.Save(&i) + err := store.Save(i) assert.NoError(t, err) diff --git a/pkg/service/order.go b/pkg/service/order.go index 7d68350223..8cdff47ec1 100644 --- a/pkg/service/order.go +++ b/pkg/service/order.go @@ -6,10 +6,11 @@ import ( "strings" "time" + sq "github.com/Masterminds/squirrel" "github.com/jmoiron/sqlx" - "github.com/pkg/errors" log "github.com/sirupsen/logrus" + exchange2 "github.com/c9s/bbgo/pkg/exchange" "github.com/c9s/bbgo/pkg/exchange/batch" "github.com/c9s/bbgo/pkg/types" ) @@ -19,100 +20,83 @@ type OrderService struct { } func (s *OrderService) Sync(ctx context.Context, exchange types.Exchange, symbol string, startTime time.Time) error { - isMargin := false - isFutures := false - isIsolated := false - - if marginExchange, ok := exchange.(types.MarginExchange); ok { - marginSettings := marginExchange.GetMarginSettings() - isMargin = marginSettings.IsMargin - isIsolated = marginSettings.IsIsolatedMargin - if marginSettings.IsIsolatedMargin { - symbol = marginSettings.IsolatedMarginSymbol - } + isMargin, isFutures, isIsolated, isolatedSymbol := exchange2.GetSessionAttributes(exchange) + // override symbol if isolatedSymbol is not empty + if isIsolated && len(isolatedSymbol) > 0 { + symbol = isolatedSymbol } - if futuresExchange, ok := exchange.(types.FuturesExchange); ok { - futuresSettings := futuresExchange.GetFuturesSettings() - isFutures = futuresSettings.IsFutures - isIsolated = futuresSettings.IsIsolatedFutures - if futuresSettings.IsIsolatedFutures { - symbol = futuresSettings.IsolatedFuturesSymbol - } + api, ok := exchange.(types.ExchangeTradeHistoryService) + if !ok { + return nil } - records, err := s.QueryLast(exchange.Name(), symbol, isMargin, isFutures, isIsolated, 50) - if err != nil { - return err - } - - orderKeys := make(map[uint64]struct{}) - - var lastID uint64 = 0 - if len(records) > 0 { - for _, record := range records { - orderKeys[record.OrderID] = struct{}{} - } - - lastID = records[0].OrderID - startTime = records[0].CreationTime.Time() + lastOrderID := uint64(0) + tasks := []SyncTask{ + { + Type: types.Order{}, + Time: func(obj interface{}) time.Time { + return obj.(types.Order).CreationTime.Time() + }, + ID: func(obj interface{}) string { + order := obj.(types.Order) + return strconv.FormatUint(order.OrderID, 10) + }, + Select: SelectLastOrders(exchange.Name(), symbol, isMargin, isFutures, isIsolated, 100), + OnLoad: func(objs interface{}) { + // update last order ID + orders := objs.([]types.Order) + if len(orders) > 0 { + end := len(orders) - 1 + last := orders[end] + lastOrderID = last.OrderID + } + }, + BatchQuery: func(ctx context.Context, startTime, endTime time.Time) (interface{}, chan error) { + query := &batch.ClosedOrderBatchQuery{ + ExchangeTradeHistoryService: api, + } + + return query.Query(ctx, symbol, startTime, endTime, lastOrderID) + }, + Filter: func(obj interface{}) bool { + // skip canceled and not filled orders + order := obj.(types.Order) + if order.Status == types.OrderStatusCanceled && order.ExecutedQuantity.IsZero() { + return false + } + + return true + }, + Insert: func(obj interface{}) error { + order := obj.(types.Order) + return s.Insert(order) + }, + LogInsert: true, + }, } - b := &batch.ClosedOrderBatchQuery{Exchange: exchange} - ordersC, errC := b.Query(ctx, symbol, startTime, time.Now(), lastID) - for order := range ordersC { - select { - - case <-ctx.Done(): - return ctx.Err() - - case err := <-errC: - if err != nil { - return err - } - - default: - - } - - if _, exists := orderKeys[order.OrderID]; exists { - continue - } - - // skip canceled and not filled orders - if order.Status == types.OrderStatusCanceled && order.ExecutedQuantity.IsZero() { - continue - } - - if err := s.Insert(order); err != nil { + for _, sel := range tasks { + if err := sel.execute(ctx, s.DB, startTime); err != nil { return err } } - return <-errC + return nil } - -// QueryLast queries the last order from the database -func (s *OrderService) QueryLast(ex types.ExchangeName, symbol string, isMargin, isFutures, isIsolated bool, limit int) ([]types.Order, error) { - log.Infof("querying last order exchange = %s AND symbol = %s AND is_margin = %v AND is_futures = %v AND is_isolated = %v", ex, symbol, isMargin, isFutures, isIsolated) - - sql := `SELECT * FROM orders WHERE exchange = :exchange AND symbol = :symbol AND is_margin = :is_margin AND is_futures = :is_futures AND is_isolated = :is_isolated ORDER BY gid DESC LIMIT :limit` - rows, err := s.DB.NamedQuery(sql, map[string]interface{}{ - "exchange": ex, - "symbol": symbol, - "is_margin": isMargin, - "is_futures": isFutures, - "is_isolated": isIsolated, - "limit": limit, - }) - - if err != nil { - return nil, errors.Wrap(err, "query last order error") - } - - defer rows.Close() - return s.scanRows(rows) +func SelectLastOrders(ex types.ExchangeName, symbol string, isMargin, isFutures, isIsolated bool, limit uint64) sq.SelectBuilder { + return sq.Select("*"). + From("orders"). + Where(sq.And{ + sq.Eq{"symbol": symbol}, + sq.Eq{"exchange": ex}, + sq.Eq{"is_margin": isMargin}, + sq.Eq{"is_futures": isFutures}, + sq.Eq{"is_isolated": isIsolated}, + }). + OrderBy("created_at DESC"). + Limit(limit) } type AggOrder struct { diff --git a/pkg/service/persistence_json.go b/pkg/service/persistence_json.go index 20ce52fa29..3bea745567 100644 --- a/pkg/service/persistence_json.go +++ b/pkg/service/persistence_json.go @@ -38,7 +38,7 @@ func (store JsonStore) Reset() error { func (store JsonStore) Load(val interface{}) error { if _, err := os.Stat(store.Directory); os.IsNotExist(err) { - if err2 := os.Mkdir(store.Directory, 0777); err2 != nil { + if err2 := os.MkdirAll(store.Directory, 0777); err2 != nil { return err2 } } @@ -63,7 +63,7 @@ func (store JsonStore) Load(val interface{}) error { func (store JsonStore) Save(val interface{}) error { if _, err := os.Stat(store.Directory); os.IsNotExist(err) { - if err2 := os.Mkdir(store.Directory, 0777); err2 != nil { + if err2 := os.MkdirAll(store.Directory, 0777); err2 != nil { return err2 } } @@ -76,4 +76,3 @@ func (store JsonStore) Save(val interface{}) error { p := filepath.Join(store.Directory, store.ID) + ".json" return ioutil.WriteFile(p, data, 0666) } - diff --git a/pkg/service/persistence_redis.go b/pkg/service/persistence_redis.go index 29a9f233ec..6b91d05832 100644 --- a/pkg/service/persistence_redis.go +++ b/pkg/service/persistence_redis.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/go-redis/redis/v8" + log "github.com/sirupsen/logrus" ) type RedisPersistenceService struct { @@ -18,6 +19,7 @@ func NewRedisPersistenceService(config *RedisPersistenceConfig) *RedisPersistenc client := redis.NewClient(&redis.Options{ Addr: net.JoinHostPort(config.Host, config.Port), // Username: "", // username is only for redis 6.0 + // pragma: allowlist nextline secret Password: config.Password, // no password set DB: config.DB, // use default DB }) @@ -49,9 +51,11 @@ func (store *RedisStore) Load(val interface{}) error { return errors.New("can not load from redis, possible cause: redis persistence is not configured, or you are trying to use redis in back-test") } - cmd := store.redis.Get(context.Background(), store.ID) data, err := cmd.Result() + + log.Debugf("[redis] get key %q, data = %s", store.ID, string(data)) + if err != nil { if err == redis.Nil { return ErrPersistenceNotExists @@ -60,7 +64,8 @@ func (store *RedisStore) Load(val interface{}) error { return err } - if len(data) == 0 { + // skip null data + if len(data) == 0 || data == "null" { return ErrPersistenceNotExists } @@ -68,6 +73,10 @@ func (store *RedisStore) Load(val interface{}) error { } func (store *RedisStore) Save(val interface{}) error { + if val == nil { + return nil + } + data, err := json.Marshal(val) if err != nil { return err @@ -75,6 +84,9 @@ func (store *RedisStore) Save(val interface{}) error { cmd := store.redis.Set(context.Background(), store.ID, data, 0) _, err = cmd.Result() + + log.Debugf("[redis] set key %q, data = %s", store.ID, string(data)) + return err } diff --git a/pkg/service/profit.go b/pkg/service/profit.go index 15af10261c..9396e69535 100644 --- a/pkg/service/profit.go +++ b/pkg/service/profit.go @@ -13,10 +13,6 @@ type ProfitService struct { DB *sqlx.DB } -func NewProfitService(db *sqlx.DB) *ProfitService { - return &ProfitService{db} -} - func (s *ProfitService) Load(ctx context.Context, id int64) (*types.Trade, error) { var trade types.Trade diff --git a/pkg/service/reflect.go b/pkg/service/reflect.go new file mode 100644 index 0000000000..b60c0c3371 --- /dev/null +++ b/pkg/service/reflect.go @@ -0,0 +1,231 @@ +package service + +import ( + "context" + "reflect" + "strings" + + "github.com/Masterminds/squirrel" + "github.com/fatih/camelcase" + gopluralize "github.com/gertd/go-pluralize" + "github.com/jmoiron/sqlx" + "github.com/sirupsen/logrus" +) + +var pluralize = gopluralize.NewClient() + +func tableNameOf(record interface{}) string { + rt := reflect.TypeOf(record) + if rt.Kind() == reflect.Ptr { + rt = rt.Elem() + } + + typeName := rt.Name() + tableName := strings.Join(camelcase.Split(typeName), "_") + tableName = strings.ToLower(tableName) + return pluralize.Plural(tableName) +} + +func placeholdersOf(record interface{}) []string { + rt := reflect.TypeOf(record) + if rt.Kind() == reflect.Ptr { + rt = rt.Elem() + } + + if rt.Kind() != reflect.Struct { + return nil + } + + var dbFields []string + for i := 0; i < rt.NumField(); i++ { + fieldType := rt.Field(i) + if tag, ok := fieldType.Tag.Lookup("db"); ok { + if tag == "gid" { + continue + } + + dbFields = append(dbFields, ":"+tag) + } + } + + return dbFields +} + +func fieldsNamesOf(record interface{}) []string { + rt := reflect.TypeOf(record) + if rt.Kind() == reflect.Ptr { + rt = rt.Elem() + } + + if rt.Kind() != reflect.Struct { + return nil + } + + var dbFields []string + for i := 0; i < rt.NumField(); i++ { + fieldType := rt.Field(i) + if tag, ok := fieldType.Tag.Lookup("db"); ok { + if tag == "gid" { + continue + } + + dbFields = append(dbFields, tag) + } + } + + return dbFields +} + +func ParseStructTag(s string) (string, map[string]string) { + opts := make(map[string]string) + ss := strings.Split(s, ",") + if len(ss) > 1 { + for _, opt := range ss[1:] { + aa := strings.SplitN(opt, "=", 2) + if len(aa) == 2 { + opts[aa[0]] = aa[1] + } else { + opts[aa[0]] = "" + } + } + } + + return ss[0], opts +} + +type ReflectCache struct { + tableNames map[string]string + fields map[string][]string + placeholders map[string][]string + insertSqls map[string]string +} + +func NewReflectCache() *ReflectCache { + return &ReflectCache{ + tableNames: make(map[string]string), + fields: make(map[string][]string), + placeholders: make(map[string][]string), + insertSqls: make(map[string]string), + } +} + +func (c *ReflectCache) InsertSqlOf(t interface{}) string { + rt := reflect.TypeOf(t) + if rt.Kind() == reflect.Ptr { + rt = rt.Elem() + } + + typeName := rt.Name() + sql, ok := c.insertSqls[typeName] + if ok { + return sql + } + + tableName := dbCache.TableNameOf(t) + fields := dbCache.FieldsOf(t) + placeholders := dbCache.PlaceholderOf(t) + fieldClause := strings.Join(fields, ", ") + placeholderClause := strings.Join(placeholders, ", ") + + sql = `INSERT INTO ` + tableName + ` (` + fieldClause + `) VALUES (` + placeholderClause + `)` + c.insertSqls[typeName] = sql + return sql +} + +func (c *ReflectCache) TableNameOf(t interface{}) string { + rt := reflect.TypeOf(t) + if rt.Kind() == reflect.Ptr { + rt = rt.Elem() + } + + typeName := rt.Name() + tableName, ok := c.tableNames[typeName] + if ok { + return tableName + } + + tableName = tableNameOf(t) + c.tableNames[typeName] = tableName + return tableName +} + +func (c *ReflectCache) PlaceholderOf(t interface{}) []string { + rt := reflect.TypeOf(t) + if rt.Kind() == reflect.Ptr { + rt = rt.Elem() + } + typeName := rt.Name() + placeholders, ok := c.placeholders[typeName] + if ok { + return placeholders + } + + placeholders = placeholdersOf(t) + c.placeholders[typeName] = placeholders + return placeholders +} + +func (c *ReflectCache) FieldsOf(t interface{}) []string { + rt := reflect.TypeOf(t) + if rt.Kind() == reflect.Ptr { + rt = rt.Elem() + } + + typeName := rt.Name() + fields, ok := c.fields[typeName] + if ok { + return fields + } + + fields = fieldsNamesOf(t) + c.fields[typeName] = fields + return fields +} + +// scanRowsOfType use the given type to scan rows +// this is usually slower than the native one since it uses reflect. +func scanRowsOfType(rows *sqlx.Rows, tpe interface{}) (interface{}, error) { + refType := reflect.TypeOf(tpe) + + if refType.Kind() == reflect.Ptr { + refType = refType.Elem() + } + + sliceRef := reflect.MakeSlice(reflect.SliceOf(refType), 0, 100) + // sliceRef := reflect.New(reflect.SliceOf(refType)) + for rows.Next() { + var recordRef = reflect.New(refType) + var record = recordRef.Interface() + if err := rows.StructScan(record); err != nil { + return sliceRef.Interface(), err + } + + sliceRef = reflect.Append(sliceRef, recordRef.Elem()) + } + + return sliceRef.Interface(), rows.Err() +} + +func insertType(db *sqlx.DB, record interface{}) error { + sql := dbCache.InsertSqlOf(record) + _, err := db.NamedExec(sql, record) + return err +} + +func selectAndScanType(ctx context.Context, db *sqlx.DB, sel squirrel.SelectBuilder, tpe interface{}) (interface{}, error) { + sql, args, err := sel.ToSql() + if err != nil { + return nil, err + } + + logrus.Debugf("selectAndScanType: %T <- %s", tpe, sql) + logrus.Debugf("queryArgs: %v", args) + + rows, err := db.QueryxContext(ctx, sql, args...) + if err != nil { + return nil, err + } + + defer rows.Close() + return scanRowsOfType(rows, tpe) +} diff --git a/pkg/service/reflect_test.go b/pkg/service/reflect_test.go new file mode 100644 index 0000000000..9eb525ae94 --- /dev/null +++ b/pkg/service/reflect_test.go @@ -0,0 +1,71 @@ +package service + +import ( + "reflect" + "testing" + + "github.com/c9s/bbgo/pkg/types" +) + +func Test_tableNameOf(t *testing.T) { + type args struct { + record interface{} + } + tests := []struct { + name string + args args + want string + }{ + { + name: "MarginInterest", + args: args{record: &types.MarginInterest{}}, + want: "margin_interests", + }, + { + name: "MarginLoan", + args: args{record: &types.MarginLoan{}}, + want: "margin_loans", + }, + { + name: "MarginRepay", + args: args{record: &types.MarginRepay{}}, + want: "margin_repays", + }, + { + name: "MarginLiquidation", + args: args{record: &types.MarginLiquidation{}}, + want: "margin_liquidations", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tableNameOf(tt.args.record); got != tt.want { + t.Errorf("tableNameOf() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_fieldsNamesOf(t *testing.T) { + type args struct { + record interface{} + } + tests := []struct { + name string + args args + want []string + }{ + { + name: "MarginInterest", + args: args{record: &types.MarginInterest{}}, + want: []string{"exchange", "asset", "principle", "interest", "interest_rate", "isolated_symbol", "time"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := fieldsNamesOf(tt.args.record); !reflect.DeepEqual(got, tt.want) { + t.Errorf("fieldsNamesOf() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/service/reward.go b/pkg/service/reward.go index c90f73abbe..bf657a6010 100644 --- a/pkg/service/reward.go +++ b/pkg/service/reward.go @@ -7,9 +7,10 @@ import ( "strings" "time" + sq "github.com/Masterminds/squirrel" "github.com/jmoiron/sqlx" - "github.com/sirupsen/logrus" + exchange2 "github.com/c9s/bbgo/pkg/exchange" "github.com/c9s/bbgo/pkg/exchange/batch" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" @@ -23,78 +24,47 @@ type RewardService struct { DB *sqlx.DB } -func (s *RewardService) QueryLast(ex types.ExchangeName, limit int) ([]types.Reward, error) { - sql := "SELECT * FROM `rewards` WHERE `exchange` = :exchange ORDER BY `created_at` DESC LIMIT :limit" - rows, err := s.DB.NamedQuery(sql, map[string]interface{}{ - "exchange": ex, - "limit": limit, - }) - if err != nil { - return nil, err - } - - defer rows.Close() - return s.scanRows(rows) -} - -func (s *RewardService) Sync(ctx context.Context, exchange types.Exchange) error { - service, ok := exchange.(types.ExchangeRewardService) +func (s *RewardService) Sync(ctx context.Context, exchange types.Exchange, startTime time.Time) error { + api, ok := exchange.(types.ExchangeRewardService) if !ok { return ErrExchangeRewardServiceNotImplemented } - var rewardKeys = map[string]struct{}{} - - var startTime time.Time - - records, err := s.QueryLast(exchange.Name(), 50) - if err != nil { - return err - } - - if len(records) > 0 { - lastRecord := records[0] - startTime = lastRecord.CreatedAt.Time() - - for _, record := range records { - rewardKeys[record.UUID] = struct{}{} - } - } - - batchQuery := &batch.RewardBatchQuery{Service: service} - rewardsC, errC := batchQuery.Query(ctx, startTime, time.Now()) - - for reward := range rewardsC { - select { - - case <-ctx.Done(): - return ctx.Err() - - case err := <-errC: - if err != nil { - return err - } - - default: - - } - - if _, ok := rewardKeys[reward.UUID]; ok { - continue - } - - logrus.Infof("inserting reward: %s %s %s %f %s", reward.Exchange, reward.Type, reward.Currency, reward.Quantity.Float64(), reward.CreatedAt) - - if err := s.Insert(reward); err != nil { + isMargin, isFutures, _, _ := exchange2.GetSessionAttributes(exchange) + if isMargin || isFutures { + return nil + } + + tasks := []SyncTask{ + { + Type: types.Reward{}, + Select: SelectLastRewards(exchange.Name(), 100), + BatchQuery: func(ctx context.Context, startTime, endTime time.Time) (interface{}, chan error) { + query := &batch.RewardBatchQuery{ + Service: api, + } + return query.Query(ctx, startTime, endTime) + }, + Time: func(obj interface{}) time.Time { + return obj.(types.Reward).CreatedAt.Time() + }, + ID: func(obj interface{}) string { + reward := obj.(types.Reward) + return string(reward.Type) + "_" + reward.UUID + }, + LogInsert: true, + }, + } + + for _, sel := range tasks { + if err := sel.execute(ctx, s.DB, startTime); err != nil { return err } } - return <-errC + return nil } - - type CurrencyPositionMap map[string]fixedpoint.Value func (s *RewardService) AggregateUnspentCurrencyPosition(ctx context.Context, ex types.ExchangeName, since time.Time) (CurrencyPositionMap, error) { @@ -128,8 +98,8 @@ func (s *RewardService) QueryUnspentSince(ctx context.Context, ex types.Exchange sql += " ORDER BY created_at ASC" rows, err := s.DB.NamedQueryContext(ctx, sql, map[string]interface{}{ - "exchange": ex, - "since": since, + "exchange": ex, + "since": since, }) if err != nil { @@ -217,3 +187,13 @@ func (s *RewardService) Insert(reward types.Reward) error { reward) return err } + +func SelectLastRewards(ex types.ExchangeName, limit uint64) sq.SelectBuilder { + return sq.Select("*"). + From("rewards"). + Where(sq.And{ + sq.Eq{"exchange": ex}, + }). + OrderBy("created_at DESC"). + Limit(limit) +} diff --git a/pkg/service/reward_test.go b/pkg/service/reward_test.go index b136724c68..2485d81998 100644 --- a/pkg/service/reward_test.go +++ b/pkg/service/reward_test.go @@ -74,7 +74,6 @@ func TestRewardService_InsertAndQueryUnspent(t *testing.T) { assert.Equal(t, types.RewardCommission, rewards[0].Type) } - func TestRewardService_AggregateUnspentCurrencyPosition(t *testing.T) { db, err := prepareDB(t) if err != nil { @@ -126,7 +125,7 @@ func TestRewardService_AggregateUnspentCurrencyPosition(t *testing.T) { }) assert.NoError(t, err) - currencyPositions, err := service.AggregateUnspentCurrencyPosition(ctx, types.ExchangeMax, now.Add(-10 * time.Second)) + currencyPositions, err := service.AggregateUnspentCurrencyPosition(ctx, types.ExchangeMax, now.Add(-10*time.Second)) assert.NoError(t, err) assert.NotEmpty(t, currencyPositions) assert.Len(t, currencyPositions, 2) diff --git a/pkg/service/sync.go b/pkg/service/sync.go index 5db3b549b4..aaf757ddb2 100644 --- a/pkg/service/sync.go +++ b/pkg/service/sync.go @@ -10,7 +10,6 @@ import ( log "github.com/sirupsen/logrus" "github.com/c9s/bbgo/pkg/types" - "github.com/c9s/bbgo/pkg/util" ) var ErrNotImplemented = errors.New("not implemented") @@ -22,11 +21,7 @@ type SyncService struct { RewardService *RewardService WithdrawService *WithdrawService DepositService *DepositService -} - -func paperTrade() bool { - v, ok := util.GetEnvVarBool("PAPER_TRADE") - return ok && v + MarginService *MarginService } // SyncSessionSymbols syncs the trades from the given exchange session @@ -50,28 +45,52 @@ func (s *SyncService) SyncSessionSymbols(ctx context.Context, exchange types.Exc } } - if paperTrade() { + return nil +} + +func (s *SyncService) SyncMarginHistory(ctx context.Context, exchange types.Exchange, startTime time.Time, assets ...string) error { + if _, implemented := exchange.(types.MarginHistory); !implemented { + log.Debugf("exchange %T does not support types.MarginHistory", exchange) + return nil + } + + if marginExchange, implemented := exchange.(types.MarginExchange); !implemented { + log.Debugf("exchange %T does not implement types.MarginExchange", exchange) return nil + } else { + marginSettings := marginExchange.GetMarginSettings() + if !marginSettings.IsMargin { + log.Debugf("exchange %T is not using margin", exchange) + return nil + } + } + + log.Infof("syncing %s margin history: %v...", exchange.Name(), assets) + for _, asset := range assets { + if err := s.MarginService.Sync(ctx, exchange, asset, startTime); err != nil { + return err + } } return nil } -func (s *SyncService) SyncRewardHistory(ctx context.Context, exchange types.Exchange) error { +func (s *SyncService) SyncRewardHistory(ctx context.Context, exchange types.Exchange, startTime time.Time) error { + if _, implemented := exchange.(types.ExchangeRewardService); !implemented { + return nil + } + log.Infof("syncing %s reward records...", exchange.Name()) - if err := s.RewardService.Sync(ctx, exchange); err != nil { - if err != ErrExchangeRewardServiceNotImplemented { - log.Warnf("%s reward service is not supported", exchange.Name()) - return err - } + if err := s.RewardService.Sync(ctx, exchange, startTime); err != nil { + return err } return nil } -func (s *SyncService) SyncDepositHistory(ctx context.Context, exchange types.Exchange) error { +func (s *SyncService) SyncDepositHistory(ctx context.Context, exchange types.Exchange, startTime time.Time) error { log.Infof("syncing %s deposit records...", exchange.Name()) - if err := s.DepositService.Sync(ctx, exchange); err != nil { + if err := s.DepositService.Sync(ctx, exchange, startTime); err != nil { if err != ErrNotImplemented { log.Warnf("%s deposit service is not supported", exchange.Name()) return err @@ -81,9 +100,9 @@ func (s *SyncService) SyncDepositHistory(ctx context.Context, exchange types.Exc return nil } -func (s *SyncService) SyncWithdrawHistory(ctx context.Context, exchange types.Exchange) error { +func (s *SyncService) SyncWithdrawHistory(ctx context.Context, exchange types.Exchange, startTime time.Time) error { log.Infof("syncing %s withdraw records...", exchange.Name()) - if err := s.WithdrawService.Sync(ctx, exchange); err != nil { + if err := s.WithdrawService.Sync(ctx, exchange, startTime); err != nil { if err != ErrNotImplemented { log.Warnf("%s withdraw service is not supported", exchange.Name()) return err diff --git a/pkg/service/sync_task.go b/pkg/service/sync_task.go new file mode 100644 index 0000000000..8ccfc99804 --- /dev/null +++ b/pkg/service/sync_task.go @@ -0,0 +1,210 @@ +package service + +import ( + "context" + "reflect" + "sort" + "time" + + "github.com/Masterminds/squirrel" + "github.com/jmoiron/sqlx" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +// SyncTask defines the behaviors for syncing remote records +type SyncTask struct { + // Type is the element type of this sync task + // Since it will create a []Type slice from this type, you should not set pointer to this field + Type interface{} + + // ID is a function that returns the unique identity of the object + // This function will be used for detecting duplicated objects. + ID func(obj interface{}) string + + // Time is a function that returns the time of the object + // This function will be used for sorting records + Time func(obj interface{}) time.Time + + // Select is the select query builder for querying existing db records + // The built SQL will be used for querying existing db records. + // And then the ID function will be used for filtering duplicated object. + Select squirrel.SelectBuilder + + // OnLoad is an optional field, which is called when the records are loaded from the database + OnLoad func(objs interface{}) + + // Filter is an optional field, which is used for filtering the remote records + // Return true to keep the record, + // Return false to filter the record. + Filter func(obj interface{}) bool + + // BatchQuery is used for querying remote records. + BatchQuery func(ctx context.Context, startTime, endTime time.Time) (interface{}, chan error) + + // Insert is an option field, which is used for customizing the record insert + Insert func(obj interface{}) error + + // Insert is an option field, which is used for customizing the record batch insert + BatchInsert func(obj interface{}) error + + BatchInsertBuffer int + + // LogInsert logs the insert record in INFO level + LogInsert bool +} + +func (sel SyncTask) execute(ctx context.Context, db *sqlx.DB, startTime time.Time, args ...time.Time) error { + batchBufferRefVal := reflect.MakeSlice(reflect.SliceOf(reflect.TypeOf(sel.Type)), 0, sel.BatchInsertBuffer) + + // query from db + recordSlice, err := selectAndScanType(ctx, db, sel.Select, sel.Type) + if err != nil { + return err + } + + recordSliceRef := reflect.ValueOf(recordSlice) + if recordSliceRef.Kind() == reflect.Ptr { + recordSliceRef = recordSliceRef.Elem() + } + + logrus.Debugf("loaded %d %T records", recordSliceRef.Len(), sel.Type) + + ids := buildIdMap(sel, recordSliceRef) + + if err := sortRecordsAscending(sel, recordSliceRef); err != nil { + return err + } + + if sel.OnLoad != nil { + sel.OnLoad(recordSliceRef.Interface()) + } + + // default since time point + startTime = lastRecordTime(sel, recordSliceRef, startTime) + + endTime := time.Now() + if len(args) > 0 { + endTime = args[0] + } + + // asset "" means all assets + dataC, errC := sel.BatchQuery(ctx, startTime, endTime) + dataCRef := reflect.ValueOf(dataC) + + defer func() { + if sel.BatchInsert != nil && batchBufferRefVal.Len() > 0 { + slice := batchBufferRefVal.Interface() + if err := sel.BatchInsert(slice); err != nil { + logrus.WithError(err).Errorf("batch insert error: %+v", slice) + } + } + }() + + for { + select { + case <-ctx.Done(): + logrus.Warnf("context is cancelled, stop syncing") + return ctx.Err() + + default: + v, ok := dataCRef.Recv() + if !ok { + err := <-errC + return err + } + + obj := v.Interface() + id := sel.ID(obj) + if _, exists := ids[id]; exists { + logrus.Debugf("object %s already exists, skipping", id) + continue + } + + tt := sel.Time(obj) + if tt.Before(startTime) || tt.After(endTime) { + logrus.Debugf("object %s time %s is outside of the time range", id, tt) + continue + } + + if sel.Filter != nil { + if !sel.Filter(obj) { + logrus.Debugf("object %s is filtered", id) + continue + } + } + + ids[id] = struct{}{} + if sel.BatchInsert != nil { + if batchBufferRefVal.Len() > sel.BatchInsertBuffer-1 { + if sel.LogInsert { + logrus.Infof("batch inserting %d %T", batchBufferRefVal.Len(), obj) + } else { + logrus.Debugf("batch inserting %d %T", batchBufferRefVal.Len(), obj) + } + + if err := sel.BatchInsert(batchBufferRefVal.Interface()); err != nil { + return err + } + + batchBufferRefVal = reflect.MakeSlice(reflect.SliceOf(reflect.TypeOf(sel.Type)), 0, sel.BatchInsertBuffer) + } + batchBufferRefVal = reflect.Append(batchBufferRefVal, v) + } else { + if sel.LogInsert { + logrus.Infof("inserting %T: %+v", obj, obj) + } else { + logrus.Debugf("inserting %T: %+v", obj, obj) + } + if sel.Insert != nil { + // for custom insert + if err := sel.Insert(obj); err != nil { + logrus.WithError(err).Errorf("can not insert record: %v", obj) + return err + } + } else { + if err := insertType(db, obj); err != nil { + logrus.WithError(err).Errorf("can not insert record: %v", obj) + return err + } + } + } + } + } +} + +func lastRecordTime(sel SyncTask, recordSlice reflect.Value, defaultTime time.Time) time.Time { + since := defaultTime + length := recordSlice.Len() + if length > 0 { + last := recordSlice.Index(length - 1) + since = sel.Time(last.Interface()) + } + + return since +} + +func sortRecordsAscending(sel SyncTask, recordSlice reflect.Value) error { + if sel.Time == nil { + return errors.New("time field is not set, can not sort records") + } + + // always sort + sort.Slice(recordSlice.Interface(), func(i, j int) bool { + a := sel.Time(recordSlice.Index(i).Interface()) + b := sel.Time(recordSlice.Index(j).Interface()) + return a.Before(b) + }) + return nil +} + +func buildIdMap(sel SyncTask, recordSliceRef reflect.Value) map[string]struct{} { + ids := map[string]struct{}{} + for i := 0; i < recordSliceRef.Len(); i++ { + entryRef := recordSliceRef.Index(i) + id := sel.ID(entryRef.Interface()) + ids[id] = struct{}{} + } + + return ids +} diff --git a/pkg/service/totp.go b/pkg/service/totp.go index 8e58c7d51e..4193cc1e6b 100644 --- a/pkg/service/totp.go +++ b/pkg/service/totp.go @@ -1,12 +1,12 @@ package service import ( - "fmt" "os" "github.com/pkg/errors" "github.com/pquerna/otp" "github.com/pquerna/otp/totp" + log "github.com/sirupsen/logrus" "github.com/spf13/viper" ) @@ -39,7 +39,8 @@ func NewDefaultTotpKey() (*otp.Key, error) { } if !ok { - return nil, fmt.Errorf("can not get USER or USERNAME env var for totp account name") + log.Warnf("can not get USER or USERNAME env var, use default name 'bbgo' for totp account name") + user = "bbgo" } totpAccountName = user diff --git a/pkg/service/trade.go b/pkg/service/trade.go index 60640fe3cf..ae8379fa0d 100644 --- a/pkg/service/trade.go +++ b/pkg/service/trade.go @@ -2,15 +2,16 @@ package service import ( "context" - "fmt" "strconv" "strings" "time" + sq "github.com/Masterminds/squirrel" "github.com/jmoiron/sqlx" "github.com/pkg/errors" log "github.com/sirupsen/logrus" + exchange2 "github.com/c9s/bbgo/pkg/exchange" "github.com/c9s/bbgo/pkg/exchange/batch" "github.com/c9s/bbgo/pkg/types" ) @@ -19,12 +20,14 @@ var ErrTradeNotFound = errors.New("trade not found") type QueryTradesOptions struct { Exchange types.ExchangeName + Sessions []string Symbol string LastGID int64 + Since *time.Time // ASC or DESC Ordering string - Limit int + Limit uint64 } type TradingVolume struct { @@ -51,94 +54,59 @@ func NewTradeService(db *sqlx.DB) *TradeService { } func (s *TradeService) Sync(ctx context.Context, exchange types.Exchange, symbol string, startTime time.Time) error { - isMargin := false - isFutures := false - isIsolated := false - - if marginExchange, ok := exchange.(types.MarginExchange); ok { - marginSettings := marginExchange.GetMarginSettings() - isMargin = marginSettings.IsMargin - isIsolated = marginSettings.IsIsolatedMargin - if marginSettings.IsIsolatedMargin { - symbol = marginSettings.IsolatedMarginSymbol - } - } - - if futuresExchange, ok := exchange.(types.FuturesExchange); ok { - futuresSettings := futuresExchange.GetFuturesSettings() - isFutures = futuresSettings.IsFutures - isIsolated = futuresSettings.IsIsolatedFutures - if futuresSettings.IsIsolatedFutures { - symbol = futuresSettings.IsolatedFuturesSymbol - } - } - - // buffer 50 trades and use the trades ID to scan if the new trades are duplicated - records, err := s.QueryLast(exchange.Name(), symbol, isMargin, isFutures, isIsolated, 100) - if err != nil { - return err - } - - var tradeKeys = map[types.TradeKey]struct{}{} - - // for exchange supports trade id query, we should always try to query from the first trade. - // 0 means disable. - var lastTradeID uint64 = 1 - var now = time.Now() - if len(records) > 0 { - for _, record := range records { - tradeKeys[record.Key()] = struct{}{} - } - - end := len(records) - 1 - last := records[end] - lastTradeID = last.ID - startTime = last.Time.Time() - } - - b := &batch.TradeBatchQuery{Exchange: exchange} - tradeC, errC := b.Query(ctx, symbol, &types.TradeQueryOptions{ - LastTradeID: lastTradeID, - StartTime: &startTime, - EndTime: &now, - }) - - for trade := range tradeC { - select { - case <-ctx.Done(): - return ctx.Err() - - case err := <-errC: - if err != nil { - return err - } - - default: - } - - key := trade.Key() - if _, exists := tradeKeys[key]; exists { - continue - } - - tradeKeys[key] = struct{}{} - - log.Infof("inserting trade: %s %d %s %-4s price: %-13v volume: %-11v %5s %s", - trade.Exchange, - trade.ID, - trade.Symbol, - trade.Side, - trade.Price, - trade.Quantity, - trade.Liquidity(), - trade.Time.String()) - - if err := s.Insert(trade); err != nil { + isMargin, isFutures, isIsolated, isolatedSymbol := exchange2.GetSessionAttributes(exchange) + // override symbol if isolatedSymbol is not empty + if isIsolated && len(isolatedSymbol) > 0 { + symbol = isolatedSymbol + } + + api, ok := exchange.(types.ExchangeTradeHistoryService) + if !ok { + return nil + } + + lastTradeID := uint64(1) + tasks := []SyncTask{ + { + Type: types.Trade{}, + Select: SelectLastTrades(exchange.Name(), symbol, isMargin, isFutures, isIsolated, 100), + OnLoad: func(objs interface{}) { + // update last trade ID + trades := objs.([]types.Trade) + if len(trades) > 0 { + end := len(trades) - 1 + last := trades[end] + lastTradeID = last.ID + } + }, + BatchQuery: func(ctx context.Context, startTime, endTime time.Time) (interface{}, chan error) { + query := &batch.TradeBatchQuery{ + ExchangeTradeHistoryService: api, + } + return query.Query(ctx, symbol, &types.TradeQueryOptions{ + StartTime: &startTime, + EndTime: &endTime, + LastTradeID: lastTradeID, + }) + }, + Time: func(obj interface{}) time.Time { + return obj.(types.Trade).Time.Time() + }, + ID: func(obj interface{}) string { + trade := obj.(types.Trade) + return strconv.FormatUint(trade.ID, 10) + trade.Side.String() + }, + LogInsert: true, + }, + } + + for _, sel := range tasks { + if err := sel.execute(ctx, s.DB, startTime); err != nil { return err } } - return <-errC + return nil } func (s *TradeService) QueryTradingVolume(startTime time.Time, options TradingVolumeQueryOptions) ([]TradingVolume, error) { @@ -179,7 +147,7 @@ func (s *TradeService) QueryTradingVolume(startTime time.Time, options TradingVo return records, err } - record.Time = time.Date(record.Year, time.Month(record.Month), record.Day, 0, 0, 0, 0, time.UTC) + record.Time = time.Date(record.Year, time.Month(record.Month), record.Day, 0, 0, 0, 0, time.Local) records = append(records, record) } @@ -284,34 +252,6 @@ func generateMysqlTradingVolumeQuerySQL(options TradingVolumeQueryOptions) strin return sql } -// QueryLast queries the last trade from the database -func (s *TradeService) QueryLast(ex types.ExchangeName, symbol string, isMargin, isFutures, isIsolated bool, limit int) ([]types.Trade, error) { - log.Debugf("querying last trade exchange = %s AND symbol = %s AND is_margin = %v AND is_futures = %v AND is_isolated = %v", ex, symbol, isMargin, isFutures, isIsolated) - - sql := "SELECT * FROM trades WHERE exchange = :exchange AND symbol = :symbol AND is_margin = :is_margin AND is_futures = :is_futures AND is_isolated = :is_isolated ORDER BY traded_at DESC LIMIT :limit" - rows, err := s.DB.NamedQuery(sql, map[string]interface{}{ - "symbol": symbol, - "exchange": ex, - "is_margin": isMargin, - "is_futures": isFutures, - "is_isolated": isIsolated, - "limit": limit, - }) - if err != nil { - return nil, errors.Wrap(err, "query last trade error") - } - - defer rows.Close() - - trades, err := s.scanRows(rows) - if err != nil { - return nil, err - } - - trades = types.SortTradesAscending(trades) - return trades, nil -} - func (s *TradeService) QueryForTradingFeeCurrency(ex types.ExchangeName, symbol string, feeCurrency string) ([]types.Trade, error) { sql := "SELECT * FROM trades WHERE exchange = :exchange AND (symbol = :symbol OR fee_currency = :fee_currency) ORDER BY traded_at ASC" rows, err := s.DB.NamedQuery(sql, map[string]interface{}{ @@ -329,15 +269,43 @@ func (s *TradeService) QueryForTradingFeeCurrency(ex types.ExchangeName, symbol } func (s *TradeService) Query(options QueryTradesOptions) ([]types.Trade, error) { - sql := queryTradesSQL(options) + sel := sq.Select("*"). + From("trades") - log.Debug(sql) + if options.Since != nil { + sel = sel.Where(sq.GtOrEq{"traded_at": options.Since}) + } - args := map[string]interface{}{ - "exchange": options.Exchange, - "symbol": options.Symbol, + sel = sel.Where(sq.Eq{"symbol": options.Symbol}) + + if options.Exchange != "" { + sel = sel.Where(sq.Eq{"exchange": options.Exchange}) } - rows, err := s.DB.NamedQuery(sql, args) + + if len(options.Sessions) > 0 { + // FIXME: right now we only have the exchange field in the db, we might need to add the session field too. + sel = sel.Where(sq.Eq{"exchange": options.Sessions}) + } + + if options.Ordering != "" { + sel = sel.OrderBy("traded_at " + options.Ordering) + } else { + sel = sel.OrderBy("traded_at ASC") + } + + if options.Limit > 0 { + sel = sel.Limit(options.Limit) + } + + sql, args, err := sel.ToSql() + if err != nil { + return nil, err + } + + log.Debug(sql) + log.Debug(args) + + rows, err := s.DB.Queryx(sql, args...) if err != nil { return nil, err } @@ -367,49 +335,6 @@ func (s *TradeService) Load(ctx context.Context, id int64) (*types.Trade, error) return nil, errors.Wrapf(ErrTradeNotFound, "trade id:%d not found", id) } -func (s *TradeService) Mark(ctx context.Context, id int64, strategyID string) error { - result, err := s.DB.NamedExecContext(ctx, "UPDATE `trades` SET `strategy` = :strategy WHERE `id` = :id", map[string]interface{}{ - "id": id, - "strategy": strategyID, - }) - if err != nil { - return err - } - - cnt, err := result.RowsAffected() - if err != nil { - return err - } - - if cnt == 0 { - return fmt.Errorf("trade id:%d not found", id) - } - - return nil -} - -func (s *TradeService) UpdatePnL(ctx context.Context, id int64, pnl float64) error { - result, err := s.DB.NamedExecContext(ctx, "UPDATE `trades` SET `pnl` = :pnl WHERE `id` = :id", map[string]interface{}{ - "id": id, - "pnl": pnl, - }) - if err != nil { - return err - } - - cnt, err := result.RowsAffected() - if err != nil { - return err - } - - if cnt == 0 { - return fmt.Errorf("trade id:%d not found", id) - } - - return nil - -} - func queryTradesSQL(options QueryTradesOptions) string { ordering := "ASC" switch v := strings.ToUpper(options.Ordering); v { @@ -444,7 +369,7 @@ func queryTradesSQL(options QueryTradesOptions) string { sql += ` ORDER BY gid ` + ordering if options.Limit > 0 { - sql += ` LIMIT ` + strconv.Itoa(options.Limit) + sql += ` LIMIT ` + strconv.FormatUint(options.Limit, 10) } return sql @@ -464,43 +389,8 @@ func (s *TradeService) scanRows(rows *sqlx.Rows) (trades []types.Trade, err erro } func (s *TradeService) Insert(trade types.Trade) error { - _, err := s.DB.NamedExec(` - INSERT INTO trades ( - id, - exchange, - order_id, - symbol, - price, - quantity, - quote_quantity, - side, - is_buyer, - is_maker, - fee, - fee_currency, - traded_at, - is_margin, - is_futures, - is_isolated) - VALUES ( - :id, - :exchange, - :order_id, - :symbol, - :price, - :quantity, - :quote_quantity, - :side, - :is_buyer, - :is_maker, - :fee, - :fee_currency, - :traded_at, - :is_margin, - :is_futures, - :is_isolated - )`, - trade) + sql := dbCache.InsertSqlOf(trade) + _, err := s.DB.NamedExec(sql, trade) return err } @@ -508,3 +398,18 @@ func (s *TradeService) DeleteAll() error { _, err := s.DB.Exec(`DELETE FROM trades`) return err } + +func SelectLastTrades(ex types.ExchangeName, symbol string, isMargin, isFutures, isIsolated bool, limit uint64) sq.SelectBuilder { + return sq.Select("*"). + From("trades"). + Where(sq.And{ + sq.Eq{"symbol": symbol}, + sq.Eq{"exchange": ex}, + sq.Eq{"is_margin": isMargin}, + sq.Eq{"is_futures": isFutures}, + sq.Eq{"is_isolated": isIsolated}, + }). + OrderBy("traded_at DESC"). + Limit(limit) +} + diff --git a/pkg/service/trade_test.go b/pkg/service/trade_test.go index 3188fe9daa..d24e0b16fa 100644 --- a/pkg/service/trade_test.go +++ b/pkg/service/trade_test.go @@ -1,7 +1,6 @@ package service import ( - "context" "testing" "time" @@ -20,8 +19,6 @@ func Test_tradeService(t *testing.T) { defer db.Close() - ctx := context.Background() - xdb := sqlx.NewDb(db.DB, "sqlite3") service := &TradeService{DB: xdb} @@ -38,24 +35,6 @@ func Test_tradeService(t *testing.T) { Time: types.Time(time.Now()), }) assert.NoError(t, err) - - err = service.Mark(ctx, 1, "grid") - assert.NoError(t, err) - - tradeRecord, err := service.Load(ctx, 1) - assert.NoError(t, err) - assert.NotNil(t, tradeRecord) - assert.True(t, tradeRecord.StrategyID.Valid) - assert.Equal(t, "grid", tradeRecord.StrategyID.String) - - err = service.UpdatePnL(ctx, 1, 10.0) - assert.NoError(t, err) - - tradeRecord, err = service.Load(ctx, 1) - assert.NoError(t, err) - assert.NotNil(t, tradeRecord) - assert.True(t, tradeRecord.PnL.Valid) - assert.Equal(t, 10.0, tradeRecord.PnL.Float64) } func Test_queryTradingVolumeSQL(t *testing.T) { diff --git a/pkg/service/withdraw.go b/pkg/service/withdraw.go index 1881e4f1c4..f8448d0e0a 100644 --- a/pkg/service/withdraw.go +++ b/pkg/service/withdraw.go @@ -2,12 +2,13 @@ package service import ( "context" - "fmt" "time" + sq "github.com/Masterminds/squirrel" "github.com/jmoiron/sqlx" - log "github.com/sirupsen/logrus" + "github.com/c9s/bbgo/pkg/exchange" + "github.com/c9s/bbgo/pkg/exchange/batch" "github.com/c9s/bbgo/pkg/types" ) @@ -15,51 +16,54 @@ type WithdrawService struct { DB *sqlx.DB } -// Sync syncs the withdraw records into db -func (s *WithdrawService) Sync(ctx context.Context, ex types.Exchange) error { - txnIDs := map[string]struct{}{} - - // query descending - records, err := s.QueryLast(ex.Name(), 10) - if err != nil { - return err - } - - for _, record := range records { - txnIDs[record.TransactionID] = struct{}{} +// Sync syncs the withdrawal records into db +func (s *WithdrawService) Sync(ctx context.Context, ex types.Exchange, startTime time.Time) error { + isMargin, isFutures, isIsolated, _ := exchange.GetSessionAttributes(ex) + if isMargin || isFutures || isIsolated { + // only works in spot + return nil } transferApi, ok := ex.(types.ExchangeTransferService) if !ok { - return ErrNotImplemented + return nil } - since := time.Time{} - if len(records) > 0 { - since = records[len(records)-1].ApplyTime.Time() + tasks := []SyncTask{ + { + Type: types.Withdraw{}, + Select: SelectLastWithdraws(ex.Name(), 100), + BatchQuery: func(ctx context.Context, startTime, endTime time.Time) (interface{}, chan error) { + query := &batch.WithdrawBatchQuery{ + ExchangeTransferService: transferApi, + } + return query.Query(ctx, "", startTime, endTime) + }, + Time: func(obj interface{}) time.Time { + return obj.(types.Withdraw).ApplyTime.Time() + }, + ID: func(obj interface{}) string { + withdraw := obj.(types.Withdraw) + return withdraw.TransactionID + }, + Filter: func(obj interface{}) bool { + withdraw := obj.(types.Withdraw) + if withdraw.Status == "rejected" { + return false + } + + if len(withdraw.TransactionID) == 0 { + return false + } + + return true + }, + LogInsert: true, + }, } - // asset "" means all assets - withdraws, err := transferApi.QueryWithdrawHistory(ctx, "", since, time.Now()) - if err != nil { - return err - } - - for _, withdraw := range withdraws { - if _, exists := txnIDs[withdraw.TransactionID]; exists { - continue - } - - if withdraw.Status == "rejected" { - log.Warnf("skip record, withdraw transaction rejected: %+v", withdraw) - continue - } - - if len(withdraw.TransactionID) == 0 { - return fmt.Errorf("empty withdraw transacion ID: %+v", withdraw) - } - - if err := s.Insert(withdraw); err != nil { + for _, sel := range tasks { + if err := sel.execute(ctx, s.DB, startTime); err != nil { return err } } @@ -67,6 +71,16 @@ func (s *WithdrawService) Sync(ctx context.Context, ex types.Exchange) error { return nil } +func SelectLastWithdraws(ex types.ExchangeName, limit uint64) sq.SelectBuilder { + return sq.Select("*"). + From("withdraws"). + Where(sq.And{ + sq.Eq{"exchange": ex}, + }). + OrderBy("time DESC"). + Limit(limit) +} + func (s *WithdrawService) QueryLast(ex types.ExchangeName, limit int) ([]types.Withdraw, error) { sql := "SELECT * FROM `withdraws` WHERE `exchange` = :exchange ORDER BY `time` DESC LIMIT :limit" rows, err := s.DB.NamedQuery(sql, map[string]interface{}{ diff --git a/pkg/service/withdraw_test.go b/pkg/service/withdraw_test.go index 3be4399339..3328a0eee3 100644 --- a/pkg/service/withdraw_test.go +++ b/pkg/service/withdraw_test.go @@ -7,8 +7,8 @@ import ( "github.com/jmoiron/sqlx" "github.com/stretchr/testify/assert" - "github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" ) func TestWithdrawService(t *testing.T) { diff --git a/pkg/slack/slacklog/logrus_look.go b/pkg/slack/slacklog/logrus_look.go index 5d56375b35..dce4c2685b 100644 --- a/pkg/slack/slacklog/logrus_look.go +++ b/pkg/slack/slacklog/logrus_look.go @@ -11,7 +11,7 @@ import ( "golang.org/x/time/rate" ) -var limiter = rate.NewLimiter(rate.Every(time.Minute), 45) +var limiter = rate.NewLimiter(rate.Every(time.Minute), 3) type LogHook struct { Slack *slack.Client @@ -40,7 +40,7 @@ func (t *LogHook) Fire(e *logrus.Entry) error { return nil } - var color = "" + var color string switch e.Level { case logrus.DebugLevel: diff --git a/pkg/slack/slackstyle/style.go b/pkg/slack/slackstyle/style.go index 1f3fca63e6..46914aa8e9 100644 --- a/pkg/slack/slackstyle/style.go +++ b/pkg/slack/slackstyle/style.go @@ -1,8 +1,14 @@ package slackstyle +// Green is the green hex color const Green = "#228B22" + +// Red is the red hex color const Red = "#800000" +// TrendIcon returns the slack emoji of trends +// 1: uptrend +// -1: downtrend func TrendIcon(trend int) string { if trend < 0 { return ":chart_with_downwards_trend:" diff --git a/pkg/statistics/sortino.go b/pkg/statistics/sortino.go new file mode 100644 index 0000000000..12c0dbc25e --- /dev/null +++ b/pkg/statistics/sortino.go @@ -0,0 +1 @@ +package statistics diff --git a/pkg/strategy/audacitymaker/orderflow.go b/pkg/strategy/audacitymaker/orderflow.go new file mode 100644 index 0000000000..b8b0d80c7d --- /dev/null +++ b/pkg/strategy/audacitymaker/orderflow.go @@ -0,0 +1,176 @@ +package audacitymaker + +import ( + "context" + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" + + "gonum.org/v1/gonum/stat" +) + +type PerTrade struct { + Symbol string + Market types.Market `json:"-"` + types.IntervalWindow + + // MarketOrder is the option to enable market order short. + MarketOrder bool `json:"marketOrder"` + + Quantity fixedpoint.Value `json:"quantity"` + + orderExecutor *bbgo.GeneralOrderExecutor + session *bbgo.ExchangeSession + activeOrders *bbgo.ActiveOrderBook + + StreamBook *types.StreamOrderBook + + midPrice fixedpoint.Value + + bbgo.QuantityOrAmount +} + +func (s *PerTrade) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor) { + s.session = session + s.orderExecutor = orderExecutor + + position := orderExecutor.Position() + symbol := position.Symbol + // ger best bid/ask, not used yet + s.StreamBook = types.NewStreamBook(symbol) + s.StreamBook.BindStream(session.MarketDataStream) + + // use queue to do time-series rolling + buyTradeSize := types.NewQueue(200) + sellTradeSize := types.NewQueue(200) + buyTradesNumber := types.NewQueue(200) + sellTradesNumber := types.NewQueue(200) + // [WIP] Order Aggressiveness refers to the percentage of orders that are submitted at market prices, as opposed to limit prices. + + // Order flow is the difference between buyer-initiated and seller-initiated trading volume or number of trades. + var orderFlowSize floats.Slice + var orderFlowNumber floats.Slice + + var orderFlowSizeMinMax floats.Slice + var orderFlowNumberMinMax floats.Slice + + threshold := 3. + + session.MarketDataStream.OnMarketTrade(func(trade types.Trade) { + + //log.Infof("%s trade @ %f", trade.Side, trade.Price.Float64()) + + ctx := context.Background() + + if trade.Side == types.SideTypeBuy { + // accumulating trading volume from buyer + buyTradeSize.Update(trade.Quantity.Float64()) + sellTradeSize.Update(0) + // counting trades of number from seller + buyTradesNumber.Update(1) + sellTradesNumber.Update(0) + + } else if trade.Side == types.SideTypeSell { + // accumulating trading volume from buyer + buyTradeSize.Update(0) + sellTradeSize.Update(trade.Quantity.Float64()) + // counting trades of number from seller + buyTradesNumber.Update(0) + sellTradesNumber.Update(1) + } + + //canceled := s.orderExecutor.GracefulCancel(ctx) + //if canceled != nil { + // _ = s.orderExecutor.GracefulCancel(ctx) + //} + + sizeFraction := buyTradeSize.Sum() / sellTradeSize.Sum() + numberFraction := buyTradesNumber.Sum() / sellTradesNumber.Sum() + orderFlowSize.Push(sizeFraction) + if orderFlowSize.Length() > 100 { + // min-max scaling + ofsMax := orderFlowSize.Tail(100).Max() + ofsMin := orderFlowSize.Tail(100).Min() + ofsMinMax := (orderFlowSize.Last() - ofsMin) / (ofsMax - ofsMin) + // preserves temporal dependency via polar encoded angles + orderFlowSizeMinMax.Push(ofsMinMax) + } + + orderFlowNumber.Push(numberFraction) + if orderFlowNumber.Length() > 100 { + // min-max scaling + ofnMax := orderFlowNumber.Tail(100).Max() + ofnMin := orderFlowNumber.Tail(100).Min() + ofnMinMax := (orderFlowNumber.Last() - ofnMin) / (ofnMax - ofnMin) + // preserves temporal dependency via polar encoded angles + orderFlowNumberMinMax.Push(ofnMinMax) + } + + if orderFlowSizeMinMax.Length() > 100 && orderFlowNumberMinMax.Length() > 100 { + bid, ask, _ := s.StreamBook.BestBidAndAsk() + if outlier(orderFlowSizeMinMax.Tail(100), threshold) > 0 && outlier(orderFlowNumberMinMax.Tail(100), threshold) > 0 { + _ = s.orderExecutor.GracefulCancel(ctx) + log.Infof("long!!") + //_ = s.placeTrade(ctx, types.SideTypeBuy, s.Quantity, symbol) + _ = s.placeOrder(ctx, types.SideTypeBuy, s.Quantity, bid.Price, symbol) + //_ = s.placeOrder(ctx, types.SideTypeSell, s.Quantity, ask.Price.Mul(fixedpoint.NewFromFloat(1.0005)), symbol) + } else if outlier(orderFlowSizeMinMax.Tail(100), threshold) < 0 && outlier(orderFlowNumberMinMax.Tail(100), threshold) < 0 { + _ = s.orderExecutor.GracefulCancel(ctx) + log.Infof("short!!") + //_ = s.placeTrade(ctx, types.SideTypeSell, s.Quantity, symbol) + _ = s.placeOrder(ctx, types.SideTypeSell, s.Quantity, ask.Price, symbol) + //_ = s.placeOrder(ctx, types.SideTypeBuy, s.Quantity, bid.Price.Mul(fixedpoint.NewFromFloat(0.9995)), symbol) + } + } + + }) + + session.MarketDataStream.OnKLineClosed(types.KLineWith(symbol, s.Interval, func(kline types.KLine) { + + log.Info(kline.NumberOfTrades) + + })) + + if !bbgo.IsBackTesting { + session.MarketDataStream.OnMarketTrade(func(trade types.Trade) { + }) + } +} + +func (s *PerTrade) placeOrder(ctx context.Context, side types.SideType, quantity fixedpoint.Value, price fixedpoint.Value, symbol string) error { + market, _ := s.session.Market(symbol) + _, err := s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: symbol, + Market: market, + Side: side, + Type: types.OrderTypeLimitMaker, + Quantity: quantity, + Price: price, + Tag: "audacity-limit", + }) + return err +} + +func (s *PerTrade) placeTrade(ctx context.Context, side types.SideType, quantity fixedpoint.Value, symbol string) error { + market, _ := s.session.Market(symbol) + _, err := s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: symbol, + Market: market, + Side: side, + Type: types.OrderTypeMarket, + Quantity: quantity, + Tag: "audacity-market", + }) + return err +} + +func outlier(fs floats.Slice, multiplier float64) int { + stddev := stat.StdDev(fs, nil) + if fs.Last() > fs.Mean()+multiplier*stddev { + return 1 + } else if fs.Last() < fs.Mean()-multiplier*stddev { + return -1 + } + return 0 +} diff --git a/pkg/strategy/audacitymaker/strategy.go b/pkg/strategy/audacitymaker/strategy.go new file mode 100644 index 0000000000..c87b16f025 --- /dev/null +++ b/pkg/strategy/audacitymaker/strategy.go @@ -0,0 +1,135 @@ +package audacitymaker + +import ( + "context" + "fmt" + "os" + "sync" + + "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +const ID = "audacitymaker" + +var one = fixedpoint.One +var zero = fixedpoint.Zero + +var log = logrus.WithField("strategy", ID) + +func init() { + bbgo.RegisterStrategy(ID, &Strategy{}) +} + +type IntervalWindowSetting struct { + types.IntervalWindow +} + +type Strategy struct { + Environment *bbgo.Environment + Symbol string `json:"symbol"` + Market types.Market + + types.IntervalWindow + + // persistence fields + Position *types.Position `persistence:"position"` + ProfitStats *types.ProfitStats `persistence:"profit_stats"` + TradeStats *types.TradeStats `persistence:"trade_stats"` + + activeOrders *bbgo.ActiveOrderBook + + OrderFlow *PerTrade `json:"orderFlow"` + + ExitMethods bbgo.ExitMethodSet `json:"exits"` + + session *bbgo.ExchangeSession + orderExecutor *bbgo.GeneralOrderExecutor + + // StrategyController + bbgo.StrategyController +} + +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { + session.Subscribe(types.BookChannel, s.Symbol, types.SubscribeOptions{}) + session.Subscribe(types.MarketTradeChannel, s.Symbol, types.SubscribeOptions{}) + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.OrderFlow.Interval}) + + if !bbgo.IsBackTesting { + session.Subscribe(types.MarketTradeChannel, s.Symbol, types.SubscribeOptions{}) + } + + s.ExitMethods.SetAndSubscribe(session, s) +} + +func (s *Strategy) ID() string { + return ID +} + +func (s *Strategy) InstanceID() string { + return fmt.Sprintf("%s:%s", ID, s.Symbol) +} + +func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + var instanceID = s.InstanceID() + + if s.Position == nil { + s.Position = types.NewPositionFromMarket(s.Market) + } + + if s.ProfitStats == nil { + s.ProfitStats = types.NewProfitStats(s.Market) + } + + if s.TradeStats == nil { + s.TradeStats = types.NewTradeStats(s.Symbol) + } + + // StrategyController + s.Status = types.StrategyStatusRunning + + s.OnSuspend(func() { + // Cancel active orders + _ = s.orderExecutor.GracefulCancel(ctx) + }) + + s.OnEmergencyStop(func() { + // Cancel active orders + _ = s.orderExecutor.GracefulCancel(ctx) + // Close 100% position + // _ = s.ClosePosition(ctx, fixedpoint.One) + }) + + // initial required information + s.session = session + + s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) + s.orderExecutor.BindEnvironment(s.Environment) + s.orderExecutor.BindProfitStats(s.ProfitStats) + s.orderExecutor.BindTradeStats(s.TradeStats) + s.orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { + bbgo.Sync(ctx, s) + }) + s.orderExecutor.Bind() + s.activeOrders = bbgo.NewActiveOrderBook(s.Symbol) + + for _, method := range s.ExitMethods { + method.Bind(session, s.orderExecutor) + } + + if s.OrderFlow != nil { + s.OrderFlow.Bind(session, s.orderExecutor) + } + + bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { + defer wg.Done() + + _, _ = fmt.Fprintln(os.Stderr, s.TradeStats.String()) + _ = s.orderExecutor.GracefulCancel(ctx) + }) + + return nil +} diff --git a/pkg/strategy/autoborrow/strategy.go b/pkg/strategy/autoborrow/strategy.go index b485f9b9f9..31e625001f 100644 --- a/pkg/strategy/autoborrow/strategy.go +++ b/pkg/strategy/autoborrow/strategy.go @@ -47,11 +47,10 @@ type MarginAsset struct { MaxTotalBorrow fixedpoint.Value `json:"maxTotalBorrow"` MaxQuantityPerBorrow fixedpoint.Value `json:"maxQuantityPerBorrow"` MinQuantityPerBorrow fixedpoint.Value `json:"minQuantityPerBorrow"` + MinDebtRatio fixedpoint.Value `json:"debtRatio"` } type Strategy struct { - *bbgo.Notifiability - Interval types.Interval `json:"interval"` MinMarginLevel fixedpoint.Value `json:"minMarginLevel"` MaxMarginLevel fixedpoint.Value `json:"maxMarginLevel"` @@ -61,7 +60,7 @@ type Strategy struct { ExchangeSession *bbgo.ExchangeSession - marginBorrowRepay types.MarginBorrowRepay + marginBorrowRepay types.MarginBorrowRepayService } func (s *Strategy) ID() string { @@ -75,12 +74,12 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { func (s *Strategy) tryToRepayAnyDebt(ctx context.Context) { log.Infof("trying to repay any debt...") - if err := s.ExchangeSession.UpdateAccount(ctx); err != nil { + account, err := s.ExchangeSession.UpdateAccount(ctx) + if err != nil { log.WithError(err).Errorf("can not update account") return } - account := s.ExchangeSession.GetAccount() minMarginLevel := s.MinMarginLevel curMarginLevel := account.MarginLevel @@ -94,8 +93,9 @@ func (s *Strategy) tryToRepayAnyDebt(ctx context.Context) { continue } - toRepay := b.Available - s.Notifiability.Notify(&MarginAction{ + toRepay := fixedpoint.Min(b.Available, b.Debt()) + bbgo.Notify(&MarginAction{ + Exchange: s.ExchangeSession.ExchangeName, Action: "Repay", Asset: b.Currency, Amount: toRepay, @@ -110,21 +110,94 @@ func (s *Strategy) tryToRepayAnyDebt(ctx context.Context) { } } +func (s *Strategy) reBalanceDebt(ctx context.Context) { + account, err := s.ExchangeSession.UpdateAccount(ctx) + if err != nil { + log.WithError(err).Errorf("can not update account") + return + } + + minMarginLevel := s.MinMarginLevel + curMarginLevel := account.MarginLevel + + balances := account.Balances() + if len(balances) == 0 { + log.Warn("balance is empty, skip autoborrow") + return + } + + for _, marginAsset := range s.Assets { + b, ok := balances[marginAsset.Asset] + if !ok { + continue + } + + // debt / total + debtRatio := b.Debt().Div(b.Total()) + if marginAsset.MinDebtRatio.IsZero() { + marginAsset.MinDebtRatio = fixedpoint.One + } + + if b.Total().Compare(marginAsset.Low) <= 0 { + continue + } + + log.Infof("checking debtRatio: session = %s asset = %s, debtRatio = %f", s.ExchangeSession.Name, marginAsset.Asset, debtRatio.Float64()) + + // if debt is greater than total, skip repay + if b.Debt().Compare(b.Total()) > 0 { + log.Infof("%s debt %f is less than total %f", marginAsset.Asset, b.Debt().Float64(), b.Total().Float64()) + continue + } + + // the current debt ratio is less than the minimal ratio, + // we need to repay and reduce the debt + if debtRatio.Compare(marginAsset.MinDebtRatio) > 0 { + log.Infof("%s debt ratio %f is less than min debt ratio %f, skip", marginAsset.Asset, debtRatio.Float64(), marginAsset.MinDebtRatio.Float64()) + continue + } + + toRepay := fixedpoint.Min(b.Borrowed, b.Available) + toRepay = toRepay.Sub(marginAsset.Low) + + if toRepay.Sign() <= 0 { + log.Warnf("%s repay amount = 0, can not repay", marginAsset.Asset) + continue + } + + bbgo.Notify(&MarginAction{ + Exchange: s.ExchangeSession.ExchangeName, + Action: fmt.Sprintf("Repay for Debt Ratio %f", debtRatio.Float64()), + Asset: b.Currency, + Amount: toRepay, + MarginLevel: curMarginLevel, + MinMarginLevel: minMarginLevel, + }) + + if err := s.marginBorrowRepay.RepayMarginAsset(context.Background(), b.Currency, toRepay); err != nil { + log.WithError(err).Errorf("margin repay error") + } + } +} + func (s *Strategy) checkAndBorrow(ctx context.Context) { + s.reBalanceDebt(ctx) + if s.MinMarginLevel.IsZero() { return } - if err := s.ExchangeSession.UpdateAccount(ctx); err != nil { + account, err := s.ExchangeSession.UpdateAccount(ctx) + if err != nil { log.WithError(err).Errorf("can not update account") return } minMarginLevel := s.MinMarginLevel - account := s.ExchangeSession.GetAccount() curMarginLevel := account.MarginLevel - log.Infof("current account margin level: %s margin ratio: %s, margin tolerance: %s", + log.Infof("%s: current margin level: %s, margin ratio: %s, margin tolerance: %s", + s.ExchangeSession.Name, account.MarginLevel.String(), account.MarginRatio.String(), account.MarginTolerance.String(), @@ -133,17 +206,25 @@ func (s *Strategy) checkAndBorrow(ctx context.Context) { // if margin ratio is too low, do not borrow if curMarginLevel.Compare(minMarginLevel) < 0 { log.Infof("current margin level %f < min margin level %f, skip autoborrow", curMarginLevel.Float64(), minMarginLevel.Float64()) + bbgo.Notify("Warning!!! %s Current Margin Level %f < Minimal Margin Level %f", + s.ExchangeSession.Name, + curMarginLevel.Float64(), + minMarginLevel.Float64(), + account.Balances().Debts(), + ) s.tryToRepayAnyDebt(ctx) return } - balances := s.ExchangeSession.GetAccount().Balances() + balances := account.Balances() if len(balances) == 0 { log.Warn("balance is empty, skip autoborrow") return } for _, marginAsset := range s.Assets { + changed := false + if marginAsset.Low.IsZero() { log.Warnf("margin asset low balance is not set: %+v", marginAsset) continue @@ -176,7 +257,12 @@ func (s *Strategy) checkAndBorrow(ctx context.Context) { } } - s.Notifiability.Notify(&MarginAction{ + if toBorrow.IsZero() { + continue + } + + bbgo.Notify(&MarginAction{ + Exchange: s.ExchangeSession.ExchangeName, Action: "Borrow", Asset: marginAsset.Asset, Amount: toBorrow, @@ -184,7 +270,11 @@ func (s *Strategy) checkAndBorrow(ctx context.Context) { MinMarginLevel: minMarginLevel, }) log.Infof("sending borrow request %f %s", toBorrow.Float64(), marginAsset.Asset) - s.marginBorrowRepay.BorrowMarginAsset(ctx, marginAsset.Asset, toBorrow) + if err := s.marginBorrowRepay.BorrowMarginAsset(ctx, marginAsset.Asset, toBorrow); err != nil { + log.WithError(err).Errorf("borrow error") + continue + } + changed = true } else { // available balance is less than marginAsset.Low, we should trigger borrow toBorrow := marginAsset.Low @@ -193,7 +283,12 @@ func (s *Strategy) checkAndBorrow(ctx context.Context) { toBorrow = fixedpoint.Min(toBorrow, marginAsset.MaxQuantityPerBorrow) } - s.Notifiability.Notify(&MarginAction{ + if toBorrow.IsZero() { + continue + } + + bbgo.Notify(&MarginAction{ + Exchange: s.ExchangeSession.ExchangeName, Action: "Borrow", Asset: marginAsset.Asset, Amount: toBorrow, @@ -202,7 +297,21 @@ func (s *Strategy) checkAndBorrow(ctx context.Context) { }) log.Infof("sending borrow request %f %s", toBorrow.Float64(), marginAsset.Asset) - s.marginBorrowRepay.BorrowMarginAsset(ctx, marginAsset.Asset, toBorrow) + if err := s.marginBorrowRepay.BorrowMarginAsset(ctx, marginAsset.Asset, toBorrow); err != nil { + log.WithError(err).Errorf("borrow error") + continue + } + + changed = true + } + + // if debt is changed, we need to update account + if changed { + account, err = s.ExchangeSession.UpdateAccount(ctx) + if err != nil { + log.WithError(err).Errorf("can not update account") + return + } } } } @@ -245,7 +354,8 @@ func (s *Strategy) handleBinanceBalanceUpdateEvent(event *binance.BalanceUpdateE return } - if s.ExchangeSession.GetAccount().MarginLevel.Compare(s.MinMarginLevel) > 0 { + account := s.ExchangeSession.GetAccount() + if account.MarginLevel.Compare(s.MinMarginLevel) > 0 { return } @@ -256,7 +366,6 @@ func (s *Strategy) handleBinanceBalanceUpdateEvent(event *binance.BalanceUpdateE return } - account := s.ExchangeSession.GetAccount() minMarginLevel := s.MinMarginLevel curMarginLevel := account.MarginLevel @@ -265,14 +374,20 @@ func (s *Strategy) handleBinanceBalanceUpdateEvent(event *binance.BalanceUpdateE return } - toRepay := b.Available - s.Notifiability.Notify(&MarginAction{ + toRepay := fixedpoint.Min(b.Borrowed, b.Available) + if toRepay.IsZero() { + return + } + + bbgo.Notify(&MarginAction{ + Exchange: s.ExchangeSession.ExchangeName, Action: "Repay", Asset: b.Currency, Amount: toRepay, MarginLevel: curMarginLevel, MinMarginLevel: minMarginLevel, }) + if err := s.marginBorrowRepay.RepayMarginAsset(context.Background(), event.Asset, toRepay); err != nil { log.WithError(err).Errorf("margin repay error") } @@ -280,11 +395,12 @@ func (s *Strategy) handleBinanceBalanceUpdateEvent(event *binance.BalanceUpdateE } type MarginAction struct { - Action string - Asset string - Amount fixedpoint.Value - MarginLevel fixedpoint.Value - MinMarginLevel fixedpoint.Value + Exchange types.ExchangeName `json:"exchange"` + Action string `json:"action"` + Asset string `json:"asset"` + Amount fixedpoint.Value `json:"amount"` + MarginLevel fixedpoint.Value `json:"marginLevel"` + MinMarginLevel fixedpoint.Value `json:"minMarginLevel"` } func (a *MarginAction) SlackAttachment() slack.Attachment { @@ -292,6 +408,11 @@ func (a *MarginAction) SlackAttachment() slack.Attachment { Title: fmt.Sprintf("%s %s %s", a.Action, a.Amount, a.Asset), Color: "warning", Fields: []slack.AttachmentField{ + { + Title: "Exchange", + Value: a.Exchange.String(), + Short: true, + }, { Title: "Action", Value: a.Action, @@ -324,14 +445,14 @@ func (a *MarginAction) SlackAttachment() slack.Attachment { // This strategy simply spent all available quote currency to buy the symbol whenever kline gets closed func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { if s.MinMarginLevel.IsZero() { - log.Warnf("minMarginLevel is 0, you should configure this minimal margin ratio for controlling the liquidation risk") + log.Warnf("%s: minMarginLevel is 0, you should configure this minimal margin ratio for controlling the liquidation risk", session.Name) } s.ExchangeSession = session - marginBorrowRepay, ok := session.Exchange.(types.MarginBorrowRepay) + marginBorrowRepay, ok := session.Exchange.(types.MarginBorrowRepayService) if !ok { - return fmt.Errorf("exchange %s does not implement types.MarginBorrowRepay", session.ExchangeName) + return fmt.Errorf("exchange %s does not implement types.MarginBorrowRepayService", session.Name) } s.marginBorrowRepay = marginBorrowRepay diff --git a/pkg/strategy/bollgrid/strategy.go b/pkg/strategy/bollgrid/strategy.go index f604401437..61be952022 100644 --- a/pkg/strategy/bollgrid/strategy.go +++ b/pkg/strategy/bollgrid/strategy.go @@ -25,10 +25,6 @@ func init() { } type Strategy struct { - // The notification system will be injected into the strategy automatically. - // This field will be injected automatically since it's a single exchange strategy. - *bbgo.Notifiability - // OrderExecutor is an interface for submitting order. // This field will be injected automatically since it's a single exchange strategy. bbgo.OrderExecutor @@ -45,9 +41,6 @@ type Strategy struct { // This field will be injected automatically since we defined the Symbol field. *bbgo.StandardIndicatorSet - // Graceful let you define the graceful shutdown handler - *bbgo.Graceful - // Market stores the configuration of the market, for example, VolumePrecision, PricePrecision, MinLotSize... etc // This field will be injected automatically since we defined the Symbol field. types.Market @@ -74,9 +67,9 @@ type Strategy struct { Quantity fixedpoint.Value `json:"quantity"` // activeOrders is the locally maintained active order book of the maker orders. - activeOrders *bbgo.LocalActiveOrderBook + activeOrders *bbgo.ActiveOrderBook - profitOrders *bbgo.LocalActiveOrderBook + profitOrders *bbgo.ActiveOrderBook orders *bbgo.OrderStore @@ -108,10 +101,10 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { } // currently we need the 1m kline to update the last close price and indicators - session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval.String()}) + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) if len(s.RepostInterval) > 0 && s.Interval != s.RepostInterval { - session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.RepostInterval.String()}) + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.RepostInterval}) } } @@ -341,20 +334,20 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.orders.BindStream(session.UserDataStream) // we don't persist orders so that we can not clear the previous orders for now. just need time to support this. - s.activeOrders = bbgo.NewLocalActiveOrderBook(s.Symbol) + s.activeOrders = bbgo.NewActiveOrderBook(s.Symbol) s.activeOrders.OnFilled(func(o types.Order) { s.submitReverseOrder(o, session) }) s.activeOrders.BindStream(session.UserDataStream) - s.profitOrders = bbgo.NewLocalActiveOrderBook(s.Symbol) + s.profitOrders = bbgo.NewActiveOrderBook(s.Symbol) s.profitOrders.OnFilled(func(o types.Order) { // we made profit here! }) s.profitOrders.BindStream(session.UserDataStream) // setup graceful shutting down handler - s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { // call Done to notify the main process. defer wg.Done() log.Infof("canceling active orders...") diff --git a/pkg/strategy/bollmaker/doc.go b/pkg/strategy/bollmaker/doc.go new file mode 100644 index 0000000000..f4320091e9 --- /dev/null +++ b/pkg/strategy/bollmaker/doc.go @@ -0,0 +1,6 @@ +// bollmaker is a maker strategy depends on the bollinger band +// +// bollmaker uses two bollinger bands for trading: +// 1) the first bollinger is a long-term time frame bollinger, it controls your position. (how much you can hold) +// 2) the second bollinger is a short-term time frame bollinger, it controls whether places the orders or not. +package bollmaker diff --git a/pkg/strategy/bollmaker/dynamic_spread.go b/pkg/strategy/bollmaker/dynamic_spread.go new file mode 100644 index 0000000000..f7583d1105 --- /dev/null +++ b/pkg/strategy/bollmaker/dynamic_spread.go @@ -0,0 +1,247 @@ +package bollmaker + +import ( + "github.com/pkg/errors" + "math" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +type DynamicSpreadSettings struct { + AmpSpreadSettings *DynamicSpreadAmpSettings `json:"amplitude"` + WeightedBollWidthRatioSpreadSettings *DynamicSpreadBollWidthRatioSettings `json:"weightedBollWidth"` + + // deprecated + Enabled *bool `json:"enabled"` + + // deprecated + types.IntervalWindow + + // deprecated. AskSpreadScale is used to define the ask spread range with the given percentage. + AskSpreadScale *bbgo.PercentageScale `json:"askSpreadScale"` + + // deprecated. BidSpreadScale is used to define the bid spread range with the given percentage. + BidSpreadScale *bbgo.PercentageScale `json:"bidSpreadScale"` +} + +// Initialize dynamic spreads and preload SMAs +func (ds *DynamicSpreadSettings) Initialize(symbol string, session *bbgo.ExchangeSession, neutralBoll, defaultBoll *indicator.BOLL) { + switch { + case ds.AmpSpreadSettings != nil: + ds.AmpSpreadSettings.initialize(symbol, session) + case ds.WeightedBollWidthRatioSpreadSettings != nil: + ds.WeightedBollWidthRatioSpreadSettings.initialize(neutralBoll, defaultBoll) + case ds.Enabled != nil && *ds.Enabled: + // backward compatibility + ds.AmpSpreadSettings = &DynamicSpreadAmpSettings{ + IntervalWindow: ds.IntervalWindow, + AskSpreadScale: ds.AskSpreadScale, + BidSpreadScale: ds.BidSpreadScale, + } + ds.AmpSpreadSettings.initialize(symbol, session) + } +} + +func (ds *DynamicSpreadSettings) IsEnabled() bool { + return ds.AmpSpreadSettings != nil || ds.WeightedBollWidthRatioSpreadSettings != nil +} + +// Update dynamic spreads +func (ds *DynamicSpreadSettings) Update(kline types.KLine) { + switch { + case ds.AmpSpreadSettings != nil: + ds.AmpSpreadSettings.update(kline) + case ds.WeightedBollWidthRatioSpreadSettings != nil: + // Boll bands are updated outside of settings. Do nothing. + default: + // Disabled. Do nothing. + } +} + +// GetAskSpread returns current ask spread +func (ds *DynamicSpreadSettings) GetAskSpread() (askSpread float64, err error) { + switch { + case ds.AmpSpreadSettings != nil: + return ds.AmpSpreadSettings.getAskSpread() + case ds.WeightedBollWidthRatioSpreadSettings != nil: + return ds.WeightedBollWidthRatioSpreadSettings.getAskSpread() + default: + return 0, errors.New("dynamic spread is not enabled") + } +} + +// GetBidSpread returns current dynamic bid spread +func (ds *DynamicSpreadSettings) GetBidSpread() (bidSpread float64, err error) { + switch { + case ds.AmpSpreadSettings != nil: + return ds.AmpSpreadSettings.getBidSpread() + case ds.WeightedBollWidthRatioSpreadSettings != nil: + return ds.WeightedBollWidthRatioSpreadSettings.getBidSpread() + default: + return 0, errors.New("dynamic spread is not enabled") + } +} + +type DynamicSpreadAmpSettings struct { + types.IntervalWindow + + // AskSpreadScale is used to define the ask spread range with the given percentage. + AskSpreadScale *bbgo.PercentageScale `json:"askSpreadScale"` + + // BidSpreadScale is used to define the bid spread range with the given percentage. + BidSpreadScale *bbgo.PercentageScale `json:"bidSpreadScale"` + + dynamicAskSpread *indicator.SMA + dynamicBidSpread *indicator.SMA +} + +func (ds *DynamicSpreadAmpSettings) initialize(symbol string, session *bbgo.ExchangeSession) { + ds.dynamicBidSpread = &indicator.SMA{IntervalWindow: types.IntervalWindow{Interval: ds.Interval, Window: ds.Window}} + ds.dynamicAskSpread = &indicator.SMA{IntervalWindow: types.IntervalWindow{Interval: ds.Interval, Window: ds.Window}} + kLineStore, _ := session.MarketDataStore(symbol) + if klines, ok := kLineStore.KLinesOfInterval(ds.Interval); ok { + for i := 0; i < len(*klines); i++ { + ds.update((*klines)[i]) + } + } +} + +func (ds *DynamicSpreadAmpSettings) update(kline types.KLine) { + ampl := (kline.GetHigh().Float64() - kline.GetLow().Float64()) / kline.GetOpen().Float64() + + switch kline.Direction() { + case types.DirectionUp: + ds.dynamicAskSpread.Update(ampl) + ds.dynamicBidSpread.Update(0) + case types.DirectionDown: + ds.dynamicBidSpread.Update(ampl) + ds.dynamicAskSpread.Update(0) + default: + ds.dynamicAskSpread.Update(0) + ds.dynamicBidSpread.Update(0) + } +} + +func (ds *DynamicSpreadAmpSettings) getAskSpread() (askSpread float64, err error) { + if ds.AskSpreadScale != nil && ds.dynamicAskSpread.Length() >= ds.Window { + askSpread, err = ds.AskSpreadScale.Scale(ds.dynamicAskSpread.Last()) + if err != nil { + log.WithError(err).Errorf("can not calculate dynamicAskSpread") + return 0, err + } + + return askSpread, nil + } + + return 0, errors.New("incomplete dynamic spread settings or not enough data yet") +} + +func (ds *DynamicSpreadAmpSettings) getBidSpread() (bidSpread float64, err error) { + if ds.BidSpreadScale != nil && ds.dynamicBidSpread.Length() >= ds.Window { + bidSpread, err = ds.BidSpreadScale.Scale(ds.dynamicBidSpread.Last()) + if err != nil { + log.WithError(err).Errorf("can not calculate dynamicBidSpread") + return 0, err + } + + return bidSpread, nil + } + + return 0, errors.New("incomplete dynamic spread settings or not enough data yet") +} + +type DynamicSpreadBollWidthRatioSettings struct { + // AskSpreadScale is used to define the ask spread range with the given percentage. + AskSpreadScale *bbgo.PercentageScale `json:"askSpreadScale"` + + // BidSpreadScale is used to define the bid spread range with the given percentage. + BidSpreadScale *bbgo.PercentageScale `json:"bidSpreadScale"` + + // Sensitivity factor of the weighting function: 1 / (1 + exp(-(x - mid) * sensitivity / width)) + // A positive number. The greater factor, the sharper weighting function. Default set to 1.0 . + Sensitivity float64 `json:"sensitivity"` + + neutralBoll *indicator.BOLL + defaultBoll *indicator.BOLL +} + +func (ds *DynamicSpreadBollWidthRatioSettings) initialize(neutralBoll, defaultBoll *indicator.BOLL) { + ds.neutralBoll = neutralBoll + ds.defaultBoll = defaultBoll + if ds.Sensitivity <= 0. { + ds.Sensitivity = 1. + } +} + +func (ds *DynamicSpreadBollWidthRatioSettings) getAskSpread() (askSpread float64, err error) { + askSpread, err = ds.AskSpreadScale.Scale(ds.getWeightedBBWidthRatio(true)) + if err != nil { + log.WithError(err).Errorf("can not calculate dynamicAskSpread") + return 0, err + } + + return askSpread, nil +} + +func (ds *DynamicSpreadBollWidthRatioSettings) getBidSpread() (bidSpread float64, err error) { + bidSpread, err = ds.BidSpreadScale.Scale(ds.getWeightedBBWidthRatio(false)) + if err != nil { + log.WithError(err).Errorf("can not calculate dynamicAskSpread") + return 0, err + } + + return bidSpread, nil +} + +func (ds *DynamicSpreadBollWidthRatioSettings) getWeightedBBWidthRatio(positiveSigmoid bool) float64 { + // Weight the width of Boll bands with sigmoid function and calculate the ratio after integral. + // + // Given the default band: moving average default_BB_mid, band from default_BB_lower to default_BB_upper. + // And the neutral band: from neutral_BB_lower to neutral_BB_upper. + // And a sensitivity factor alpha, which is a positive constant. + // + // width of default BB w = default_BB_upper - default_BB_lower + // + // 1 x - default_BB_mid + // sigmoid weighting function f(y) = ------------- where y = -------------------- + // 1 + exp(-y) w / alpha + // Set the sigmoid weighting function: + // - To ask spread, the weighting density function d_weight(x) is sigmoid((x - default_BB_mid) / (w / alpha)) + // - To bid spread, the weighting density function d_weight(x) is sigmoid((default_BB_mid - x) / (w / alpha)) + // - The higher sensitivity factor alpha, the sharper weighting function. + // + // Then calculate the weighted band width ratio by taking integral of d_weight(x) from neutral_BB_lower to neutral_BB_upper: + // infinite integral of ask spread sigmoid weighting density function F(x) = (w / alpha) * ln(exp(x / (w / alpha)) + exp(default_BB_mid / (w / alpha))) + // infinite integral of bid spread sigmoid weighting density function F(x) = x - (w / alpha) * ln(exp(x / (w / alpha)) + exp(default_BB_mid / (w / alpha))) + // Note that we've rescaled the sigmoid function to fit default BB, + // the weighted default BB width is always calculated by integral(f of x from default_BB_lower to default_BB_upper) + // F(neutral_BB_upper) - F(neutral_BB_lower) + // weighted ratio = ------------------------------------------- + // F(default_BB_upper) - F(default_BB_lower) + // - The wider neutral band get greater ratio + // - To ask spread, the higher neutral band get greater ratio + // - To bid spread, the lower neutral band get greater ratio + + defaultMid := ds.defaultBoll.SMA.Last() + defaultUpper := ds.defaultBoll.UpBand.Last() + defaultLower := ds.defaultBoll.DownBand.Last() + defaultWidth := defaultUpper - defaultLower + neutralUpper := ds.neutralBoll.UpBand.Last() + neutralLower := ds.neutralBoll.DownBand.Last() + factor := defaultWidth / ds.Sensitivity + var weightedUpper, weightedLower, weightedDivUpper, weightedDivLower float64 + if positiveSigmoid { + weightedUpper = factor * math.Log(math.Exp(neutralUpper/factor)+math.Exp(defaultMid/factor)) + weightedLower = factor * math.Log(math.Exp(neutralLower/factor)+math.Exp(defaultMid/factor)) + weightedDivUpper = factor * math.Log(math.Exp(defaultUpper/factor)+math.Exp(defaultMid/factor)) + weightedDivLower = factor * math.Log(math.Exp(defaultLower/factor)+math.Exp(defaultMid/factor)) + } else { + weightedUpper = neutralUpper - factor*math.Log(math.Exp(neutralUpper/factor)+math.Exp(defaultMid/factor)) + weightedLower = neutralLower - factor*math.Log(math.Exp(neutralLower/factor)+math.Exp(defaultMid/factor)) + weightedDivUpper = defaultUpper - factor*math.Log(math.Exp(defaultUpper/factor)+math.Exp(defaultMid/factor)) + weightedDivLower = defaultLower - factor*math.Log(math.Exp(defaultLower/factor)+math.Exp(defaultMid/factor)) + } + return (weightedUpper - weightedLower) / (weightedDivUpper - weightedDivLower) +} diff --git a/pkg/strategy/bollmaker/strategy.go b/pkg/strategy/bollmaker/strategy.go index 3c32c38e8f..959bdff734 100644 --- a/pkg/strategy/bollmaker/strategy.go +++ b/pkg/strategy/bollmaker/strategy.go @@ -5,17 +5,15 @@ import ( "fmt" "math" "sync" - "time" "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/util" "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/c9s/bbgo/pkg/bbgo" - "github.com/c9s/bbgo/pkg/exchange/max" "github.com/c9s/bbgo/pkg/fixedpoint" - "github.com/c9s/bbgo/pkg/service" "github.com/c9s/bbgo/pkg/types" ) @@ -25,9 +23,6 @@ import ( const ID = "bollmaker" -const stateKey = "state-v1" - -var defaultFeeRate = fixedpoint.NewFromFloat(0.001) var notionModifier = fixedpoint.NewFromFloat(1.1) var two = fixedpoint.NewFromInt(2) @@ -37,8 +32,12 @@ func init() { bbgo.RegisterStrategy(ID, &Strategy{}) } +// Deprecated: State is deprecated, please use the persistence tag type State struct { - Position *types.Position `json:"position,omitempty"` + // Deprecated: Position is deprecated, please define the Position field in the strategy struct directly. + Position *types.Position `json:"position,omitempty"` + + // Deprecated: ProfitStats is deprecated, please define the ProfitStats field in the strategy struct directly. ProfitStats types.ProfitStats `json:"profitStats,omitempty"` } @@ -48,10 +47,6 @@ type BollingerSetting struct { } type Strategy struct { - *bbgo.Graceful - *bbgo.Notifiability - *bbgo.Persistence - Environment *bbgo.Environment StandardIndicatorSet *bbgo.StandardIndicatorSet Market types.Market @@ -59,11 +54,14 @@ type Strategy struct { // Symbol is the market symbol you want to trade Symbol string `json:"symbol"` - // Interval is how long do you want to update your order price and quantity - Interval types.Interval `json:"interval"` + types.IntervalWindow bbgo.QuantityOrAmount + // TrendEMA is used for detecting the trend by a given EMA + // you can define interval and window + TrendEMA *bbgo.TrendEMA `json:"trendEMA"` + // Spread is the price spread from the middle price. // For ask orders, the ask price is ((bestAsk + bestBid) / 2 * (1.0 + spread)) // For bid orders, the bid price is ((bestAsk + bestBid) / 2 * (1.0 - spread)) @@ -76,6 +74,9 @@ type Strategy struct { // AskSpread overrides the spread setting, this spread will be used for the sell order AskSpread fixedpoint.Value `json:"askSpread,omitempty"` + // DynamicSpread enables the automatic adjustment to bid and ask spread. + DynamicSpread DynamicSpreadSettings `json:"dynamicSpread,omitempty"` + // MinProfitSpread is the minimal order price spread from the current average cost. // For long position, you will only place sell order above the price (= average cost * (1 + minProfitSpread)) // For short position, you will only place buy order below the price (= average cost * (1 - minProfitSpread)) @@ -140,20 +141,18 @@ type Strategy struct { ShadowProtection bool `json:"shadowProtection"` ShadowProtectionRatio fixedpoint.Value `json:"shadowProtectionRatio"` - bbgo.SmartStops - session *bbgo.ExchangeSession book *types.StreamOrderBook - state *State + ExitMethods bbgo.ExitMethodSet `json:"exits"` - activeMakerOrders *bbgo.LocalActiveOrderBook - orderStore *bbgo.OrderStore - tradeCollector *bbgo.TradeCollector + // persistence fields + Position *types.Position `json:"position,omitempty" persistence:"position"` + ProfitStats *types.ProfitStats `json:"profitStats,omitempty" persistence:"profit_stats"` - groupID uint32 + orderExecutor *bbgo.GeneralOrderExecutor - stopC chan struct{} + groupID uint32 // defaultBoll is the BOLLINGER indicator we used for predicting the price. defaultBoll *indicator.BOLL @@ -162,35 +161,39 @@ type Strategy struct { neutralBoll *indicator.BOLL // StrategyController - status types.StrategyStatus + bbgo.StrategyController } func (s *Strategy) ID() string { return ID } -func (s *Strategy) Initialize() error { - return s.SmartStops.InitializeStopControllers(s.Symbol) +func (s *Strategy) InstanceID() string { + return fmt.Sprintf("%s:%s", ID, s.Symbol) } func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ - Interval: string(s.Interval), + Interval: s.Interval, }) if s.DefaultBollinger != nil && s.DefaultBollinger.Interval != "" { session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ - Interval: string(s.DefaultBollinger.Interval), + Interval: s.DefaultBollinger.Interval, }) } if s.NeutralBollinger != nil && s.NeutralBollinger.Interval != "" { session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ - Interval: string(s.NeutralBollinger.Interval), + Interval: s.NeutralBollinger.Interval, }) } - s.SmartStops.Subscribe(session) + if s.TrendEMA != nil { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.TrendEMA.Interval}) + } + + s.ExitMethods.SetAndSubscribe(session, s) } func (s *Strategy) Validate() error { @@ -202,130 +205,11 @@ func (s *Strategy) Validate() error { } func (s *Strategy) CurrentPosition() *types.Position { - return s.state.Position + return s.Position } func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Value) error { - base := s.state.Position.GetBase() - if base.IsZero() { - return fmt.Errorf("no opened %s position", s.state.Position.Symbol) - } - - // make it negative - quantity := base.Mul(percentage).Abs() - side := types.SideTypeBuy - if base.Sign() > 0 { - side = types.SideTypeSell - } - - if quantity.Compare(s.Market.MinQuantity) < 0 { - return fmt.Errorf("order quantity %v is too small, less than %v", quantity, s.Market.MinQuantity) - } - - submitOrder := types.SubmitOrder{ - Symbol: s.Symbol, - Side: side, - Type: types.OrderTypeMarket, - Quantity: quantity, - Market: s.Market, - } - - s.Notify("Submitting %s %s order to close position by %v", s.Symbol, side.String(), percentage, submitOrder) - - createdOrders, err := s.session.Exchange.SubmitOrders(ctx, submitOrder) - if err != nil { - log.WithError(err).Errorf("can not place position close order") - } - - s.orderStore.Add(createdOrders...) - s.activeMakerOrders.Add(createdOrders...) - return err -} - -// StrategyController - -func (s *Strategy) GetStatus() types.StrategyStatus { - return s.status -} - -func (s *Strategy) Suspend(ctx context.Context) error { - s.status = types.StrategyStatusStopped - - // Cancel all order - if err := s.activeMakerOrders.GracefulCancel(ctx, s.session.Exchange); err != nil { - log.WithError(err).Errorf("graceful cancel order error") - s.Notify("graceful cancel order error") - } else { - s.Notify("All orders cancelled.") - } - - s.tradeCollector.Process() - - // Save state - if err := s.SaveState(); err != nil { - log.WithError(err).Errorf("can not save state: %+v", s.state) - } else { - log.Infof("%s position is saved.", s.Symbol) - } - - return nil -} - -func (s *Strategy) Resume(ctx context.Context) error { - s.status = types.StrategyStatusRunning - - return nil -} - -func (s *Strategy) EmergencyStop(ctx context.Context) error { - // Close 100% position - percentage, _ := fixedpoint.NewFromString("100%") - err := s.ClosePosition(ctx, percentage) - - // Suspend strategy - _ = s.Suspend(ctx) - - return err -} - -func (s *Strategy) SaveState() error { - if err := s.Persistence.Save(s.state, ID, s.Symbol, stateKey); err != nil { - return err - } - - log.Infof("state is saved => %+v", s.state) - return nil -} - -func (s *Strategy) LoadState() error { - var state State - - // load position - if err := s.Persistence.Load(&state, ID, s.Symbol, stateKey); err != nil { - if err != service.ErrPersistenceNotExists { - return err - } - - s.state = &State{} - } else { - s.state = &state - log.Infof("state is restored: %+v", s.state) - } - - // if position is nil, we need to allocate a new position for calculation - if s.state.Position == nil { - s.state.Position = types.NewPositionFromMarket(s.Market) - } - - // init profit states - s.state.ProfitStats.Symbol = s.Market.Symbol - s.state.ProfitStats.BaseCurrency = s.Market.BaseCurrency - s.state.ProfitStats.QuoteCurrency = s.Market.QuoteCurrency - if s.state.ProfitStats.AccumulatedSince == 0 { - s.state.ProfitStats.AccumulatedSince = time.Now().Unix() - } - - return nil + return s.orderExecutor.ClosePosition(ctx, percentage) } func (s *Strategy) getCurrentAllowedExposurePosition(bandPercentage float64) (fixedpoint.Value, error) { @@ -340,7 +224,7 @@ func (s *Strategy) getCurrentAllowedExposurePosition(bandPercentage float64) (fi return s.MaxExposurePosition, nil } -func (s *Strategy) placeOrders(ctx context.Context, orderExecutor bbgo.OrderExecutor, midPrice fixedpoint.Value, kline *types.KLine) { +func (s *Strategy) placeOrders(ctx context.Context, midPrice fixedpoint.Value, kline *types.KLine) { bidSpread := s.Spread if s.BidSpread.Sign() > 0 { bidSpread = s.BidSpread @@ -353,7 +237,7 @@ func (s *Strategy) placeOrders(ctx context.Context, orderExecutor bbgo.OrderExec askPrice := midPrice.Mul(fixedpoint.One.Add(askSpread)) bidPrice := midPrice.Mul(fixedpoint.One.Sub(bidSpread)) - base := s.state.Position.GetBase() + base := s.Position.GetBase() balances := s.session.GetAccount().Balances() log.Infof("mid price:%v spread: %s ask:%v bid: %v position: %s", @@ -361,7 +245,7 @@ func (s *Strategy) placeOrders(ctx context.Context, orderExecutor bbgo.OrderExec s.Spread.Percentage(), askPrice, bidPrice, - s.state.Position, + s.Position, ) sellQuantity := s.QuantityOrAmount.CalculateQuantity(askPrice) @@ -391,22 +275,28 @@ func (s *Strategy) placeOrders(ctx context.Context, orderExecutor bbgo.OrderExec baseBalance, hasBaseBalance := balances[s.Market.BaseCurrency] quoteBalance, hasQuoteBalance := balances[s.Market.QuoteCurrency] - downBand := s.defaultBoll.LastDownBand() - upBand := s.defaultBoll.LastUpBand() - sma := s.defaultBoll.LastSMA() - log.Infof("bollinger band: up %f sma %f down %f", upBand, sma, downBand) + downBand := s.defaultBoll.DownBand.Last() + upBand := s.defaultBoll.UpBand.Last() + sma := s.defaultBoll.SMA.Last() + log.Infof("%s bollinger band: up %f sma %f down %f", s.Symbol, upBand, sma, downBand) bandPercentage := calculateBandPercentage(upBand, downBand, sma, midPrice.Float64()) - log.Infof("mid price band percentage: %v", bandPercentage) + log.Infof("%s mid price band percentage: %v", s.Symbol, bandPercentage) maxExposurePosition, err := s.getCurrentAllowedExposurePosition(bandPercentage) if err != nil { - log.WithError(err).Errorf("can not calculate CurrentAllowedExposurePosition") + log.WithError(err).Errorf("can not calculate %s CurrentAllowedExposurePosition", s.Symbol) return } - log.Infof("calculated max exposure position: %v", maxExposurePosition) + log.Infof("calculated %s max exposure position: %v", s.Symbol, maxExposurePosition) + + if !s.Position.IsClosed() && !s.Position.IsDust(midPrice) { + log.Infof("current %s unrealized profit: %f %s", s.Symbol, s.Position.UnrealizedProfit(midPrice).Float64(), s.Market.QuoteCurrency) + } + // by default, we turn both sell and buy on, + // which means we will place buy and sell orders canSell := true canBuy := true @@ -415,7 +305,7 @@ func (s *Strategy) placeOrders(ctx context.Context, orderExecutor bbgo.OrderExec } if maxExposurePosition.Sign() > 0 { - if s.Long != nil && *s.Long && base.Sign() < 0 { + if s.hasLongSet() && base.Sign() < 0 { canSell = false } else if base.Compare(maxExposurePosition.Neg()) < 0 { canSell = false @@ -458,13 +348,13 @@ func (s *Strategy) placeOrders(ctx context.Context, orderExecutor bbgo.OrderExec // WHEN: price breaks the upper band (price > window 2) == strongUpTrend // THEN: we apply strongUpTrend skew if s.TradeInBand { - if !inBetween(midPrice.Float64(), s.neutralBoll.LastDownBand(), s.neutralBoll.LastUpBand()) { + if !inBetween(midPrice.Float64(), s.neutralBoll.DownBand.Last(), s.neutralBoll.UpBand.Last()) { log.Infof("tradeInBand is set, skip placing orders when the price is outside of the band") return } } - trend := s.detectPriceTrend(s.neutralBoll, midPrice.Float64()) + trend := detectPriceTrend(s.neutralBoll, midPrice.Float64()) switch trend { case NeutralTrend: // do nothing @@ -480,6 +370,7 @@ func (s *Strategy) placeOrders(ctx context.Context, orderExecutor bbgo.OrderExec } + // check balance and switch the orders if !hasQuoteBalance || buyOrder.Quantity.Mul(buyOrder.Price).Compare(quoteBalance.Available) > 0 { canBuy = false } @@ -488,18 +379,43 @@ func (s *Strategy) placeOrders(ctx context.Context, orderExecutor bbgo.OrderExec canSell = false } - if midPrice.Compare(s.state.Position.AverageCost.Mul(fixedpoint.One.Add(s.MinProfitSpread))) < 0 { - canSell = false + isLongPosition := s.Position.IsLong() + isShortPosition := s.Position.IsShort() + minProfitPrice := s.Position.AverageCost.Mul(fixedpoint.One.Add(s.MinProfitSpread)) + if isShortPosition { + minProfitPrice = s.Position.AverageCost.Mul(fixedpoint.One.Sub(s.MinProfitSpread)) + } + + if isLongPosition { + // for long position if the current price is lower than the minimal profitable price then we should stop sell + // this avoid loss trade + if midPrice.Compare(minProfitPrice) < 0 { + canSell = false + } + } else if isShortPosition { + // for short position if the current price is higher than the minimal profitable price then we should stop buy + // this avoid loss trade + if midPrice.Compare(minProfitPrice) > 0 { + canBuy = false + } } - if s.Long != nil && *s.Long && base.Sub(sellOrder.Quantity).Sign() < 0 { + if s.hasLongSet() && base.Sub(sellOrder.Quantity).Sign() < 0 { canSell = false } - if s.BuyBelowNeutralSMA && midPrice.Float64() > s.neutralBoll.LastSMA() { + if s.BuyBelowNeutralSMA && midPrice.Float64() > s.neutralBoll.SMA.Last() { canBuy = false } + // trend EMA protection + if s.TrendEMA != nil { + if !s.TrendEMA.GradientAllowed() { + log.Infof("trendEMA protection: midPrice price %f, gradient %f, turning buy order off", midPrice.Float64(), s.TrendEMA.Gradient()) + canBuy = false + } + } + if canSell { submitOrders = append(submitOrders, sellOrder) } @@ -509,7 +425,7 @@ func (s *Strategy) placeOrders(ctx context.Context, orderExecutor bbgo.OrderExec // condition for lower the average cost /* - if midPrice < s.state.Position.AverageCost.MulFloat64(1.0-s.MinProfitSpread.Float64()) && canBuy { + if midPrice < s.Position.AverageCost.MulFloat64(1.0-s.MinProfitSpread.Float64()) && canBuy { submitOrders = append(submitOrders, buyOrder) } */ @@ -519,57 +435,37 @@ func (s *Strategy) placeOrders(ctx context.Context, orderExecutor bbgo.OrderExec } for i := range submitOrders { - submitOrders[i] = s.adjustOrderQuantity(submitOrders[i]) + submitOrders[i] = adjustOrderQuantity(submitOrders[i], s.Market) } - createdOrders, err := orderExecutor.SubmitOrders(ctx, submitOrders...) - if err != nil { - log.WithError(err).Errorf("can not place ping pong orders") - } - s.orderStore.Add(createdOrders...) - s.activeMakerOrders.Add(createdOrders...) + _, _ = s.orderExecutor.SubmitOrders(ctx, submitOrders...) } -type PriceTrend string - -const ( - NeutralTrend PriceTrend = "neutral" - UpTrend PriceTrend = "upTrend" - DownTrend PriceTrend = "downTrend" - UnknownTrend PriceTrend = "unknown" -) - -func (s *Strategy) detectPriceTrend(inc *indicator.BOLL, price float64) PriceTrend { - if inBetween(price, inc.LastDownBand(), inc.LastUpBand()) { - return NeutralTrend - } - - if price < inc.LastDownBand() { - return DownTrend - } - - if price > inc.LastUpBand() { - return UpTrend - } - - return UnknownTrend +func (s *Strategy) hasLongSet() bool { + return s.Long != nil && *s.Long } -func (s *Strategy) adjustOrderQuantity(submitOrder types.SubmitOrder) types.SubmitOrder { - if submitOrder.Quantity.Mul(submitOrder.Price).Compare(s.Market.MinNotional) < 0 { - submitOrder.Quantity = bbgo.AdjustFloatQuantityByMinAmount(submitOrder.Quantity, submitOrder.Price, s.Market.MinNotional.Mul(notionModifier)) - } - - if submitOrder.Quantity.Compare(s.Market.MinQuantity) < 0 { - submitOrder.Quantity = fixedpoint.Max(submitOrder.Quantity, s.Market.MinQuantity) - } - - return submitOrder +func (s *Strategy) hasShortSet() bool { + return s.Short != nil && *s.Short } func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + // initial required information + s.session = session + // StrategyController - s.status = types.StrategyStatusRunning + s.Status = types.StrategyStatusRunning + + s.neutralBoll = s.StandardIndicatorSet.BOLL(s.NeutralBollinger.IntervalWindow, s.NeutralBollinger.BandWidth) + s.defaultBoll = s.StandardIndicatorSet.BOLL(s.DefaultBollinger.IntervalWindow, s.DefaultBollinger.BandWidth) + + // Setup dynamic spread + if s.DynamicSpread.IsEnabled() { + if s.DynamicSpread.Interval == "" { + s.DynamicSpread.Interval = s.Interval + } + s.DynamicSpread.Initialize(s.Symbol, s.session, s.neutralBoll, s.defaultBoll) + } if s.DisableShort { s.Long = &[]bool{true}[0] @@ -591,68 +487,59 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.ShadowProtectionRatio = fixedpoint.NewFromFloat(0.01) } - // initial required information - s.session = session - s.neutralBoll = s.StandardIndicatorSet.BOLL(s.NeutralBollinger.IntervalWindow, s.NeutralBollinger.BandWidth) - s.defaultBoll = s.StandardIndicatorSet.BOLL(s.DefaultBollinger.IntervalWindow, s.DefaultBollinger.BandWidth) - // calculate group id for orders - instanceID := fmt.Sprintf("%s-%s", ID, s.Symbol) - s.groupID = max.GenerateGroupID(instanceID) - log.Infof("using group id %d from fnv(%s)", s.groupID, instanceID) + instanceID := s.InstanceID() + s.groupID = util.FNV32(instanceID) - // restore state - if err := s.LoadState(); err != nil { - return err + // If position is nil, we need to allocate a new position for calculation + if s.Position == nil { + s.Position = types.NewPositionFromMarket(s.Market) } - s.state.Position.Strategy = ID - s.state.Position.StrategyInstanceID = instanceID - - s.stopC = make(chan struct{}) - - s.activeMakerOrders = bbgo.NewLocalActiveOrderBook(s.Symbol) - s.activeMakerOrders.BindStream(session.UserDataStream) + if s.session.MakerFeeRate.Sign() > 0 || s.session.TakerFeeRate.Sign() > 0 { + s.Position.SetExchangeFeeRate(s.session.ExchangeName, types.ExchangeFee{ + MakerFeeRate: s.session.MakerFeeRate, + TakerFeeRate: s.session.TakerFeeRate, + }) + } - s.orderStore = bbgo.NewOrderStore(s.Symbol) - s.orderStore.BindStream(session.UserDataStream) + if s.ProfitStats == nil { + s.ProfitStats = types.NewProfitStats(s.Market) + } - s.tradeCollector = bbgo.NewTradeCollector(s.Symbol, s.state.Position, s.orderStore) + // Always update the position fields + s.Position.Strategy = ID + s.Position.StrategyInstanceID = instanceID - s.tradeCollector.OnTrade(func(trade types.Trade, profit, netProfit fixedpoint.Value) { - // StrategyController - if s.status != types.StrategyStatusRunning { - return - } - - s.Notifiability.Notify(trade) - s.state.ProfitStats.AddTrade(trade) + s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) + s.orderExecutor.BindEnvironment(s.Environment) + s.orderExecutor.BindProfitStats(s.ProfitStats) + s.orderExecutor.Bind() + s.orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { + bbgo.Sync(ctx, s) + }) + s.ExitMethods.Bind(session, s.orderExecutor) - if profit.Compare(fixedpoint.Zero) == 0 { - s.Environment.RecordPosition(s.state.Position, trade, nil) - } else { - log.Infof("%s generated profit: %v", s.Symbol, profit) - p := s.state.Position.NewProfit(trade, profit, netProfit) - p.Strategy = ID - p.StrategyInstanceID = instanceID - s.Notify(&p) + if s.TrendEMA != nil { + s.TrendEMA.Bind(session, s.orderExecutor) + } - s.state.ProfitStats.AddProfit(p) - s.Notify(&s.state.ProfitStats) + if bbgo.IsBackTesting { + log.Warn("turning of useTickerPrice option in the back-testing environment...") + s.UseTickerPrice = false + } - s.Environment.RecordPosition(s.state.Position, trade, &p) - } + s.OnSuspend(func() { + _ = s.orderExecutor.GracefulCancel(ctx) + bbgo.Sync(ctx, s) }) - s.tradeCollector.OnPositionUpdate(func(position *types.Position) { - log.Infof("position changed: %s", s.state.Position) - s.Notify(s.state.Position) + s.OnEmergencyStop(func() { + // Close 100% position + percentage := fixedpoint.NewFromFloat(1.0) + _ = s.ClosePosition(ctx, percentage) }) - s.tradeCollector.BindStream(session.UserDataStream) - - s.SmartStops.RunStopControllers(ctx, session, s.tradeCollector) - session.UserDataStream.OnStart(func() { if s.UseTickerPrice { ticker, err := s.session.Exchange.QueryTicker(ctx, s.Symbol) @@ -661,30 +548,36 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } midPrice := ticker.Buy.Add(ticker.Sell).Div(two) - s.placeOrders(ctx, orderExecutor, midPrice, nil) + s.placeOrders(ctx, midPrice, nil) } else { if price, ok := session.LastPrice(s.Symbol); ok { - s.placeOrders(ctx, orderExecutor, price, nil) + s.placeOrders(ctx, price, nil) } } }) - session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { // StrategyController - if s.status != types.StrategyStatusRunning { + if s.Status != types.StrategyStatusRunning { return } - if kline.Symbol != s.Symbol || kline.Interval != s.Interval { - return - } - - if err := s.activeMakerOrders.GracefulCancel(ctx, s.session.Exchange); err != nil { - log.WithError(err).Errorf("graceful cancel order error") + // Update spreads with dynamic spread + if s.DynamicSpread.IsEnabled() { + s.DynamicSpread.Update(kline) + dynamicBidSpread, err := s.DynamicSpread.GetBidSpread() + if err == nil && dynamicBidSpread > 0 { + s.BidSpread = fixedpoint.NewFromFloat(dynamicBidSpread) + log.Infof("%s dynamic bid spread updated: %s", s.Symbol, s.BidSpread.Percentage()) + } + dynamicAskSpread, err := s.DynamicSpread.GetAskSpread() + if err == nil && dynamicAskSpread > 0 { + s.AskSpread = fixedpoint.NewFromFloat(dynamicAskSpread) + log.Infof("%s dynamic ask spread updated: %s", s.Symbol, s.AskSpread.Percentage()) + } } - // check if there is a canceled order had partially filled. - s.tradeCollector.Process() + _ = s.orderExecutor.GracefulCancel(ctx) if s.UseTickerPrice { ticker, err := s.session.Exchange.QueryTicker(ctx, s.Symbol) @@ -694,28 +587,19 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se midPrice := ticker.Buy.Add(ticker.Sell).Div(two) log.Infof("using ticker price: bid %v / ask %v, mid price %v", ticker.Buy, ticker.Sell, midPrice) - s.placeOrders(ctx, orderExecutor, midPrice, &kline) + s.placeOrders(ctx, midPrice, &kline) } else { - s.placeOrders(ctx, orderExecutor, kline.Close, &kline) + s.placeOrders(ctx, kline.Close, &kline) } - }) + })) // s.book = types.NewStreamBook(s.Symbol) // s.book.BindStreamForBackground(session.MarketDataStream) - s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() - close(s.stopC) - - if err := s.activeMakerOrders.GracefulCancel(ctx, s.session.Exchange); err != nil { - log.WithError(err).Errorf("graceful cancel order error") - } - - s.tradeCollector.Process() - if err := s.SaveState(); err != nil { - log.WithError(err).Errorf("can not save state: %+v", s.state) - } + _ = s.orderExecutor.GracefulCancel(ctx) }) return nil @@ -736,3 +620,15 @@ func calculateBandPercentage(up, down, sma, midPrice float64) float64 { func inBetween(x, a, b float64) bool { return a < x && x < b } + +func adjustOrderQuantity(submitOrder types.SubmitOrder, market types.Market) types.SubmitOrder { + if submitOrder.Quantity.Mul(submitOrder.Price).Compare(market.MinNotional) < 0 { + submitOrder.Quantity = bbgo.AdjustFloatQuantityByMinAmount(submitOrder.Quantity, submitOrder.Price, market.MinNotional.Mul(notionModifier)) + } + + if submitOrder.Quantity.Compare(market.MinQuantity) < 0 { + submitOrder.Quantity = fixedpoint.Max(submitOrder.Quantity, market.MinQuantity) + } + + return submitOrder +} diff --git a/pkg/strategy/bollmaker/trend.go b/pkg/strategy/bollmaker/trend.go new file mode 100644 index 0000000000..33167967b5 --- /dev/null +++ b/pkg/strategy/bollmaker/trend.go @@ -0,0 +1,28 @@ +package bollmaker + +import "github.com/c9s/bbgo/pkg/indicator" + +type PriceTrend string + +const ( + NeutralTrend PriceTrend = "neutral" + UpTrend PriceTrend = "upTrend" + DownTrend PriceTrend = "downTrend" + UnknownTrend PriceTrend = "unknown" +) + +func detectPriceTrend(inc *indicator.BOLL, price float64) PriceTrend { + if inBetween(price, inc.DownBand.Last(), inc.UpBand.Last()) { + return NeutralTrend + } + + if price < inc.LastDownBand() { + return DownTrend + } + + if price > inc.LastUpBand() { + return UpTrend + } + + return UnknownTrend +} diff --git a/pkg/strategy/dca/strategy.go b/pkg/strategy/dca/strategy.go new file mode 100644 index 0000000000..db6f1abda5 --- /dev/null +++ b/pkg/strategy/dca/strategy.go @@ -0,0 +1,157 @@ +package dca + +import ( + "context" + "fmt" + "time" + + "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +const ID = "dca" + +var log = logrus.WithField("strategy", ID) + +func init() { + bbgo.RegisterStrategy(ID, &Strategy{}) +} + +type BudgetPeriod string + +const ( + BudgetPeriodDay BudgetPeriod = "day" + BudgetPeriodWeek BudgetPeriod = "week" + BudgetPeriodMonth BudgetPeriod = "month" +) + +func (b BudgetPeriod) Duration() time.Duration { + var period time.Duration + switch b { + case BudgetPeriodDay: + period = 24 * time.Hour + + case BudgetPeriodWeek: + period = 24 * time.Hour * 7 + + case BudgetPeriodMonth: + period = 24 * time.Hour * 30 + + } + + return period +} + +// Strategy is the Dollar-Cost-Average strategy +type Strategy struct { + Environment *bbgo.Environment + Symbol string `json:"symbol"` + Market types.Market + + // BudgetPeriod is how long your budget quota will be reset. + // day, week, month + BudgetPeriod BudgetPeriod `json:"budgetPeriod"` + + // Budget is the amount you invest per budget period + Budget fixedpoint.Value `json:"budget"` + + // InvestmentInterval is the interval of each investment + InvestmentInterval types.Interval `json:"investmentInterval"` + + budgetPerInvestment fixedpoint.Value + + Position *types.Position `persistence:"position"` + ProfitStats *types.ProfitStats `persistence:"profit_stats"` + BudgetQuota fixedpoint.Value `persistence:"budget_quota"` + BudgetPeriodStartTime time.Time `persistence:"budget_period_start_time"` + + session *bbgo.ExchangeSession + orderExecutor *bbgo.GeneralOrderExecutor + + bbgo.StrategyController +} + +func (s *Strategy) ID() string { + return ID +} + +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.InvestmentInterval}) +} + +func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Value) error { + return s.orderExecutor.ClosePosition(ctx, percentage) +} + +func (s *Strategy) InstanceID() string { + return fmt.Sprintf("%s:%s", ID, s.Symbol) +} + +func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + if s.BudgetQuota.IsZero() { + s.BudgetQuota = s.Budget + } + + if s.Position == nil { + s.Position = types.NewPositionFromMarket(s.Market) + } + + if s.ProfitStats == nil { + s.ProfitStats = types.NewProfitStats(s.Market) + } + + instanceID := s.InstanceID() + s.session = session + s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) + s.orderExecutor.BindEnvironment(s.Environment) + s.orderExecutor.BindProfitStats(s.ProfitStats) + s.orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { + bbgo.Sync(ctx, s) + }) + s.orderExecutor.Bind() + + numOfInvestmentPerPeriod := fixedpoint.NewFromFloat(float64(s.BudgetPeriod.Duration()) / float64(s.InvestmentInterval.Duration())) + s.budgetPerInvestment = s.Budget.Div(numOfInvestmentPerPeriod) + + session.UserDataStream.OnStart(func() {}) + session.MarketDataStream.OnKLine(func(kline types.KLine) {}) + session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { + if kline.Symbol != s.Symbol || kline.Interval != s.InvestmentInterval { + return + } + + if s.BudgetPeriodStartTime == (time.Time{}) { + s.BudgetPeriodStartTime = kline.StartTime.Time().Truncate(time.Minute) + } + + if kline.EndTime.Time().Sub(s.BudgetPeriodStartTime) >= s.BudgetPeriod.Duration() { + // reset budget quota + s.BudgetQuota = s.Budget + s.BudgetPeriodStartTime = kline.StartTime.Time() + } + + // check if we have quota + if s.BudgetQuota.Compare(s.budgetPerInvestment) <= 0 { + return + } + + price := kline.Close + quantity := s.budgetPerInvestment.Div(price) + + _, err := s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeMarket, + Quantity: quantity, + Market: s.Market, + }) + if err != nil { + log.WithError(err).Errorf("submit order failed") + } + }) + + return nil +} diff --git a/pkg/strategy/drift/driftma.go b/pkg/strategy/drift/driftma.go new file mode 100644 index 0000000000..0ff0a9c70b --- /dev/null +++ b/pkg/strategy/drift/driftma.go @@ -0,0 +1,57 @@ +package drift + +import ( + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +type DriftMA struct { + types.SeriesBase + drift *indicator.WeightedDrift + ma1 types.UpdatableSeriesExtend + ma2 types.UpdatableSeriesExtend +} + +func (s *DriftMA) Update(value, weight float64) { + s.ma1.Update(value) + if s.ma1.Length() == 0 { + return + } + s.drift.Update(s.ma1.Last(), weight) + if s.drift.Length() == 0 { + return + } + s.ma2.Update(s.drift.Last()) +} + +func (s *DriftMA) Last() float64 { + return s.ma2.Last() +} + +func (s *DriftMA) Index(i int) float64 { + return s.ma2.Index(i) +} + +func (s *DriftMA) Length() int { + return s.ma2.Length() +} + +func (s *DriftMA) ZeroPoint() float64 { + return s.drift.ZeroPoint() +} + +func (s *DriftMA) Clone() *DriftMA { + out := DriftMA{ + drift: s.drift.Clone(), + ma1: types.Clone(s.ma1), + ma2: types.Clone(s.ma2), + } + out.SeriesBase.Series = &out + return &out +} + +func (s *DriftMA) TestUpdate(v, weight float64) *DriftMA { + out := s.Clone() + out.Update(v, weight) + return out +} diff --git a/pkg/strategy/drift/output.go b/pkg/strategy/drift/output.go new file mode 100644 index 0000000000..e078a15793 --- /dev/null +++ b/pkg/strategy/drift/output.go @@ -0,0 +1,18 @@ +package drift + +import ( + "io" + + "github.com/jedib0t/go-pretty/v6/table" + + "github.com/c9s/bbgo/pkg/dynamic" + "github.com/c9s/bbgo/pkg/style" +) + +func (s *Strategy) Print(f io.Writer, pretty bool, withColor ...bool) { + var tableStyle *table.Style + if pretty { + tableStyle = style.NewDefaultTableStyle() + } + dynamic.PrintConfig(s, f, tableStyle, len(withColor) > 0 && withColor[0], dynamic.DefaultWhiteList()...) +} diff --git a/pkg/strategy/drift/stoploss.go b/pkg/strategy/drift/stoploss.go new file mode 100644 index 0000000000..bd78efa1dc --- /dev/null +++ b/pkg/strategy/drift/stoploss.go @@ -0,0 +1,19 @@ +package drift + +func (s *Strategy) CheckStopLoss() bool { + if s.UseStopLoss { + stoploss := s.StopLoss.Float64() + if s.sellPrice > 0 && s.sellPrice*(1.+stoploss) <= s.highestPrice || + s.buyPrice > 0 && s.buyPrice*(1.-stoploss) >= s.lowestPrice { + return true + } + } + if s.UseAtr { + atr := s.atr.Last() + if s.sellPrice > 0 && s.sellPrice+atr <= s.highestPrice || + s.buyPrice > 0 && s.buyPrice-atr >= s.lowestPrice { + return true + } + } + return false +} diff --git a/pkg/strategy/drift/stoploss_test.go b/pkg/strategy/drift/stoploss_test.go new file mode 100644 index 0000000000..b6e245d02b --- /dev/null +++ b/pkg/strategy/drift/stoploss_test.go @@ -0,0 +1,58 @@ +package drift + +import ( + "testing" + + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/stretchr/testify/assert" +) + +func Test_StopLossLong(t *testing.T) { + s := &Strategy{} + s.highestPrice = 30. + s.buyPrice = 30. + s.lowestPrice = 29.7 + s.StopLoss = fixedpoint.NewFromFloat(0.01) + s.UseAtr = false + s.UseStopLoss = true + assert.True(t, s.CheckStopLoss()) +} + +func Test_StopLossShort(t *testing.T) { + s := &Strategy{} + s.lowestPrice = 30. + s.sellPrice = 30. + s.highestPrice = 30.3 + s.StopLoss = fixedpoint.NewFromFloat(0.01) + s.UseAtr = false + s.UseStopLoss = true + assert.True(t, s.CheckStopLoss()) +} + +func Test_ATRLong(t *testing.T) { + s := &Strategy{} + s.highestPrice = 30. + s.buyPrice = 30. + s.lowestPrice = 28.7 + s.UseAtr = true + s.UseStopLoss = false + s.atr = &indicator.ATR{RMA: &indicator.RMA{ + Values: floats.Slice{1., 1.2, 1.3}, + }} + assert.True(t, s.CheckStopLoss()) +} + +func Test_ATRShort(t *testing.T) { + s := &Strategy{} + s.highestPrice = 31.3 + s.sellPrice = 30. + s.lowestPrice = 30. + s.UseAtr = true + s.UseStopLoss = false + s.atr = &indicator.ATR{RMA: &indicator.RMA{ + Values: floats.Slice{1., 1.2, 1.3}, + }} + assert.True(t, s.CheckStopLoss()) +} diff --git a/pkg/strategy/drift/strategy.go b/pkg/strategy/drift/strategy.go new file mode 100644 index 0000000000..5c8f010473 --- /dev/null +++ b/pkg/strategy/drift/strategy.go @@ -0,0 +1,974 @@ +package drift + +import ( + "bytes" + "context" + "errors" + "fmt" + "math" + "os" + "strconv" + "sync" + "time" + + "github.com/sirupsen/logrus" + "github.com/wcharczuk/go-chart/v2" + "go.uber.org/multierr" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/dynamic" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/interact" + "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/util" +) + +const ID = "drift" + +var log = logrus.WithField("strategy", ID) +var Four fixedpoint.Value = fixedpoint.NewFromInt(4) +var Three fixedpoint.Value = fixedpoint.NewFromInt(3) +var Two fixedpoint.Value = fixedpoint.NewFromInt(2) +var Delta fixedpoint.Value = fixedpoint.NewFromFloat(0.01) +var Fee = 0.0008 // taker fee % * 2, for upper bound + +func init() { + bbgo.RegisterStrategy(ID, &Strategy{}) +} + +func filterErrors(errs []error) (es []error) { + for _, e := range errs { + if _, ok := e.(types.ZeroAssetError); ok { + continue + } + if bbgo.ErrExceededSubmitOrderRetryLimit == e { + continue + } + es = append(es, e) + } + return es +} + +type Strategy struct { + Symbol string `json:"symbol"` + + bbgo.OpenPositionOptions + bbgo.StrategyController + types.Market + types.IntervalWindow + bbgo.SourceSelector + + *bbgo.Environment + *types.Position `persistence:"position"` + *types.ProfitStats `persistence:"profit_stats"` + *types.TradeStats `persistence:"trade_stats"` + + p *types.Position + + priceLines *types.Queue + trendLine types.UpdatableSeriesExtend + ma types.UpdatableSeriesExtend + stdevHigh *indicator.StdDev + stdevLow *indicator.StdDev + drift *DriftMA + atr *indicator.ATR + midPrice fixedpoint.Value + lock sync.RWMutex `ignore:"true"` + positionLock sync.RWMutex `ignore:"true"` + startTime time.Time + minutesCounter int + orderPendingCounter map[uint64]int + frameKLine *types.KLine + kline1m *types.KLine + + beta float64 + + UseStopLoss bool `json:"useStopLoss" modifiable:"true"` + UseAtr bool `json:"useAtr" modifiable:"true"` + StopLoss fixedpoint.Value `json:"stoploss" modifiable:"true"` + CanvasPath string `json:"canvasPath"` + PredictOffset int `json:"predictOffset"` + HighLowVarianceMultiplier float64 `json:"hlVarianceMultiplier" modifiable:"true"` + NoTrailingStopLoss bool `json:"noTrailingStopLoss" modifiable:"true"` + TrailingStopLossType string `json:"trailingStopLossType" modifiable:"true"` // trailing stop sources. Possible options are `kline` for 1m kline and `realtime` from order updates + HLRangeWindow int `json:"hlRangeWindow"` + SmootherWindow int `json:"smootherWindow"` + FisherTransformWindow int `json:"fisherTransformWindow"` + ATRWindow int `json:"atrWindow"` + PendingMinutes int `json:"pendingMinutes" modifiable:"true"` // if order not be traded for pendingMinutes of time, cancel it. + NoRebalance bool `json:"noRebalance" modifiable:"true"` // disable rebalance + TrendWindow int `json:"trendWindow"` // trendLine is used for rebalancing the position. When trendLine goes up, hold base, otherwise hold quote + RebalanceFilter float64 `json:"rebalanceFilter" modifiable:"true"` // beta filter on the Linear Regression of trendLine + TrailingCallbackRate []float64 `json:"trailingCallbackRate" modifiable:"true"` + TrailingActivationRatio []float64 `json:"trailingActivationRatio" modifiable:"true"` + + buyPrice float64 `persistence:"buy_price"` + sellPrice float64 `persistence:"sell_price"` + highestPrice float64 `persistence:"highest_price"` + lowestPrice float64 `persistence:"lowest_price"` + + // This is not related to trade but for statistics graph generation + // Will deduct fee in percentage from every trade + GraphPNLDeductFee bool `json:"graphPNLDeductFee"` + GraphPNLPath string `json:"graphPNLPath"` + GraphCumPNLPath string `json:"graphCumPNLPath"` + // Whether to generate graph when shutdown + GenerateGraph bool `json:"generateGraph"` + + ExitMethods bbgo.ExitMethodSet `json:"exits"` + Session *bbgo.ExchangeSession + *bbgo.GeneralOrderExecutor + + getLastPrice func() fixedpoint.Value +} + +func (s *Strategy) ID() string { + return ID +} + +func (s *Strategy) InstanceID() string { + return fmt.Sprintf("%s:%s:%v", ID, s.Symbol, bbgo.IsBackTesting) +} + +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { + // by default, bbgo only pre-subscribe 1000 klines. + // this is not enough if we're subscribing 30m intervals using SerialMarketDataStore + maxWindow := (s.Window + s.SmootherWindow + s.FisherTransformWindow) * s.Interval.Minutes() + bbgo.KLinePreloadLimit = int64((maxWindow/1000 + 1) * 1000) + log.Errorf("set kLinePreloadLimit to %d, %d %d", bbgo.KLinePreloadLimit, s.Interval.Minutes(), maxWindow) + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ + Interval: types.Interval1m, + }) + + if !bbgo.IsBackTesting { + session.Subscribe(types.BookTickerChannel, s.Symbol, types.SubscribeOptions{}) + } + s.ExitMethods.SetAndSubscribe(session, s) +} + +func (s *Strategy) CurrentPosition() *types.Position { + return s.Position +} + +const closeOrderRetryLimit = 5 + +func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Value) error { + order := s.p.NewMarketCloseOrder(percentage) + if order == nil { + return nil + } + order.Tag = "close" + order.TimeInForce = "" + + order.MarginSideEffect = types.SideEffectTypeAutoRepay + for i := 0; i < closeOrderRetryLimit; i++ { + price := s.getLastPrice() + balances := s.GeneralOrderExecutor.Session().GetAccount().Balances() + baseBalance := balances[s.Market.BaseCurrency].Available + if order.Side == types.SideTypeBuy { + quoteAmount := balances[s.Market.QuoteCurrency].Available.Div(price) + if order.Quantity.Compare(quoteAmount) > 0 { + order.Quantity = quoteAmount + } + } else if order.Side == types.SideTypeSell && order.Quantity.Compare(baseBalance) > 0 { + order.Quantity = baseBalance + } + if s.Market.IsDustQuantity(order.Quantity, price) { + return nil + } + _, err := s.GeneralOrderExecutor.SubmitOrders(ctx, *order) + if err != nil { + order.Quantity = order.Quantity.Mul(fixedpoint.One.Sub(Delta)) + continue + } + return nil + } + return errors.New("exceed retry limit") +} + +func (s *Strategy) initIndicators(store *bbgo.SerialMarketDataStore) error { + s.ma = &indicator.SMA{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.HLRangeWindow}} + s.stdevHigh = &indicator.StdDev{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.HLRangeWindow}} + s.stdevLow = &indicator.StdDev{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.HLRangeWindow}} + s.drift = &DriftMA{ + drift: &indicator.WeightedDrift{ + MA: &indicator.SMA{IntervalWindow: s.IntervalWindow}, + IntervalWindow: s.IntervalWindow, + }, + ma1: &indicator.EWMA{ + IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.SmootherWindow}, + }, + ma2: &indicator.FisherTransform{ + IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.FisherTransformWindow}, + }, + } + s.drift.SeriesBase.Series = s.drift + s.atr = &indicator.ATR{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.ATRWindow}} + s.trendLine = &indicator.EWMA{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.TrendWindow}} + + klines, ok := store.KLinesOfInterval(s.Interval) + klinesLength := len(*klines) + if !ok || klinesLength == 0 { + return errors.New("klines not exists") + } + log.Infof("loaded %d klines", klinesLength) + for _, kline := range *klines { + source := s.GetSource(&kline).Float64() + high := kline.High.Float64() + low := kline.Low.Float64() + s.ma.Update(source) + s.stdevHigh.Update(high - s.ma.Last()) + s.stdevLow.Update(s.ma.Last() - low) + s.drift.Update(source, kline.Volume.Abs().Float64()) + s.trendLine.Update(source) + s.atr.PushK(kline) + s.priceLines.Update(source) + } + if s.frameKLine != nil && klines != nil { + s.frameKLine.Set(&(*klines)[len(*klines)-1]) + } + klines, ok = store.KLinesOfInterval(types.Interval1m) + klinesLength = len(*klines) + if !ok || klinesLength == 0 { + return errors.New("klines not exists") + } + log.Infof("loaded %d klines1m", klinesLength) + if s.kline1m != nil && klines != nil { + s.kline1m.Set(&(*klines)[len(*klines)-1]) + } + s.startTime = s.kline1m.StartTime.Time().Add(s.kline1m.Interval.Duration()) + return nil +} + +func (s *Strategy) smartCancel(ctx context.Context, pricef, atr float64) (int, error) { + nonTraded := s.GeneralOrderExecutor.ActiveMakerOrders().Orders() + if len(nonTraded) > 0 { + if len(nonTraded) > 1 { + log.Errorf("should only have one order to cancel, got %d", len(nonTraded)) + } + toCancel := false + + for _, order := range nonTraded { + if order.Status != types.OrderStatusNew && order.Status != types.OrderStatusPartiallyFilled { + continue + } + log.Warnf("%v | counter: %d, system: %d", order, s.orderPendingCounter[order.OrderID], s.minutesCounter) + if s.minutesCounter-s.orderPendingCounter[order.OrderID] > s.PendingMinutes { + toCancel = true + } else if order.Side == types.SideTypeBuy { + // 75% of the probability + if order.Price.Float64()+s.stdevHigh.Last()*2 <= pricef { + toCancel = true + } + } else if order.Side == types.SideTypeSell { + // 75% of the probability + if order.Price.Float64()-s.stdevLow.Last()*2 >= pricef { + toCancel = true + } + } else { + panic("not supported side for the order") + } + } + if toCancel { + err := s.GeneralOrderExecutor.GracefulCancel(ctx) + // TODO: clean orderPendingCounter on cancel/trade + for _, order := range nonTraded { + delete(s.orderPendingCounter, order.OrderID) + } + log.Warnf("cancel all %v", err) + return 0, err + } + } + return len(nonTraded), nil +} + +func (s *Strategy) trailingCheck(price float64, direction string) bool { + if s.highestPrice > 0 && s.highestPrice < price { + s.highestPrice = price + } + if s.lowestPrice > 0 && s.lowestPrice > price { + s.lowestPrice = price + } + isShort := direction == "short" + if isShort && s.sellPrice == 0 || !isShort && s.buyPrice == 0 { + return false + } + for i := len(s.TrailingCallbackRate) - 1; i >= 0; i-- { + trailingCallbackRate := s.TrailingCallbackRate[i] + trailingActivationRatio := s.TrailingActivationRatio[i] + if isShort { + if (s.sellPrice-s.lowestPrice)/s.lowestPrice > trailingActivationRatio { + return (price-s.lowestPrice)/s.lowestPrice > trailingCallbackRate + } + } else { + if (s.highestPrice-s.buyPrice)/s.buyPrice > trailingActivationRatio { + return (s.highestPrice-price)/s.buyPrice > trailingCallbackRate + } + } + } + return false +} + +func (s *Strategy) initTickerFunctions(ctx context.Context) { + if s.IsBackTesting() { + s.getLastPrice = func() fixedpoint.Value { + lastPrice, ok := s.Session.LastPrice(s.Symbol) + if !ok { + log.Error("cannot get lastprice") + } + return lastPrice + } + } else { + s.Session.MarketDataStream.OnBookTickerUpdate(func(ticker types.BookTicker) { + bestBid := ticker.Buy + bestAsk := ticker.Sell + + var pricef float64 + if !util.TryLock(&s.lock) { + return + } + if !bestAsk.IsZero() && !bestBid.IsZero() { + s.midPrice = bestAsk.Add(bestBid).Div(Two) + } else if !bestAsk.IsZero() { + s.midPrice = bestAsk + } else { + s.midPrice = bestBid + } + pricef = s.midPrice.Float64() + + s.lock.Unlock() + + if !util.TryLock(&s.positionLock) { + return + } + + if s.highestPrice > 0 && s.highestPrice < pricef { + s.highestPrice = pricef + } + if s.lowestPrice > 0 && s.lowestPrice > pricef { + s.lowestPrice = pricef + } + if s.CheckStopLoss() { + s.positionLock.Unlock() + s.ClosePosition(ctx, fixedpoint.One) + return + } + // for trailing stoploss during the realtime + if s.NoTrailingStopLoss || s.TrailingStopLossType == "kline" { + s.positionLock.Unlock() + return + } + + exitCondition := s.trailingCheck(pricef, "short") || s.trailingCheck(pricef, "long") + + s.positionLock.Unlock() + if exitCondition { + s.ClosePosition(ctx, fixedpoint.One) + } + }) + s.getLastPrice = func() (lastPrice fixedpoint.Value) { + var ok bool + s.lock.RLock() + defer s.lock.RUnlock() + if s.midPrice.IsZero() { + lastPrice, ok = s.Session.LastPrice(s.Symbol) + if !ok { + log.Error("cannot get lastprice") + return lastPrice + } + } else { + lastPrice = s.midPrice + } + return lastPrice + } + } + +} + +func (s *Strategy) DrawIndicators(time types.Time) *types.Canvas { + canvas := types.NewCanvas(s.InstanceID(), s.Interval) + Length := s.priceLines.Length() + if Length > 300 { + Length = 300 + } + log.Infof("draw indicators with %d data", Length) + mean := s.priceLines.Mean(Length) + highestPrice := s.priceLines.Minus(mean).Abs().Highest(Length) + highestDrift := s.drift.Abs().Highest(Length) + hi := s.drift.drift.Abs().Highest(Length) + ratio := highestPrice / highestDrift + + // canvas.Plot("upband", s.ma.Add(s.stdevHigh), time, Length) + canvas.Plot("ma", s.ma, time, Length) + // canvas.Plot("downband", s.ma.Minus(s.stdevLow), time, Length) + fmt.Printf("%f %f\n", highestPrice, hi) + + canvas.Plot("trend", s.trendLine, time, Length) + canvas.Plot("drift", s.drift.Mul(ratio).Add(mean), time, Length) + canvas.Plot("driftOrig", s.drift.drift.Mul(highestPrice/hi).Add(mean), time, Length) + canvas.Plot("zero", types.NumberSeries(mean), time, Length) + canvas.Plot("price", s.priceLines, time, Length) + return canvas +} + +func (s *Strategy) DrawPNL(profit types.Series) *types.Canvas { + canvas := types.NewCanvas(s.InstanceID()) + log.Errorf("pnl Highest: %f, Lowest: %f", types.Highest(profit, profit.Length()), types.Lowest(profit, profit.Length())) + length := profit.Length() + if s.GraphPNLDeductFee { + canvas.PlotRaw("pnl % (with Fee Deducted)", profit, length) + } else { + canvas.PlotRaw("pnl %", profit, length) + } + canvas.YAxis = chart.YAxis{ + ValueFormatter: func(v interface{}) string { + if vf, isFloat := v.(float64); isFloat { + return fmt.Sprintf("%.4f", vf) + } + return "" + }, + } + canvas.PlotRaw("1", types.NumberSeries(1), length) + return canvas +} + +func (s *Strategy) DrawCumPNL(cumProfit types.Series) *types.Canvas { + canvas := types.NewCanvas(s.InstanceID()) + canvas.PlotRaw("cummulative pnl", cumProfit, cumProfit.Length()) + canvas.YAxis = chart.YAxis{ + ValueFormatter: func(v interface{}) string { + if vf, isFloat := v.(float64); isFloat { + return fmt.Sprintf("%.4f", vf) + } + return "" + }, + } + return canvas +} + +func (s *Strategy) Draw(time types.Time, profit types.Series, cumProfit types.Series) { + canvas := s.DrawIndicators(time) + f, err := os.Create(s.CanvasPath) + if err != nil { + log.WithError(err).Errorf("cannot create on %s", s.CanvasPath) + return + } + defer f.Close() + if err := canvas.Render(chart.PNG, f); err != nil { + log.WithError(err).Errorf("cannot render in drift") + } + + canvas = s.DrawPNL(profit) + f, err = os.Create(s.GraphPNLPath) + if err != nil { + log.WithError(err).Errorf("open pnl") + return + } + defer f.Close() + if err := canvas.Render(chart.PNG, f); err != nil { + log.WithError(err).Errorf("render pnl") + } + + canvas = s.DrawCumPNL(cumProfit) + f, err = os.Create(s.GraphCumPNLPath) + if err != nil { + log.WithError(err).Errorf("open cumpnl") + return + } + defer f.Close() + if err := canvas.Render(chart.PNG, f); err != nil { + log.WithError(err).Errorf("render cumpnl") + } +} + +// Sending new rebalance orders cost too much. +// Modify the position instead to expect the strategy itself rebalance on Close +func (s *Strategy) Rebalance(ctx context.Context) { + price := s.getLastPrice() + _, beta := types.LinearRegression(s.trendLine, 3) + if math.Abs(beta) > s.RebalanceFilter && math.Abs(s.beta) > s.RebalanceFilter || math.Abs(s.beta) < s.RebalanceFilter && math.Abs(beta) < s.RebalanceFilter { + return + } + balances := s.GeneralOrderExecutor.Session().GetAccount().Balances() + baseBalance := balances[s.Market.BaseCurrency].Total() + quoteBalance := balances[s.Market.QuoteCurrency].Total() + total := baseBalance.Add(quoteBalance.Div(price)) + percentage := fixedpoint.One.Sub(Delta) + log.Infof("rebalance beta %f %v", beta, s.p) + if beta > s.RebalanceFilter { + if total.Mul(percentage).Compare(baseBalance) > 0 { + q := total.Mul(percentage).Sub(baseBalance) + s.p.Lock() + defer s.p.Unlock() + s.p.Base = q.Neg() + s.p.Quote = q.Mul(price) + s.p.AverageCost = price + } + } else if beta <= -s.RebalanceFilter { + if total.Mul(percentage).Compare(quoteBalance.Div(price)) > 0 { + q := total.Mul(percentage).Sub(quoteBalance.Div(price)) + s.p.Lock() + defer s.p.Unlock() + s.p.Base = q + s.p.Quote = q.Mul(price).Neg() + s.p.AverageCost = price + } + } else { + if total.Div(Two).Compare(quoteBalance.Div(price)) > 0 { + q := total.Div(Two).Sub(quoteBalance.Div(price)) + s.p.Lock() + defer s.p.Unlock() + s.p.Base = q + s.p.Quote = q.Mul(price).Neg() + s.p.AverageCost = price + } else if total.Div(Two).Compare(baseBalance) > 0 { + q := total.Div(Two).Sub(baseBalance) + s.p.Lock() + defer s.p.Unlock() + s.p.Base = q.Neg() + s.p.Quote = q.Mul(price) + s.p.AverageCost = price + } else { + s.p.Lock() + defer s.p.Unlock() + s.p.Reset() + } + } + log.Infof("rebalanceafter %v %v %v", baseBalance, quoteBalance, s.p) + s.beta = beta +} + +func (s *Strategy) CalcAssetValue(price fixedpoint.Value) fixedpoint.Value { + balances := s.Session.GetAccount().Balances() + return balances[s.Market.BaseCurrency].Total().Mul(price).Add(balances[s.Market.QuoteCurrency].Total()) +} + +func (s *Strategy) klineHandler1m(ctx context.Context, kline types.KLine) { + s.kline1m.Set(&kline) + if s.Status != types.StrategyStatusRunning { + return + } + // for doing the trailing stoploss during backtesting + atr := s.atr.Last() + price := s.getLastPrice() + pricef := price.Float64() + + lowf := math.Min(kline.Low.Float64(), pricef) + highf := math.Max(kline.High.Float64(), pricef) + s.positionLock.Lock() + if s.lowestPrice > 0 && lowf < s.lowestPrice { + s.lowestPrice = lowf + } + if s.highestPrice > 0 && highf > s.highestPrice { + s.highestPrice = highf + } + + numPending := 0 + var err error + if numPending, err = s.smartCancel(ctx, pricef, atr); err != nil { + log.WithError(err).Errorf("cannot cancel orders") + s.positionLock.Unlock() + return + } + if numPending > 0 { + s.positionLock.Unlock() + return + } + + if s.NoTrailingStopLoss || s.TrailingStopLossType == "realtime" { + s.positionLock.Unlock() + return + } + + exitCondition := s.CheckStopLoss() || s.trailingCheck(highf, "short") || s.trailingCheck(lowf, "long") + s.positionLock.Unlock() + if exitCondition { + _ = s.ClosePosition(ctx, fixedpoint.One) + } +} + +func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) { + var driftPred, atr float64 + var drift []float64 + + s.frameKLine.Set(&kline) + + source := s.GetSource(&kline) + sourcef := source.Float64() + s.priceLines.Update(sourcef) + s.ma.Update(sourcef) + s.trendLine.Update(sourcef) + s.drift.Update(sourcef, kline.Volume.Abs().Float64()) + + s.atr.PushK(kline) + + driftPred = s.drift.Predict(s.PredictOffset) + ddriftPred := s.drift.drift.Predict(s.PredictOffset) + atr = s.atr.Last() + price := s.getLastPrice() + pricef := price.Float64() + lowf := math.Min(kline.Low.Float64(), pricef) + highf := math.Max(kline.High.Float64(), pricef) + lowdiff := s.ma.Last() - lowf + s.stdevLow.Update(lowdiff) + highdiff := highf - s.ma.Last() + s.stdevHigh.Update(highdiff) + drift = s.drift.Array(2) + if len(drift) < 2 || len(drift) < s.PredictOffset { + return + } + ddrift := s.drift.drift.Array(2) + if len(ddrift) < 2 || len(ddrift) < s.PredictOffset { + return + } + + if s.Status != types.StrategyStatusRunning { + return + } + + s.positionLock.Lock() + log.Infof("highdiff: %3.2f ma: %.2f, open: %8v, close: %8v, high: %8v, low: %8v, time: %v %v", s.stdevHigh.Last(), s.ma.Last(), kline.Open, kline.Close, kline.High, kline.Low, kline.StartTime, kline.EndTime) + if s.lowestPrice > 0 && lowf < s.lowestPrice { + s.lowestPrice = lowf + } + if s.highestPrice > 0 && highf > s.highestPrice { + s.highestPrice = highf + } + + if !s.NoRebalance { + s.Rebalance(ctx) + } + + balances := s.GeneralOrderExecutor.Session().GetAccount().Balances() + bbgo.Notify("source: %.4f, price: %.4f, driftPred: %.4f, ddriftPred: %.4f, drift[1]: %.4f, ddrift[1]: %.4f, atr: %.4f, lowf %.4f, highf: %.4f lowest: %.4f highest: %.4f sp %.4f bp %.4f", + sourcef, pricef, driftPred, ddriftPred, drift[1], ddrift[1], atr, lowf, highf, s.lowestPrice, s.highestPrice, s.sellPrice, s.buyPrice) + // Notify will parse args to strings and process separately + bbgo.Notify("balances: [Total] %v %s [Base] %s(%v %s) [Quote] %s", + s.CalcAssetValue(price), + s.Market.QuoteCurrency, + balances[s.Market.BaseCurrency].String(), + balances[s.Market.BaseCurrency].Total().Mul(price), + s.Market.QuoteCurrency, + balances[s.Market.QuoteCurrency].String(), + ) + + shortCondition := drift[1] >= 0 && drift[0] <= 0 || (drift[1] >= drift[0] && drift[1] <= 0) || ddrift[1] >= 0 && ddrift[0] <= 0 || (ddrift[1] >= ddrift[0] && ddrift[1] <= 0) + longCondition := drift[1] <= 0 && drift[0] >= 0 || (drift[1] <= drift[0] && drift[1] >= 0) || ddrift[1] <= 0 && ddrift[0] >= 0 || (ddrift[1] <= ddrift[0] && ddrift[1] >= 0) + if shortCondition && longCondition { + if drift[1] > drift[0] { + longCondition = false + } else { + shortCondition = false + } + } + exitCondition := s.CheckStopLoss() || s.trailingCheck(pricef, "short") || s.trailingCheck(pricef, "long") + + if exitCondition { + s.positionLock.Unlock() + if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil { + log.WithError(err).Errorf("cannot cancel orders") + return + } + _ = s.ClosePosition(ctx, fixedpoint.One) + if shortCondition || longCondition { + s.positionLock.Lock() + } else { + return + } + } + + if longCondition { + if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil { + log.WithError(err).Errorf("cannot cancel orders") + s.positionLock.Unlock() + return + } + source = source.Sub(fixedpoint.NewFromFloat(s.stdevLow.Last() * s.HighLowVarianceMultiplier)) + if source.Compare(price) > 0 { + source = price + } + /*source = fixedpoint.NewFromFloat(s.ma.Last() - s.stdevLow.Last()*s.HighLowVarianceMultiplier) + if source.Compare(price) > 0 { + source = price + } + sourcef = source.Float64()*/ + log.Infof("source in long %v %v %f", source, price, s.stdevLow.Last()) + + s.positionLock.Unlock() + opt := s.OpenPositionOptions + opt.Long = true + opt.Price = source + opt.Tags = []string{"long"} + createdOrders, err := s.GeneralOrderExecutor.OpenPosition(ctx, opt) + if err != nil { + errs := filterErrors(multierr.Errors(err)) + if len(errs) > 0 { + log.Errorf("%v", errs) + log.WithError(err).Errorf("cannot place buy order") + } + return + } + log.Infof("orders %v", createdOrders) + if createdOrders != nil { + s.orderPendingCounter[createdOrders[0].OrderID] = s.minutesCounter + } + return + } + if shortCondition { + if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil { + log.WithError(err).Errorf("cannot cancel orders") + s.positionLock.Unlock() + return + } + + source = source.Add(fixedpoint.NewFromFloat(s.stdevHigh.Last() * s.HighLowVarianceMultiplier)) + if source.Compare(price) < 0 { + source = price + } + /*source = fixedpoint.NewFromFloat(s.ma.Last() + s.stdevHigh.Last()*s.HighLowVarianceMultiplier) + if source.Compare(price) < 0 { + source = price + } + sourcef = source.Float64()*/ + + log.Infof("source in short: %v", source) + + s.positionLock.Unlock() + opt := s.OpenPositionOptions + opt.Short = true + opt.Price = source + opt.Tags = []string{"short"} + createdOrders, err := s.GeneralOrderExecutor.OpenPosition(ctx, opt) + if err != nil { + errs := filterErrors(multierr.Errors(err)) + if len(errs) > 0 { + log.WithError(err).Errorf("cannot place sell order") + } + return + } + log.Infof("orders %v", createdOrders) + if createdOrders != nil { + s.orderPendingCounter[createdOrders[0].OrderID] = s.minutesCounter + } + return + } + s.positionLock.Unlock() +} + +func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + if s.Leverage == fixedpoint.Zero { + s.Leverage = fixedpoint.One + } + instanceID := s.InstanceID() + // Will be set by persistence if there's any from DB + if s.Position == nil { + s.Position = types.NewPositionFromMarket(s.Market) + s.p = types.NewPositionFromMarket(s.Market) + } else { + s.p = types.NewPositionFromMarket(s.Market) + s.p.Base = s.Position.Base + s.p.Quote = s.Position.Quote + s.p.AverageCost = s.Position.AverageCost + } + if s.ProfitStats == nil { + s.ProfitStats = types.NewProfitStats(s.Market) + } + if s.TradeStats == nil { + s.TradeStats = types.NewTradeStats(s.Symbol) + } + // StrategyController + s.Status = types.StrategyStatusRunning + + s.OnSuspend(func() { + _ = s.GeneralOrderExecutor.GracefulCancel(ctx) + }) + + s.OnEmergencyStop(func() { + _ = s.GeneralOrderExecutor.GracefulCancel(ctx) + _ = s.ClosePosition(ctx, fixedpoint.One) + }) + + s.GeneralOrderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) + s.GeneralOrderExecutor.BindEnvironment(s.Environment) + s.GeneralOrderExecutor.BindProfitStats(s.ProfitStats) + s.GeneralOrderExecutor.BindTradeStats(s.TradeStats) + s.GeneralOrderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { + bbgo.Sync(ctx, s) + }) + s.GeneralOrderExecutor.Bind() + + s.orderPendingCounter = make(map[uint64]int) + s.minutesCounter = 0 + + // Exit methods from config + for _, method := range s.ExitMethods { + method.Bind(session, s.GeneralOrderExecutor) + } + + profit := floats.Slice{1., 1.} + price, _ := s.Session.LastPrice(s.Symbol) + initAsset := s.CalcAssetValue(price).Float64() + cumProfit := floats.Slice{initAsset, initAsset} + modify := func(p float64) float64 { + return p + } + if s.GraphPNLDeductFee { + modify = func(p float64) float64 { + return p * (1. - Fee) + } + } + s.GeneralOrderExecutor.TradeCollector().OnTrade(func(trade types.Trade, _profit, _netProfit fixedpoint.Value) { + s.p.AddTrade(trade) + order, ok := s.GeneralOrderExecutor.TradeCollector().OrderStore().Get(trade.OrderID) + if !ok { + panic(fmt.Sprintf("cannot find order: %v", trade)) + } + tag := order.Tag + + price := trade.Price.Float64() + + if s.buyPrice > 0 { + profit.Update(modify(price / s.buyPrice)) + cumProfit.Update(s.CalcAssetValue(trade.Price).Float64()) + } else if s.sellPrice > 0 { + profit.Update(modify(s.sellPrice / price)) + cumProfit.Update(s.CalcAssetValue(trade.Price).Float64()) + } + s.positionLock.Lock() + defer s.positionLock.Unlock() + if s.p.IsDust(trade.Price) { + s.buyPrice = 0 + s.sellPrice = 0 + s.highestPrice = 0 + s.lowestPrice = 0 + } else if s.p.IsLong() { + s.buyPrice = s.p.ApproximateAverageCost.Float64() // trade.Price.Float64() + s.sellPrice = 0 + s.highestPrice = math.Max(s.buyPrice, s.highestPrice) + s.lowestPrice = s.buyPrice + } else if s.p.IsShort() { + s.sellPrice = s.p.ApproximateAverageCost.Float64() // trade.Price.Float64() + s.buyPrice = 0 + s.highestPrice = s.sellPrice + if s.lowestPrice == 0 { + s.lowestPrice = s.sellPrice + } else { + s.lowestPrice = math.Min(s.lowestPrice, s.sellPrice) + } + } + bbgo.Notify("tag: %s, sp: %.4f bp: %.4f hp: %.4f lp: %.4f, trade: %s, pos: %s", tag, s.sellPrice, s.buyPrice, s.highestPrice, s.lowestPrice, trade.String(), s.p.String()) + }) + + s.frameKLine = &types.KLine{} + s.kline1m = &types.KLine{} + s.priceLines = types.NewQueue(300) + + s.initTickerFunctions(ctx) + startTime := s.Environment.StartTime() + s.TradeStats.SetIntervalProfitCollector(types.NewIntervalProfitCollector(types.Interval1d, startTime)) + s.TradeStats.SetIntervalProfitCollector(types.NewIntervalProfitCollector(types.Interval1w, startTime)) + + // default value: use 1m kline + if !s.NoTrailingStopLoss && s.IsBackTesting() || s.TrailingStopLossType == "" { + s.TrailingStopLossType = "kline" + } + + bbgo.RegisterCommand("/draw", "Draw Indicators", func(reply interact.Reply) { + canvas := s.DrawIndicators(s.frameKLine.StartTime) + var buffer bytes.Buffer + if err := canvas.Render(chart.PNG, &buffer); err != nil { + log.WithError(err).Errorf("cannot render indicators in drift") + reply.Message(fmt.Sprintf("[error] cannot render indicators in drift: %v", err)) + return + } + bbgo.SendPhoto(&buffer) + }) + + bbgo.RegisterCommand("/pnl", "Draw PNL(%) per trade", func(reply interact.Reply) { + canvas := s.DrawPNL(&profit) + var buffer bytes.Buffer + if err := canvas.Render(chart.PNG, &buffer); err != nil { + log.WithError(err).Errorf("cannot render pnl in drift") + reply.Message(fmt.Sprintf("[error] cannot render pnl in drift: %v", err)) + return + } + bbgo.SendPhoto(&buffer) + }) + + bbgo.RegisterCommand("/cumpnl", "Draw Cummulative PNL(Quote)", func(reply interact.Reply) { + canvas := s.DrawCumPNL(&cumProfit) + var buffer bytes.Buffer + if err := canvas.Render(chart.PNG, &buffer); err != nil { + log.WithError(err).Errorf("cannot render cumpnl in drift") + reply.Message(fmt.Sprintf("[error] canot render cumpnl in drift: %v", err)) + return + } + bbgo.SendPhoto(&buffer) + }) + + bbgo.RegisterCommand("/config", "Show latest config", func(reply interact.Reply) { + var buffer bytes.Buffer + s.Print(&buffer, false) + reply.Message(buffer.String()) + }) + + bbgo.RegisterCommand("/pos", "Show internal position", func(reply interact.Reply) { + reply.Message(s.p.String()) + }) + + bbgo.RegisterCommand("/dump", "Dump internal params", func(reply interact.Reply) { + reply.Message("Please enter series output length:") + }).Next(func(length string, reply interact.Reply) { + var buffer bytes.Buffer + l, err := strconv.Atoi(length) + if err != nil { + dynamic.ParamDump(s, &buffer) + } else { + dynamic.ParamDump(s, &buffer, l) + } + reply.Message(buffer.String()) + }) + + bbgo.RegisterModifier(s) + + // event trigger order: s.Interval => Interval1m + store, ok := session.SerialMarketDataStore(s.Symbol, []types.Interval{s.Interval, types.Interval1m}) + if !ok { + panic("cannot get 1m history") + } + if err := s.initIndicators(store); err != nil { + log.WithError(err).Errorf("initIndicator failed") + return nil + } + store.OnKLineClosed(func(kline types.KLine) { + s.minutesCounter = int(kline.StartTime.Time().Add(kline.Interval.Duration()).Sub(s.startTime).Minutes()) + if kline.Interval == s.Interval { + s.klineHandler(ctx, kline) + } else if kline.Interval == types.Interval1m { + s.klineHandler1m(ctx, kline) + } + }) + + bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { + + var buffer bytes.Buffer + + s.Print(&buffer, true, true) + + fmt.Fprintln(&buffer, "--- NonProfitable Dates ---") + for _, daypnl := range s.TradeStats.IntervalProfits[types.Interval1d].GetNonProfitableIntervals() { + fmt.Fprintf(&buffer, "%s\n", daypnl) + } + fmt.Fprintln(&buffer, s.TradeStats.BriefString()) + + os.Stdout.Write(buffer.Bytes()) + + if s.GenerateGraph { + s.Draw(s.frameKLine.StartTime, &profit, &cumProfit) + } + wg.Done() + }) + return nil +} diff --git a/pkg/strategy/drift/strategy_test.go b/pkg/strategy/drift/strategy_test.go new file mode 100644 index 0000000000..7ab05f78ee --- /dev/null +++ b/pkg/strategy/drift/strategy_test.go @@ -0,0 +1,37 @@ +package drift + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_TrailingCheckLong(t *testing.T) { + s := &Strategy{} + s.highestPrice = 30. + s.buyPrice = 30. + s.TrailingActivationRatio = []float64{0.002, 0.01} + s.TrailingCallbackRate = []float64{0.0008, 0.0016} + assert.False(t, s.trailingCheck(31., "long")) + assert.False(t, s.trailingCheck(31., "short")) + assert.False(t, s.trailingCheck(30.96, "short")) + assert.False(t, s.trailingCheck(30.96, "long")) + assert.False(t, s.trailingCheck(30.95, "short")) + assert.True(t, s.trailingCheck(30.95, "long")) +} + +func Test_TrailingCheckShort(t *testing.T) { + s := &Strategy{} + s.lowestPrice = 30. + s.sellPrice = 30. + s.TrailingActivationRatio = []float64{0.002, 0.01} + s.TrailingCallbackRate = []float64{0.0008, 0.0016} + assert.False(t, s.trailingCheck(29.96, "long")) + assert.False(t, s.trailingCheck(29.96, "short")) + assert.False(t, s.trailingCheck(29.99, "short")) + assert.False(t, s.trailingCheck(29.99, "long")) + assert.False(t, s.trailingCheck(29.93, "short")) + assert.False(t, s.trailingCheck(29.93, "long")) + assert.True(t, s.trailingCheck(29.96, "short")) + assert.False(t, s.trailingCheck(29.96, "long")) +} diff --git a/pkg/strategy/elliottwave/draw.go b/pkg/strategy/elliottwave/draw.go new file mode 100644 index 0000000000..2bf767ce91 --- /dev/null +++ b/pkg/strategy/elliottwave/draw.go @@ -0,0 +1,138 @@ +package elliottwave + +import ( + "bytes" + "fmt" + "os" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/interact" + "github.com/c9s/bbgo/pkg/types" + "github.com/wcharczuk/go-chart/v2" +) + +func (s *Strategy) InitDrawCommands(store *bbgo.SerialMarketDataStore, profit, cumProfit types.Series) { + bbgo.RegisterCommand("/draw", "Draw Indicators", func(reply interact.Reply) { + canvas := s.DrawIndicators(store) + if canvas == nil { + reply.Message("cannot render indicators") + return + } + var buffer bytes.Buffer + if err := canvas.Render(chart.PNG, &buffer); err != nil { + log.WithError(err).Errorf("cannot render indicators in ewo") + reply.Message(fmt.Sprintf("[error] cannot render indicators in ewo: %v", err)) + return + } + bbgo.SendPhoto(&buffer) + }) + bbgo.RegisterCommand("/pnl", "Draw PNL(%) per trade", func(reply interact.Reply) { + canvas := s.DrawPNL(profit) + var buffer bytes.Buffer + if err := canvas.Render(chart.PNG, &buffer); err != nil { + log.WithError(err).Errorf("cannot render pnl in drift") + reply.Message(fmt.Sprintf("[error] cannot render pnl in ewo: %v", err)) + return + } + bbgo.SendPhoto(&buffer) + }) + bbgo.RegisterCommand("/cumpnl", "Draw Cummulative PNL(Quote)", func(reply interact.Reply) { + canvas := s.DrawCumPNL(cumProfit) + var buffer bytes.Buffer + if err := canvas.Render(chart.PNG, &buffer); err != nil { + log.WithError(err).Errorf("cannot render cumpnl in drift") + reply.Message(fmt.Sprintf("[error] canot render cumpnl in drift: %v", err)) + return + } + bbgo.SendPhoto(&buffer) + }) +} + +func (s *Strategy) DrawIndicators(store *bbgo.SerialMarketDataStore) *types.Canvas { + klines, ok := store.KLinesOfInterval(types.Interval1m) + if !ok { + return nil + } + time := (*klines)[len(*klines)-1].StartTime + canvas := types.NewCanvas(s.InstanceID(), s.Interval) + Length := s.priceLines.Length() + if Length > 300 { + Length = 300 + } + log.Infof("draw indicators with %d data", Length) + mean := s.priceLines.Mean(Length) + high := s.priceLines.Highest(Length) + low := s.priceLines.Lowest(Length) + ehigh := types.Highest(s.ewo, Length) + elow := types.Lowest(s.ewo, Length) + canvas.Plot("ewo", types.Add(types.Mul(s.ewo, (high-low)/(ehigh-elow)), mean), time, Length) + canvas.Plot("zero", types.NumberSeries(mean), time, Length) + canvas.Plot("price", s.priceLines, time, Length) + return canvas +} + +func (s *Strategy) DrawPNL(profit types.Series) *types.Canvas { + canvas := types.NewCanvas(s.InstanceID()) + length := profit.Length() + log.Errorf("pnl Highest: %f, Lowest: %f", types.Highest(profit, length), types.Lowest(profit, length)) + canvas.PlotRaw("pnl %", profit, length) + canvas.YAxis = chart.YAxis{ + ValueFormatter: func(v interface{}) string { + if vf, isFloat := v.(float64); isFloat { + return fmt.Sprintf("%.4f", vf) + } + return "" + }, + } + canvas.PlotRaw("1", types.NumberSeries(1), length) + return canvas +} + +func (s *Strategy) DrawCumPNL(cumProfit types.Series) *types.Canvas { + canvas := types.NewCanvas(s.InstanceID()) + canvas.PlotRaw("cummulative pnl", cumProfit, cumProfit.Length()) + canvas.YAxis = chart.YAxis{ + ValueFormatter: func(v interface{}) string { + if vf, isFloat := v.(float64); isFloat { + return fmt.Sprintf("%.4f", vf) + } + return "" + }, + } + return canvas +} + +func (s *Strategy) Draw(store *bbgo.SerialMarketDataStore, profit, cumProfit types.Series) { + canvas := s.DrawIndicators(store) + f, err := os.Create(s.GraphIndicatorPath) + if err != nil { + log.WithError(err).Errorf("cannot create on path " + s.GraphIndicatorPath) + return + } + defer f.Close() + if err = canvas.Render(chart.PNG, f); err != nil { + log.WithError(err).Errorf("cannot render elliottwave") + } + + canvas = s.DrawPNL(profit) + f, err = os.Create(s.GraphPNLPath) + if err != nil { + log.WithError(err).Errorf("cannot create on path " + s.GraphPNLPath) + return + } + defer f.Close() + if err = canvas.Render(chart.PNG, f); err != nil { + log.WithError(err).Errorf("cannot render pnl") + return + } + canvas = s.DrawCumPNL(cumProfit) + f, err = os.Create(s.GraphCumPNLPath) + if err != nil { + log.WithError(err).Errorf("cannot create on path " + s.GraphCumPNLPath) + return + } + defer f.Close() + if err = canvas.Render(chart.PNG, f); err != nil { + log.WithError(err).Errorf("cannot render cumpnl") + } +} diff --git a/pkg/strategy/elliottwave/ewo.go b/pkg/strategy/elliottwave/ewo.go new file mode 100644 index 0000000000..ff6b7cf2ec --- /dev/null +++ b/pkg/strategy/elliottwave/ewo.go @@ -0,0 +1,25 @@ +package elliottwave + +import "github.com/c9s/bbgo/pkg/indicator" + +type ElliottWave struct { + maSlow *indicator.SMA + maQuick *indicator.SMA +} + +func (s *ElliottWave) Index(i int) float64 { + return s.maQuick.Index(i)/s.maSlow.Index(i) - 1.0 +} + +func (s *ElliottWave) Last() float64 { + return s.maQuick.Last()/s.maSlow.Last() - 1.0 +} + +func (s *ElliottWave) Length() int { + return s.maSlow.Length() +} + +func (s *ElliottWave) Update(v float64) { + s.maSlow.Update(v) + s.maQuick.Update(v) +} diff --git a/pkg/strategy/elliottwave/heikinashi.go b/pkg/strategy/elliottwave/heikinashi.go new file mode 100644 index 0000000000..b712ce30fc --- /dev/null +++ b/pkg/strategy/elliottwave/heikinashi.go @@ -0,0 +1,46 @@ +package elliottwave + +import ( + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +type HeikinAshi struct { + Values []types.KLine + size int +} + +func NewHeikinAshi(size int) *HeikinAshi { + return &HeikinAshi{ + Values: make([]types.KLine, size), + size: size, + } +} + +func (inc *HeikinAshi) Last() *types.KLine { + if len(inc.Values) == 0 { + return &types.KLine{} + } + return &inc.Values[len(inc.Values)-1] +} + +func (inc *HeikinAshi) Update(kline types.KLine) { + open := kline.Open + cloze := kline.Close + high := kline.High + low := kline.Low + lastOpen := inc.Last().Open + lastClose := inc.Last().Close + + newClose := open.Add(high).Add(low).Add(cloze).Div(Four) + newOpen := lastOpen.Add(lastClose).Div(Two) + + kline.Close = newClose + kline.Open = newOpen + kline.High = fixedpoint.Max(fixedpoint.Max(high, newOpen), newClose) + kline.Low = fixedpoint.Max(fixedpoint.Min(low, newOpen), newClose) + inc.Values = append(inc.Values, kline) + if len(inc.Values) > inc.size { + inc.Values = inc.Values[len(inc.Values)-inc.size:] + } +} diff --git a/pkg/strategy/elliottwave/output.go b/pkg/strategy/elliottwave/output.go new file mode 100644 index 0000000000..ebfac2ae42 --- /dev/null +++ b/pkg/strategy/elliottwave/output.go @@ -0,0 +1,43 @@ +package elliottwave + +import ( + "bytes" + "io" + "strconv" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/dynamic" + "github.com/c9s/bbgo/pkg/interact" + "github.com/c9s/bbgo/pkg/style" + "github.com/jedib0t/go-pretty/v6/table" +) + +func (s *Strategy) initOutputCommands() { + bbgo.RegisterCommand("/config", "Show latest config", func(reply interact.Reply) { + var buffer bytes.Buffer + s.Print(&buffer, false) + reply.Message(buffer.String()) + }) + bbgo.RegisterCommand("/dump", "Dump internal params", func(reply interact.Reply) { + reply.Message("Please enter series output length:") + }).Next(func(length string, reply interact.Reply) { + var buffer bytes.Buffer + l, err := strconv.Atoi(length) + if err != nil { + dynamic.ParamDump(s, &buffer) + } else { + dynamic.ParamDump(s, &buffer, l) + } + reply.Message(buffer.String()) + }) + + bbgo.RegisterModifier(s) +} + +func (s *Strategy) Print(f io.Writer, pretty bool, withColor ...bool) { + var tableStyle *table.Style + if pretty { + tableStyle = style.NewDefaultTableStyle() + } + dynamic.PrintConfig(s, f, tableStyle, len(withColor) > 0 && withColor[0], dynamic.DefaultWhiteList()...) +} diff --git a/pkg/strategy/elliottwave/strategy.go b/pkg/strategy/elliottwave/strategy.go new file mode 100644 index 0000000000..4f7998799b --- /dev/null +++ b/pkg/strategy/elliottwave/strategy.go @@ -0,0 +1,546 @@ +package elliottwave + +import ( + "bytes" + "context" + "errors" + "fmt" + "math" + "os" + "sync" + "time" + + "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/util" +) + +const ID = "elliottwave" + +var log = logrus.WithField("strategy", ID) +var Two fixedpoint.Value = fixedpoint.NewFromInt(2) +var Three fixedpoint.Value = fixedpoint.NewFromInt(3) +var Four fixedpoint.Value = fixedpoint.NewFromInt(4) +var Delta fixedpoint.Value = fixedpoint.NewFromFloat(0.00001) + +func init() { + bbgo.RegisterStrategy(ID, &Strategy{}) +} + +type SourceFunc func(*types.KLine) fixedpoint.Value + +type Strategy struct { + Symbol string `json:"symbol"` + + bbgo.OpenPositionOptions + bbgo.StrategyController + bbgo.SourceSelector + types.Market + Session *bbgo.ExchangeSession + + Interval types.Interval `json:"interval"` + Stoploss fixedpoint.Value `json:"stoploss" modifiable:"true"` + WindowATR int `json:"windowATR"` + WindowQuick int `json:"windowQuick"` + WindowSlow int `json:"windowSlow"` + PendingMinutes int `json:"pendingMinutes" modifiable:"true"` + UseHeikinAshi bool `json:"useHeikinAshi"` + + // whether to draw graph or not by the end of backtest + DrawGraph bool `json:"drawGraph"` + GraphIndicatorPath string `json:"graphIndicatorPath"` + GraphPNLPath string `json:"graphPNLPath"` + GraphCumPNLPath string `json:"graphCumPNLPath"` + + *bbgo.Environment + *bbgo.GeneralOrderExecutor + *types.Position `persistence:"position"` + *types.ProfitStats `persistence:"profit_stats"` + *types.TradeStats `persistence:"trade_stats"` + + ewo *ElliottWave + atr *indicator.ATR + heikinAshi *HeikinAshi + + priceLines *types.Queue + + getLastPrice func() fixedpoint.Value + + // for smart cancel + orderPendingCounter map[uint64]int + startTime time.Time + minutesCounter int + + // for position + buyPrice float64 `persistence:"buy_price"` + sellPrice float64 `persistence:"sell_price"` + highestPrice float64 `persistence:"highest_price"` + lowestPrice float64 `persistence:"lowest_price"` + + TrailingCallbackRate []float64 `json:"trailingCallbackRate" modifiable:"true"` + TrailingActivationRatio []float64 `json:"trailingActivationRatio" modifiable:"true"` + ExitMethods bbgo.ExitMethodSet `json:"exits"` + + midPrice fixedpoint.Value + lock sync.RWMutex `ignore:"true"` +} + +func (s *Strategy) ID() string { + return ID +} + +func (s *Strategy) InstanceID() string { + return fmt.Sprintf("%s:%s:%v", ID, s.Symbol, bbgo.IsBackTesting) +} + +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { + // by default, bbgo only pre-subscribe 1000 klines. + // this is not enough if we're subscribing 30m intervals using SerialMarketDataStore + bbgo.KLinePreloadLimit = int64((s.Interval.Minutes()*s.WindowSlow/1000 + 1) + 1000) + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ + Interval: types.Interval1m, + }) + if !bbgo.IsBackTesting { + session.Subscribe(types.BookTickerChannel, s.Symbol, types.SubscribeOptions{}) + } + s.ExitMethods.SetAndSubscribe(session, s) +} + +func (s *Strategy) CurrentPosition() *types.Position { + return s.Position +} + +func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Value) error { + order := s.Position.NewMarketCloseOrder(percentage) + if order == nil { + return nil + } + order.Tag = "close" + order.TimeInForce = "" + balances := s.GeneralOrderExecutor.Session().GetAccount().Balances() + baseBalance := balances[s.Market.BaseCurrency].Available + price := s.getLastPrice() + if order.Side == types.SideTypeBuy { + quoteAmount := balances[s.Market.QuoteCurrency].Available.Div(price) + if order.Quantity.Compare(quoteAmount) > 0 { + order.Quantity = quoteAmount + } + } else if order.Side == types.SideTypeSell && order.Quantity.Compare(baseBalance) > 0 { + order.Quantity = baseBalance + } + order.MarginSideEffect = types.SideEffectTypeAutoRepay + for { + if s.Market.IsDustQuantity(order.Quantity, price) { + return nil + } + _, err := s.GeneralOrderExecutor.SubmitOrders(ctx, *order) + if err != nil { + order.Quantity = order.Quantity.Mul(fixedpoint.One.Sub(Delta)) + continue + } + return nil + } + +} + +func (s *Strategy) initIndicators(store *bbgo.SerialMarketDataStore) error { + s.priceLines = types.NewQueue(300) + maSlow := &indicator.SMA{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.WindowSlow}} + maQuick := &indicator.SMA{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.WindowQuick}} + s.ewo = &ElliottWave{ + maSlow: maSlow, + maQuick: maQuick, + } + s.atr = &indicator.ATR{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.WindowATR}} + klines, ok := store.KLinesOfInterval(s.Interval) + klineLength := len(*klines) + if !ok || klineLength == 0 { + return errors.New("klines not exists") + } + tmpK := (*klines)[klineLength-1] + s.startTime = tmpK.StartTime.Time().Add(tmpK.Interval.Duration()) + s.heikinAshi = NewHeikinAshi(500) + + for _, kline := range *klines { + source := s.GetSource(&kline).Float64() + s.ewo.Update(source) + s.atr.PushK(kline) + s.priceLines.Update(source) + s.heikinAshi.Update(kline) + } + return nil +} + +// FIXME: stdevHigh +func (s *Strategy) smartCancel(ctx context.Context, pricef float64) int { + nonTraded := s.GeneralOrderExecutor.ActiveMakerOrders().Orders() + if len(nonTraded) > 0 { + left := 0 + for _, order := range nonTraded { + if order.Status != types.OrderStatusNew && order.Status != types.OrderStatusPartiallyFilled { + continue + } + log.Warnf("%v | counter: %d, system: %d", order, s.orderPendingCounter[order.OrderID], s.minutesCounter) + toCancel := false + if s.minutesCounter-s.orderPendingCounter[order.OrderID] >= s.PendingMinutes { + toCancel = true + } else if order.Side == types.SideTypeBuy { + if order.Price.Float64()+s.atr.Last()*2 <= pricef { + toCancel = true + } + } else if order.Side == types.SideTypeSell { + // 75% of the probability + if order.Price.Float64()-s.atr.Last()*2 >= pricef { + toCancel = true + } + } else { + panic("not supported side for the order") + } + if toCancel { + err := s.GeneralOrderExecutor.GracefulCancel(ctx, order) + if err == nil { + delete(s.orderPendingCounter, order.OrderID) + } else { + log.WithError(err).Errorf("failed to cancel %v", order.OrderID) + } + log.Warnf("cancel %v", order.OrderID) + } else { + left += 1 + } + } + return left + } + return len(nonTraded) +} + +func (s *Strategy) trailingCheck(price float64, direction string) bool { + if s.highestPrice > 0 && s.highestPrice < price { + s.highestPrice = price + } + if s.lowestPrice > 0 && s.lowestPrice > price { + s.lowestPrice = price + } + isShort := direction == "short" + for i := len(s.TrailingCallbackRate) - 1; i >= 0; i-- { + trailingCallbackRate := s.TrailingCallbackRate[i] + trailingActivationRatio := s.TrailingActivationRatio[i] + if isShort { + if (s.sellPrice-s.lowestPrice)/s.lowestPrice > trailingActivationRatio { + return (price-s.lowestPrice)/s.lowestPrice > trailingCallbackRate + } + } else { + if (s.highestPrice-s.buyPrice)/s.buyPrice > trailingActivationRatio { + return (s.highestPrice-price)/price > trailingCallbackRate + } + } + } + return false +} + +func (s *Strategy) initTickerFunctions() { + if s.IsBackTesting() { + s.getLastPrice = func() fixedpoint.Value { + lastPrice, ok := s.Session.LastPrice(s.Symbol) + if !ok { + log.Error("cannot get lastprice") + } + return lastPrice + } + } else { + s.Session.MarketDataStream.OnBookTickerUpdate(func(ticker types.BookTicker) { + bestBid := ticker.Buy + bestAsk := ticker.Sell + if !util.TryLock(&s.lock) { + return + } + if !bestAsk.IsZero() && !bestBid.IsZero() { + s.midPrice = bestAsk.Add(bestBid).Div(Two) + } else if !bestAsk.IsZero() { + s.midPrice = bestAsk + } else if !bestBid.IsZero() { + s.midPrice = bestBid + } + s.lock.Unlock() + }) + s.getLastPrice = func() (lastPrice fixedpoint.Value) { + var ok bool + s.lock.RLock() + defer s.lock.RUnlock() + if s.midPrice.IsZero() { + lastPrice, ok = s.Session.LastPrice(s.Symbol) + if !ok { + log.Error("cannot get lastprice") + return lastPrice + } + } else { + lastPrice = s.midPrice + } + return lastPrice + } + } +} + +func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + instanceID := s.InstanceID() + if s.Position == nil { + s.Position = types.NewPositionFromMarket(s.Market) + } + if s.ProfitStats == nil { + s.ProfitStats = types.NewProfitStats(s.Market) + } + if s.TradeStats == nil { + s.TradeStats = types.NewTradeStats(s.Symbol) + } + // StrategyController + s.Status = types.StrategyStatusRunning + s.OnSuspend(func() { + _ = s.GeneralOrderExecutor.GracefulCancel(ctx) + }) + s.OnEmergencyStop(func() { + _ = s.GeneralOrderExecutor.GracefulCancel(ctx) + _ = s.ClosePosition(ctx, fixedpoint.One) + }) + s.GeneralOrderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) + s.GeneralOrderExecutor.BindEnvironment(s.Environment) + s.GeneralOrderExecutor.BindProfitStats(s.ProfitStats) + s.GeneralOrderExecutor.BindTradeStats(s.TradeStats) + s.GeneralOrderExecutor.TradeCollector().OnPositionUpdate(func(p *types.Position) { + bbgo.Sync(ctx, s) + }) + s.GeneralOrderExecutor.Bind() + + s.orderPendingCounter = make(map[uint64]int) + s.minutesCounter = 0 + + for _, method := range s.ExitMethods { + method.Bind(session, s.GeneralOrderExecutor) + } + profit := floats.Slice{1., 1.} + price, _ := s.Session.LastPrice(s.Symbol) + initAsset := s.CalcAssetValue(price).Float64() + cumProfit := floats.Slice{initAsset, initAsset} + modify := func(p float64) float64 { + return p + } + s.GeneralOrderExecutor.TradeCollector().OnTrade(func(trade types.Trade, _profit, _netProfit fixedpoint.Value) { + price := trade.Price.Float64() + if s.buyPrice > 0 { + profit.Update(modify(price / s.buyPrice)) + cumProfit.Update(s.CalcAssetValue(trade.Price).Float64()) + } else if s.sellPrice > 0 { + profit.Update(modify(s.sellPrice / price)) + cumProfit.Update(s.CalcAssetValue(trade.Price).Float64()) + } + if s.Position.IsDust(trade.Price) { + s.buyPrice = 0 + s.sellPrice = 0 + s.highestPrice = 0 + s.lowestPrice = 0 + } else if s.Position.IsLong() { + s.buyPrice = price + s.sellPrice = 0 + s.highestPrice = s.buyPrice + s.lowestPrice = 0 + } else { + s.sellPrice = price + s.buyPrice = 0 + s.highestPrice = 0 + s.lowestPrice = s.sellPrice + } + }) + s.initTickerFunctions() + + startTime := s.Environment.StartTime() + s.TradeStats.SetIntervalProfitCollector(types.NewIntervalProfitCollector(types.Interval1d, startTime)) + s.TradeStats.SetIntervalProfitCollector(types.NewIntervalProfitCollector(types.Interval1w, startTime)) + + s.initOutputCommands() + + // event trigger order: s.Interval => Interval1m + store, ok := session.SerialMarketDataStore(s.Symbol, []types.Interval{s.Interval, types.Interval1m}) + if !ok { + panic("cannot get 1m history") + } + if err := s.initIndicators(store); err != nil { + log.WithError(err).Errorf("initIndicator failed") + return nil + } + s.InitDrawCommands(store, &profit, &cumProfit) + store.OnKLineClosed(func(kline types.KLine) { + s.minutesCounter = int(kline.StartTime.Time().Add(kline.Interval.Duration()).Sub(s.startTime).Minutes()) + if kline.Interval == s.Interval { + s.klineHandler(ctx, kline) + } else if kline.Interval == types.Interval1m { + s.klineHandler1m(ctx, kline) + } + }) + + bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { + var buffer bytes.Buffer + for _, daypnl := range s.TradeStats.IntervalProfits[types.Interval1d].GetNonProfitableIntervals() { + fmt.Fprintf(&buffer, "%s\n", daypnl) + } + fmt.Fprintln(&buffer, s.TradeStats.BriefString()) + s.Print(&buffer, true, true) + os.Stdout.Write(buffer.Bytes()) + if s.DrawGraph { + s.Draw(store, &profit, &cumProfit) + } + wg.Done() + }) + return nil +} + +func (s *Strategy) CalcAssetValue(price fixedpoint.Value) fixedpoint.Value { + balances := s.Session.GetAccount().Balances() + return balances[s.Market.BaseCurrency].Total().Mul(price).Add(balances[s.Market.QuoteCurrency].Total()) +} + +func (s *Strategy) klineHandler1m(ctx context.Context, kline types.KLine) { + if s.Status != types.StrategyStatusRunning { + return + } + + stoploss := s.Stoploss.Float64() + price := s.getLastPrice() + pricef := price.Float64() + atr := s.atr.Last() + + numPending := s.smartCancel(ctx, pricef) + if numPending > 0 { + log.Infof("pending orders: %d, exit", numPending) + return + } + lowf := math.Min(kline.Low.Float64(), pricef) + highf := math.Max(kline.High.Float64(), pricef) + if s.lowestPrice > 0 && lowf < s.lowestPrice { + s.lowestPrice = lowf + } + if s.highestPrice > 0 && highf > s.highestPrice { + s.highestPrice = highf + } + exitShortCondition := s.sellPrice > 0 && (s.sellPrice*(1.+stoploss) <= highf || s.sellPrice+atr <= highf || + s.trailingCheck(highf, "short")) + exitLongCondition := s.buyPrice > 0 && (s.buyPrice*(1.-stoploss) >= lowf || s.buyPrice-atr >= lowf || + s.trailingCheck(lowf, "long")) + + if exitShortCondition || exitLongCondition { + _ = s.ClosePosition(ctx, fixedpoint.One) + } +} + +func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) { + s.heikinAshi.Update(kline) + source := s.GetSource(&kline) + sourcef := source.Float64() + s.priceLines.Update(sourcef) + if s.UseHeikinAshi { + source := s.GetSource(s.heikinAshi.Last()) + sourcef := source.Float64() + s.ewo.Update(sourcef) + } else { + s.ewo.Update(sourcef) + } + s.atr.PushK(kline) + + if s.Status != types.StrategyStatusRunning { + return + } + + stoploss := s.Stoploss.Float64() + price := s.getLastPrice() + pricef := price.Float64() + lowf := math.Min(kline.Low.Float64(), pricef) + highf := math.Min(kline.High.Float64(), pricef) + + s.smartCancel(ctx, pricef) + + atr := s.atr.Last() + ewo := types.Array(s.ewo, 4) + if len(ewo) < 4 { + return + } + bull := kline.Close.Compare(kline.Open) > 0 + + balances := s.GeneralOrderExecutor.Session().GetAccount().Balances() + bbgo.Notify("source: %.4f, price: %.4f lowest: %.4f highest: %.4f sp %.4f bp %.4f", sourcef, pricef, s.lowestPrice, s.highestPrice, s.sellPrice, s.buyPrice) + bbgo.Notify("balances: [Total] %v %s [Base] %s(%v %s) [Quote] %s", + s.CalcAssetValue(price), + s.Market.QuoteCurrency, + balances[s.Market.BaseCurrency].String(), + balances[s.Market.BaseCurrency].Total().Mul(price), + s.Market.QuoteCurrency, + balances[s.Market.QuoteCurrency].String(), + ) + + shortCondition := ewo[0] < ewo[1] && ewo[1] >= ewo[2] && (ewo[1] <= ewo[2] || ewo[2] >= ewo[3]) || s.sellPrice == 0 && ewo[0] < ewo[1] && ewo[1] < ewo[2] + longCondition := ewo[0] > ewo[1] && ewo[1] <= ewo[2] && (ewo[1] >= ewo[2] || ewo[2] <= ewo[3]) || s.buyPrice == 0 && ewo[0] > ewo[1] && ewo[1] > ewo[2] + + exitShortCondition := s.sellPrice > 0 && !shortCondition && s.sellPrice*(1.+stoploss) <= highf || s.sellPrice+atr <= highf || s.trailingCheck(highf, "short") + exitLongCondition := s.buyPrice > 0 && !longCondition && s.buyPrice*(1.-stoploss) >= lowf || s.buyPrice-atr >= lowf || s.trailingCheck(lowf, "long") + + if exitShortCondition || exitLongCondition { + if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil { + log.WithError(err).Errorf("cannot cancel orders") + return + } + s.ClosePosition(ctx, fixedpoint.One) + } + if longCondition && bull { + if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil { + log.WithError(err).Errorf("cannot cancel orders") + return + } + if source.Compare(price) > 0 { + source = price + } + opt := s.OpenPositionOptions + opt.Long = true + opt.Price = source + opt.Tags = []string{"long"} + log.Infof("source in long %v %v", source, price) + createdOrders, err := s.GeneralOrderExecutor.OpenPosition(ctx, opt) + if err != nil { + if _, ok := err.(types.ZeroAssetError); ok { + return + } + log.WithError(err).Errorf("cannot place buy order: %v %v", source, kline) + return + } + if createdOrders != nil { + s.orderPendingCounter[createdOrders[0].OrderID] = s.minutesCounter + } + return + } + if shortCondition && !bull { + if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil { + log.WithError(err).Errorf("cannot cancel orders") + return + } + if source.Compare(price) < 0 { + source = price + } + opt := s.OpenPositionOptions + opt.Short = true + opt.Price = source + opt.Tags = []string{"short"} + log.Infof("source in short %v %v", source, price) + createdOrders, err := s.GeneralOrderExecutor.OpenPosition(ctx, opt) + if err != nil { + if _, ok := err.(types.ZeroAssetError); ok { + return + } + log.WithError(err).Errorf("cannot place sell order: %v %v", source, kline) + return + } + if createdOrders != nil { + s.orderPendingCounter[createdOrders[0].OrderID] = s.minutesCounter + } + return + } +} diff --git a/pkg/strategy/emastop/strategy.go b/pkg/strategy/emastop/strategy.go index 222430713c..2326f5f78c 100644 --- a/pkg/strategy/emastop/strategy.go +++ b/pkg/strategy/emastop/strategy.go @@ -25,12 +25,6 @@ func init() { } type Strategy struct { - *bbgo.Graceful - - // The notification system will be injected into the strategy automatically. - // This field will be injected automatically since it's a single exchange strategy. - *bbgo.Notifiability - SourceExchangeName string `json:"sourceExchange"` TargetExchangeName string `json:"targetExchange"` @@ -73,8 +67,8 @@ func (s *Strategy) ID() string { } func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { - session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval.String()}) - session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.MovingAverageInterval.String()}) + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.MovingAverageInterval}) } func (s *Strategy) CrossSubscribe(sessions map[string]*bbgo.ExchangeSession) { @@ -83,7 +77,7 @@ func (s *Strategy) CrossSubscribe(sessions map[string]*bbgo.ExchangeSession) { // make sure we have the connection alive targetSession := sessions[s.TargetExchangeName] - targetSession.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval.String()}) + targetSession.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) } func (s *Strategy) clear(ctx context.Context, orderExecutor bbgo.OrderExecutor) { @@ -180,11 +174,7 @@ func (s *Strategy) handleOrderUpdate(order types.Order) { } func (s *Strategy) loadIndicator(sourceSession *bbgo.ExchangeSession) (types.Float64Indicator, error) { - var standardIndicatorSet, ok = sourceSession.StandardIndicatorSet(s.Symbol) - if !ok { - return nil, fmt.Errorf("standardIndicatorSet is nil, symbol %s", s.Symbol) - } - + var standardIndicatorSet = sourceSession.StandardIndicatorSet(s.Symbol) var iw = types.IntervalWindow{Interval: s.MovingAverageInterval, Window: s.MovingAverageWindow} switch strings.ToUpper(s.MovingAverageType) { @@ -221,7 +211,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.place(ctx, orderExecutor, session, indicator, closePrice) }) - s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() log.Infof("canceling trailingstop order...") s.clear(ctx, orderExecutor) @@ -265,7 +255,7 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se s.place(ctx, &orderExecutor, session, indicator, closePrice) }) - s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() log.Infof("canceling trailingstop order...") s.clear(ctx, &orderExecutor) diff --git a/pkg/strategy/etf/strategy.go b/pkg/strategy/etf/strategy.go index 2f5d16138a..f14c0a102d 100644 --- a/pkg/strategy/etf/strategy.go +++ b/pkg/strategy/etf/strategy.go @@ -22,8 +22,6 @@ func init() { type Strategy struct { Market types.Market - Notifiability *bbgo.Notifiability - TotalAmount fixedpoint.Value `json:"totalAmount,omitempty"` // Interval is the period that you want to submit order @@ -52,7 +50,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se ticker := time.NewTicker(s.Duration.Duration()) defer ticker.Stop() - s.Notifiability.Notify("ETF orders will be executed every %s", s.Duration.Duration().String()) + bbgo.Notify("ETF orders will be executed every %s", s.Duration.Duration().String()) for { select { @@ -66,7 +64,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se ticker, err := session.Exchange.QueryTicker(ctx, symbol) if err != nil { - s.Notifiability.Notify("query ticker error: %s", err.Error()) + bbgo.Notify("query ticker error: %s", err.Error()) log.WithError(err).Error("query ticker error") break } @@ -80,11 +78,11 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se break } if quoteBalance.Available.Compare(amount) < 0 { - s.Notifiability.Notify("Quote balance %s is not enough: %s < %s", s.Market.QuoteCurrency, quoteBalance.Available.String(), amount.String()) + bbgo.Notify("Quote balance %s is not enough: %s < %s", s.Market.QuoteCurrency, quoteBalance.Available.String(), amount.String()) break } - s.Notifiability.Notify("Submitting etf order %s quantity %s at price %s (index ratio %s)", + bbgo.Notify("Submitting etf order %s quantity %s at price %s (index ratio %s)", symbol, quantity.String(), askPrice.String(), diff --git a/pkg/strategy/ewoDgtrd/heikinashi.go b/pkg/strategy/ewoDgtrd/heikinashi.go new file mode 100644 index 0000000000..fca1934c03 --- /dev/null +++ b/pkg/strategy/ewoDgtrd/heikinashi.go @@ -0,0 +1,49 @@ +package ewoDgtrd + +import ( + "fmt" + "math" + + "github.com/c9s/bbgo/pkg/types" +) + +type HeikinAshi struct { + Close *types.Queue + Open *types.Queue + High *types.Queue + Low *types.Queue + Volume *types.Queue +} + +func NewHeikinAshi(size int) *HeikinAshi { + return &HeikinAshi{ + Close: types.NewQueue(size), + Open: types.NewQueue(size), + High: types.NewQueue(size), + Low: types.NewQueue(size), + Volume: types.NewQueue(size), + } +} + +func (s *HeikinAshi) Print() string { + return fmt.Sprintf("Heikin c: %.3f, o: %.3f, h: %.3f, l: %.3f, v: %.3f", + s.Close.Last(), + s.Open.Last(), + s.High.Last(), + s.Low.Last(), + s.Volume.Last()) +} + +func (inc *HeikinAshi) Update(kline types.KLine) { + open := kline.Open.Float64() + cloze := kline.Close.Float64() + high := kline.High.Float64() + low := kline.Low.Float64() + newClose := (open + high + low + cloze) / 4. + newOpen := (inc.Open.Last() + inc.Close.Last()) / 2. + inc.Close.Update(newClose) + inc.Open.Update(newOpen) + inc.High.Update(math.Max(math.Max(high, newOpen), newClose)) + inc.Low.Update(math.Min(math.Min(low, newOpen), newClose)) + inc.Volume.Update(kline.Volume.Float64()) +} diff --git a/pkg/strategy/ewoDgtrd/strategy.go b/pkg/strategy/ewoDgtrd/strategy.go index 3579e312aa..6af38bc119 100644 --- a/pkg/strategy/ewoDgtrd/strategy.go +++ b/pkg/strategy/ewoDgtrd/strategy.go @@ -2,16 +2,20 @@ package ewoDgtrd import ( "context" + "errors" "fmt" "math" + "os" "sync" + "github.com/fatih/color" "github.com/sirupsen/logrus" "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/indicator" "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/util" ) const ID = "ewo_dgtrd" @@ -23,52 +27,157 @@ func init() { } type Strategy struct { - Market types.Market - Session *bbgo.ExchangeSession - UseHeikinAshi bool `json:"useHeikinAshi"` // use heikinashi kline - Stoploss fixedpoint.Value `json:"stoploss"` - Symbol string `json:"symbol"` - Interval types.Interval `json:"interval"` - UseEma bool `json:"useEma"` // use exponential ma or not - UseSma bool `json:"useSma"` // if UseEma == false, use simple ma or not - SignalWindow int `json:"sigWin"` // signal window - DisableShortStop bool `json:"disableShortStop"` // disable TP/SL on short - - *bbgo.Graceful - bbgo.SmartStops - tradeCollector *bbgo.TradeCollector - atr *indicator.ATR - ma5 types.Series - ma34 types.Series - ewo types.Series - ewoSignal types.Series - heikinAshi *HeikinAshi - peakPrice fixedpoint.Value - bottomPrice fixedpoint.Value + // Embedded components + // =================== + *bbgo.Environment + bbgo.StrategyController + + // Auto-Injection fields + // ==================== + + // Market of the symbol + Market types.Market + + // Session is the trading session of this strategy + Session *bbgo.ExchangeSession + + orderExecutor *bbgo.GeneralOrderExecutor + + // Persistence fields + // ==================== + // Position + Position *types.Position `json:"position,omitempty" persistence:"position"` + + // ProfitStats + ProfitStats *types.ProfitStats `json:"profitStats,omitempty" persistence:"profit_stats"` + + // Settings fields + // ========================= + UseHeikinAshi bool `json:"useHeikinAshi"` // use heikinashi kline + StopLoss fixedpoint.Value `json:"stoploss"` + Symbol string `json:"symbol"` + Interval types.Interval `json:"interval"` + UseEma bool `json:"useEma"` // use exponential ma or not + UseSma bool `json:"useSma"` // if UseEma == false, use simple ma or not + SignalWindow int `json:"sigWin"` // signal window + DisableShortStop bool `json:"disableShortStop"` // disable SL on short + DisableLongStop bool `json:"disableLongStop"` // disable SL on long + FilterHigh float64 `json:"cciStochFilterHigh"` // high filter for CCI Stochastic indicator + FilterLow float64 `json:"cciStochFilterLow"` // low filter for CCI Stochastic indicator + EwoChangeFilterHigh float64 `json:"ewoChangeFilterHigh"` // high filter for ewo histogram + EwoChangeFilterLow float64 `json:"ewoChangeFilterLow"` // low filter for ewo histogram + + Record bool `json:"record"` // print record messages on position exit point + + KLineStartTime types.Time + KLineEndTime types.Time + + entryPrice fixedpoint.Value + waitForTrade bool + + atr *indicator.ATR + emv *indicator.EMV + ccis *CCISTOCH + ma5 types.SeriesExtend + ma34 types.SeriesExtend + ewo types.SeriesExtend + ewoSignal types.SeriesExtend + ewoHistogram types.SeriesExtend + ewoChangeRate float64 + heikinAshi *HeikinAshi + peakPrice fixedpoint.Value + bottomPrice fixedpoint.Value + midPrice fixedpoint.Value + lock sync.RWMutex + + buyPrice fixedpoint.Value + sellPrice fixedpoint.Value } func (s *Strategy) ID() string { return ID } +func (s *Strategy) InstanceID() string { + return fmt.Sprintf("%s:%s", ID, s.Symbol) +} + func (s *Strategy) Initialize() error { - return s.SmartStops.InitializeStopControllers(s.Symbol) + return nil } func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { - log.Infof("subscribe %s", s.Symbol) - session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval.String()}) - s.SmartStops.Subscribe(session) + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: types.Interval1m}) + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) + + if !bbgo.IsBackTesting { + session.Subscribe(types.BookTickerChannel, s.Symbol, types.SubscribeOptions{}) + } +} + +// Refer: https://tw.tradingview.com/script/XZyG5SOx-CCI-Stochastic-and-a-quick-lesson-on-Scalping-Trading-Systems/ +type CCISTOCH struct { + cci *indicator.CCI + stoch *indicator.STOCH + ma *indicator.SMA + filterHigh float64 + filterLow float64 +} + +func NewCCISTOCH(i types.Interval, filterHigh, filterLow float64) *CCISTOCH { + cci := &indicator.CCI{IntervalWindow: types.IntervalWindow{Interval: i, Window: 28}} + stoch := &indicator.STOCH{IntervalWindow: types.IntervalWindow{Interval: i, Window: 28}} + ma := &indicator.SMA{IntervalWindow: types.IntervalWindow{Interval: i, Window: 3}} + return &CCISTOCH{ + cci: cci, + stoch: stoch, + ma: ma, + filterHigh: filterHigh, + filterLow: filterLow, + } } -type UpdatableSeries interface { - types.Series - Update(value float64) +func (inc *CCISTOCH) Update(cloze float64) { + inc.cci.Update(cloze) + inc.stoch.Update(inc.cci.Last(), inc.cci.Last(), inc.cci.Last()) + inc.ma.Update(inc.stoch.LastD()) +} + +func (inc *CCISTOCH) BuySignal() bool { + hasGrey := false + for i := 0; i < len(inc.ma.Values); i++ { + v := inc.ma.Index(i) + if v > inc.filterHigh { + return false + } else if v >= inc.filterLow && v <= inc.filterHigh { + hasGrey = true + continue + } else if v < inc.filterLow { + return hasGrey + } + } + return false +} + +func (inc *CCISTOCH) SellSignal() bool { + hasGrey := false + for i := 0; i < len(inc.ma.Values); i++ { + v := inc.ma.Index(i) + if v < inc.filterLow { + return false + } else if v >= inc.filterLow && v <= inc.filterHigh { + hasGrey = true + continue + } else if v > inc.filterHigh { + return hasGrey + } + } + return false } type VWEMA struct { - PV UpdatableSeries - V UpdatableSeries + PV types.UpdatableSeries + V types.UpdatableSeries } func (inc *VWEMA) Last() float64 { @@ -105,246 +214,143 @@ func (inc *VWEMA) UpdateVal(price float64, vol float64) { inc.V.Update(vol) } -type Queue struct { - arr []float64 - size int -} - -func NewQueue(size int) *Queue { - return &Queue{ - arr: make([]float64, 0, size), - size: size, - } -} - -func (inc *Queue) Last() float64 { - if len(inc.arr) == 0 { - return 0 - } - return inc.arr[len(inc.arr)-1] -} - -func (inc *Queue) Index(i int) float64 { - if len(inc.arr)-i-1 < 0 { - return 0 - } - return inc.arr[len(inc.arr)-i-1] -} - -func (inc *Queue) Length() int { - return len(inc.arr) -} - -func (inc *Queue) Update(v float64) { - inc.arr = append(inc.arr, v) - if len(inc.arr) > inc.size { - inc.arr = inc.arr[len(inc.arr)-inc.size:] - } -} - -type HeikinAshi struct { - Close *Queue - Open *Queue - High *Queue - Low *Queue - Volume *Queue -} +// Setup the Indicators going to be used +func (s *Strategy) SetupIndicators(store *bbgo.MarketDataStore) { + window5 := types.IntervalWindow{Interval: s.Interval, Window: 5} + window34 := types.IntervalWindow{Interval: s.Interval, Window: 34} + s.atr = &indicator.ATR{IntervalWindow: window34} + s.emv = &indicator.EMV{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: 14}} + s.ccis = NewCCISTOCH(s.Interval, s.FilterHigh, s.FilterLow) -func NewHeikinAshi(size int) *HeikinAshi { - return &HeikinAshi{ - Close: NewQueue(size), - Open: NewQueue(size), - High: NewQueue(size), - Low: NewQueue(size), - Volume: NewQueue(size), + getSource := func(window types.KLineWindow) types.Series { + if s.UseHeikinAshi { + return s.heikinAshi.Close + } + return window.Close() } -} - -func (s *HeikinAshi) Print() string { - return fmt.Sprintf("Heikin c: %.3f, o: %.3f, h: %.3f, l: %.3f, v: %.3f", - s.Close.Last(), - s.Open.Last(), - s.High.Last(), - s.Low.Last(), - s.Volume.Last()) -} - -func (inc *HeikinAshi) Update(kline types.KLine) { - open := kline.Open.Float64() - cloze := kline.Close.Float64() - high := kline.High.Float64() - low := kline.Low.Float64() - newClose := (open + high + low + cloze) / 4. - newOpen := (inc.Open.Last() + inc.Close.Last()) / 2. - inc.Close.Update(newClose) - inc.Open.Update(newOpen) - inc.High.Update(math.Max(math.Max(high, newOpen), newClose)) - inc.Low.Update(math.Min(math.Min(low, newOpen), newClose)) - inc.Volume.Update(kline.Volume.Float64()) -} - -func (s *Strategy) SetupIndicators() { - store, ok := s.Session.MarketDataStore(s.Symbol) - if !ok { - log.Errorf("cannot get marketdatastore of %s", s.Symbol) - return + getVol := func(window types.KLineWindow) types.Series { + if s.UseHeikinAshi { + return s.heikinAshi.Volume + } + return window.Volume() } - - s.atr = &indicator.ATR{IntervalWindow: types.IntervalWindow{s.Interval, 34}} - - if s.UseHeikinAshi { - s.heikinAshi = NewHeikinAshi(50) + s.heikinAshi = NewHeikinAshi(500) + store.OnKLineWindowUpdate(func(interval types.Interval, window types.KLineWindow) { + if interval == s.atr.Interval { + if s.atr.RMA == nil { + for _, kline := range window { + high := kline.High.Float64() + low := kline.Low.Float64() + cloze := kline.Close.Float64() + vol := kline.Volume.Float64() + s.atr.Update(high, low, cloze) + s.emv.Update(high, low, vol) + } + } else { + kline := window[len(window)-1] + high := kline.High.Float64() + low := kline.Low.Float64() + cloze := kline.Close.Float64() + vol := kline.Volume.Float64() + s.atr.Update(high, low, cloze) + s.emv.Update(high, low, vol) + } + } + if s.Interval != interval { + return + } + if s.heikinAshi.Close.Length() == 0 { + for _, kline := range window { + s.heikinAshi.Update(kline) + s.ccis.Update(getSource(window).Last()) + } + } else { + s.heikinAshi.Update(window[len(window)-1]) + s.ccis.Update(getSource(window).Last()) + } + }) + if s.UseEma { + ema5 := &indicator.EWMA{IntervalWindow: window5} + ema34 := &indicator.EWMA{IntervalWindow: window34} store.OnKLineWindowUpdate(func(interval types.Interval, window types.KLineWindow) { if s.Interval != interval { return } - if s.heikinAshi.Close.Length() == 0 { - for _, kline := range window { - s.heikinAshi.Update(kline) + if ema5.Length() == 0 { + closes := types.Reverse(getSource(window)) + for _, cloze := range closes { + ema5.Update(cloze) + ema34.Update(cloze) } } else { - s.heikinAshi.Update(window[len(window)-1]) + cloze := getSource(window).Last() + ema5.Update(cloze) + ema34.Update(cloze) } + }) - if s.UseEma { - ema5 := &indicator.EWMA{IntervalWindow: types.IntervalWindow{s.Interval, 5}} - ema34 := &indicator.EWMA{IntervalWindow: types.IntervalWindow{s.Interval, 34}} - store.OnKLineWindowUpdate(func(interval types.Interval, _ types.KLineWindow) { - if s.Interval != interval { - return - } - if ema5.Length() == 0 { - closes := types.ToReverseArray(s.heikinAshi.Close) - for _, cloze := range closes { - ema5.Update(cloze) - ema34.Update(cloze) - } - } else { - cloze := s.heikinAshi.Close.Last() - ema5.Update(cloze) - ema34.Update(cloze) - } - s.atr.Update( - s.heikinAshi.High.Last(), - s.heikinAshi.Low.Last(), - s.heikinAshi.Close.Last()) - }) - s.ma5 = ema5 - s.ma34 = ema34 - } else if s.UseSma { - sma5 := &indicator.SMA{IntervalWindow: types.IntervalWindow{s.Interval, 5}} - sma34 := &indicator.SMA{IntervalWindow: types.IntervalWindow{s.Interval, 34}} - store.OnKLineWindowUpdate(func(interval types.Interval, _ types.KLineWindow) { - if s.Interval != interval { - return - } - if sma5.Length() == 0 { - closes := types.ToReverseArray(s.heikinAshi.Close) - for _, cloze := range closes { - sma5.Update(cloze) - sma34.Update(cloze) - } - } else { - cloze := s.heikinAshi.Close.Last() + + s.ma5 = ema5 + s.ma34 = ema34 + } else if s.UseSma { + sma5 := &indicator.SMA{IntervalWindow: window5} + sma34 := &indicator.SMA{IntervalWindow: window34} + store.OnKLineWindowUpdate(func(interval types.Interval, window types.KLineWindow) { + if s.Interval != interval { + return + } + if sma5.Length() == 0 { + closes := types.Reverse(getSource(window)) + for _, cloze := range closes { sma5.Update(cloze) sma34.Update(cloze) } - s.atr.Update( - s.heikinAshi.High.Last(), - s.heikinAshi.Low.Last(), - s.heikinAshi.Close.Last()) - }) - s.ma5 = sma5 - s.ma34 = sma34 - } else { - evwma5 := &VWEMA{ - PV: &indicator.EWMA{IntervalWindow: types.IntervalWindow{s.Interval, 5}}, - V: &indicator.EWMA{IntervalWindow: types.IntervalWindow{s.Interval, 5}}, + } else { + cloze := getSource(window).Last() + sma5.Update(cloze) + sma34.Update(cloze) } - evwma34 := &VWEMA{ - PV: &indicator.EWMA{IntervalWindow: types.IntervalWindow{s.Interval, 34}}, - V: &indicator.EWMA{IntervalWindow: types.IntervalWindow{s.Interval, 34}}, + }) + s.ma5 = sma5 + s.ma34 = sma34 + } else { + evwma5 := &VWEMA{ + PV: &indicator.EWMA{IntervalWindow: window5}, + V: &indicator.EWMA{IntervalWindow: window5}, + } + evwma34 := &VWEMA{ + PV: &indicator.EWMA{IntervalWindow: window34}, + V: &indicator.EWMA{IntervalWindow: window34}, + } + store.OnKLineWindowUpdate(func(interval types.Interval, window types.KLineWindow) { + if s.Interval != interval { + return } - store.OnKLineWindowUpdate(func(interval types.Interval, _ types.KLineWindow) { - if s.Interval != interval { - return - } - if evwma5.PV.Length() == 0 { - for i := s.heikinAshi.Close.Length() - 1; i >= 0; i-- { - price := s.heikinAshi.Close.Index(i) - vol := s.heikinAshi.Volume.Index(i) - evwma5.UpdateVal(price, vol) - evwma34.UpdateVal(price, vol) - } - } else { - price := s.heikinAshi.Close.Last() - vol := s.heikinAshi.Volume.Last() + clozes := getSource(window) + vols := getVol(window) + if evwma5.PV.Length() == 0 { + for i := clozes.Length() - 1; i >= 0; i-- { + price := clozes.Index(i) + vol := vols.Index(i) evwma5.UpdateVal(price, vol) evwma34.UpdateVal(price, vol) } - s.atr.Update( - s.heikinAshi.High.Last(), - s.heikinAshi.Low.Last(), - s.heikinAshi.Close.Last()) - }) - s.ma5 = evwma5 - s.ma34 = evwma34 - } - } else { - indicatorSet, ok := s.Session.StandardIndicatorSet(s.Symbol) - if !ok { - log.Errorf("cannot get indicator set of %s", s.Symbol) - return - } - if s.UseEma { - s.ma5 = indicatorSet.EWMA(types.IntervalWindow{s.Interval, 5}) - s.ma34 = indicatorSet.EWMA(types.IntervalWindow{s.Interval, 34}) - s.atr.Bind(store) - } else if s.UseSma { - s.ma5 = indicatorSet.SMA(types.IntervalWindow{s.Interval, 5}) - s.ma34 = indicatorSet.SMA(types.IntervalWindow{s.Interval, 34}) - s.atr.Bind(store) - } else { - evwma5 := &VWEMA{ - PV: &indicator.EWMA{IntervalWindow: types.IntervalWindow{s.Interval, 5}}, - V: &indicator.EWMA{IntervalWindow: types.IntervalWindow{s.Interval, 5}}, - } - evwma34 := &VWEMA{ - PV: &indicator.EWMA{IntervalWindow: types.IntervalWindow{s.Interval, 34}}, - V: &indicator.EWMA{IntervalWindow: types.IntervalWindow{s.Interval, 34}}, + } else { + price := clozes.Last() + vol := vols.Last() + evwma5.UpdateVal(price, vol) + evwma34.UpdateVal(price, vol) } - store.OnKLineWindowUpdate(func(interval types.Interval, window types.KLineWindow) { - if s.Interval != interval { - return - } - if evwma5.PV.Length() == 0 { - for _, kline := range window { - evwma5.Update(kline) - evwma34.Update(kline) - s.atr.Update( - kline.High.Float64(), - kline.Low.Float64(), - kline.Close.Float64(), - ) - } - } else { - evwma5.Update(window[len(window)-1]) - evwma34.Update(window[len(window)-1]) - s.atr.Update( - window[len(window)-1].High.Float64(), - window[len(window)-1].Low.Float64(), - window[len(window)-1].Close.Float64(), - ) - } - }) - s.ma5 = evwma5 - s.ma34 = evwma34 - } + }) + s.ma5 = types.NewSeries(evwma5) + s.ma34 = types.NewSeries(evwma34) } - s.ewo = types.Mul(types.Minus(types.Div(s.ma5, s.ma34), 1.0), 100.) + s.ewo = s.ma5.Div(s.ma34).Minus(1.0).Mul(100.) + s.ewoHistogram = s.ma5.Minus(s.ma34) + windowSignal := types.IntervalWindow{Interval: s.Interval, Window: s.SignalWindow} if s.UseEma { - sig := &indicator.EWMA{IntervalWindow: types.IntervalWindow{s.Interval, s.SignalWindow}} + sig := &indicator.EWMA{IntervalWindow: windowSignal} store.OnKLineWindowUpdate(func(interval types.Interval, _ types.KLineWindow) { if interval != s.Interval { return @@ -352,7 +358,7 @@ func (s *Strategy) SetupIndicators() { if sig.Length() == 0 { // lazy init - ewoVals := types.ToReverseArray(s.ewo) + ewoVals := types.Reverse(s.ewo) for _, ewoValue := range ewoVals { sig.Update(ewoValue) } @@ -362,7 +368,7 @@ func (s *Strategy) SetupIndicators() { }) s.ewoSignal = sig } else if s.UseSma { - sig := &indicator.SMA{IntervalWindow: types.IntervalWindow{s.Interval, s.SignalWindow}} + sig := &indicator.SMA{IntervalWindow: windowSignal} store.OnKLineWindowUpdate(func(interval types.Interval, _ types.KLineWindow) { if interval != s.Interval { return @@ -370,7 +376,7 @@ func (s *Strategy) SetupIndicators() { if sig.Length() == 0 { // lazy init - ewoVals := types.ToReverseArray(s.ewo) + ewoVals := s.ewo.Reverse() for _, ewoValue := range ewoVals { sig.Update(ewoValue) } @@ -381,418 +387,881 @@ func (s *Strategy) SetupIndicators() { s.ewoSignal = sig } else { sig := &VWEMA{ - PV: &indicator.EWMA{IntervalWindow: types.IntervalWindow{s.Interval, s.SignalWindow}}, - V: &indicator.EWMA{IntervalWindow: types.IntervalWindow{s.Interval, s.SignalWindow}}, + PV: &indicator.EWMA{IntervalWindow: windowSignal}, + V: &indicator.EWMA{IntervalWindow: windowSignal}, } store.OnKLineWindowUpdate(func(interval types.Interval, window types.KLineWindow) { if interval != s.Interval { return } - var vol float64 if sig.Length() == 0 { // lazy init - ewoVals := types.ToReverseArray(s.ewo) + ewoVals := s.ewo.Reverse() for i, ewoValue := range ewoVals { - if s.UseHeikinAshi { - vol = s.heikinAshi.Volume.Index(len(ewoVals) - 1 - i) - } else { - vol = window[len(ewoVals)-1-i].Volume.Float64() - } + vol := window.Volume().Index(i) sig.PV.Update(ewoValue * vol) sig.V.Update(vol) } } else { - if s.UseHeikinAshi { - vol = s.heikinAshi.Volume.Last() - } else { - vol = window[len(window)-1].Volume.Float64() - } + vol := window.Volume().Last() sig.PV.Update(s.ewo.Last() * vol) sig.V.Update(vol) } }) - s.ewoSignal = sig + s.ewoSignal = types.NewSeries(sig) } } -func (s *Strategy) validateOrder(order *types.SubmitOrder) bool { +// Utility to evaluate if the order is valid or not to send to the exchange +func (s *Strategy) validateOrder(order *types.SubmitOrder) error { if order.Type == types.OrderTypeMarket && order.TimeInForce != "" { - return false + return errors.New("wrong field: market vs TimeInForce") } if order.Side == types.SideTypeSell { baseBalance, ok := s.Session.GetAccount().Balance(s.Market.BaseCurrency) if !ok { - return false + log.Error("cannot get account") + return errors.New("cannot get account") } if order.Quantity.Compare(baseBalance.Available) > 0 { - return false + log.Errorf("qty %v > avail %v", order.Quantity, baseBalance.Available) + return errors.New("qty > avail") } price := order.Price if price.IsZero() { price, ok = s.Session.LastPrice(s.Symbol) if !ok { - return false + log.Error("no price") + return errors.New("no price") } } orderAmount := order.Quantity.Mul(price) if order.Quantity.Sign() <= 0 || order.Quantity.Compare(s.Market.MinQuantity) < 0 || orderAmount.Compare(s.Market.MinNotional) < 0 { - return false + log.Debug("amount fail") + return fmt.Errorf("amount fail: quantity: %v, amount: %v", order.Quantity, orderAmount) } - return true + return nil } else if order.Side == types.SideTypeBuy { quoteBalance, ok := s.Session.GetAccount().Balance(s.Market.QuoteCurrency) if !ok { - return false + log.Error("cannot get account") + return errors.New("cannot get account") } price := order.Price if price.IsZero() { price, ok = s.Session.LastPrice(s.Symbol) if !ok { - return false + log.Error("no price") + return errors.New("no price") } } totalQuantity := quoteBalance.Available.Div(price) if order.Quantity.Compare(totalQuantity) > 0 { - return false + log.Errorf("qty %v > avail %v", order.Quantity, totalQuantity) + return errors.New("qty > avail") } orderAmount := order.Quantity.Mul(price) if order.Quantity.Sign() <= 0 || orderAmount.Compare(s.Market.MinNotional) < 0 || order.Quantity.Compare(s.Market.MinQuantity) < 0 { - return false + log.Debug("amount fail") + return fmt.Errorf("amount fail: quantity: %v, amount: %v", order.Quantity, orderAmount) } - return true + return nil } - return false + log.Error("side error") + return errors.New("side error") } +func (s *Strategy) PlaceBuyOrder(ctx context.Context, price fixedpoint.Value) (*types.Order, *types.Order) { + var closeOrder *types.Order + var ok bool + waitForTrade := false + base := s.Position.GetBase() + if base.Abs().Compare(s.Market.MinQuantity) >= 0 && base.Mul(s.GetLastPrice()).Abs().Compare(s.Market.MinNotional) >= 0 && base.Sign() < 0 { + if closeOrder, ok = s.ClosePosition(ctx); !ok { + log.Errorf("sell position %v remained not closed, skip placing order", base) + return closeOrder, nil + } + } + if s.Position.GetBase().Sign() < 0 { + // we are not able to make close trade at this moment, + // will close the rest of the position by normal limit order + // s.entryPrice is set in the last trade + waitForTrade = true + } + quoteBalance, ok := s.Session.GetAccount().Balance(s.Market.QuoteCurrency) + if !ok { + log.Infof("buy order at price %v failed", price) + return closeOrder, nil + } + quantityAmount := quoteBalance.Available + totalQuantity := quantityAmount.Div(price) + order := types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeLimit, + Price: price, + Quantity: totalQuantity, + Market: s.Market, + TimeInForce: types.TimeInForceGTC, + } + if err := s.validateOrder(&order); err != nil { + log.Infof("validation failed %v: %v", order, err) + return closeOrder, nil + } + log.Warnf("long at %v, position %v, closeOrder %v, timestamp: %s", price, s.Position.GetBase(), closeOrder, s.KLineStartTime) + + createdOrders, err := s.orderExecutor.SubmitOrders(ctx, order) + if err != nil { + log.WithError(err).Errorf("cannot place order") + return closeOrder, nil + } + + log.Infof("post order c: %v, entryPrice: %v o: %v", waitForTrade, s.entryPrice, createdOrders) + s.waitForTrade = waitForTrade + return closeOrder, &createdOrders[0] +} + +func (s *Strategy) PlaceSellOrder(ctx context.Context, price fixedpoint.Value) (*types.Order, *types.Order) { + var closeOrder *types.Order + var ok bool + waitForTrade := false + base := s.Position.GetBase() + if base.Abs().Compare(s.Market.MinQuantity) >= 0 && base.Abs().Mul(s.GetLastPrice()).Compare(s.Market.MinNotional) >= 0 && base.Sign() > 0 { + if closeOrder, ok = s.ClosePosition(ctx); !ok { + log.Errorf("buy position %v remained not closed, skip placing order", base) + return closeOrder, nil + } + } + if s.Position.GetBase().Sign() > 0 { + // we are not able to make close trade at this moment, + // will close the rest of the position by normal limit order + // s.entryPrice is set in the last trade + waitForTrade = true + } + baseBalance, ok := s.Session.GetAccount().Balance(s.Market.BaseCurrency) + if !ok { + return closeOrder, nil + } + order := types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeSell, + Type: types.OrderTypeLimit, + Market: s.Market, + Quantity: baseBalance.Available, + Price: price, + TimeInForce: types.TimeInForceGTC, + } + if err := s.validateOrder(&order); err != nil { + log.Infof("validation failed %v: %v", order, err) + return closeOrder, nil + } + + log.Warnf("short at %v, position %v closeOrder %v, timestamp: %s", price, s.Position.GetBase(), closeOrder, s.KLineStartTime) + createdOrders, err := s.orderExecutor.SubmitOrders(ctx, order) + if err != nil { + log.WithError(err).Errorf("cannot place order") + return closeOrder, nil + } + + log.Infof("post order, c: %v, entryPrice: %v o: %v", waitForTrade, s.entryPrice, createdOrders) + s.waitForTrade = waitForTrade + return closeOrder, &createdOrders[0] +} + +// ClosePosition(context.Context) -> (closeOrder *types.Order, ok bool) +// this will decorate the generated order from NewMarketCloseOrder +// add do necessary checks +// if available quantity is zero, will return (nil, true) +// if any of the checks failed, will return (nil, false) +// otherwise, return the created close order and true +func (s *Strategy) ClosePosition(ctx context.Context) (*types.Order, bool) { + order := s.Position.NewMarketCloseOrder(fixedpoint.One) + // no position exists + if order == nil { + // no base + s.sellPrice = fixedpoint.Zero + s.buyPrice = fixedpoint.Zero + return nil, true + } + order.TimeInForce = "" + // If there's any order not yet been traded in the orderbook, + // we need this additional check to make sure we have enough balance to post a close order + balances := s.Session.GetAccount().Balances() + baseBalance := balances[s.Market.BaseCurrency].Available + if order.Side == types.SideTypeBuy { + price := s.GetLastPrice() + quoteAmount := balances[s.Market.QuoteCurrency].Available.Div(price) + if order.Quantity.Compare(quoteAmount) > 0 { + order.Quantity = quoteAmount + } + } else if order.Side == types.SideTypeSell && order.Quantity.Compare(baseBalance) > 0 { + order.Quantity = baseBalance + } + + // if no available balance... + if order.Quantity.IsZero() { + return nil, true + } + if err := s.validateOrder(order); err != nil { + log.Errorf("cannot place close order %v: %v", order, err) + return nil, false + } + + createdOrders, err := s.orderExecutor.SubmitOrders(ctx, *order) + if err != nil { + log.WithError(err).Errorf("cannot place close order") + return nil, false + } + + log.Infof("close order %v", createdOrders) + return &createdOrders[0], true +} + +func (s *Strategy) CancelAll(ctx context.Context) { + if err := s.orderExecutor.GracefulCancel(ctx); err != nil { + log.WithError(err).Errorf("graceful cancel order error") + } + + s.waitForTrade = false +} + +func (s *Strategy) GetLastPrice() fixedpoint.Value { + var lastPrice fixedpoint.Value + var ok bool + if s.Environment.IsBackTesting() { + lastPrice, ok = s.Session.LastPrice(s.Symbol) + if !ok { + log.Errorf("cannot get last price") + return lastPrice + } + } else { + s.lock.RLock() + if s.midPrice.IsZero() { + lastPrice, ok = s.Session.LastPrice(s.Symbol) + if !ok { + log.Errorf("cannot get last price") + return lastPrice + } + } else { + lastPrice = s.midPrice + } + s.lock.RUnlock() + } + return lastPrice +} + // Trading Rules: // - buy / sell the whole asset -// - SL/TP by atr (buyprice - 2 * atr, sellprice + 2 * atr) -// - SL by s.Stoploss (Abs(price_diff / price) > s.Stoploss) +// - SL by atr (lastprice < buyprice - atr) || (lastprice > sellprice + atr) +// - TP by detecting if there's a ewo pivotHigh(1,1) -> close long, or pivotLow(1,1) -> close short +// - TP by ma34 +- atr * 2 +// - TP by (lastprice < peak price - atr) || (lastprice > bottom price + atr) +// - SL by s.StopLoss (Abs(price_diff / price) > s.StopLoss) // - entry condition on ewo(Elliott wave oscillator) Crosses ewoSignal(ma on ewo, signalWindow) -// * buy signal on crossover -// * sell signal on crossunder +// * buy signal on (crossover on previous K bar and no crossunder on latest K bar) +// * sell signal on (crossunder on previous K bar and no crossunder on latest K bar) // - and filtered by the following rules: -// * buy: prev buy signal ON and current sell signal OFF, kline Close > Open, Close > ma(Window=5), ewo > Mean(ewo, Window=5) -// * sell: prev buy signal OFF and current sell signal ON, kline Close < Open, Close < ma(Window=5), ewo < Mean(ewo, Window=5) -// Cancel and repost on non-fully filed orders every 1m within Window=1 +// * buy: buy signal ON, kline Close > Open, Close > ma5, Close > ma34, CCI Stochastic Buy signal +// * sell: sell signal ON, kline Close < Open, Close < ma5, Close < ma34, CCI Stochastic Sell signal +// - or entry when ma34 +- atr * 3 gets touched +// - entry price: latestPrice +- atr / 2 (short,long), close at market price +// Cancel non-fully filled orders on new signal (either in same direction or not) // // ps: kline might refer to heikinashi or normal ohlc func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { - buyPrice := fixedpoint.Zero - sellPrice := fixedpoint.Zero + s.buyPrice = fixedpoint.Zero + s.sellPrice = fixedpoint.Zero s.peakPrice = fixedpoint.Zero s.bottomPrice = fixedpoint.Zero - orderbook, ok := session.OrderStore(s.Symbol) - if !ok { - log.Errorf("cannot get orderbook of %s", s.Symbol) - return nil + counterTPfromPeak := 0 + percentAvgTPfromPeak := 0.0 + counterTPfromCCI := 0 + percentAvgTPfromCCI := 0.0 + counterTPfromLongShort := 0 + percentAvgTPfromLongShort := 0.0 + counterTPfromAtr := 0 + percentAvgTPfromAtr := 0.0 + counterTPfromOrder := 0 + percentAvgTPfromOrder := 0.0 + counterSLfromSL := 0 + percentAvgSLfromSL := 0.0 + counterSLfromOrder := 0 + percentAvgSLfromOrder := 0.0 + + if s.Position == nil { + s.Position = types.NewPositionFromMarket(s.Market) } - position, ok := session.Position(s.Symbol) - if !ok { - log.Errorf("cannot get position of %s", s.Symbol) - return nil + + if s.ProfitStats == nil { + s.ProfitStats = types.NewProfitStats(s.Market) } - s.tradeCollector = bbgo.NewTradeCollector(s.Symbol, position, orderbook) - s.tradeCollector.OnTrade(func(trade types.Trade, profit, netprofit fixedpoint.Value) { - if !profit.IsZero() { - log.Warnf("generate profit: %v, netprofit: %v, trade: %v", profit, netprofit, trade) - } - if trade.Side == types.SideTypeBuy { - if sellPrice.IsZero() { - buyPrice = trade.Price - s.peakPrice = trade.Price - } else { - sellPrice = fixedpoint.Zero + + instanceID := s.InstanceID() + s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) + s.orderExecutor.BindEnvironment(s.Environment) + s.orderExecutor.BindProfitStats(s.ProfitStats) + // s.orderExecutor.BindTradeStats(s.TradeStats) + s.orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { + bbgo.Sync(ctx, s) + }) + s.orderExecutor.Bind() + + s.orderExecutor.TradeCollector().OnTrade(func(trade types.Trade, profit, netprofit fixedpoint.Value) { + // calculate report for the position that cannot be closed by close order (amount too small) + if s.waitForTrade { + price := s.entryPrice + if price.IsZero() { + panic("no price found") + } + pnlRate := trade.Price.Sub(price).Abs().Div(trade.Price).Float64() + if s.Record { + log.Errorf("record avg %v trade %v", price, trade) } - } else if trade.Side == types.SideTypeSell { - if buyPrice.IsZero() { - sellPrice = trade.Price - s.bottomPrice = trade.Price + if trade.Side == types.SideTypeBuy { + if trade.Price.Compare(price) < 0 { + percentAvgTPfromOrder = percentAvgTPfromOrder*float64(counterTPfromOrder) + pnlRate + counterTPfromOrder += 1 + percentAvgTPfromOrder /= float64(counterTPfromOrder) + } else { + percentAvgSLfromOrder = percentAvgSLfromOrder*float64(counterSLfromOrder) + pnlRate + counterSLfromOrder += 1 + percentAvgSLfromOrder /= float64(counterSLfromOrder) + } + } else if trade.Side == types.SideTypeSell { + if trade.Price.Compare(price) > 0 { + percentAvgTPfromOrder = percentAvgTPfromOrder*float64(counterTPfromOrder) + pnlRate + counterTPfromOrder += 1 + percentAvgTPfromOrder /= float64(counterTPfromOrder) + } else { + percentAvgSLfromOrder = percentAvgSLfromOrder*float64(counterSLfromOrder) + pnlRate + counterSLfromOrder += 1 + percentAvgSLfromOrder /= float64(counterSLfromOrder) + } } else { - buyPrice = fixedpoint.Zero + panic(fmt.Sprintf("no sell(%v) or buy price(%v), %v", s.sellPrice, s.buyPrice, trade)) } + s.waitForTrade = false } - }) - s.tradeCollector.OnPositionUpdate(func(position *types.Position) { - log.Infof("position changed: %s", position) + if s.Position.GetBase().Abs().Compare(s.Market.MinQuantity) >= 0 && s.Position.GetBase().Abs().Mul(trade.Price).Compare(s.Market.MinNotional) >= 0 { + sign := s.Position.GetBase().Sign() + if sign > 0 { + log.Infof("base become positive, %v", trade) + s.buyPrice = s.Position.AverageCost + s.sellPrice = fixedpoint.Zero + s.peakPrice = s.Position.AverageCost + } else if sign == 0 { + panic("not going to happen") + } else { + log.Infof("base become negative, %v", trade) + s.buyPrice = fixedpoint.Zero + s.sellPrice = s.Position.AverageCost + s.bottomPrice = s.Position.AverageCost + } + s.entryPrice = trade.Price + } else { + log.Infof("base become zero, rest of base: %v", s.Position.GetBase()) + if s.Position.GetBase().IsZero() { + s.entryPrice = fixedpoint.Zero + } + s.buyPrice = fixedpoint.Zero + s.sellPrice = fixedpoint.Zero + s.peakPrice = fixedpoint.Zero + s.bottomPrice = fixedpoint.Zero + } }) - s.tradeCollector.BindStream(session.UserDataStream) - s.SmartStops.RunStopControllers(ctx, session, s.tradeCollector) + store, ok := s.Session.MarketDataStore(s.Symbol) + if !ok { + return fmt.Errorf("cannot get marketdatastore of %s", s.Symbol) + } + s.SetupIndicators(store) - s.SetupIndicators() + // local peak of ewo + shortSig := s.ewo.Last() < s.ewo.Index(1) && s.ewo.Index(1) > s.ewo.Index(2) + longSig := s.ewo.Last() > s.ewo.Index(1) && s.ewo.Index(1) < s.ewo.Index(2) - session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { - if kline.Symbol != s.Symbol { + sellOrderTPSL := func(price fixedpoint.Value) { + lastPrice := s.GetLastPrice() + base := s.Position.GetBase().Abs() + if base.Mul(lastPrice).Compare(s.Market.MinNotional) < 0 || base.Compare(s.Market.MinQuantity) < 0 { return } - - lastPrice, ok := session.LastPrice(s.Symbol) - if !ok { - log.Errorf("cannot get last price") + if s.sellPrice.IsZero() { return } balances := session.GetAccount().Balances() - baseBalance := balances[s.Market.BaseCurrency].Available quoteBalance := balances[s.Market.QuoteCurrency].Available + atr := fixedpoint.NewFromFloat(s.atr.Last()) + atrx2 := fixedpoint.NewFromFloat(s.atr.Last() * 2) + buyall := false + if s.bottomPrice.IsZero() || s.bottomPrice.Compare(price) > 0 { + s.bottomPrice = price + } + takeProfit := false + bottomBack := s.bottomPrice + spBack := s.sellPrice + reason := -1 + if quoteBalance.Div(lastPrice).Compare(s.Market.MinQuantity) >= 0 && quoteBalance.Compare(s.Market.MinNotional) >= 0 { + base := fixedpoint.NewFromFloat(s.ma34.Last()) + // TP + if lastPrice.Compare(s.sellPrice) < 0 && (longSig || + (!atrx2.IsZero() && base.Sub(atrx2).Compare(lastPrice) >= 0)) { + buyall = true + takeProfit = true + + // calculate report + if longSig { + reason = 1 + } else { + reason = 2 + } - // cancel non-traded orders - var toCancel []types.Order - var toRepost []types.SubmitOrder - for _, order := range orderbook.Orders() { - if order.Status == types.OrderStatusNew || order.Status == types.OrderStatusPartiallyFilled { - toCancel = append(toCancel, order) } - } - if len(toCancel) > 0 { - if err := orderExecutor.CancelOrders(ctx, toCancel...); err != nil { - log.WithError(err).Errorf("cancel order error") + if !atr.IsZero() && s.bottomPrice.Add(atr).Compare(lastPrice) <= 0 && + lastPrice.Compare(s.sellPrice) < 0 { + buyall = true + takeProfit = true + reason = 3 } - s.tradeCollector.Process() - } - - // well, only track prices on 1m - if kline.Interval == types.Interval1m { - for _, order := range toCancel { - if order.Side == types.SideTypeBuy && order.Price.Compare(kline.Low) < 0 { - newPrice := lastPrice - order.Quantity = order.Quantity.Mul(order.Price).Div(newPrice) - order.Price = newPrice - toRepost = append(toRepost, order.SubmitOrder) - } else if order.Side == types.SideTypeSell && order.Price.Compare(kline.High) > 0 { - newPrice := lastPrice - order.Price = newPrice - toRepost = append(toRepost, order.SubmitOrder) + // SL + /*if (!atrx2.IsZero() && s.bottomPrice.Add(atrx2).Compare(lastPrice) <= 0) || + lastPrice.Sub(s.bottomPrice).Div(lastPrice).Compare(s.StopLoss) > 0 { + if lastPrice.Compare(s.sellPrice) < 0 { + takeProfit = true } + buyall = true + s.bottomPrice = fixedpoint.Zero + }*/ + if !s.DisableShortStop && ((!atr.IsZero() && s.sellPrice.Sub(atr).Compare(lastPrice) >= 0) || + lastPrice.Sub(s.sellPrice).Div(s.sellPrice).Compare(s.StopLoss) > 0) { + buyall = true + reason = 4 } + } + if buyall { + log.Warnf("buyall TPSL %v %v", s.Position.GetBase(), quoteBalance) + p := s.sellPrice + if order, ok := s.ClosePosition(ctx); order != nil && ok { + if takeProfit { + log.Errorf("takeprofit buy at %v, avg %v, l: %v, atrx2: %v", lastPrice, spBack, bottomBack, atrx2) + } else { + log.Errorf("stoploss buy at %v, avg %v, l: %v, atrx2: %v", lastPrice, spBack, bottomBack, atrx2) + } - if len(toRepost) > 0 { - createdOrders, err := orderExecutor.SubmitOrders(ctx, toRepost...) - if err != nil { - log.WithError(err).Errorf("cannot place order") - return + // calculate report + if s.Record { + log.Error("record ba") } - log.Infof("repost order %v", createdOrders) - s.tradeCollector.Process() - } - sellall := false - buyall := false - if !baseBalance.IsZero() { - if s.peakPrice.IsZero() && !buyPrice.IsZero() { - s.peakPrice = kline.High - } else if s.peakPrice.Compare(kline.High) < 0 { - s.peakPrice = kline.High + var pnlRate float64 + if takeProfit { + pnlRate = p.Sub(lastPrice).Div(lastPrice).Float64() + } else { + pnlRate = lastPrice.Sub(p).Div(lastPrice).Float64() } - } + switch reason { + case 0: + percentAvgTPfromCCI = percentAvgTPfromCCI*float64(counterTPfromCCI) + pnlRate + counterTPfromCCI += 1 + percentAvgTPfromCCI /= float64(counterTPfromCCI) + case 1: + percentAvgTPfromLongShort = percentAvgTPfromLongShort*float64(counterTPfromLongShort) + pnlRate + counterTPfromLongShort += 1 + percentAvgTPfromLongShort /= float64(counterTPfromLongShort) + case 2: + percentAvgTPfromAtr = percentAvgTPfromAtr*float64(counterTPfromAtr) + pnlRate + counterTPfromAtr += 1 + percentAvgTPfromAtr /= float64(counterTPfromAtr) + case 3: + percentAvgTPfromPeak = percentAvgTPfromPeak*float64(counterTPfromPeak) + pnlRate + counterTPfromPeak += 1 + percentAvgTPfromPeak /= float64(counterTPfromPeak) + case 4: + percentAvgSLfromSL = percentAvgSLfromSL*float64(counterSLfromSL) + pnlRate + counterSLfromSL += 1 + percentAvgSLfromSL /= float64(counterSLfromSL) - if !quoteBalance.IsZero() { - if s.bottomPrice.IsZero() && !sellPrice.IsZero() { - s.bottomPrice = kline.Low - } else if s.bottomPrice.Compare(kline.Low) > 0 { - s.bottomPrice = kline.Low } } - - atrx2 := fixedpoint.NewFromFloat(s.atr.Last() * 2) - - takeProfit := false - peakBack := s.peakPrice - bottomBack := s.bottomPrice - if !baseBalance.IsZero() && !buyPrice.IsZero() { - - // TP - if !atrx2.IsZero() && s.peakPrice.Sub(atrx2).Compare(lastPrice) >= 0 && - lastPrice.Compare(buyPrice) > 0 { - sellall = true - s.peakPrice = fixedpoint.Zero - takeProfit = true - } - - // SL - if buyPrice.Sub(lastPrice).Div(buyPrice).Compare(s.Stoploss) > 0 || - (!atrx2.IsZero() && buyPrice.Sub(atrx2).Compare(lastPrice) >= 0) { - sellall = true - s.peakPrice = fixedpoint.Zero + } + } + buyOrderTPSL := func(price fixedpoint.Value) { + lastPrice := s.GetLastPrice() + base := s.Position.GetBase().Abs() + if base.Mul(lastPrice).Compare(s.Market.MinNotional) < 0 || base.Compare(s.Market.MinQuantity) < 0 { + return + } + if s.buyPrice.IsZero() { + return + } + balances := session.GetAccount().Balances() + baseBalance := balances[s.Market.BaseCurrency].Available + atr := fixedpoint.NewFromFloat(s.atr.Last()) + atrx2 := fixedpoint.NewFromFloat(s.atr.Last() * 2) + sellall := false + if s.peakPrice.IsZero() || s.peakPrice.Compare(price) < 0 { + s.peakPrice = price + } + takeProfit := false + peakBack := s.peakPrice + bpBack := s.buyPrice + reason := -1 + if baseBalance.Compare(s.Market.MinQuantity) >= 0 && baseBalance.Mul(lastPrice).Compare(s.Market.MinNotional) >= 0 { + // TP + base := fixedpoint.NewFromFloat(s.ma34.Last()) + if lastPrice.Compare(s.buyPrice) > 0 && (shortSig || + (!atrx2.IsZero() && base.Add(atrx2).Compare(lastPrice) <= 0)) { + sellall = true + takeProfit = true + + // calculate report + if shortSig { + reason = 1 + } else { + reason = 2 } } + if !atr.IsZero() && s.peakPrice.Sub(atr).Compare(lastPrice) >= 0 && + lastPrice.Compare(s.buyPrice) > 0 { + sellall = true + takeProfit = true + reason = 3 + } - if !quoteBalance.IsZero() && !sellPrice.IsZero() && !s.DisableShortStop { - // TP - if !atrx2.IsZero() && s.bottomPrice.Add(atrx2).Compare(lastPrice) >= 0 && - lastPrice.Compare(sellPrice) < 0 { - buyall = true - s.bottomPrice = fixedpoint.Zero + // SL + /*if s.peakPrice.Sub(lastPrice).Div(s.peakPrice).Compare(s.StopLoss) > 0 || + (!atrx2.IsZero() && s.peakPrice.Sub(atrx2).Compare(lastPrice) >= 0) { + if lastPrice.Compare(s.buyPrice) > 0 { takeProfit = true } + sellall = true + s.peakPrice = fixedpoint.Zero + }*/ + if !s.DisableLongStop && (s.buyPrice.Sub(lastPrice).Div(s.buyPrice).Compare(s.StopLoss) > 0 || + (!atr.IsZero() && s.buyPrice.Sub(atr).Compare(lastPrice) >= 0)) { + sellall = true + reason = 4 + } + } - // SL - if (!atrx2.IsZero() && sellPrice.Add(atrx2).Compare(lastPrice) <= 0) || - lastPrice.Sub(sellPrice).Div(sellPrice).Compare(s.Stoploss) > 0 { - buyall = true - s.bottomPrice = fixedpoint.Zero + if sellall { + log.Warnf("sellall TPSL %v", s.Position.GetBase()) + p := s.buyPrice + if order, ok := s.ClosePosition(ctx); order != nil && ok { + if takeProfit { + log.Errorf("takeprofit sell at %v, avg %v, h: %v, atrx2: %v", lastPrice, bpBack, peakBack, atrx2) + } else { + log.Errorf("stoploss sell at %v, avg %v, h: %v, atrx2: %v", lastPrice, bpBack, peakBack, atrx2) } - } - if sellall { - order := types.SubmitOrder{ - Symbol: s.Symbol, - Side: types.SideTypeSell, - Type: types.OrderTypeMarket, - Market: s.Market, - Quantity: baseBalance, + // calculate report + if s.Record { + log.Error("record sa") } - if s.validateOrder(&order) { - if takeProfit { - log.Errorf("takeprofit sell at %v, avg %v, h: %v, atrx2: %v, timestamp: %s", lastPrice, buyPrice, peakBack, atrx2, kline.StartTime) - } else { - log.Errorf("stoploss sell at %v, avg %v, h: %v, atrx2: %v, timestamp %s", lastPrice, buyPrice, peakBack, atrx2, kline.StartTime) - } - createdOrders, err := orderExecutor.SubmitOrders(ctx, order) - if err != nil { - log.WithError(err).Errorf("cannot place order") - return - } - log.Infof("stoploss sold order %v", createdOrders) - s.tradeCollector.Process() + var pnlRate float64 + if takeProfit { + pnlRate = lastPrice.Sub(p).Div(p).Float64() + } else { + pnlRate = p.Sub(lastPrice).Div(p).Float64() + } + switch reason { + case 0: + percentAvgTPfromCCI = percentAvgTPfromCCI*float64(counterTPfromCCI) + pnlRate + counterTPfromCCI += 1 + percentAvgTPfromCCI /= float64(counterTPfromCCI) + case 1: + percentAvgTPfromLongShort = percentAvgTPfromLongShort*float64(counterTPfromLongShort) + pnlRate + counterTPfromLongShort += 1 + percentAvgTPfromLongShort /= float64(counterTPfromLongShort) + case 2: + percentAvgTPfromAtr = percentAvgTPfromAtr*float64(counterTPfromAtr) + pnlRate + counterTPfromAtr += 1 + percentAvgTPfromAtr /= float64(counterTPfromAtr) + case 3: + percentAvgTPfromPeak = percentAvgTPfromPeak*float64(counterTPfromPeak) + pnlRate + counterTPfromPeak += 1 + percentAvgTPfromPeak /= float64(counterTPfromPeak) + case 4: + percentAvgSLfromSL = percentAvgSLfromSL*float64(counterSLfromSL) + pnlRate + counterSLfromSL += 1 + percentAvgSLfromSL /= float64(counterSLfromSL) } } + } + } - if buyall { - totalQuantity := quoteBalance.Div(lastPrice) - order := types.SubmitOrder{ - Symbol: kline.Symbol, - Side: types.SideTypeBuy, - Type: types.OrderTypeMarket, - Quantity: totalQuantity, - Market: s.Market, - } - if s.validateOrder(&order) { - if takeProfit { - log.Errorf("takeprofit buy at %v, avg %v, l: %v, atrx2: %v, timestamp: %s", lastPrice, sellPrice, bottomBack, atrx2, kline.StartTime) - } else { - log.Errorf("stoploss buy at %v, avg %v, l: %v, atrx2: %v, timestamp: %s", lastPrice, sellPrice, bottomBack, atrx2, kline.StartTime) - } + // set last price by realtime book ticker update + // to trigger TP/SL + session.MarketDataStream.OnBookTickerUpdate(func(ticker types.BookTicker) { + if s.Environment.IsBackTesting() { + return + } + bestBid := ticker.Buy + bestAsk := ticker.Sell + var midPrice fixedpoint.Value + + if util.TryLock(&s.lock) { + if !bestAsk.IsZero() && !bestBid.IsZero() { + s.midPrice = bestAsk.Add(bestBid).Div(types.Two) + } else if !bestAsk.IsZero() { + s.midPrice = bestAsk + } else { + s.midPrice = bestBid + } + midPrice = s.midPrice + s.lock.Unlock() + } - createdOrders, err := orderExecutor.SubmitOrders(ctx, order) - if err != nil { - log.WithError(err).Errorf("cannot place order") - return - } - log.Infof("stoploss bought order %v", createdOrders) - s.tradeCollector.Process() + if !midPrice.IsZero() { + buyOrderTPSL(midPrice) + sellOrderTPSL(midPrice) + // log.Debugf("best bid %v, best ask %v, mid %v", bestBid, bestAsk, midPrice) + } + }) + + getHigh := func(window types.KLineWindow) types.Series { + if s.UseHeikinAshi { + return s.heikinAshi.High + } + return window.High() + } + getLow := func(window types.KLineWindow) types.Series { + if s.UseHeikinAshi { + return s.heikinAshi.Low + } + return window.Low() + } + getClose := func(window types.KLineWindow) types.Series { + if s.UseHeikinAshi { + return s.heikinAshi.Close + } + return window.Close() + } + getOpen := func(window types.KLineWindow) types.Series { + if s.UseHeikinAshi { + return s.heikinAshi.Open + } + return window.Open() + } + + store.OnKLineWindowUpdate(func(interval types.Interval, window types.KLineWindow) { + kline := window[len(window)-1] + s.KLineStartTime = kline.StartTime + s.KLineEndTime = kline.EndTime + + // well, only track prices on 1m + if interval == types.Interval1m { + + if s.Environment.IsBackTesting() { + buyOrderTPSL(kline.High) + sellOrderTPSL(kline.Low) + + } + } + + var lastPrice fixedpoint.Value + var ok bool + if s.Environment.IsBackTesting() { + lastPrice, ok = session.LastPrice(s.Symbol) + if !ok { + log.Errorf("cannot get last price") + return + } + } else { + s.lock.RLock() + if s.midPrice.IsZero() { + lastPrice, ok = session.LastPrice(s.Symbol) + if !ok { + log.Errorf("cannot get last price") + return } + } else { + lastPrice = s.midPrice } + s.lock.RUnlock() + } + balances := session.GetAccount().Balances() + baseBalance := balances[s.Market.BaseCurrency].Total() + quoteBalance := balances[s.Market.QuoteCurrency].Total() + atr := fixedpoint.NewFromFloat(s.atr.Last()) + if !s.Environment.IsBackTesting() { + log.Infof("Get last price: %v, ewo %f, ewoSig %f, ccis: %f, atr %v, kline: %v, balance[base]: %v balance[quote]: %v", + lastPrice, s.ewo.Last(), s.ewoSignal.Last(), s.ccis.ma.Last(), atr, kline, baseBalance, quoteBalance) } if kline.Interval != s.Interval { return } - // To get the threshold for ewo - mean := types.Mean(types.Abs(s.ewo), 5) + priceHighest := types.Highest(getHigh(window), 233) + priceLowest := types.Lowest(getLow(window), 233) + priceChangeRate := (priceHighest - priceLowest) / priceHighest / 14 + ewoHighest := types.Highest(s.ewoHistogram, 233) + + s.ewoChangeRate = math.Abs(s.ewoHistogram.Last() / ewoHighest * priceChangeRate) longSignal := types.CrossOver(s.ewo, s.ewoSignal) shortSignal := types.CrossUnder(s.ewo, s.ewoSignal) + + base := s.ma34.Last() + sellLine := base + s.atr.Last()*3 + buyLine := base - s.atr.Last()*3 + clozes := getClose(window) + opens := getOpen(window) + // get trend flags - var bull, breakThrough, breakDown bool - if s.UseHeikinAshi { - bull = s.heikinAshi.Close.Last() > s.heikinAshi.Open.Last() - breakThrough = s.heikinAshi.Close.Last() > s.ma5.Last() - breakDown = s.heikinAshi.Close.Last() < s.ma5.Last() - } else { - bull = kline.Close.Compare(kline.Open) > 0 - breakThrough = kline.Close.Float64() > s.ma5.Last() - breakDown = kline.Close.Float64() < s.ma5.Last() + bull := clozes.Last() > opens.Last() + breakThrough := clozes.Last() > s.ma5.Last() && clozes.Last() > s.ma34.Last() + breakDown := clozes.Last() < s.ma5.Last() && clozes.Last() < s.ma34.Last() + + // kline breakthrough ma5, ma34 trend up, and cci Stochastic bull + IsBull := bull && breakThrough && s.ccis.BuySignal() && s.ewoChangeRate < s.EwoChangeFilterHigh && s.ewoChangeRate > s.EwoChangeFilterLow + // kline downthrough ma5, ma34 trend down, and cci Stochastic bear + IsBear := !bull && breakDown && s.ccis.SellSignal() && s.ewoChangeRate < s.EwoChangeFilterHigh && s.ewoChangeRate > s.EwoChangeFilterLow + + if !s.Environment.IsBackTesting() { + log.Infof("IsBull: %v, bull: %v, longSignal[1]: %v, shortSignal: %v, lastPrice: %v", + IsBull, bull, longSignal.Index(1), shortSignal.Last(), lastPrice) + log.Infof("IsBear: %v, bear: %v, shortSignal[1]: %v, longSignal: %v, lastPrice: %v", + IsBear, !bull, shortSignal.Index(1), longSignal.Last(), lastPrice) } - // kline breakthrough ma5, ma50 trend up, and ewo > threshold - IsBull := bull && breakThrough && s.ewo.Last() >= mean - // kline downthrough ma5, ma50 trend down, and ewo < threshold - IsBear := !bull && breakDown && s.ewo.Last() <= -mean - var orders []types.SubmitOrder - var price fixedpoint.Value - - if longSignal.Index(1) && !shortSignal.Last() && IsBull { - if s.UseHeikinAshi { - price = fixedpoint.NewFromFloat(s.heikinAshi.Close.Last()) - } else { - price = kline.Low - } - quoteBalance, ok := session.GetAccount().Balance(s.Market.QuoteCurrency) - if !ok { - return + if (longSignal.Index(1) && !shortSignal.Last() && IsBull) || lastPrice.Float64() <= buyLine { + price := lastPrice.Sub(atr.Div(types.Two)) + // if total asset (including locked) could be used to buy + if quoteBalance.Div(price).Compare(s.Market.MinQuantity) >= 0 && quoteBalance.Compare(s.Market.MinNotional) >= 0 { + // cancel all orders to release lock + s.CancelAll(ctx) + + // backup, since the s.sellPrice will be cleared when doing ClosePosition + sellPrice := s.sellPrice + log.Errorf("ewoChangeRate %v, emv %v", s.ewoChangeRate, s.emv.Last()) + + // calculate report + if closeOrder, _ := s.PlaceBuyOrder(ctx, price); closeOrder != nil { + if s.Record { + log.Error("record l") + } + if !sellPrice.IsZero() { + if lastPrice.Compare(sellPrice) > 0 { + pnlRate := lastPrice.Sub(sellPrice).Div(lastPrice).Float64() + percentAvgTPfromOrder = percentAvgTPfromOrder*float64(counterTPfromOrder) + pnlRate + counterTPfromOrder += 1 + percentAvgTPfromOrder /= float64(counterTPfromOrder) + } else { + pnlRate := sellPrice.Sub(lastPrice).Div(lastPrice).Float64() + percentAvgSLfromOrder = percentAvgSLfromOrder*float64(counterSLfromOrder) + pnlRate + counterSLfromOrder += 1 + percentAvgSLfromOrder /= float64(counterSLfromOrder) + } + } else { + panic("no sell price") + } + } } - quantityAmount := quoteBalance.Available - totalQuantity := quantityAmount.Div(price) - order := types.SubmitOrder{ - Symbol: kline.Symbol, - Side: types.SideTypeBuy, - Type: types.OrderTypeLimit, - Price: price, - Quantity: totalQuantity, - Market: s.Market, - TimeInForce: types.TimeInForceGTC, - } - if s.validateOrder(&order) { - // strong long - log.Warnf("long at %v, timestamp: %s", price, kline.StartTime) - - orders = append(orders, order) - } - } else if shortSignal.Index(1) && !longSignal.Last() && IsBear { - if s.UseHeikinAshi { - price = fixedpoint.NewFromFloat(s.heikinAshi.Close.Last()) - } else { - price = kline.High - } - balances := session.GetAccount().Balances() - baseBalance := balances[s.Market.BaseCurrency].Available - order := types.SubmitOrder{ - Symbol: s.Symbol, - Side: types.SideTypeSell, - Type: types.OrderTypeLimit, - Market: s.Market, - Quantity: baseBalance, - Price: price, - TimeInForce: types.TimeInForceGTC, - } - if s.validateOrder(&order) { - log.Warnf("short at %v, timestamp: %s", price, kline.StartTime) - orders = append(orders, order) - } - } - if len(orders) > 0 { - createdOrders, err := orderExecutor.SubmitOrders(ctx, orders...) - if err != nil { - log.WithError(err).Errorf("cannot place order") - return + } + if (shortSignal.Index(1) && !longSignal.Last() && IsBear) || lastPrice.Float64() >= sellLine { + price := lastPrice.Add(atr.Div(types.Two)) + // if total asset (including locked) could be used to sell + if baseBalance.Mul(price).Compare(s.Market.MinNotional) >= 0 && baseBalance.Compare(s.Market.MinQuantity) >= 0 { + // cancel all orders to release lock + s.CancelAll(ctx) + + // backup, since the s.buyPrice will be cleared when doing ClosePosition + buyPrice := s.buyPrice + log.Errorf("ewoChangeRate: %v, emv %v", s.ewoChangeRate, s.emv.Last()) + + // calculate report + if closeOrder, _ := s.PlaceSellOrder(ctx, price); closeOrder != nil { + if s.Record { + log.Error("record s") + } + if !buyPrice.IsZero() { + if lastPrice.Compare(buyPrice) > 0 { + pnlRate := lastPrice.Sub(buyPrice).Div(buyPrice).Float64() + percentAvgTPfromOrder = percentAvgTPfromOrder*float64(counterTPfromOrder) + pnlRate + counterTPfromOrder += 1 + percentAvgTPfromOrder /= float64(counterTPfromOrder) + } else { + pnlRate := buyPrice.Sub(lastPrice).Div(buyPrice).Float64() + percentAvgSLfromOrder = percentAvgSLfromOrder*float64(counterSLfromOrder) + pnlRate + counterSLfromOrder += 1 + percentAvgSLfromOrder /= float64(counterSLfromOrder) + } + } else { + panic("no buy price") + } + } } - log.Infof("post order %v", createdOrders) - s.tradeCollector.Process() } }) - s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + + bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() log.Infof("canceling active orders...") - var toCancel []types.Order - for _, order := range orderbook.Orders() { - if order.Status == types.OrderStatusNew || order.Status == types.OrderStatusPartiallyFilled { - toCancel = append(toCancel, order) - } + _ = s.orderExecutor.GracefulCancel(ctx) + + hiblue := color.New(color.FgHiBlue).FprintfFunc() + blue := color.New(color.FgBlue).FprintfFunc() + hiyellow := color.New(color.FgHiYellow).FprintfFunc() + hiblue(os.Stderr, "---- Trade Report (Without Fee) ----\n") + hiblue(os.Stderr, "TP:\n") + blue(os.Stderr, "\tpeak / bottom with atr: %d, avg pnl rate: %f\n", counterTPfromPeak, percentAvgTPfromPeak) + blue(os.Stderr, "\tCCI Stochastic: %d, avg pnl rate: %f\n", counterTPfromCCI, percentAvgTPfromCCI) + blue(os.Stderr, "\tLongSignal/ShortSignal: %d, avg pnl rate: %f\n", counterTPfromLongShort, percentAvgTPfromLongShort) + blue(os.Stderr, "\tma34 and Atrx2: %d, avg pnl rate: %f\n", counterTPfromAtr, percentAvgTPfromAtr) + blue(os.Stderr, "\tActive Order: %d, avg pnl rate: %f\n", counterTPfromOrder, percentAvgTPfromOrder) + + totalTP := counterTPfromPeak + counterTPfromCCI + counterTPfromLongShort + counterTPfromAtr + counterTPfromOrder + avgProfit := (float64(counterTPfromPeak)*percentAvgTPfromPeak + + float64(counterTPfromCCI)*percentAvgTPfromCCI + + float64(counterTPfromLongShort)*percentAvgTPfromLongShort + + float64(counterTPfromAtr)*percentAvgTPfromAtr + + float64(counterTPfromOrder)*percentAvgTPfromOrder) / float64(totalTP) + hiblue(os.Stderr, "\tSum: %d, avg pnl rate: %f\n", totalTP, avgProfit) + + hiblue(os.Stderr, "SL:\n") + blue(os.Stderr, "\tentry SL: %d, avg pnl rate: -%f\n", counterSLfromSL, percentAvgSLfromSL) + blue(os.Stderr, "\tActive Order: %d, avg pnl rate: -%f\n", counterSLfromOrder, percentAvgSLfromOrder) + + totalSL := counterSLfromSL + counterSLfromOrder + avgLoss := (float64(counterSLfromSL)*percentAvgSLfromSL + float64(counterSLfromOrder)*percentAvgSLfromOrder) / float64(totalSL) + hiblue(os.Stderr, "\tSum: %d, avg pnl rate: -%f\n", totalSL, avgLoss) + + hiblue(os.Stderr, "WinRate: %f\n", float64(totalTP)/float64(totalTP+totalSL)) + + maString := "vwema" + if s.UseSma { + maString = "sma" } - - if err := orderExecutor.CancelOrders(ctx, toCancel...); err != nil { - log.WithError(err).Errorf("cancel order error") + if s.UseEma { + maString = "ema" } - s.tradeCollector.Process() + + hiyellow(os.Stderr, "----- EWO Settings -------\n") + hiyellow(os.Stderr, "General:\n") + hiyellow(os.Stderr, "\tuseHeikinAshi: %v\n", s.UseHeikinAshi) + hiyellow(os.Stderr, "\tstoploss: %v\n", s.StopLoss) + hiyellow(os.Stderr, "\tsymbol: %s\n", s.Symbol) + hiyellow(os.Stderr, "\tinterval: %s\n", s.Interval) + hiyellow(os.Stderr, "\tMA type: %s\n", maString) + hiyellow(os.Stderr, "\tdisableShortStop: %v\n", s.DisableShortStop) + hiyellow(os.Stderr, "\tdisableLongStop: %v\n", s.DisableLongStop) + hiyellow(os.Stderr, "\trecord: %v\n", s.Record) + hiyellow(os.Stderr, "CCI Stochastic:\n") + hiyellow(os.Stderr, "\tccistochFilterHigh: %f\n", s.FilterHigh) + hiyellow(os.Stderr, "\tccistochFilterLow: %f\n", s.FilterLow) + hiyellow(os.Stderr, "Ewo && Ewo Histogram:\n") + hiyellow(os.Stderr, "\tsigWin: %d\n", s.SignalWindow) + hiyellow(os.Stderr, "\tewoChngFilterHigh: %f\n", s.EwoChangeFilterHigh) + hiyellow(os.Stderr, "\tewoChngFilterLow: %f\n", s.EwoChangeFilterLow) }) return nil } diff --git a/pkg/strategy/factorzoo/correlation_callbacks.go b/pkg/strategy/factorzoo/correlation_callbacks.go deleted file mode 100644 index 2ef6323eae..0000000000 --- a/pkg/strategy/factorzoo/correlation_callbacks.go +++ /dev/null @@ -1,15 +0,0 @@ -// Code generated by "callbackgen -type Correlation"; DO NOT EDIT. - -package factorzoo - -import () - -func (inc *Correlation) OnUpdate(cb func(value float64)) { - inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) -} - -func (inc *Correlation) EmitUpdate(value float64) { - for _, cb := range inc.UpdateCallbacks { - cb(value) - } -} diff --git a/pkg/strategy/factorzoo/factors/mom_callbacks.go b/pkg/strategy/factorzoo/factors/mom_callbacks.go new file mode 100644 index 0000000000..055aa51c32 --- /dev/null +++ b/pkg/strategy/factorzoo/factors/mom_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type MOM"; DO NOT EDIT. + +package factorzoo + +import () + +func (inc *MOM) OnUpdate(cb func(val float64)) { + inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) +} + +func (inc *MOM) EmitUpdate(val float64) { + for _, cb := range inc.UpdateCallbacks { + cb(val) + } +} diff --git a/pkg/strategy/factorzoo/factors/momentum.go b/pkg/strategy/factorzoo/factors/momentum.go new file mode 100644 index 0000000000..c9dc34877b --- /dev/null +++ b/pkg/strategy/factorzoo/factors/momentum.go @@ -0,0 +1,114 @@ +package factorzoo + +import ( + "fmt" + "time" + + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +// gap jump momentum +// if the gap between current open price and previous close price gets larger +// meaning an opening price jump was happened, the larger momentum we get is our alpha, MOM + +//go:generate callbackgen -type MOM +type MOM struct { + types.SeriesBase + types.IntervalWindow + + // Values + Values floats.Slice + LastValue float64 + + opens *types.Queue + closes *types.Queue + + EndTime time.Time + + UpdateCallbacks []func(val float64) +} + +func (inc *MOM) Index(i int) float64 { + if inc.Values == nil { + return 0 + } + return inc.Values.Index(i) +} + +func (inc *MOM) Last() float64 { + if inc.Values.Length() == 0 { + return 0 + } + return inc.Values.Last() +} + +func (inc *MOM) Length() int { + if inc.Values == nil { + return 0 + } + return inc.Values.Length() +} + +//var _ types.SeriesExtend = &MOM{} + +func (inc *MOM) Update(open, close float64) { + if inc.SeriesBase.Series == nil { + inc.SeriesBase.Series = inc + inc.opens = types.NewQueue(inc.Window) + inc.closes = types.NewQueue(inc.Window + 1) + } + inc.opens.Update(open) + inc.closes.Update(close) + if inc.opens.Length() >= inc.Window && inc.closes.Length() >= inc.Window { + gap := inc.opens.Last()/inc.closes.Index(1) - 1 + inc.Values.Push(gap) + } +} + +func (inc *MOM) CalculateAndUpdate(allKLines []types.KLine) { + if len(inc.Values) == 0 { + for _, k := range allKLines { + inc.PushK(k) + } + inc.EmitUpdate(inc.Last()) + } else { + k := allKLines[len(allKLines)-1] + inc.PushK(k) + inc.EmitUpdate(inc.Last()) + } +} + +func (inc *MOM) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { + if inc.Interval != interval { + return + } + + inc.CalculateAndUpdate(window) +} + +func (inc *MOM) Bind(updater indicator.KLineWindowUpdater) { + updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) +} + +func (inc *MOM) PushK(k types.KLine) { + if inc.EndTime != zeroTime && k.EndTime.Before(inc.EndTime) { + return + } + + inc.Update(k.Open.Float64(), k.Close.Float64()) + inc.EndTime = k.EndTime.Time() + inc.EmitUpdate(inc.Last()) +} + +func calculateMomentum(klines []types.KLine, window int, valA KLineValueMapper, valB KLineValueMapper) (float64, error) { + length := len(klines) + if length == 0 || length < window { + return 0.0, fmt.Errorf("insufficient elements for calculating VOL with window = %d", window) + } + + momentum := (1 - valA(klines[length-1])/valB(klines[length-1])) * -1 + + return momentum, nil +} diff --git a/pkg/strategy/factorzoo/factors/pmr_callbacks.go b/pkg/strategy/factorzoo/factors/pmr_callbacks.go new file mode 100644 index 0000000000..a90c99ac2b --- /dev/null +++ b/pkg/strategy/factorzoo/factors/pmr_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type PMR"; DO NOT EDIT. + +package factorzoo + +import () + +func (inc *PMR) OnUpdate(cb func(value float64)) { + inc.updateCallbacks = append(inc.updateCallbacks, cb) +} + +func (inc *PMR) EmitUpdate(value float64) { + for _, cb := range inc.updateCallbacks { + cb(value) + } +} diff --git a/pkg/strategy/factorzoo/factors/price_mean_reversion.go b/pkg/strategy/factorzoo/factors/price_mean_reversion.go new file mode 100644 index 0000000000..2d2bd5ce89 --- /dev/null +++ b/pkg/strategy/factorzoo/factors/price_mean_reversion.go @@ -0,0 +1,110 @@ +package factorzoo + +import ( + "time" + + "gonum.org/v1/gonum/stat" + + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +// price mean reversion +// assume that the quotient of SMA over close price will dynamically revert into one. +// so this fraction value is our alpha, PMR + +//go:generate callbackgen -type PMR +type PMR struct { + types.IntervalWindow + types.SeriesBase + + Values floats.Slice + SMA *indicator.SMA + EndTime time.Time + + updateCallbacks []func(value float64) +} + +var _ types.SeriesExtend = &PMR{} + +func (inc *PMR) Update(price float64) { + if inc.SeriesBase.Series == nil { + inc.SeriesBase.Series = inc + inc.SMA = &indicator.SMA{IntervalWindow: inc.IntervalWindow} + } + inc.SMA.Update(price) + if inc.SMA.Length() >= inc.Window { + reversion := inc.SMA.Last() / price + inc.Values.Push(reversion) + } +} + +func (inc *PMR) Last() float64 { + if len(inc.Values) == 0 { + return 0 + } + + return inc.Values[len(inc.Values)-1] +} + +func (inc *PMR) Index(i int) float64 { + if i >= len(inc.Values) { + return 0 + } + + return inc.Values[len(inc.Values)-1-i] +} + +func (inc *PMR) Length() int { + return len(inc.Values) +} + +func (inc *PMR) CalculateAndUpdate(allKLines []types.KLine) { + if len(inc.Values) == 0 { + for _, k := range allKLines { + inc.PushK(k) + } + inc.EmitUpdate(inc.Last()) + } else { + k := allKLines[len(allKLines)-1] + inc.PushK(k) + inc.EmitUpdate(inc.Last()) + } +} + +func (inc *PMR) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { + if inc.Interval != interval { + return + } + + inc.CalculateAndUpdate(window) +} + +func (inc *PMR) Bind(updater indicator.KLineWindowUpdater) { + updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) +} + +func (inc *PMR) PushK(k types.KLine) { + if inc.EndTime != zeroTime && k.EndTime.Before(inc.EndTime) { + return + } + + inc.Update(indicator.KLineClosePriceMapper(k)) + inc.EndTime = k.EndTime.Time() + inc.EmitUpdate(inc.Last()) +} + +func CalculateKLinesPMR(allKLines []types.KLine, window int) float64 { + return pmr(indicator.MapKLinePrice(allKLines, indicator.KLineClosePriceMapper), window) +} + +func pmr(prices []float64, window int) float64 { + var end = len(prices) - 1 + if end == 0 { + return prices[0] + } + + reversion := -stat.Mean(prices[end-window:end], nil) / prices[end] + return reversion +} diff --git a/pkg/strategy/factorzoo/factors/price_volume_divergence.go b/pkg/strategy/factorzoo/factors/price_volume_divergence.go new file mode 100644 index 0000000000..54b07a2659 --- /dev/null +++ b/pkg/strategy/factorzoo/factors/price_volume_divergence.go @@ -0,0 +1,117 @@ +package factorzoo + +import ( + "time" + + "gonum.org/v1/gonum/stat" + + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +// price volume divergence +// if the correlation of two time series gets smaller, they are diverging. +// so the negative value of the correlation of close price and volume is our alpha, PVD + +var zeroTime time.Time + +type KLineValueMapper func(k types.KLine) float64 + +//go:generate callbackgen -type PVD +type PVD struct { + types.IntervalWindow + types.SeriesBase + + Values floats.Slice + Prices *types.Queue + Volumes *types.Queue + EndTime time.Time + + updateCallbacks []func(value float64) +} + +var _ types.SeriesExtend = &PVD{} + +func (inc *PVD) Update(price float64, volume float64) { + if inc.SeriesBase.Series == nil { + inc.SeriesBase.Series = inc + inc.Prices = types.NewQueue(inc.Window) + inc.Volumes = types.NewQueue(inc.Window) + } + inc.Prices.Update(price) + inc.Volumes.Update(volume) + if inc.Prices.Length() >= inc.Window && inc.Volumes.Length() >= inc.Window { + divergence := -types.Correlation(inc.Prices, inc.Volumes, inc.Window) + inc.Values.Push(divergence) + } +} + +func (inc *PVD) Last() float64 { + if len(inc.Values) == 0 { + return 0 + } + + return inc.Values[len(inc.Values)-1] +} + +func (inc *PVD) Index(i int) float64 { + if i >= len(inc.Values) { + return 0 + } + + return inc.Values[len(inc.Values)-1-i] +} + +func (inc *PVD) Length() int { + return len(inc.Values) +} + +func (inc *PVD) CalculateAndUpdate(allKLines []types.KLine) { + if len(inc.Values) == 0 { + for _, k := range allKLines { + inc.PushK(k) + } + inc.EmitUpdate(inc.Last()) + } else { + k := allKLines[len(allKLines)-1] + inc.PushK(k) + inc.EmitUpdate(inc.Last()) + } +} + +func (inc *PVD) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { + if inc.Interval != interval { + return + } + + inc.CalculateAndUpdate(window) +} + +func (inc *PVD) Bind(updater indicator.KLineWindowUpdater) { + updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) +} + +func (inc *PVD) PushK(k types.KLine) { + if inc.EndTime != zeroTime && k.EndTime.Before(inc.EndTime) { + return + } + + inc.Update(indicator.KLineClosePriceMapper(k), indicator.KLineVolumeMapper(k)) + inc.EndTime = k.EndTime.Time() + inc.EmitUpdate(inc.Last()) +} + +func CalculateKLinesPVD(allKLines []types.KLine, window int) float64 { + return pvd(indicator.MapKLinePrice(allKLines, indicator.KLineClosePriceMapper), indicator.MapKLinePrice(allKLines, indicator.KLineVolumeMapper), window) +} + +func pvd(prices []float64, volumes []float64, window int) float64 { + var end = len(prices) - 1 + if end == 0 { + return prices[0] + } + + divergence := -stat.Correlation(prices[end-window:end], volumes[end-window:end], nil) + return divergence +} diff --git a/pkg/strategy/factorzoo/factors/pvd_callbacks.go b/pkg/strategy/factorzoo/factors/pvd_callbacks.go new file mode 100644 index 0000000000..f8dead4a81 --- /dev/null +++ b/pkg/strategy/factorzoo/factors/pvd_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type PVD"; DO NOT EDIT. + +package factorzoo + +import () + +func (inc *PVD) OnUpdate(cb func(value float64)) { + inc.updateCallbacks = append(inc.updateCallbacks, cb) +} + +func (inc *PVD) EmitUpdate(value float64) { + for _, cb := range inc.updateCallbacks { + cb(value) + } +} diff --git a/pkg/strategy/factorzoo/factors/return_rate.go b/pkg/strategy/factorzoo/factors/return_rate.go new file mode 100644 index 0000000000..057f2ac5c2 --- /dev/null +++ b/pkg/strategy/factorzoo/factors/return_rate.go @@ -0,0 +1,113 @@ +package factorzoo + +import ( + "time" + + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +// simply internal return rate over certain timeframe(interval) + +//go:generate callbackgen -type RR +type RR struct { + types.IntervalWindow + types.SeriesBase + + prices *types.Queue + Values floats.Slice + EndTime time.Time + + updateCallbacks []func(value float64) +} + +var _ types.SeriesExtend = &RR{} + +func (inc *RR) Update(price float64) { + if inc.SeriesBase.Series == nil { + inc.SeriesBase.Series = inc + inc.prices = types.NewQueue(inc.Window) + } + inc.prices.Update(price) + irr := inc.prices.Last()/inc.prices.Index(1) - 1 + inc.Values.Push(irr) + +} + +func (inc *RR) Last() float64 { + if len(inc.Values) == 0 { + return 0 + } + + return inc.Values[len(inc.Values)-1] +} + +func (inc *RR) Index(i int) float64 { + if i >= len(inc.Values) { + return 0 + } + + return inc.Values[len(inc.Values)-1-i] +} + +func (inc *RR) Length() int { + return len(inc.Values) +} + +func (inc *RR) CalculateAndUpdate(allKLines []types.KLine) { + if len(inc.Values) == 0 { + for _, k := range allKLines { + inc.PushK(k) + } + inc.EmitUpdate(inc.Last()) + } else { + k := allKLines[len(allKLines)-1] + inc.PushK(k) + inc.EmitUpdate(inc.Last()) + } +} + +func (inc *RR) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { + if inc.Interval != interval { + return + } + + inc.CalculateAndUpdate(window) +} + +func (inc *RR) Bind(updater indicator.KLineWindowUpdater) { + updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) +} + +func (inc *RR) BindK(target indicator.KLineClosedEmitter, symbol string, interval types.Interval) { + target.OnKLineClosed(types.KLineWith(symbol, interval, inc.PushK)) +} + +func (inc *RR) PushK(k types.KLine) { + if inc.EndTime != zeroTime && k.EndTime.Before(inc.EndTime) { + return + } + + inc.Update(indicator.KLineClosePriceMapper(k)) + inc.EndTime = k.EndTime.Time() + inc.EmitUpdate(inc.Last()) +} + +func (inc *RR) LoadK(allKLines []types.KLine) { + for _, k := range allKLines { + inc.PushK(k) + } + inc.EmitUpdate(inc.Last()) +} + +//func calculateReturn(klines []types.KLine, window int, val KLineValueMapper) (float64, error) { +// length := len(klines) +// if length == 0 || length < window { +// return 0.0, fmt.Errorf("insufficient elements for calculating VOL with window = %d", window) +// } +// +// rate := val(klines[length-1])/val(klines[length-2]) - 1 +// +// return rate, nil +//} diff --git a/pkg/strategy/factorzoo/factors/rr_callbacks.go b/pkg/strategy/factorzoo/factors/rr_callbacks.go new file mode 100644 index 0000000000..301837d574 --- /dev/null +++ b/pkg/strategy/factorzoo/factors/rr_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type RR"; DO NOT EDIT. + +package factorzoo + +import () + +func (inc *RR) OnUpdate(cb func(value float64)) { + inc.updateCallbacks = append(inc.updateCallbacks, cb) +} + +func (inc *RR) EmitUpdate(value float64) { + for _, cb := range inc.updateCallbacks { + cb(value) + } +} diff --git a/pkg/strategy/factorzoo/factors/vmom_callbacks.go b/pkg/strategy/factorzoo/factors/vmom_callbacks.go new file mode 100644 index 0000000000..9ef858e38f --- /dev/null +++ b/pkg/strategy/factorzoo/factors/vmom_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type VMOM"; DO NOT EDIT. + +package factorzoo + +import () + +func (inc *VMOM) OnUpdate(cb func(val float64)) { + inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) +} + +func (inc *VMOM) EmitUpdate(val float64) { + for _, cb := range inc.UpdateCallbacks { + cb(val) + } +} diff --git a/pkg/strategy/factorzoo/factors/volume_momentum.go b/pkg/strategy/factorzoo/factors/volume_momentum.go new file mode 100644 index 0000000000..602065273c --- /dev/null +++ b/pkg/strategy/factorzoo/factors/volume_momentum.go @@ -0,0 +1,116 @@ +package factorzoo + +import ( + "fmt" + "time" + + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +// quarterly volume momentum +// assume that the quotient of volume SMA over latest volume will dynamically revert into one. +// so this fraction value is our alpha, PMR + +//go:generate callbackgen -type VMOM +type VMOM struct { + types.SeriesBase + types.IntervalWindow + + // Values + Values floats.Slice + LastValue float64 + + volumes *types.Queue + + EndTime time.Time + + UpdateCallbacks []func(val float64) +} + +func (inc *VMOM) Index(i int) float64 { + if inc.Values == nil { + return 0 + } + return inc.Values.Index(i) +} + +func (inc *VMOM) Last() float64 { + if inc.Values.Length() == 0 { + return 0 + } + return inc.Values.Last() +} + +func (inc *VMOM) Length() int { + if inc.Values == nil { + return 0 + } + return inc.Values.Length() +} + +var _ types.SeriesExtend = &VMOM{} + +func (inc *VMOM) Update(volume float64) { + if inc.SeriesBase.Series == nil { + inc.SeriesBase.Series = inc + inc.volumes = types.NewQueue(inc.Window) + } + inc.volumes.Update(volume) + if inc.volumes.Length() >= inc.Window { + v := inc.volumes.Last() / inc.volumes.Mean() + inc.Values.Push(v) + } +} + +func (inc *VMOM) CalculateAndUpdate(allKLines []types.KLine) { + if len(inc.Values) == 0 { + for _, k := range allKLines { + inc.PushK(k) + } + inc.EmitUpdate(inc.Last()) + } else { + k := allKLines[len(allKLines)-1] + inc.PushK(k) + inc.EmitUpdate(inc.Last()) + } +} + +func (inc *VMOM) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { + if inc.Interval != interval { + return + } + + inc.CalculateAndUpdate(window) +} + +func (inc *VMOM) Bind(updater indicator.KLineWindowUpdater) { + updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) +} + +func (inc *VMOM) PushK(k types.KLine) { + if inc.EndTime != zeroTime && k.EndTime.Before(inc.EndTime) { + return + } + + inc.Update(k.Volume.Float64()) + inc.EndTime = k.EndTime.Time() + inc.EmitUpdate(inc.Last()) +} + +func calculateVolumeMomentum(klines []types.KLine, window int, valV KLineValueMapper, valP KLineValueMapper) (float64, error) { + length := len(klines) + if length == 0 || length < window { + return 0.0, fmt.Errorf("insufficient elements for calculating VOL with window = %d", window) + } + + vma := 0. + for _, p := range klines[length-window : length-1] { + vma += valV(p) + } + vma /= float64(window) + momentum := valV(klines[length-1]) / vma //* (valP(klines[length-1-2]) / valP(klines[length-1])) + + return momentum, nil +} diff --git a/pkg/strategy/factorzoo/linear_regression.go b/pkg/strategy/factorzoo/linear_regression.go new file mode 100644 index 0000000000..80c84e6870 --- /dev/null +++ b/pkg/strategy/factorzoo/linear_regression.go @@ -0,0 +1,194 @@ +package factorzoo + +import ( + "context" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/strategy/factorzoo/factors" + "github.com/c9s/bbgo/pkg/types" +) + +type Linear struct { + Symbol string + Market types.Market `json:"-"` + types.IntervalWindow + + // MarketOrder is the option to enable market order short. + MarketOrder bool `json:"marketOrder"` + + Quantity fixedpoint.Value `json:"quantity"` + StopEMARange fixedpoint.Value `json:"stopEMARange"` + StopEMA *types.IntervalWindow `json:"stopEMA"` + + // Xs (input), factors & indicators + divergence *factorzoo.PVD // price volume divergence + reversion *factorzoo.PMR // price mean reversion + momentum *factorzoo.MOM // price momentum from paper, alpha 101 + drift *indicator.Drift // GBM + volume *factorzoo.VMOM // quarterly volume momentum + + // Y (output), internal rate of return + irr *factorzoo.RR + + orderExecutor *bbgo.GeneralOrderExecutor + session *bbgo.ExchangeSession + activeOrders *bbgo.ActiveOrderBook + + bbgo.QuantityOrAmount +} + +func (s *Linear) Subscribe(session *bbgo.ExchangeSession) { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) +} + +func (s *Linear) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor) { + s.session = session + s.orderExecutor = orderExecutor + + position := orderExecutor.Position() + symbol := position.Symbol + store, _ := session.MarketDataStore(symbol) + + // initialize factor indicators + s.divergence = &factorzoo.PVD{IntervalWindow: types.IntervalWindow{Window: 60, Interval: s.Interval}} + s.divergence.Bind(store) + s.reversion = &factorzoo.PMR{IntervalWindow: types.IntervalWindow{Window: 60, Interval: s.Interval}} + s.reversion.Bind(store) + s.drift = &indicator.Drift{IntervalWindow: types.IntervalWindow{Window: 7, Interval: s.Interval}} + s.drift.Bind(store) + s.momentum = &factorzoo.MOM{IntervalWindow: types.IntervalWindow{Window: 1, Interval: s.Interval}} + s.momentum.Bind(store) + s.volume = &factorzoo.VMOM{IntervalWindow: types.IntervalWindow{Window: 90, Interval: s.Interval}} + s.volume.Bind(store) + + s.irr = &factorzoo.RR{IntervalWindow: types.IntervalWindow{Window: 2, Interval: s.Interval}} + s.irr.Bind(store) + + predLst := types.NewQueue(s.Window) + session.MarketDataStream.OnKLineClosed(types.KLineWith(symbol, s.Interval, func(kline types.KLine) { + + ctx := context.Background() + + // graceful cancel all active orders + _ = orderExecutor.GracefulCancel(ctx) + + // take past window days' values to predict future return + // (e.g., 5 here in default configuration file) + a := []floats.Slice{ + s.divergence.Values[len(s.divergence.Values)-s.Window-2 : len(s.divergence.Values)-2], + s.reversion.Values[len(s.reversion.Values)-s.Window-2 : len(s.reversion.Values)-2], + s.drift.Values[len(s.drift.Values)-s.Window-2 : len(s.drift.Values)-2], + s.momentum.Values[len(s.momentum.Values)-s.Window-2 : len(s.momentum.Values)-2], + s.volume.Values[len(s.volume.Values)-s.Window-2 : len(s.volume.Values)-2], + } + // e.g., s.window is 5 + // factors array from day -4 to day 0, [[0.1, 0.2, 0.35, 0.3 , 0.25], [1.1, -0.2, 1.35, -0.3 , -0.25], ...] + // the binary(+/-) daily return rate from day -3 to day 1, [0, 1, 1, 0, 0] + // then we take the latest available factors array into linear regression model + b := []floats.Slice{filter(s.irr.Values[len(s.irr.Values)-s.Window-1:len(s.irr.Values)-1], binary)} + var x []types.Series + var y []types.Series + + x = append(x, &a[0]) + x = append(x, &a[1]) + x = append(x, &a[2]) + x = append(x, &a[3]) + x = append(x, &a[4]) + //x = append(x, &a[5]) + + y = append(y, &b[0]) + model := types.LogisticRegression(x, y[0], s.Window, 8000, 0.0001) + + // use the last value from indicators, or the SeriesExtends' predict function. (e.g., look back: 5) + input := []float64{ + s.divergence.Last(), + s.reversion.Last(), + s.drift.Last(), + s.momentum.Last(), + s.volume.Last(), + } + pred := model.Predict(input) + predLst.Update(pred) + + qty := s.Quantity //s.QuantityOrAmount.CalculateQuantity(kline.Close) + + // the scale of pred is from 0.0 to 1.0 + // 0.5 can be used as the threshold + // we use the time-series rolling prediction values here + if pred > predLst.Mean() { + if position.IsShort() { + s.ClosePosition(ctx, one) + s.placeMarketOrder(ctx, types.SideTypeBuy, qty, symbol) + } else if position.IsClosed() { + s.placeMarketOrder(ctx, types.SideTypeBuy, qty, symbol) + } + } else if pred < predLst.Mean() { + if position.IsLong() { + s.ClosePosition(ctx, one) + s.placeMarketOrder(ctx, types.SideTypeSell, qty, symbol) + } else if position.IsClosed() { + s.placeMarketOrder(ctx, types.SideTypeSell, qty, symbol) + } + } + // pass if position is opened and not dust, and remain the same direction with alpha signal + + // alpha-weighted inventory and cash + //alpha := fixedpoint.NewFromFloat(s.r1.Last()) + //targetBase := s.QuantityOrAmount.CalculateQuantity(kline.Close).Mul(alpha) + ////s.ClosePosition(ctx, one) + //diffQty := targetBase.Sub(position.Base) + //log.Info(alpha.Float64(), position.Base, diffQty.Float64()) + // + //if diffQty.Sign() > 0 { + // s.placeMarketOrder(ctx, types.SideTypeBuy, diffQty.Abs(), symbol) + //} else if diffQty.Sign() < 0 { + // s.placeMarketOrder(ctx, types.SideTypeSell, diffQty.Abs(), symbol) + //} + })) + + if !bbgo.IsBackTesting { + session.MarketDataStream.OnMarketTrade(func(trade types.Trade) { + }) + } +} + +func (s *Linear) ClosePosition(ctx context.Context, percentage fixedpoint.Value) error { + return s.orderExecutor.ClosePosition(ctx, percentage) +} + +func (s *Linear) placeMarketOrder(ctx context.Context, side types.SideType, quantity fixedpoint.Value, symbol string) { + market, _ := s.session.Market(symbol) + _, err := s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: symbol, + Market: market, + Side: side, + Type: types.OrderTypeMarket, + Quantity: quantity, + //TimeInForce: types.TimeInForceGTC, + Tag: "linear", + }) + if err != nil { + log.WithError(err).Errorf("can not place market order") + } +} + +func binary(val float64) float64 { + if val > 0. { + return 1. + } else { + return 0. + } +} + +func filter(data []float64, f func(float64) float64) []float64 { + fltd := make([]float64, 0) + for _, e := range data { + //if f(e) >= 0. { + fltd = append(fltd, f(e)) + //} + } + return fltd +} diff --git a/pkg/strategy/factorzoo/strategy.go b/pkg/strategy/factorzoo/strategy.go index acc67c5ac3..b332dc8e65 100644 --- a/pkg/strategy/factorzoo/strategy.go +++ b/pkg/strategy/factorzoo/strategy.go @@ -3,16 +3,20 @@ package factorzoo import ( "context" "fmt" + "os" + "sync" + + "github.com/sirupsen/logrus" + "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" - "github.com/sajari/regression" - "github.com/sirupsen/logrus" ) const ID = "factorzoo" -var three = fixedpoint.NewFromInt(3) +var one = fixedpoint.One +var zero = fixedpoint.Zero var log = logrus.WithField("strategy", ID) @@ -25,222 +29,104 @@ type IntervalWindowSetting struct { } type Strategy struct { - Symbol string `json:"symbol"` - Market types.Market - Interval types.Interval `json:"interval"` - Quantity fixedpoint.Value `json:"quantity"` + Environment *bbgo.Environment + Symbol string `json:"symbol"` + Market types.Market + + types.IntervalWindow + + // persistence fields + Position *types.Position `persistence:"position"` + ProfitStats *types.ProfitStats `persistence:"profit_stats"` + TradeStats *types.TradeStats `persistence:"trade_stats"` + + activeOrders *bbgo.ActiveOrderBook - Position *types.Position `json:"position,omitempty"` + Linear *Linear `json:"linear"` - activeMakerOrders *bbgo.LocalActiveOrderBook - orderStore *bbgo.OrderStore - tradeCollector *bbgo.TradeCollector + ExitMethods bbgo.ExitMethodSet `json:"exits"` - session *bbgo.ExchangeSession - book *types.StreamOrderBook + session *bbgo.ExchangeSession + orderExecutor *bbgo.GeneralOrderExecutor - prevClose fixedpoint.Value + // StrategyController + bbgo.StrategyController +} - pvDivergenceSetting *IntervalWindowSetting `json:"pvDivergence"` - pvDivergence *Correlation +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Linear.Interval}) - Ret []float64 - Alpha [][]float64 + if !bbgo.IsBackTesting { + session.Subscribe(types.MarketTradeChannel, s.Symbol, types.SubscribeOptions{}) + } - T int64 - prevER fixedpoint.Value + s.ExitMethods.SetAndSubscribe(session, s) } func (s *Strategy) ID() string { return ID } -func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { - log.Infof("subscribe %s", s.Symbol) - session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval.String()}) +func (s *Strategy) InstanceID() string { + return fmt.Sprintf("%s:%s", ID, s.Symbol) } -func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Value) error { - base := s.Position.GetBase() - if base.IsZero() { - return fmt.Errorf("no opened %s position", s.Position.Symbol) - } +func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + var instanceID = s.InstanceID() - // make it negative - quantity := base.Mul(percentage).Abs() - side := types.SideTypeBuy - if base.Sign() > 0 { - side = types.SideTypeSell + if s.Position == nil { + s.Position = types.NewPositionFromMarket(s.Market) } - if quantity.Compare(s.Market.MinQuantity) < 0 { - return fmt.Errorf("order quantity %v is too small, less than %v", quantity, s.Market.MinQuantity) + if s.ProfitStats == nil { + s.ProfitStats = types.NewProfitStats(s.Market) } - submitOrder := types.SubmitOrder{ - Symbol: s.Symbol, - Side: side, - Type: types.OrderTypeMarket, - Quantity: quantity, - Market: s.Market, + if s.TradeStats == nil { + s.TradeStats = types.NewTradeStats(s.Symbol) } - //s.Notify("Submitting %s %s order to close position by %v", s.Symbol, side.String(), percentage, submitOrder) + // StrategyController + s.Status = types.StrategyStatusRunning - createdOrders, err := s.session.Exchange.SubmitOrders(ctx, submitOrder) - if err != nil { - log.WithError(err).Errorf("can not place position close order") - } - - s.orderStore.Add(createdOrders...) - s.activeMakerOrders.Add(createdOrders...) - return err -} + s.OnSuspend(func() { + // Cancel active orders + _ = s.orderExecutor.GracefulCancel(ctx) + }) -func (s *Strategy) placeOrders(ctx context.Context, orderExecutor bbgo.OrderExecutor, er fixedpoint.Value) { - - //if s.prevER.Sign() < 0 && er.Sign() > 0 { - if er.Sign() >= 0 { - submitOrder := types.SubmitOrder{ - Symbol: s.Symbol, - Side: types.SideTypeBuy, - Type: types.OrderTypeMarket, - Quantity: s.Quantity, //er.Abs().Mul(fixedpoint.NewFromInt(20)), - } - createdOrders, err := orderExecutor.SubmitOrders(ctx, submitOrder) - if err != nil { - log.WithError(err).Errorf("can not place orders") - } - s.orderStore.Add(createdOrders...) - s.activeMakerOrders.Add(createdOrders...) - //} else if s.prevER.Sign() > 0 && er.Sign() < 0 { - } else { - submitOrder := types.SubmitOrder{ - Symbol: s.Symbol, - Side: types.SideTypeSell, - Type: types.OrderTypeMarket, - Quantity: s.Quantity, //er.Abs().Mul(fixedpoint.NewFromInt(20)), - } - createdOrders, err := orderExecutor.SubmitOrders(ctx, submitOrder) - if err != nil { - log.WithError(err).Errorf("can not place orders") - } - s.orderStore.Add(createdOrders...) - s.activeMakerOrders.Add(createdOrders...) - } - s.prevER = er -} + s.OnEmergencyStop(func() { + // Cancel active orders + _ = s.orderExecutor.GracefulCancel(ctx) + // Close 100% position + // _ = s.ClosePosition(ctx, fixedpoint.One) + }) -func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { // initial required information s.session = session - s.prevClose = fixedpoint.Zero - - // first we need to get market data store(cached market data) from the exchange session - st, _ := session.MarketDataStore(s.Symbol) - // setup the time frame size - iw := types.IntervalWindow{Window: 50, Interval: s.Interval} - // construct CORR indicator - s.pvDivergence = &Correlation{IntervalWindow: iw} - // bind indicator to the data store, so that our callback could be triggered - s.pvDivergence.Bind(st) - //s.pvDivergence.OnUpdate(func(corr float64) { - // //fmt.Printf("now we've got corr: %f\n", corr) - //}) - - s.Alpha = [][]float64{{}, {}, {}, {}, {}} - s.Ret = []float64{} - //thetas := []float64{0, 0, 0, 0} - preCompute := 0 - - s.activeMakerOrders = bbgo.NewLocalActiveOrderBook(s.Symbol) - s.activeMakerOrders.BindStream(session.UserDataStream) - - s.orderStore = bbgo.NewOrderStore(s.Symbol) - s.orderStore.BindStream(session.UserDataStream) - if s.Position == nil { - s.Position = types.NewPositionFromMarket(s.Market) - } + s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) + s.orderExecutor.BindEnvironment(s.Environment) + s.orderExecutor.BindProfitStats(s.ProfitStats) + s.orderExecutor.BindTradeStats(s.TradeStats) + s.orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { + bbgo.Sync(ctx, s) + }) + s.orderExecutor.Bind() + s.activeOrders = bbgo.NewActiveOrderBook(s.Symbol) - s.tradeCollector = bbgo.NewTradeCollector(s.Symbol, s.Position, s.orderStore) - s.tradeCollector.BindStream(session.UserDataStream) + for _, method := range s.ExitMethods { + method.Bind(session, s.orderExecutor) + } - session.UserDataStream.OnStart(func() { - log.Infof("connected") - }) + if s.Linear != nil { + s.Linear.Bind(session, s.orderExecutor) + } - session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { - - if kline.Symbol != s.Symbol || kline.Interval != s.Interval { - return - } - - if err := s.activeMakerOrders.GracefulCancel(ctx, s.session.Exchange); err != nil { - log.WithError(err).Errorf("graceful cancel order error") - } - - // amplitude volume divergence - corr := fixedpoint.NewFromFloat(s.pvDivergence.Last()).Neg() - // price mean reversion - rev := fixedpoint.NewFromInt(1).Div(kline.Close) - // alpha150 from GTJA's 191 paper - a150 := kline.High.Add(kline.Low).Add(kline.Close).Div(three).Mul(kline.Volume) - // momentum from WQ's 101 paper - mom := fixedpoint.One.Sub(kline.Open.Div(kline.Close)).Mul(fixedpoint.NegOne) - // opening gap - ogap := kline.Open.Div(s.prevClose) - - log.Infof("corr: %f, rev: %f, a150: %f, mom: %f, ogap: %f", corr.Float64(), rev.Float64(), a150.Float64(), mom.Float64(), ogap.Float64()) - s.Alpha[0] = append(s.Alpha[0], corr.Float64()) - s.Alpha[1] = append(s.Alpha[1], rev.Float64()) - s.Alpha[2] = append(s.Alpha[2], a150.Float64()) - s.Alpha[3] = append(s.Alpha[3], mom.Float64()) - s.Alpha[4] = append(s.Alpha[4], ogap.Float64()) - - //s.Alpha[5] = append(s.Alpha[4], 1.0) // constant - - ret := kline.Close.Sub(s.prevClose).Div(s.prevClose).Float64() - s.Ret = append(s.Ret, ret) - log.Infof("Current Return: %f", s.Ret[len(s.Ret)-1]) - - // accumulate enough data for cross-sectional regression, not time-series regression - s.T = 20 - if preCompute < int(s.T)+1 { - preCompute++ - } else { - s.ClosePosition(ctx, fixedpoint.One) - s.tradeCollector.Process() - // rolling regression for last 20 interval alphas - r := new(regression.Regression) - r.SetObserved("Return Rate Per Timeframe") - r.SetVar(0, "Corr") - r.SetVar(1, "Rev") - r.SetVar(2, "A150") - r.SetVar(3, "Mom") - r.SetVar(4, "OGap") - var rdp regression.DataPoints - for i := 1; i <= int(s.T); i++ { - // alphas[t-1], previous alphas, dot not take current alpha into account, will cause look-ahead bias - as := []float64{s.Alpha[0][len(s.Alpha[0])-(i+2)], s.Alpha[1][len(s.Alpha[1])-(i+2)], s.Alpha[2][len(s.Alpha[2])-(i+2)], s.Alpha[3][len(s.Alpha[3])-(i+2)], s.Alpha[4][len(s.Alpha[4])-(i+2)]} - // alphas[t], current return rate - rt := s.Ret[len(s.Ret)-(i+1)] - rdp = append(rdp, regression.DataPoint(rt, as)) - - } - r.Train(rdp...) - r.Run() - fmt.Printf("Regression formula:\n%v\n", r.Formula) - //prediction := r.Coeff(0)*corr.Float64() + r.Coeff(1)*rev.Float64() + r.Coeff(2)*factorzoo.Float64() + r.Coeff(3)*mom.Float64() + r.Coeff(4) - prediction, _ := r.Predict([]float64{corr.Float64(), rev.Float64(), a150.Float64(), mom.Float64(), ogap.Float64()}) - log.Infof("Predicted Return: %f", prediction) - - s.placeOrders(ctx, orderExecutor, fixedpoint.NewFromFloat(prediction)) - s.tradeCollector.Process() - } - - s.prevClose = kline.Close + bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { + defer wg.Done() + _, _ = fmt.Fprintln(os.Stderr, s.TradeStats.String()) + _ = s.orderExecutor.GracefulCancel(ctx) }) return nil diff --git a/pkg/strategy/flashcrash/strategy.go b/pkg/strategy/flashcrash/strategy.go index 662c3385f5..aff86f3ca5 100644 --- a/pkg/strategy/flashcrash/strategy.go +++ b/pkg/strategy/flashcrash/strategy.go @@ -37,7 +37,7 @@ type Strategy struct { BaseQuantity fixedpoint.Value `json:"baseQuantity"` // activeOrders is the locally maintained active order book of the maker orders. - activeOrders *bbgo.LocalActiveOrderBook + activeOrders *bbgo.ActiveOrderBook // Injection fields start // -------------------------- @@ -49,10 +49,6 @@ type Strategy struct { // This field will be injected automatically since we defined the Symbol field. *bbgo.StandardIndicatorSet - // Graceful shutdown function - *bbgo.Graceful - // -------------------------- - // ewma is the exponential weighted moving average indicator ewma *indicator.EWMA } @@ -62,7 +58,7 @@ func (s *Strategy) ID() string { } func (s *Strategy) updateOrders(orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) { - if err := orderExecutor.CancelOrders(context.Background(), s.activeOrders.Bids.Orders()...); err != nil { + if err := s.activeOrders.GracefulCancel(context.Background(), session.Exchange); err != nil { log.WithError(err).Errorf("cancel order error") } @@ -106,15 +102,15 @@ func (s *Strategy) updateBidOrders(orderExecutor bbgo.OrderExecutor, session *bb } func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { - session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: string(s.Interval)}) + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) } func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { // we don't persist orders so that we can not clear the previous orders for now. just need time to support this. - s.activeOrders = bbgo.NewLocalActiveOrderBook(s.Symbol) + s.activeOrders = bbgo.NewActiveOrderBook(s.Symbol) s.activeOrders.BindStream(session.UserDataStream) - s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() log.Infof("canceling active orders...") diff --git a/pkg/strategy/fmaker/A18.go b/pkg/strategy/fmaker/A18.go new file mode 100644 index 0000000000..6e7e603a00 --- /dev/null +++ b/pkg/strategy/fmaker/A18.go @@ -0,0 +1,92 @@ +package fmaker + +import ( + "fmt" + "time" + + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +//go:generate callbackgen -type A18 +type A18 struct { + types.IntervalWindow + + // Values + Values floats.Slice + + EndTime time.Time + + UpdateCallbacks []func(val float64) +} + +func (inc *A18) Last() float64 { + if len(inc.Values) == 0 { + return 0.0 + } + return inc.Values[len(inc.Values)-1] +} + +func (inc *A18) CalculateAndUpdate(klines []types.KLine) { + if len(klines) < inc.Window { + return + } + + var end = len(klines) - 1 + var lastKLine = klines[end] + + if inc.EndTime != zeroTime && lastKLine.GetEndTime().Before(inc.EndTime) { + return + } + + var recentT = klines[end-(inc.Window-1) : end+1] + + val, err := calculateA18(recentT, indicator.KLineClosePriceMapper) + if err != nil { + log.WithError(err).Error("can not calculate") + return + } + inc.Values.Push(val) + + if len(inc.Values) > indicator.MaxNumOfVOL { + inc.Values = inc.Values[indicator.MaxNumOfVOLTruncateSize-1:] + } + + inc.EndTime = klines[end].GetEndTime().Time() + + inc.EmitUpdate(val) + +} + +func (inc *A18) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { + if inc.Interval != interval { + return + } + + inc.CalculateAndUpdate(window) +} + +func (inc *A18) Bind(updater indicator.KLineWindowUpdater) { + updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) +} + +// CLOSE/DELAY(CLOSE,5) +func calculateA18(klines []types.KLine, valClose KLineValueMapper) (float64, error) { + window := 5 + length := len(klines) + if length == 0 || length < window { + return 0., fmt.Errorf("insufficient elements for calculating with window = %d", window) + } + var closes floats.Slice + + for _, k := range klines { + closes.Push(valClose(k)) + } + + delay5 := closes.Index(4) + curr := closes.Index(0) + alpha := curr / delay5 + + return alpha, nil +} diff --git a/pkg/strategy/fmaker/A2.go b/pkg/strategy/fmaker/A2.go new file mode 100644 index 0000000000..0c9cc4b3bf --- /dev/null +++ b/pkg/strategy/fmaker/A2.go @@ -0,0 +1,104 @@ +package fmaker + +import ( + "fmt" + "time" + + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +//go:generate callbackgen -type A2 +type A2 struct { + types.IntervalWindow + + // Values + Values floats.Slice + + EndTime time.Time + + UpdateCallbacks []func(val float64) +} + +func (inc *A2) Last() float64 { + if len(inc.Values) == 0 { + return 0.0 + } + return inc.Values[len(inc.Values)-1] +} + +func (inc *A2) CalculateAndUpdate(klines []types.KLine) { + if len(klines) < inc.Window { + return + } + + var end = len(klines) - 1 + var lastKLine = klines[end] + + if inc.EndTime != zeroTime && lastKLine.GetEndTime().Before(inc.EndTime) { + return + } + + var recentT = klines[end-(inc.Window-1) : end+1] + + val, err := calculateA2(recentT, KLineLowPriceMapper, KLineHighPriceMapper, indicator.KLineClosePriceMapper) + if err != nil { + log.WithError(err).Error("can not calculate") + return + } + inc.Values.Push(val) + + if len(inc.Values) > indicator.MaxNumOfVOL { + inc.Values = inc.Values[indicator.MaxNumOfVOLTruncateSize-1:] + } + + inc.EndTime = klines[end].GetEndTime().Time() + + inc.EmitUpdate(val) + +} + +func (inc *A2) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { + if inc.Interval != interval { + return + } + + inc.CalculateAndUpdate(window) +} + +func (inc *A2) Bind(updater indicator.KLineWindowUpdater) { + updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) +} + +// (-1 * DELTA((((CLOSE - LOW) - (HIGH - CLOSE)) / (HIGH - LOW)), 1)) +func calculateA2(klines []types.KLine, valLow KLineValueMapper, valHigh KLineValueMapper, valClose KLineValueMapper) (float64, error) { + window := 2 + length := len(klines) + if length == 0 || length < window { + return 0., fmt.Errorf("insufficient elements for calculating with window = %d", window) + } + var lows floats.Slice + var highs floats.Slice + var closes floats.Slice + + for _, k := range klines { + lows.Push(valLow(k)) + highs.Push(valHigh(k)) + closes.Push(valClose(k)) + } + + prev := ((closes.Index(1) - lows.Index(1)) - (highs.Index(1) - closes.Index(1))) / (highs.Index(1) - lows.Index(1)) + curr := ((closes.Index(0) - lows.Index(0)) - (highs.Index(0) - closes.Index(0))) / (highs.Index(0) - lows.Index(0)) + alpha := (curr - prev) * -1 // delta(1 interval) + + return alpha, nil +} + +func KLineLowPriceMapper(k types.KLine) float64 { + return k.Low.Float64() +} + +func KLineHighPriceMapper(k types.KLine) float64 { + return k.High.Float64() +} diff --git a/pkg/strategy/fmaker/A3.go b/pkg/strategy/fmaker/A3.go new file mode 100644 index 0000000000..945b06b670 --- /dev/null +++ b/pkg/strategy/fmaker/A3.go @@ -0,0 +1,110 @@ +package fmaker + +import ( + "fmt" + "math" + "time" + + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +//go:generate callbackgen -type A3 +type A3 struct { + types.IntervalWindow + + // Values + Values floats.Slice + + EndTime time.Time + + UpdateCallbacks []func(val float64) +} + +func (inc *A3) Last() float64 { + if len(inc.Values) == 0 { + return 0.0 + } + return inc.Values[len(inc.Values)-1] +} + +func (inc *A3) CalculateAndUpdate(klines []types.KLine) { + if len(klines) < inc.Window { + return + } + + var end = len(klines) - 1 + var lastKLine = klines[end] + + if inc.EndTime != zeroTime && lastKLine.GetEndTime().Before(inc.EndTime) { + return + } + + var recentT = klines[end-(inc.Window-1) : end+1] + + val, err := calculateA3(recentT, KLineLowPriceMapper, KLineHighPriceMapper, indicator.KLineClosePriceMapper) + if err != nil { + log.WithError(err).Error("can not calculate pivots") + return + } + inc.Values.Push(val) + + if len(inc.Values) > indicator.MaxNumOfVOL { + inc.Values = inc.Values[indicator.MaxNumOfVOLTruncateSize-1:] + } + + inc.EndTime = klines[end].GetEndTime().Time() + + inc.EmitUpdate(val) + +} + +func (inc *A3) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { + if inc.Interval != interval { + return + } + + inc.CalculateAndUpdate(window) +} + +func (inc *A3) Bind(updater indicator.KLineWindowUpdater) { + updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) +} + +// SUM((CLOSE = DELAY(CLOSE, 1)?0:CLOSE-(CLOSE>DELAY(CLOSE, 1)?MIN(LOW, DELAY(CLOSE, 1)):MAX(HIGH, DELAY(CLOSE, 1)))), 6) +func calculateA3(klines []types.KLine, valLow KLineValueMapper, valHigh KLineValueMapper, valClose KLineValueMapper) (float64, error) { + window := 6 + 2 + length := len(klines) + if length == 0 || length < window { + return 0., fmt.Errorf("insufficient elements for calculating with window = %d", window) + } + var lows floats.Slice + var highs floats.Slice + var closes floats.Slice + + for _, k := range klines { + lows.Push(valLow(k)) + highs.Push(valHigh(k)) + closes.Push(valClose(k)) + } + + a := 0. + sumA := 0. + for i := 1; i <= 6; i++ { + if closes.Index(len(closes)-i) == closes.Index(len(closes)-i-1) { + a = 0. + } else { + if closes.Index(len(closes)-i) > closes.Index(1) { + a = closes.Index(len(closes)-i) - math.Min(lows.Index(len(lows)-i), closes.Index(len(closes)-i-1)) + } else { + a = closes.Index(len(closes)-i) - math.Max(highs.Index(len(highs)-i), closes.Index(len(closes)-i-1)) + } + } + sumA += a + } + + alpha := sumA // sum(a, 6 interval) + + return alpha, nil +} diff --git a/pkg/strategy/fmaker/A34.go b/pkg/strategy/fmaker/A34.go new file mode 100644 index 0000000000..eb01799e3e --- /dev/null +++ b/pkg/strategy/fmaker/A34.go @@ -0,0 +1,98 @@ +package fmaker + +import ( + "fmt" + "time" + + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +//go:generate callbackgen -type A34 +type A34 struct { + types.IntervalWindow + + // Values + Values floats.Slice + + EndTime time.Time + + UpdateCallbacks []func(val float64) +} + +func (inc *A34) Last() float64 { + if len(inc.Values) == 0 { + return 0.0 + } + return inc.Values[len(inc.Values)-1] +} + +func (inc *A34) CalculateAndUpdate(klines []types.KLine) { + if len(klines) < inc.Window { + return + } + + var end = len(klines) - 1 + var lastKLine = klines[end] + + if inc.EndTime != zeroTime && lastKLine.GetEndTime().Before(inc.EndTime) { + return + } + + var recentT = klines[end-(inc.Window-1) : end+1] + + val, err := calculateA34(recentT, indicator.KLineClosePriceMapper) + if err != nil { + log.WithError(err).Error("can not calculate pivots") + return + } + inc.Values.Push(val) + + if len(inc.Values) > indicator.MaxNumOfVOL { + inc.Values = inc.Values[indicator.MaxNumOfVOLTruncateSize-1:] + } + + inc.EndTime = klines[end].GetEndTime().Time() + + inc.EmitUpdate(val) + +} + +func (inc *A34) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { + if inc.Interval != interval { + return + } + + inc.CalculateAndUpdate(window) +} + +func (inc *A34) Bind(updater indicator.KLineWindowUpdater) { + updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) +} + +func calculateA34(klines []types.KLine, valClose KLineValueMapper) (float64, error) { + window := 12 + length := len(klines) + if length == 0 || length < window { + return 0., fmt.Errorf("insufficient elements for calculating with window = %d", window) + } + var closes floats.Slice + + for _, k := range klines { + closes.Push(valClose(k)) + } + + c := closes.Last() + + sumC := 0. + for i := 1; i <= 12; i++ { + sumC += closes.Index(len(closes) - i) + } + + meanC := sumC / 12 + + alpha := meanC / c + + return alpha, nil +} diff --git a/pkg/strategy/fmaker/R.go b/pkg/strategy/fmaker/R.go new file mode 100644 index 0000000000..d3aa0eca21 --- /dev/null +++ b/pkg/strategy/fmaker/R.go @@ -0,0 +1,95 @@ +package fmaker + +import ( + "fmt" + "time" + + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +var zeroTime time.Time + +type KLineValueMapper func(k types.KLine) float64 + +//go:generate callbackgen -type R +type R struct { + types.IntervalWindow + + // Values + Values floats.Slice + + EndTime time.Time + + UpdateCallbacks []func(val float64) +} + +func (inc *R) Last() float64 { + if len(inc.Values) == 0 { + return 0.0 + } + return inc.Values[len(inc.Values)-1] +} + +func (inc *R) CalculateAndUpdate(klines []types.KLine) { + if len(klines) < inc.Window { + return + } + + var end = len(klines) - 1 + var lastKLine = klines[end] + + if inc.EndTime != zeroTime && lastKLine.GetEndTime().Before(inc.EndTime) { + return + } + + var recentT = klines[end-(inc.Window-1) : end+1] + + val, err := calculateR(recentT, indicator.KLineOpenPriceMapper, indicator.KLineClosePriceMapper) + if err != nil { + log.WithError(err).Error("can not calculate pivots") + return + } + inc.Values.Push(val) + + if len(inc.Values) > indicator.MaxNumOfVOL { + inc.Values = inc.Values[indicator.MaxNumOfVOLTruncateSize-1:] + } + + inc.EndTime = klines[end].GetEndTime().Time() + + inc.EmitUpdate(val) + +} + +func (inc *R) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { + if inc.Interval != interval { + return + } + + inc.CalculateAndUpdate(window) +} + +func (inc *R) Bind(updater indicator.KLineWindowUpdater) { + updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) +} + +func calculateR(klines []types.KLine, valOpen KLineValueMapper, valClose KLineValueMapper) (float64, error) { + window := 1 + length := len(klines) + if length == 0 || length < window { + return 0., fmt.Errorf("insufficient elements for calculating with window = %d", window) + } + var opens floats.Slice + var closes floats.Slice + + for _, k := range klines { + opens.Push(valOpen(k)) + closes.Push(valClose(k)) + } + + ret := opens.Index(0)/closes.Index(0) - 1 // delta(1 interval) + + return ret, nil +} diff --git a/pkg/strategy/fmaker/S0.go b/pkg/strategy/fmaker/S0.go new file mode 100644 index 0000000000..9c934ccd2c --- /dev/null +++ b/pkg/strategy/fmaker/S0.go @@ -0,0 +1,90 @@ +package fmaker + +import ( + "fmt" + "time" + + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +//go:generate callbackgen -type S0 +type S0 struct { + types.IntervalWindow + + // Values + Values floats.Slice + + EndTime time.Time + + UpdateCallbacks []func(val float64) +} + +func (inc *S0) Last() float64 { + if len(inc.Values) == 0 { + return 0.0 + } + return inc.Values[len(inc.Values)-1] +} + +func (inc *S0) CalculateAndUpdate(klines []types.KLine) { + if len(klines) < inc.Window { + return + } + + var end = len(klines) - 1 + var lastKLine = klines[end] + + if inc.EndTime != zeroTime && lastKLine.GetEndTime().Before(inc.EndTime) { + return + } + + var recentT = klines[end-(inc.Window-1) : end+1] + + val, err := calculateS0(recentT, indicator.KLineClosePriceMapper) + if err != nil { + log.WithError(err).Error("can not calculate") + return + } + inc.Values.Push(val) + + if len(inc.Values) > indicator.MaxNumOfVOL { + inc.Values = inc.Values[indicator.MaxNumOfVOLTruncateSize-1:] + } + + inc.EndTime = klines[end].GetEndTime().Time() + + inc.EmitUpdate(val) + +} + +func (inc *S0) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { + if inc.Interval != interval { + return + } + + inc.CalculateAndUpdate(window) +} + +func (inc *S0) Bind(updater indicator.KLineWindowUpdater) { + updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) +} + +func calculateS0(klines []types.KLine, valClose KLineValueMapper) (float64, error) { + window := 20 + length := len(klines) + if length == 0 || length < window { + return 0., fmt.Errorf("insufficient elements for calculating with window = %d", window) + } + var closes floats.Slice + + for _, k := range klines { + closes.Push(valClose(k)) + } + + sma := floats.Slice.Sum(closes[len(closes)-window:len(closes)-1]) / float64(window) + alpha := sma / closes.Last() + + return alpha, nil +} diff --git a/pkg/strategy/factorzoo/correlation.go b/pkg/strategy/fmaker/S1.go similarity index 69% rename from pkg/strategy/factorzoo/correlation.go rename to pkg/strategy/fmaker/S1.go index 6e666d8fa6..498efec63e 100644 --- a/pkg/strategy/factorzoo/correlation.go +++ b/pkg/strategy/fmaker/S1.go @@ -1,35 +1,32 @@ -package factorzoo +package fmaker import ( "fmt" "math" "time" + "github.com/c9s/bbgo/pkg/datatype/floats" "github.com/c9s/bbgo/pkg/indicator" "github.com/c9s/bbgo/pkg/types" ) -var zeroTime time.Time - -type KLineValueMapper func(k types.KLine) float64 - -//go:generate callbackgen -type Correlation -type Correlation struct { +//go:generate callbackgen -type S1 +type S1 struct { types.IntervalWindow - Values types.Float64Slice + Values floats.Slice EndTime time.Time UpdateCallbacks []func(value float64) } -func (inc *Correlation) Last() float64 { +func (inc *S1) Last() float64 { if len(inc.Values) == 0 { return 0.0 } return inc.Values[len(inc.Values)-1] } -func (inc *Correlation) calculateAndUpdate(klines []types.KLine) { +func (inc *S1) CalculateAndUpdate(klines []types.KLine) { if len(klines) < inc.Window { return } @@ -43,7 +40,7 @@ func (inc *Correlation) calculateAndUpdate(klines []types.KLine) { var recentT = klines[end-(inc.Window-1) : end+1] - correlation, err := calculateCORRELATION(recentT, inc.Window, KLineAmplitudeMapper, indicator.KLineVolumeMapper) + correlation, err := calculateS1(recentT, inc.Window, KLineAmplitudeMapper, indicator.KLineVolumeMapper) if err != nil { log.WithError(err).Error("can not calculate correlation") return @@ -59,19 +56,19 @@ func (inc *Correlation) calculateAndUpdate(klines []types.KLine) { inc.EmitUpdate(correlation) } -func (inc *Correlation) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { +func (inc *S1) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { if inc.Interval != interval { return } - inc.calculateAndUpdate(window) + inc.CalculateAndUpdate(window) } -func (inc *Correlation) Bind(updater indicator.KLineWindowUpdater) { +func (inc *S1) Bind(updater indicator.KLineWindowUpdater) { updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) } -func calculateCORRELATION(klines []types.KLine, window int, valA KLineValueMapper, valB KLineValueMapper) (float64, error) { +func calculateS1(klines []types.KLine, window int, valA KLineValueMapper, valB KLineValueMapper) (float64, error) { length := len(klines) if length == 0 || length < window { return 0.0, fmt.Errorf("insufficient elements for calculating VOL with window = %d", window) @@ -95,7 +92,7 @@ func calculateCORRELATION(klines []types.KLine, window int, valA KLineValueMappe corr := (float64(window)*sumAB - sumA*sumB) / math.Sqrt((float64(window)*squareSumA-sumA*sumA)*(float64(window)*squareSumB-sumB*sumB)) - return corr, nil + return -corr, nil } func KLineAmplitudeMapper(k types.KLine) float64 { diff --git a/pkg/strategy/fmaker/S2.go b/pkg/strategy/fmaker/S2.go new file mode 100644 index 0000000000..960b3c5a83 --- /dev/null +++ b/pkg/strategy/fmaker/S2.go @@ -0,0 +1,96 @@ +package fmaker + +import ( + "fmt" + "math" + "time" + + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +//go:generate callbackgen -type S2 +type S2 struct { + types.IntervalWindow + Values floats.Slice + EndTime time.Time + + UpdateCallbacks []func(value float64) +} + +func (inc *S2) Last() float64 { + if len(inc.Values) == 0 { + return 0.0 + } + return inc.Values[len(inc.Values)-1] +} + +func (inc *S2) CalculateAndUpdate(klines []types.KLine) { + if len(klines) < inc.Window { + return + } + + var end = len(klines) - 1 + var lastKLine = klines[end] + + if inc.EndTime != zeroTime && lastKLine.GetEndTime().Before(inc.EndTime) { + return + } + + var recentT = klines[end-(inc.Window-1) : end+1] + + correlation, err := calculateS2(recentT, inc.Window, indicator.KLineOpenPriceMapper, indicator.KLineVolumeMapper) + if err != nil { + log.WithError(err).Error("can not calculate correlation") + return + } + inc.Values.Push(correlation) + + if len(inc.Values) > indicator.MaxNumOfVOL { + inc.Values = inc.Values[indicator.MaxNumOfVOLTruncateSize-1:] + } + + inc.EndTime = klines[end].GetEndTime().Time() + + inc.EmitUpdate(correlation) +} + +func (inc *S2) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { + if inc.Interval != interval { + return + } + + inc.CalculateAndUpdate(window) +} + +func (inc *S2) Bind(updater indicator.KLineWindowUpdater) { + updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) +} + +func calculateS2(klines []types.KLine, window int, valA KLineValueMapper, valB KLineValueMapper) (float64, error) { + length := len(klines) + if length == 0 || length < window { + return 0.0, fmt.Errorf("insufficient elements for calculating VOL with window = %d", window) + } + + sumA, sumB, sumAB, squareSumA, squareSumB := 0., 0., 0., 0., 0. + for _, k := range klines { + // sum of elements of array A + sumA += valA(k) + // sum of elements of array B + sumB += valB(k) + + // sum of A[i] * B[i]. + sumAB = sumAB + valA(k)*valB(k) + + // sum of square of array elements. + squareSumA = squareSumA + valA(k)*valA(k) + squareSumB = squareSumB + valB(k)*valB(k) + } + // use formula for calculating correlation coefficient. + corr := (float64(window)*sumAB - sumA*sumB) / + math.Sqrt((float64(window)*squareSumA-sumA*sumA)*(float64(window)*squareSumB-sumB*sumB)) + + return -corr, nil +} diff --git a/pkg/strategy/fmaker/S3.go b/pkg/strategy/fmaker/S3.go new file mode 100644 index 0000000000..238cc62ea3 --- /dev/null +++ b/pkg/strategy/fmaker/S3.go @@ -0,0 +1,93 @@ +package fmaker + +import ( + "fmt" + "time" + + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +//go:generate callbackgen -type S3 +type S3 struct { + types.IntervalWindow + + // Values + Values floats.Slice + + EndTime time.Time + + UpdateCallbacks []func(val float64) +} + +func (inc *S3) Last() float64 { + if len(inc.Values) == 0 { + return 0.0 + } + return inc.Values[len(inc.Values)-1] +} + +func (inc *S3) CalculateAndUpdate(klines []types.KLine) { + if len(klines) < inc.Window { + return + } + + var end = len(klines) - 1 + var lastKLine = klines[end] + + if inc.EndTime != zeroTime && lastKLine.GetEndTime().Before(inc.EndTime) { + return + } + + var recentT = klines[end-(inc.Window-1) : end+1] + + val, err := calculateS3(recentT, indicator.KLineClosePriceMapper, indicator.KLineOpenPriceMapper) + if err != nil { + log.WithError(err).Error("can not calculate") + return + } + inc.Values.Push(val) + + if len(inc.Values) > indicator.MaxNumOfVOL { + inc.Values = inc.Values[indicator.MaxNumOfVOLTruncateSize-1:] + } + + inc.EndTime = klines[end].GetEndTime().Time() + + inc.EmitUpdate(val) + +} + +func (inc *S3) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { + if inc.Interval != interval { + return + } + + inc.CalculateAndUpdate(window) +} + +func (inc *S3) Bind(updater indicator.KLineWindowUpdater) { + updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) +} + +func calculateS3(klines []types.KLine, valClose KLineValueMapper, valOpen KLineValueMapper) (float64, error) { + window := 2 + length := len(klines) + if length == 0 || length < window { + return 0., fmt.Errorf("insufficient elements for calculating with window = %d", window) + } + var closes floats.Slice + var opens floats.Slice + + for _, k := range klines { + closes.Push(valClose(k)) + opens.Push(valOpen(k)) + } + + prevC := closes.Index(1) + currO := opens.Index(0) + alpha := currO / prevC + + return alpha, nil +} diff --git a/pkg/strategy/fmaker/S4.go b/pkg/strategy/fmaker/S4.go new file mode 100644 index 0000000000..9d122c4734 --- /dev/null +++ b/pkg/strategy/fmaker/S4.go @@ -0,0 +1,90 @@ +package fmaker + +import ( + "fmt" + "time" + + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +//go:generate callbackgen -type S4 +type S4 struct { + types.IntervalWindow + + // Values + Values floats.Slice + + EndTime time.Time + + UpdateCallbacks []func(val float64) +} + +func (inc *S4) Last() float64 { + if len(inc.Values) == 0 { + return 0.0 + } + return inc.Values[len(inc.Values)-1] +} + +func (inc *S4) CalculateAndUpdate(klines []types.KLine) { + if len(klines) < inc.Window { + return + } + + var end = len(klines) - 1 + var lastKLine = klines[end] + + if inc.EndTime != zeroTime && lastKLine.GetEndTime().Before(inc.EndTime) { + return + } + + var recentT = klines[end-(inc.Window-1) : end+1] + + val, err := calculateS4(recentT, indicator.KLineClosePriceMapper) + if err != nil { + log.WithError(err).Error("can not calculate") + return + } + inc.Values.Push(val) + + if len(inc.Values) > indicator.MaxNumOfVOL { + inc.Values = inc.Values[indicator.MaxNumOfVOLTruncateSize-1:] + } + + inc.EndTime = klines[end].GetEndTime().Time() + + inc.EmitUpdate(val) + +} + +func (inc *S4) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { + if inc.Interval != interval { + return + } + + inc.CalculateAndUpdate(window) +} + +func (inc *S4) Bind(updater indicator.KLineWindowUpdater) { + updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) +} + +func calculateS4(klines []types.KLine, valClose KLineValueMapper) (float64, error) { + window := 2 + length := len(klines) + if length == 0 || length < window { + return 0., fmt.Errorf("insufficient elements for calculating with window = %d", window) + } + var closes floats.Slice + + for _, k := range klines { + closes.Push(valClose(k)) + } + + currC := closes.Index(0) + alpha := 1 / currC + + return alpha, nil +} diff --git a/pkg/strategy/fmaker/S5.go b/pkg/strategy/fmaker/S5.go new file mode 100644 index 0000000000..046733b4f8 --- /dev/null +++ b/pkg/strategy/fmaker/S5.go @@ -0,0 +1,98 @@ +package fmaker + +import ( + "fmt" + "time" + + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +//go:generate callbackgen -type S5 +type S5 struct { + types.IntervalWindow + + // Values + Values floats.Slice + + EndTime time.Time + + UpdateCallbacks []func(val float64) +} + +func (inc *S5) Last() float64 { + if len(inc.Values) == 0 { + return 0.0 + } + return inc.Values[len(inc.Values)-1] +} + +func (inc *S5) CalculateAndUpdate(klines []types.KLine) { + if len(klines) < inc.Window { + return + } + + var end = len(klines) - 1 + var lastKLine = klines[end] + + if inc.EndTime != zeroTime && lastKLine.GetEndTime().Before(inc.EndTime) { + return + } + + var recentT = klines[end-(inc.Window-1) : end+1] + + val, err := calculateS5(recentT, indicator.KLineVolumeMapper) + if err != nil { + log.WithError(err).Error("can not calculate pivots") + return + } + inc.Values.Push(val) + + if len(inc.Values) > indicator.MaxNumOfVOL { + inc.Values = inc.Values[indicator.MaxNumOfVOLTruncateSize-1:] + } + + inc.EndTime = klines[end].GetEndTime().Time() + + inc.EmitUpdate(val) + +} + +func (inc *S5) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { + if inc.Interval != interval { + return + } + + inc.CalculateAndUpdate(window) +} + +func (inc *S5) Bind(updater indicator.KLineWindowUpdater) { + updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) +} + +func calculateS5(klines []types.KLine, valVolume KLineValueMapper) (float64, error) { + window := 10 + length := len(klines) + if length == 0 || length < window { + return 0., fmt.Errorf("insufficient elements for calculating with window = %d", window) + } + var volumes floats.Slice + + for _, k := range klines { + volumes.Push(valVolume(k)) + } + + v := volumes.Last() + + sumV := 0. + for i := 1; i <= 10; i++ { + sumV += volumes.Index(len(volumes) - i) + } + + meanV := sumV / 10 + + alpha := -v / meanV + + return alpha, nil +} diff --git a/pkg/strategy/fmaker/S6.go b/pkg/strategy/fmaker/S6.go new file mode 100644 index 0000000000..4bb20b158d --- /dev/null +++ b/pkg/strategy/fmaker/S6.go @@ -0,0 +1,100 @@ +package fmaker + +import ( + "fmt" + "time" + + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +//go:generate callbackgen -type S6 +type S6 struct { + types.IntervalWindow + + // Values + Values floats.Slice + + EndTime time.Time + + UpdateCallbacks []func(val float64) +} + +func (inc *S6) Last() float64 { + if len(inc.Values) == 0 { + return 0.0 + } + return inc.Values[len(inc.Values)-1] +} + +func (inc *S6) CalculateAndUpdate(klines []types.KLine) { + if len(klines) < inc.Window { + return + } + + var end = len(klines) - 1 + var lastKLine = klines[end] + + if inc.EndTime != zeroTime && lastKLine.GetEndTime().Before(inc.EndTime) { + return + } + + var recentT = klines[end-(inc.Window-1) : end+1] + + val, err := calculateS6(recentT, indicator.KLineHighPriceMapper, indicator.KLineLowPriceMapper, indicator.KLineClosePriceMapper, indicator.KLineVolumeMapper) + if err != nil { + log.WithError(err).Error("can not calculate") + return + } + inc.Values.Push(val) + + if len(inc.Values) > indicator.MaxNumOfVOL { + inc.Values = inc.Values[indicator.MaxNumOfVOLTruncateSize-1:] + } + + inc.EndTime = klines[end].GetEndTime().Time() + + inc.EmitUpdate(val) + +} + +func (inc *S6) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { + if inc.Interval != interval { + return + } + + inc.CalculateAndUpdate(window) +} + +func (inc *S6) Bind(updater indicator.KLineWindowUpdater) { + updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) +} + +func calculateS6(klines []types.KLine, valHigh KLineValueMapper, valLow KLineValueMapper, valClose KLineValueMapper, valVolume KLineValueMapper) (float64, error) { + window := 2 + length := len(klines) + if length == 0 || length < window { + return 0., fmt.Errorf("insufficient elements for calculating with window = %d", window) + } + var highs floats.Slice + var lows floats.Slice + var closes floats.Slice + var volumes floats.Slice + + for _, k := range klines { + highs.Push(valHigh(k)) + lows.Push(valLow(k)) + closes.Push(valClose(k)) + volumes.Push(valVolume(k)) + + } + + H := highs.Last() + L := lows.Last() + C := closes.Last() + V := volumes.Last() + alpha := (H + L + C) / 3 * V + + return alpha, nil +} diff --git a/pkg/strategy/fmaker/S7.go b/pkg/strategy/fmaker/S7.go new file mode 100644 index 0000000000..7000e6897f --- /dev/null +++ b/pkg/strategy/fmaker/S7.go @@ -0,0 +1,94 @@ +package fmaker + +import ( + "fmt" + "time" + + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +//go:generate callbackgen -type S7 +type S7 struct { + types.IntervalWindow + + // Values + Values floats.Slice + + EndTime time.Time + + UpdateCallbacks []func(val float64) +} + +func (inc *S7) Last() float64 { + if len(inc.Values) == 0 { + return 0.0 + } + return inc.Values[len(inc.Values)-1] +} + +func (inc *S7) CalculateAndUpdate(klines []types.KLine) { + if len(klines) < inc.Window { + return + } + + var end = len(klines) - 1 + var lastKLine = klines[end] + + if inc.EndTime != zeroTime && lastKLine.GetEndTime().Before(inc.EndTime) { + return + } + + var recentT = klines[end-(inc.Window-1) : end+1] + + val, err := calculateS7(recentT, indicator.KLineOpenPriceMapper, indicator.KLineClosePriceMapper) + if err != nil { + log.WithError(err).Error("can not calculate") + return + } + inc.Values.Push(val) + + if len(inc.Values) > indicator.MaxNumOfVOL { + inc.Values = inc.Values[indicator.MaxNumOfVOLTruncateSize-1:] + } + + inc.EndTime = klines[end].GetEndTime().Time() + + inc.EmitUpdate(val) + +} + +func (inc *S7) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { + if inc.Interval != interval { + return + } + + inc.CalculateAndUpdate(window) +} + +func (inc *S7) Bind(updater indicator.KLineWindowUpdater) { + updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) +} + +func calculateS7(klines []types.KLine, valOpen KLineValueMapper, valClose KLineValueMapper) (float64, error) { + window := 2 + length := len(klines) + if length == 0 || length < window { + return 0., fmt.Errorf("insufficient elements for calculating with window = %d", window) + } + var opens floats.Slice + var closes floats.Slice + + for _, k := range klines { + opens.Push(valOpen(k)) + closes.Push(valClose(k)) + + } + + O := opens.Last() + C := closes.Last() + alpha := -(1 - O/C) + + return alpha, nil +} diff --git a/pkg/strategy/fmaker/a18_callbacks.go b/pkg/strategy/fmaker/a18_callbacks.go new file mode 100644 index 0000000000..c6bd0c45e2 --- /dev/null +++ b/pkg/strategy/fmaker/a18_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type A18"; DO NOT EDIT. + +package fmaker + +import () + +func (inc *A18) OnUpdate(cb func(val float64)) { + inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) +} + +func (inc *A18) EmitUpdate(val float64) { + for _, cb := range inc.UpdateCallbacks { + cb(val) + } +} diff --git a/pkg/strategy/fmaker/a2_callbacks.go b/pkg/strategy/fmaker/a2_callbacks.go new file mode 100644 index 0000000000..d1fdf00f34 --- /dev/null +++ b/pkg/strategy/fmaker/a2_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type A2"; DO NOT EDIT. + +package fmaker + +import () + +func (inc *A2) OnUpdate(cb func(val float64)) { + inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) +} + +func (inc *A2) EmitUpdate(val float64) { + for _, cb := range inc.UpdateCallbacks { + cb(val) + } +} diff --git a/pkg/strategy/fmaker/a34_callbacks.go b/pkg/strategy/fmaker/a34_callbacks.go new file mode 100644 index 0000000000..fb128efadb --- /dev/null +++ b/pkg/strategy/fmaker/a34_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type A34"; DO NOT EDIT. + +package fmaker + +import () + +func (inc *A34) OnUpdate(cb func(val float64)) { + inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) +} + +func (inc *A34) EmitUpdate(val float64) { + for _, cb := range inc.UpdateCallbacks { + cb(val) + } +} diff --git a/pkg/strategy/fmaker/a3_callbacks.go b/pkg/strategy/fmaker/a3_callbacks.go new file mode 100644 index 0000000000..ad83cd8be8 --- /dev/null +++ b/pkg/strategy/fmaker/a3_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type A3"; DO NOT EDIT. + +package fmaker + +import () + +func (inc *A3) OnUpdate(cb func(val float64)) { + inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) +} + +func (inc *A3) EmitUpdate(val float64) { + for _, cb := range inc.UpdateCallbacks { + cb(val) + } +} diff --git a/pkg/strategy/fmaker/r_callbacks.go b/pkg/strategy/fmaker/r_callbacks.go new file mode 100644 index 0000000000..afc55e417e --- /dev/null +++ b/pkg/strategy/fmaker/r_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type R"; DO NOT EDIT. + +package fmaker + +import () + +func (inc *R) OnUpdate(cb func(val float64)) { + inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) +} + +func (inc *R) EmitUpdate(val float64) { + for _, cb := range inc.UpdateCallbacks { + cb(val) + } +} diff --git a/pkg/strategy/fmaker/s0_callbacks.go b/pkg/strategy/fmaker/s0_callbacks.go new file mode 100644 index 0000000000..1d384c83b0 --- /dev/null +++ b/pkg/strategy/fmaker/s0_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type S0"; DO NOT EDIT. + +package fmaker + +import () + +func (inc *S0) OnUpdate(cb func(val float64)) { + inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) +} + +func (inc *S0) EmitUpdate(val float64) { + for _, cb := range inc.UpdateCallbacks { + cb(val) + } +} diff --git a/pkg/strategy/fmaker/s1_callbacks.go b/pkg/strategy/fmaker/s1_callbacks.go new file mode 100644 index 0000000000..5d7eb0119b --- /dev/null +++ b/pkg/strategy/fmaker/s1_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type S1"; DO NOT EDIT. + +package fmaker + +import () + +func (inc *S1) OnUpdate(cb func(value float64)) { + inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) +} + +func (inc *S1) EmitUpdate(value float64) { + for _, cb := range inc.UpdateCallbacks { + cb(value) + } +} diff --git a/pkg/strategy/fmaker/s2_callbacks.go b/pkg/strategy/fmaker/s2_callbacks.go new file mode 100644 index 0000000000..c65a7af719 --- /dev/null +++ b/pkg/strategy/fmaker/s2_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type S2"; DO NOT EDIT. + +package fmaker + +import () + +func (inc *S2) OnUpdate(cb func(value float64)) { + inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) +} + +func (inc *S2) EmitUpdate(value float64) { + for _, cb := range inc.UpdateCallbacks { + cb(value) + } +} diff --git a/pkg/strategy/fmaker/s3_callbacks.go b/pkg/strategy/fmaker/s3_callbacks.go new file mode 100644 index 0000000000..01a6ea01e1 --- /dev/null +++ b/pkg/strategy/fmaker/s3_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type S3"; DO NOT EDIT. + +package fmaker + +import () + +func (inc *S3) OnUpdate(cb func(val float64)) { + inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) +} + +func (inc *S3) EmitUpdate(val float64) { + for _, cb := range inc.UpdateCallbacks { + cb(val) + } +} diff --git a/pkg/strategy/fmaker/s4_callbacks.go b/pkg/strategy/fmaker/s4_callbacks.go new file mode 100644 index 0000000000..0d00584403 --- /dev/null +++ b/pkg/strategy/fmaker/s4_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type S4"; DO NOT EDIT. + +package fmaker + +import () + +func (inc *S4) OnUpdate(cb func(val float64)) { + inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) +} + +func (inc *S4) EmitUpdate(val float64) { + for _, cb := range inc.UpdateCallbacks { + cb(val) + } +} diff --git a/pkg/strategy/fmaker/s5_callbacks.go b/pkg/strategy/fmaker/s5_callbacks.go new file mode 100644 index 0000000000..65f7f9a8f4 --- /dev/null +++ b/pkg/strategy/fmaker/s5_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type S5"; DO NOT EDIT. + +package fmaker + +import () + +func (inc *S5) OnUpdate(cb func(val float64)) { + inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) +} + +func (inc *S5) EmitUpdate(val float64) { + for _, cb := range inc.UpdateCallbacks { + cb(val) + } +} diff --git a/pkg/strategy/fmaker/s6_callbacks.go b/pkg/strategy/fmaker/s6_callbacks.go new file mode 100644 index 0000000000..33daec76e5 --- /dev/null +++ b/pkg/strategy/fmaker/s6_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type S6"; DO NOT EDIT. + +package fmaker + +import () + +func (inc *S6) OnUpdate(cb func(val float64)) { + inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) +} + +func (inc *S6) EmitUpdate(val float64) { + for _, cb := range inc.UpdateCallbacks { + cb(val) + } +} diff --git a/pkg/strategy/fmaker/s7_callbacks.go b/pkg/strategy/fmaker/s7_callbacks.go new file mode 100644 index 0000000000..fec9457d74 --- /dev/null +++ b/pkg/strategy/fmaker/s7_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type S7"; DO NOT EDIT. + +package fmaker + +import () + +func (inc *S7) OnUpdate(cb func(val float64)) { + inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb) +} + +func (inc *S7) EmitUpdate(val float64) { + for _, cb := range inc.UpdateCallbacks { + cb(val) + } +} diff --git a/pkg/strategy/fmaker/strategy.go b/pkg/strategy/fmaker/strategy.go new file mode 100644 index 0000000000..0df9b68e20 --- /dev/null +++ b/pkg/strategy/fmaker/strategy.go @@ -0,0 +1,533 @@ +package fmaker + +import ( + "context" + "fmt" + "math" + + "github.com/sajari/regression" + "github.com/sirupsen/logrus" + "gonum.org/v1/gonum/floats" + + "github.com/c9s/bbgo/pkg/bbgo" + floats2 "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +const ID = "fmaker" + +var fifteen = fixedpoint.NewFromInt(15) +var three = fixedpoint.NewFromInt(3) +var two = fixedpoint.NewFromInt(2) + +var log = logrus.WithField("strategy", ID) + +func init() { + bbgo.RegisterStrategy(ID, &Strategy{}) +} + +type IntervalWindowSetting struct { + types.IntervalWindow +} + +type Strategy struct { + Environment *bbgo.Environment + Symbol string `json:"symbol"` + Market types.Market + Interval types.Interval `json:"interval"` + Quantity fixedpoint.Value `json:"quantity"` + + // persistence fields + Position *types.Position `json:"position,omitempty" persistence:"position"` + ProfitStats *types.ProfitStats `json:"profitStats,omitempty" persistence:"profit_stats"` + + Spread fixedpoint.Value `json:"spread" persistence:"spread"` + + activeMakerOrders *bbgo.ActiveOrderBook + // closePositionOrders *bbgo.LocalActiveOrderBook + + orderStore *bbgo.OrderStore + tradeCollector *bbgo.TradeCollector + + session *bbgo.ExchangeSession + + bbgo.QuantityOrAmount + + S0 *S0 + S1 *S1 + S2 *S2 + S3 *S3 + S4 *S4 + S5 *S5 + S6 *S6 + S7 *S7 + + A2 *A2 + A3 *A3 + A18 *A18 + A34 *A34 + + R *R + + // StrategyController + bbgo.StrategyController +} + +func (s *Strategy) ID() string { + return ID +} + +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { + log.Infof("subscribe %s", s.Symbol) + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: types.Interval15m}) + +} + +func (s *Strategy) placeOrder(ctx context.Context, price fixedpoint.Value, qty fixedpoint.Value, orderExecutor bbgo.OrderExecutor) { + submitOrder := types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeSell, + Type: types.OrderTypeLimit, + Price: price, + Quantity: qty, + } + createdOrders, err := orderExecutor.SubmitOrders(ctx, submitOrder) + if err != nil { + log.WithError(err).Errorf("can not place orders") + } + s.orderStore.Add(createdOrders...) + s.activeMakerOrders.Add(createdOrders...) + // s.tradeCollector.Process() +} + +func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Value) error { + base := s.Position.GetBase() + if base.IsZero() { + return fmt.Errorf("no opened %s position", s.Position.Symbol) + } + + // make it negative + quantity := base.Mul(percentage).Abs() + side := types.SideTypeBuy + if base.Sign() > 0 { + side = types.SideTypeSell + } + + if quantity.Compare(s.Market.MinQuantity) < 0 { + return fmt.Errorf("order quantity %v is too small, less than %v", quantity, s.Market.MinQuantity) + } + + submitOrder := types.SubmitOrder{ + Symbol: s.Symbol, + Side: side, + Type: types.OrderTypeMarket, + Quantity: quantity, + // Price: closePrice, + Market: s.Market, + } + + // s.Notify("Submitting %s %s order to close position by %v", s.Symbol, side.String(), percentage, submitOrder) + + createdOrder, err := s.session.Exchange.SubmitOrder(ctx, submitOrder) + if err != nil { + log.WithError(err).Errorf("can not place position close order") + } else if createdOrder != nil { + s.orderStore.Add(*createdOrder) + s.activeMakerOrders.Add(*createdOrder) + } + + return err +} +func (s *Strategy) InstanceID() string { + return fmt.Sprintf("%s:%s", ID, s.Symbol) +} + +func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + // initial required information + s.session = session + // s.prevClose = fixedpoint.Zero + + // first we need to get market data store(cached market data) from the exchange session + // st, _ := session.MarketDataStore(s.Symbol) + + s.activeMakerOrders = bbgo.NewActiveOrderBook(s.Symbol) + s.activeMakerOrders.BindStream(session.UserDataStream) + + // s.closePositionOrders = bbgo.NewLocalActiveOrderBook(s.Symbol) + // s.closePositionOrders.BindStream(session.UserDataStream) + + s.orderStore = bbgo.NewOrderStore(s.Symbol) + s.orderStore.BindStream(session.UserDataStream) + + if s.Position == nil { + s.Position = types.NewPositionFromMarket(s.Market) + } + + // calculate group id for orders + instanceID := s.InstanceID() + // s.groupID = util.FNV32(instanceID) + + // Always update the position fields + s.Position.Strategy = ID + s.Position.StrategyInstanceID = instanceID + + s.tradeCollector = bbgo.NewTradeCollector(s.Symbol, s.Position, s.orderStore) + s.tradeCollector.OnTrade(func(trade types.Trade, profit, netProfit fixedpoint.Value) { + // StrategyController + if s.Status != types.StrategyStatusRunning { + return + } + + bbgo.Notify(trade) + s.ProfitStats.AddTrade(trade) + + if profit.Compare(fixedpoint.Zero) == 0 { + s.Environment.RecordPosition(s.Position, trade, nil) + } else { + log.Infof("%s generated profit: %v", s.Symbol, profit) + p := s.Position.NewProfit(trade, profit, netProfit) + p.Strategy = ID + p.StrategyInstanceID = instanceID + bbgo.Notify(&p) + + s.ProfitStats.AddProfit(p) + bbgo.Notify(&s.ProfitStats) + + s.Environment.RecordPosition(s.Position, trade, &p) + } + }) + + s.tradeCollector.OnPositionUpdate(func(position *types.Position) { + log.Infof("position changed: %s", s.Position) + bbgo.Notify(s.Position) + }) + s.tradeCollector.BindStream(session.UserDataStream) + st, _ := session.MarketDataStore(s.Symbol) + + riw := types.IntervalWindow{Window: 1, Interval: s.Interval} + s.R = &R{IntervalWindow: riw} + s.R.Bind(st) + + s0iw := types.IntervalWindow{Window: 20, Interval: s.Interval} + s.S0 = &S0{IntervalWindow: s0iw} + s.S0.Bind(st) + + s1iw := types.IntervalWindow{Window: 20, Interval: s.Interval} + s.S1 = &S1{IntervalWindow: s1iw} + s.S1.Bind(st) + + s2iw := types.IntervalWindow{Window: 20, Interval: s.Interval} + s.S2 = &S2{IntervalWindow: s2iw} + s.S2.Bind(st) + + s3iw := types.IntervalWindow{Window: 2, Interval: s.Interval} + s.S3 = &S3{IntervalWindow: s3iw} + s.S3.Bind(st) + + s4iw := types.IntervalWindow{Window: 2, Interval: s.Interval} + s.S4 = &S4{IntervalWindow: s4iw} + s.S4.Bind(st) + + s5iw := types.IntervalWindow{Window: 10, Interval: s.Interval} + s.S5 = &S5{IntervalWindow: s5iw} + s.S5.Bind(st) + + s6iw := types.IntervalWindow{Window: 2, Interval: s.Interval} + s.S6 = &S6{IntervalWindow: s6iw} + s.S6.Bind(st) + + s7iw := types.IntervalWindow{Window: 2, Interval: s.Interval} + s.S7 = &S7{IntervalWindow: s7iw} + s.S7.Bind(st) + + a2iw := types.IntervalWindow{Window: 2, Interval: s.Interval} + s.A2 = &A2{IntervalWindow: a2iw} + s.A2.Bind(st) + + a3iw := types.IntervalWindow{Window: 8, Interval: s.Interval} + s.A3 = &A3{IntervalWindow: a3iw} + s.A3.Bind(st) + + a18iw := types.IntervalWindow{Window: 5, Interval: s.Interval} + s.A18 = &A18{IntervalWindow: a18iw} + s.A18.Bind(st) + + a34iw := types.IntervalWindow{Window: 12, Interval: s.Interval} + s.A34 = &A34{IntervalWindow: a34iw} + s.A34.Bind(st) + + session.UserDataStream.OnStart(func() { + log.Infof("connected") + }) + + outlook := 1 + + // futuresMode := s.session.Futures || s.session.IsolatedFutures + cnt := 0 + + // var prevEr float64 + session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { + + // if kline.Interval == types.Interval15m && kline.Symbol == s.Symbol && !s.Market.IsDustQuantity(s.Position.GetBase(), kline.Close) { + // if err := s.activeMakerOrders.GracefulCancel(ctx, s.session.Exchange); err != nil { + // log.WithError(err).Errorf("graceful cancel order error") + // } + // s.ClosePosition(ctx, fixedpoint.One) + // s.tradeCollector.Process() + // } + if kline.Symbol != s.Symbol || kline.Interval != s.Interval { + return + } + + if err := s.activeMakerOrders.GracefulCancel(ctx, s.session.Exchange); err != nil { + log.WithError(err).Errorf("graceful cancel order error") + } + + cnt += 1 + if cnt < 15+1+outlook { + return + } + + r := new(regression.Regression) + r.SetObserved("Return Rate Per Interval") + r.SetVar(0, "S0") + r.SetVar(1, "S1") + r.SetVar(2, "S2") + // r.SetVar(2, "S3") + r.SetVar(3, "S4") + r.SetVar(4, "S5") + r.SetVar(5, "S6") + r.SetVar(6, "S7") + r.SetVar(7, "A2") + r.SetVar(8, "A3") + r.SetVar(9, "A18") + r.SetVar(10, "A34") + + var rdps regression.DataPoints + + for i := 1; i <= 15; i++ { + s0 := s.S0.Values[len(s.S0.Values)-i-outlook] + s1 := s.S1.Values[len(s.S1.Values)-i-outlook] + s2 := s.S2.Values[len(s.S2.Values)-i-outlook] + // s3 := s.S3.Values[len(s.S3.Values)-i-1] + s4 := s.S4.Values[len(s.S4.Values)-i-outlook] + s5 := s.S5.Values[len(s.S5.Values)-i-outlook] + s6 := s.S6.Values[len(s.S6.Values)-i-outlook] + s7 := s.S7.Values[len(s.S7.Values)-i-outlook] + a2 := s.A2.Values[len(s.A2.Values)-i-outlook] + a3 := s.A3.Values[len(s.A3.Values)-i-outlook] + a18 := s.A18.Values[len(s.A18.Values)-i-outlook] + a34 := s.A34.Values[len(s.A34.Values)-i-outlook] + + ret := s.R.Values[len(s.R.Values)-i] + rdps = append(rdps, regression.DataPoint(ret, floats2.Slice{s0, s1, s2, s4, s5, s6, s7, a2, a3, a18, a34})) + } + // for i := 40; i > 20; i-- { + // s0 := preprocessing(s.S0.Values[len(s.S0.Values)-i : len(s.S0.Values)-i+20-outlook]) + // s1 := preprocessing(s.S1.Values[len(s.S1.Values)-i : len(s.S1.Values)-i+20-outlook]) + // s2 := preprocessing(s.S2.Values[len(s.S2.Values)-i : len(s.S2.Values)-i+20-outlook]) + // //s3 := s.S3.Values[len(s.S3.Values)-i-1] + // s4 := preprocessing(s.S4.Values[len(s.S4.Values)-i : len(s.S4.Values)-i+20-outlook]) + // s5 := preprocessing(s.S5.Values[len(s.S5.Values)-i : len(s.S5.Values)-i+20-outlook]) + // a2 := preprocessing(s.A2.Values[len(s.A2.Values)-i : len(s.A2.Values)-i+20-outlook]) + // a3 := preprocessing(s.A3.Values[len(s.A3.Values)-i : len(s.A3.Values)-i+20-outlook]) + // a18 := preprocessing(s.A18.Values[len(s.A18.Values)-i : len(s.A18.Values)-i+20-outlook]) + // a34 := preprocessing(s.A18.Values[len(s.A18.Values)-i : len(s.A18.Values)-i+20-outlook]) + // + // ret := s.R.Values[len(s.R.Values)-i] + // rdps = append(rdps, regression.DataPoint(ret, types.Float64Slice{s0, s1, s2, s4, s5, a2, a3, a18, a34})) + // } + r.Train(rdps...) + r.Run() + er, _ := r.Predict(floats2.Slice{s.S0.Last(), s.S1.Last(), s.S2.Last(), s.S4.Last(), s.S5.Last(), s.S6.Last(), s.S7.Last(), s.A2.Last(), s.A3.Last(), s.A18.Last(), s.A34.Last()}) + log.Infof("Expected Return Rate: %f", er) + + q := new(regression.Regression) + q.SetObserved("Order Quantity Per Interval") + q.SetVar(0, "S0") + q.SetVar(1, "S1") + q.SetVar(2, "S2") + // q.SetVar(2, "S3") + q.SetVar(3, "S4") + q.SetVar(4, "S5") + q.SetVar(5, "S6") + q.SetVar(6, "S7") + q.SetVar(7, "A2") + q.SetVar(8, "A3") + q.SetVar(9, "A18") + q.SetVar(10, "A34") + + var qdps regression.DataPoints + + for i := 1; i <= 15; i++ { + s0 := math.Pow(s.S0.Values[len(s.S0.Values)-i-outlook], 1) + s1 := math.Pow(s.S1.Values[len(s.S1.Values)-i-outlook], 1) + s2 := math.Pow(s.S2.Values[len(s.S2.Values)-i-outlook], 1) + // s3 := s.S3.Values[len(s.S3.Values)-i-1] + s4 := math.Pow(s.S4.Values[len(s.S4.Values)-i-outlook], 1) + s5 := math.Pow(s.S5.Values[len(s.S5.Values)-i-outlook], 1) + s6 := s.S6.Values[len(s.S6.Values)-i-outlook] + s7 := s.S7.Values[len(s.S7.Values)-i-outlook] + a2 := math.Pow(s.A2.Values[len(s.A2.Values)-i-outlook], 1) + a3 := math.Pow(s.A3.Values[len(s.A3.Values)-i-outlook], 1) + a18 := math.Pow(s.A18.Values[len(s.A18.Values)-i-outlook], 1) + a34 := math.Pow(s.A34.Values[len(s.A34.Values)-i-outlook], 1) + + ret := s.R.Values[len(s.R.Values)-i] + qty := math.Abs(ret) + qdps = append(qdps, regression.DataPoint(qty, floats2.Slice{s0, s1, s2, s4, s5, s6, s7, a2, a3, a18, a34})) + } + // for i := 40; i > 20; i-- { + // s0 := preprocessing(s.S0.Values[len(s.S0.Values)-i : len(s.S0.Values)-i+20-outlook]) + // s1 := preprocessing(s.S1.Values[len(s.S1.Values)-i : len(s.S1.Values)-i+20-outlook]) + // s2 := preprocessing(s.S2.Values[len(s.S2.Values)-i : len(s.S2.Values)-i+20-outlook]) + // //s3 := s.S3.Values[len(s.S3.Values)-i-1] + // s4 := preprocessing(s.S4.Values[len(s.S4.Values)-i : len(s.S4.Values)-i+20-outlook]) + // s5 := preprocessing(s.S5.Values[len(s.S5.Values)-i : len(s.S5.Values)-i+20-outlook]) + // a2 := preprocessing(s.A2.Values[len(s.A2.Values)-i : len(s.A2.Values)-i+20-outlook]) + // a3 := preprocessing(s.A3.Values[len(s.A3.Values)-i : len(s.A3.Values)-i+20-outlook]) + // a18 := preprocessing(s.A18.Values[len(s.A18.Values)-i : len(s.A18.Values)-i+20-outlook]) + // a34 := preprocessing(s.A18.Values[len(s.A18.Values)-i : len(s.A18.Values)-i+20-outlook]) + // + // ret := s.R.Values[len(s.R.Values)-i] + // qty := math.Abs(ret) + // qdps = append(qdps, regression.DataPoint(qty, types.Float64Slice{s0, s1, s2, s4, s5, a2, a3, a18, a34})) + // } + q.Train(qdps...) + + q.Run() + + log.Info(s.S0.Last(), s.S1.Last(), s.S2.Last(), s.S3.Last(), s.S4.Last(), s.S5.Last(), s.S6.Last(), s.S7.Last(), s.A2.Last(), s.A3.Last(), s.A18.Last(), s.A34.Last()) + + log.Infof("Return Rate Regression formula:\n%v", r.Formula) + log.Infof("Order Quantity Regression formula:\n%v", q.Formula) + + // s0 := preprocessing(s.S0.Values[len(s.S0.Values)-20 : len(s.S0.Values)-1]) + // s1 := preprocessing(s.S1.Values[len(s.S1.Values)-20 : len(s.S1.Values)-1-outlook]) + // s2 := preprocessing(s.S2.Values[len(s.S2.Values)-20 : len(s.S2.Values)-1-outlook]) + // //s3 := s.S3.Values[len(s.S3.Values)-i-1] + // s4 := preprocessing(s.S4.Values[len(s.S4.Values)-20 : len(s.S4.Values)-1-outlook]) + // s5 := preprocessing(s.S5.Values[len(s.S5.Values)-20 : len(s.S5.Values)-1-outlook]) + // a2 := preprocessing(s.A2.Values[len(s.A2.Values)-20 : len(s.A2.Values)-1-outlook]) + // a3 := preprocessing(s.A3.Values[len(s.A3.Values)-20 : len(s.A3.Values)-1-outlook]) + // a18 := preprocessing(s.A18.Values[len(s.A18.Values)-20 : len(s.A18.Values)-1-outlook]) + // a34 := preprocessing(s.A18.Values[len(s.A18.Values)-20 : len(s.A18.Values)-1-outlook]) + // er, _ := r.Predict(types.Float64Slice{s0, s1, s2, s4, s5, a2, a3, a18, a34}) + // eq, _ := q.Predict(types.Float64Slice{s0, s1, s2, s4, s5, a2, a3, a18, a34}) + eq, _ := q.Predict(floats2.Slice{s.S0.Last(), s.S1.Last(), s.S2.Last(), s.S4.Last(), s.S5.Last(), s.S6.Last(), s.S7.Last(), s.A2.Last(), s.A3.Last(), s.A18.Last(), s.A34.Last(), er}) + log.Infof("Expected Order Quantity: %f", eq) + // if float64(s.Position.GetBase().Sign())*er < 0 { + // s.ClosePosition(ctx, fixedpoint.One, kline.Close) + // s.tradeCollector.Process() + // } + // prevEr = er + + // spd := s.Spread.Float64() + + // inventory = m * alpha + spread + AskAlphaBoundary := (s.Position.GetBase().Mul(kline.Close).Float64() - 100) / 10000 + BidAlphaBoundary := (s.Position.GetBase().Mul(kline.Close).Float64() + 100) / 10000 + + log.Info(s.Position.GetBase().Mul(kline.Close).Float64(), AskAlphaBoundary, er, BidAlphaBoundary) + + BidPrice := kline.Close.Mul(fixedpoint.One.Sub(s.Spread)) + BidQty := s.QuantityOrAmount.CalculateQuantity(BidPrice) + BidQty = BidQty // .Mul(fixedpoint.One.Add(fixedpoint.NewFromFloat(eq))) + + AskPrice := kline.Close.Mul(fixedpoint.One.Add(s.Spread)) + AskQty := s.QuantityOrAmount.CalculateQuantity(AskPrice) + AskQty = AskQty // .Mul(fixedpoint.One.Add(fixedpoint.NewFromFloat(eq))) + + if er > 0 || (er < 0 && er > AskAlphaBoundary/kline.Close.Float64()) { + submitOrder := types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeLimitMaker, + Price: BidPrice, + Quantity: BidQty, // 0.0005 + } + createdOrders, err := orderExecutor.SubmitOrders(ctx, submitOrder) + if err != nil { + log.WithError(err).Errorf("can not place orders") + } + s.orderStore.Add(createdOrders...) + s.activeMakerOrders.Add(createdOrders...) + s.tradeCollector.Process() + + // submitOrder = types.SubmitOrder{ + // Symbol: s.Symbol, + // Side: types.SideTypeSell, + // Type: types.OrderTypeLimitMaker, + // Price: kline.Close.Mul(fixedpoint.One.Add(s.Spread)), + // Quantity: fixedpoint.NewFromFloat(math.Max(math.Min(eq, 0.003), 0.0005)), //0.0005 + // } + // createdOrders, err = orderExecutor.SubmitOrder(ctx, submitOrder) + // if err != nil { + // log.WithError(err).Errorf("can not place orders") + // } + // s.orderStore.Add(createdOrders...) + // s.activeMakerOrders.Add(createdOrders...) + // s.tradeCollector.Process() + } + if er < 0 || (er > 0 && er < BidAlphaBoundary/kline.Close.Float64()) { + submitOrder := types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeSell, + Type: types.OrderTypeLimitMaker, + Price: AskPrice, + Quantity: AskQty, // 0.0005 + } + createdOrders, err := orderExecutor.SubmitOrders(ctx, submitOrder) + if err != nil { + log.WithError(err).Errorf("can not place orders") + } + s.orderStore.Add(createdOrders...) + s.activeMakerOrders.Add(createdOrders...) + s.tradeCollector.Process() + + // submitOrder = types.SubmitOrder{ + // Symbol: s.Symbol, + // Side: types.SideTypeBuy, + // Type: types.OrderTypeLimitMaker, + // Price: kline.Close.Mul(fixedpoint.One.Sub(s.Spread)), + // Quantity: fixedpoint.NewFromFloat(math.Max(math.Min(eq, 0.003), 0.0005)), //0.0005 + // } + // createdOrders, err = orderExecutor.SubmitOrder(ctx, submitOrder) + // if err != nil { + // log.WithError(err).Errorf("can not place orders") + // } + // s.orderStore.Add(createdOrders...) + // s.activeMakerOrders.Add(createdOrders...) + // s.tradeCollector.Process() + } + + }) + + return nil +} + +func tanh(x float64) float64 { + y := (math.Exp(x) - math.Exp(-x)) / (math.Exp(x) + math.Exp(-x)) + return y +} + +func mean(xs []float64) float64 { + return floats.Sum(xs) / float64(len(xs)) +} + +func stddev(xs []float64) float64 { + mu := mean(xs) + squaresum := 0. + for _, x := range xs { + squaresum += (x - mu) * (x - mu) + } + return math.Sqrt(squaresum / float64(len(xs)-1)) +} + +func preprocessing(xs []float64) float64 { + // return 0.5 * tanh(0.01*((xs[len(xs)-1]-mean(xs))/stddev(xs))) // tanh estimator + return tanh((xs[len(xs)-1] - mean(xs)) / stddev(xs)) // tanh z-score + return (xs[len(xs)-1] - mean(xs)) / stddev(xs) // z-score +} diff --git a/pkg/strategy/funding/strategy.go b/pkg/strategy/funding/strategy.go index 9376925773..58361e9634 100644 --- a/pkg/strategy/funding/strategy.go +++ b/pkg/strategy/funding/strategy.go @@ -3,7 +3,6 @@ package funding import ( "context" "errors" - "fmt" "strings" "github.com/sirupsen/logrus" @@ -27,13 +26,12 @@ func init() { } type Strategy struct { - *bbgo.Notifiability // These fields will be filled from the config file (it translates YAML to JSON) Symbol string `json:"symbol"` Market types.Market `json:"-"` Quantity fixedpoint.Value `json:"quantity,omitempty"` MaxExposurePosition fixedpoint.Value `json:"maxExposurePosition"` - //Interval types.Interval `json:"interval"` + // Interval types.Interval `json:"interval"` FundingRate *struct { High fixedpoint.Value `json:"high"` @@ -50,11 +48,11 @@ type Strategy struct { // MovingAverageInterval is the interval of k-lines for the moving average indicator to calculate, // it could be "1m", "5m", "1h" and so on. note that, the moving averages are calculated from // the k-line data we subscribed - //MovingAverageInterval types.Interval `json:"movingAverageInterval"` + // MovingAverageInterval types.Interval `json:"movingAverageInterval"` // - //// MovingAverageWindow is the number of the window size of the moving average indicator. - //// The number of k-lines in the window. generally used window sizes are 7, 25 and 99 in the TradingView. - //MovingAverageWindow int `json:"movingAverageWindow"` + // // MovingAverageWindow is the number of the window size of the moving average indicator. + // // The number of k-lines in the window. generally used window sizes are 7, 25 and 99 in the TradingView. + // MovingAverageWindow int `json:"movingAverageWindow"` MovingAverageIntervalWindow types.IntervalWindow `json:"movingAverageIntervalWindow"` @@ -71,16 +69,16 @@ func (s *Strategy) ID() string { func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { // session.Subscribe(types.BookChannel, s.Symbol, types.SubscribeOptions{}) - //session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ + // session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ // Interval: string(s.Interval), - //}) + // }) for _, detection := range s.SupportDetection { session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ - Interval: string(detection.Interval), + Interval: detection.Interval, }) session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ - Interval: string(detection.MovingAverageIntervalWindow.Interval), + Interval: detection.MovingAverageIntervalWindow.Interval, }) } } @@ -94,23 +92,13 @@ func (s *Strategy) Validate() error { } func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + standardIndicatorSet := session.StandardIndicatorSet(s.Symbol) - standardIndicatorSet, ok := session.StandardIndicatorSet(s.Symbol) - if !ok { - return fmt.Errorf("standardIndicatorSet is nil, symbol %s", s.Symbol) - } - //binanceExchange, ok := session.Exchange.(*binance.Exchange) - //if !ok { - // log.Error("exchange failed") - //} if !session.Futures { log.Error("futures not enabled in config for this strategy") return nil } - //if s.FundingRate != nil { - // go s.listenToFundingRate(ctx, binanceExchange) - //} premiumIndex, err := session.Exchange.(*binance.Exchange).QueryPremiumIndex(ctx, s.Symbol) if err != nil { log.Error("exchange does not support funding rate api") @@ -158,7 +146,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se fundingRate := premiumIndex.LastFundingRate if fundingRate.Compare(s.FundingRate.High) >= 0 { - s.Notifiability.Notify("%s funding rate %s is too high! threshold %s", + bbgo.Notify("%s funding rate %s is too high! threshold %s", s.Symbol, fundingRate.Percentage(), s.FundingRate.High.Percentage(), @@ -172,13 +160,13 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se prettyQuoteVolume := s.Market.QuoteCurrencyFormatter() if detection.MinVolume.Sign() > 0 && kline.Volume.Compare(detection.MinVolume) > 0 { - s.Notifiability.Notify("Detected %s %s resistance base volume %s > min base volume %s, quote volume %s", + bbgo.Notify("Detected %s %s resistance base volume %s > min base volume %s, quote volume %s", s.Symbol, detection.Interval.String(), prettyBaseVolume.FormatMoney(kline.Volume.Trunc()), prettyBaseVolume.FormatMoney(detection.MinVolume.Trunc()), prettyQuoteVolume.FormatMoney(kline.QuoteVolume.Trunc()), ) - s.Notifiability.Notify(kline) + bbgo.Notify(kline) baseBalance, ok := session.GetAccount().Balance(s.Market.BaseCurrency) if !ok { @@ -198,13 +186,13 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } } } else if detection.MinQuoteVolume.Sign() > 0 && kline.QuoteVolume.Compare(detection.MinQuoteVolume) > 0 { - s.Notifiability.Notify("Detected %s %s resistance quote volume %s > min quote volume %s, base volume %s", + bbgo.Notify("Detected %s %s resistance quote volume %s > min quote volume %s, base volume %s", s.Symbol, detection.Interval.String(), prettyQuoteVolume.FormatMoney(kline.QuoteVolume.Trunc()), prettyQuoteVolume.FormatMoney(detection.MinQuoteVolume.Trunc()), prettyBaseVolume.FormatMoney(kline.Volume.Trunc()), ) - s.Notifiability.Notify(kline) + bbgo.Notify(kline) } } }) diff --git a/pkg/strategy/grid/strategy.go b/pkg/strategy/grid/strategy.go index 19c47a5568..323d1a17aa 100644 --- a/pkg/strategy/grid/strategy.go +++ b/pkg/strategy/grid/strategy.go @@ -9,17 +9,17 @@ import ( "github.com/sirupsen/logrus" "github.com/c9s/bbgo/pkg/bbgo" - "github.com/c9s/bbgo/pkg/exchange/max" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/service" "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/util" ) const ID = "grid" var log = logrus.WithField("strategy", ID) -var NotionalModifier = fixedpoint.NewFromFloat(1.0001) +var notionalModifier = fixedpoint.NewFromFloat(1.0001) func init() { // Register the pointer of the strategy struct, @@ -40,19 +40,9 @@ type State struct { // any created orders for tracking trades // [source Order ID] -> arbitrage order ArbitrageOrders map[uint64]types.Order `json:"arbitrageOrders"` - - ProfitStats types.ProfitStats `json:"profitStats,omitempty"` } type Strategy struct { - // The notification system will be injected into the strategy automatically. - // This field will be injected automatically since it's a single exchange strategy. - *bbgo.Notifiability `json:"-" yaml:"-"` - - *bbgo.Graceful `json:"-" yaml:"-"` - - *bbgo.Persistence - // OrderExecutor is an interface for submitting order. // This field will be injected automatically since it's a single exchange strategy. bbgo.OrderExecutor `json:"-" yaml:"-"` @@ -94,13 +84,15 @@ type Strategy struct { // Long means you want to hold more base asset than the quote asset. Long bool `json:"long,omitempty" yaml:"long,omitempty"` - state *State + State *State `persistence:"state"` + + ProfitStats *types.ProfitStats `persistence:"profit_stats"` // orderStore is used to store all the created orders, so that we can filter the trades. orderStore *bbgo.OrderStore // activeOrders is the locally maintained active order book of the maker orders. - activeOrders *bbgo.LocalActiveOrderBook + activeOrders *bbgo.ActiveOrderBook tradeCollector *bbgo.TradeCollector @@ -205,7 +197,7 @@ func (s *Strategy) generateGridSellOrders(session *bbgo.ExchangeSession) ([]type baseBalance.Available.String()) } - if _, filled := s.state.FilledSellGrids[price]; filled { + if _, filled := s.State.FilledSellGrids[price]; filled { log.Debugf("sell grid at price %s is already filled, skipping", price.String()) continue } @@ -222,7 +214,7 @@ func (s *Strategy) generateGridSellOrders(session *bbgo.ExchangeSession) ([]type }) baseBalance.Available = baseBalance.Available.Sub(quantity) - s.state.FilledSellGrids[price] = struct{}{} + s.State.FilledSellGrids[price] = struct{}{} } return orders, nil @@ -306,7 +298,7 @@ func (s *Strategy) generateGridBuyOrders(session *bbgo.ExchangeSession) ([]types quoteQuantity) } - if _, filled := s.state.FilledBuyGrids[price]; filled { + if _, filled := s.State.FilledBuyGrids[price]; filled { log.Debugf("buy grid at price %v is already filled, skipping", price) continue } @@ -323,7 +315,7 @@ func (s *Strategy) generateGridBuyOrders(session *bbgo.ExchangeSession) ([]types }) balance.Available = balance.Available.Sub(quoteQuantity) - s.state.FilledBuyGrids[price] = struct{}{} + s.State.FilledBuyGrids[price] = struct{}{} } return orders, nil @@ -422,7 +414,7 @@ func (s *Strategy) handleFilledOrder(filledOrder types.Order) { if amount.Compare(s.Market.MinNotional) <= 0 { quantity = bbgo.AdjustFloatQuantityByMinAmount( - quantity, price, s.Market.MinNotional.Mul(NotionalModifier)) + quantity, price, s.Market.MinNotional.Mul(notionalModifier)) // update amount amount = quantity.Mul(price) @@ -444,7 +436,7 @@ func (s *Strategy) handleFilledOrder(filledOrder types.Order) { // create one-way link from the newly created orders for _, o := range createdOrders { - s.state.ArbitrageOrders[o.OrderID] = filledOrder + s.State.ArbitrageOrders[o.OrderID] = filledOrder } s.orderStore.Add(createdOrders...) @@ -460,53 +452,53 @@ func (s *Strategy) handleFilledOrder(filledOrder types.Order) { if s.Long { switch filledOrder.Side { case types.SideTypeSell: - if buyOrder, ok := s.state.ArbitrageOrders[filledOrder.OrderID]; ok { + if buyOrder, ok := s.State.ArbitrageOrders[filledOrder.OrderID]; ok { // use base asset quantity here baseProfit := buyOrder.Quantity.Sub(filledOrder.Quantity) - s.state.AccumulativeArbitrageProfit = s.state.AccumulativeArbitrageProfit. + s.State.AccumulativeArbitrageProfit = s.State.AccumulativeArbitrageProfit. Add(baseProfit) - s.Notify("%s grid arbitrage profit %v %s, accumulative arbitrage profit %v %s", + bbgo.Notify("%s grid arbitrage profit %v %s, accumulative arbitrage profit %v %s", s.Symbol, baseProfit, s.Market.BaseCurrency, - s.state.AccumulativeArbitrageProfit, s.Market.BaseCurrency, + s.State.AccumulativeArbitrageProfit, s.Market.BaseCurrency, ) } case types.SideTypeBuy: - if sellOrder, ok := s.state.ArbitrageOrders[filledOrder.OrderID]; ok { + if sellOrder, ok := s.State.ArbitrageOrders[filledOrder.OrderID]; ok { // use base asset quantity here baseProfit := filledOrder.Quantity.Sub(sellOrder.Quantity) - s.state.AccumulativeArbitrageProfit = s.state.AccumulativeArbitrageProfit.Add(baseProfit) - s.Notify("%s grid arbitrage profit %v %s, accumulative arbitrage profit %v %s", + s.State.AccumulativeArbitrageProfit = s.State.AccumulativeArbitrageProfit.Add(baseProfit) + bbgo.Notify("%s grid arbitrage profit %v %s, accumulative arbitrage profit %v %s", s.Symbol, baseProfit, s.Market.BaseCurrency, - s.state.AccumulativeArbitrageProfit, s.Market.BaseCurrency, + s.State.AccumulativeArbitrageProfit, s.Market.BaseCurrency, ) } } } else if !s.Long && s.Quantity.Sign() > 0 { switch filledOrder.Side { case types.SideTypeSell: - if buyOrder, ok := s.state.ArbitrageOrders[filledOrder.OrderID]; ok { + if buyOrder, ok := s.State.ArbitrageOrders[filledOrder.OrderID]; ok { // use base asset quantity here quoteProfit := filledOrder.Quantity.Mul(filledOrder.Price).Sub( buyOrder.Quantity.Mul(buyOrder.Price)) - s.state.AccumulativeArbitrageProfit = s.state.AccumulativeArbitrageProfit.Add(quoteProfit) - s.Notify("%s grid arbitrage profit %v %s, accumulative arbitrage profit %v %s", + s.State.AccumulativeArbitrageProfit = s.State.AccumulativeArbitrageProfit.Add(quoteProfit) + bbgo.Notify("%s grid arbitrage profit %v %s, accumulative arbitrage profit %v %s", s.Symbol, quoteProfit, s.Market.QuoteCurrency, - s.state.AccumulativeArbitrageProfit, s.Market.QuoteCurrency, + s.State.AccumulativeArbitrageProfit, s.Market.QuoteCurrency, ) } case types.SideTypeBuy: - if sellOrder, ok := s.state.ArbitrageOrders[filledOrder.OrderID]; ok { + if sellOrder, ok := s.State.ArbitrageOrders[filledOrder.OrderID]; ok { // use base asset quantity here quoteProfit := sellOrder.Quantity.Mul(sellOrder.Price). Sub(filledOrder.Quantity.Mul(filledOrder.Price)) - s.state.AccumulativeArbitrageProfit = s.state.AccumulativeArbitrageProfit.Add(quoteProfit) - s.Notify("%s grid arbitrage profit %v %s, accumulative arbitrage profit %v %s", s.Symbol, + s.State.AccumulativeArbitrageProfit = s.State.AccumulativeArbitrageProfit.Add(quoteProfit) + bbgo.Notify("%s grid arbitrage profit %v %s, accumulative arbitrage profit %v %s", s.Symbol, quoteProfit, s.Market.QuoteCurrency, - s.state.AccumulativeArbitrageProfit, s.Market.QuoteCurrency, + s.State.AccumulativeArbitrageProfit, s.Market.QuoteCurrency, ) } } @@ -518,58 +510,29 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { } func (s *Strategy) LoadState() error { - instanceID := s.InstanceID() - - var state State - if s.Persistence != nil { - if err := s.Persistence.Load(&state, ID, instanceID); err != nil { - if err != service.ErrPersistenceNotExists { - return errors.Wrapf(err, "state load error") - } - - s.state = &State{ - FilledBuyGrids: make(map[fixedpoint.Value]struct{}), - FilledSellGrids: make(map[fixedpoint.Value]struct{}), - ArbitrageOrders: make(map[uint64]types.Order), - Position: types.NewPositionFromMarket(s.Market), - } - } else { - s.state = &state + if s.State == nil { + s.State = &State{ + FilledBuyGrids: make(map[fixedpoint.Value]struct{}), + FilledSellGrids: make(map[fixedpoint.Value]struct{}), + ArbitrageOrders: make(map[uint64]types.Order), + Position: types.NewPositionFromMarket(s.Market), } } - // init profit stats - s.state.ProfitStats.Init(s.Market) - // field guards - if s.state.ArbitrageOrders == nil { - s.state.ArbitrageOrders = make(map[uint64]types.Order) + if s.State.ArbitrageOrders == nil { + s.State.ArbitrageOrders = make(map[uint64]types.Order) } - if s.state.FilledBuyGrids == nil { - s.state.FilledBuyGrids = make(map[fixedpoint.Value]struct{}) + if s.State.FilledBuyGrids == nil { + s.State.FilledBuyGrids = make(map[fixedpoint.Value]struct{}) } - if s.state.FilledSellGrids == nil { - s.state.FilledSellGrids = make(map[fixedpoint.Value]struct{}) + if s.State.FilledSellGrids == nil { + s.State.FilledSellGrids = make(map[fixedpoint.Value]struct{}) } return nil } -func (s *Strategy) SaveState() error { - if s.Persistence != nil { - log.Infof("backing up grid state...") - - instanceID := s.InstanceID() - submitOrders := s.activeOrders.Backup() - s.state.Orders = submitOrders - - if err := s.Persistence.Save(s.state, ID, instanceID); err != nil { - return err - } - } - return nil -} - // InstanceID returns the instance identifier from the current grid configuration parameters func (s *Strategy) InstanceID() string { return fmt.Sprintf("%s-%s-%d-%d-%d", ID, s.Symbol, s.GridNum, s.UpperPrice.Int(), s.LowerPrice.Int()) @@ -586,28 +549,32 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } instanceID := s.InstanceID() - s.groupID = max.GenerateGroupID(instanceID) + s.groupID = util.FNV32(instanceID) log.Infof("using group id %d from fnv(%s)", s.groupID, instanceID) + if s.ProfitStats == nil { + s.ProfitStats = types.NewProfitStats(s.Market) + } + if err := s.LoadState(); err != nil { return err } - s.Notify("grid %s position", s.Symbol, s.state.Position) + bbgo.Notify("grid %s position", s.Symbol, s.State.Position) s.orderStore = bbgo.NewOrderStore(s.Symbol) s.orderStore.BindStream(session.UserDataStream) // we don't persist orders so that we can not clear the previous orders for now. just need time to support this. - s.activeOrders = bbgo.NewLocalActiveOrderBook(s.Symbol) + s.activeOrders = bbgo.NewActiveOrderBook(s.Symbol) s.activeOrders.OnFilled(s.handleFilledOrder) s.activeOrders.BindStream(session.UserDataStream) - s.tradeCollector = bbgo.NewTradeCollector(s.Symbol, s.state.Position, s.orderStore) + s.tradeCollector = bbgo.NewTradeCollector(s.Symbol, s.State.Position, s.orderStore) s.tradeCollector.OnTrade(func(trade types.Trade, profit, netProfit fixedpoint.Value) { - s.Notifiability.Notify(trade) - s.state.ProfitStats.AddTrade(trade) + bbgo.Notify(trade) + s.ProfitStats.AddTrade(trade) }) /* @@ -621,18 +588,16 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se */ s.tradeCollector.OnPositionUpdate(func(position *types.Position) { - s.Notifiability.Notify(position) + bbgo.Notify(position) }) s.tradeCollector.BindStream(session.UserDataStream) - s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() - if err := s.SaveState(); err != nil { - log.WithError(err).Errorf("can not save state: %+v", s.state) - } else { - s.Notify("%s: %s grid is saved", ID, s.Symbol) - } + submitOrders := s.activeOrders.Backup() + s.State.Orders = submitOrders + bbgo.Sync(ctx, s) // now we can cancel the open orders log.Infof("canceling active orders...") @@ -643,10 +608,10 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se session.UserDataStream.OnStart(func() { // if we have orders in the state data, we can restore them - if len(s.state.Orders) > 0 { - s.Notifiability.Notify("restoring %s %d grid orders...", s.Symbol, len(s.state.Orders)) + if len(s.State.Orders) > 0 { + bbgo.Notify("restoring %s %d grid orders...", s.Symbol, len(s.State.Orders)) - createdOrders, err := orderExecutor.SubmitOrders(ctx, s.state.Orders...) + createdOrders, err := orderExecutor.SubmitOrders(ctx, s.State.Orders...) if err != nil { log.WithError(err).Error("active orders restore error") } diff --git a/pkg/strategy/harmonic/draw.go b/pkg/strategy/harmonic/draw.go new file mode 100644 index 0000000000..22a9dc2986 --- /dev/null +++ b/pkg/strategy/harmonic/draw.go @@ -0,0 +1,90 @@ +package harmonic + +import ( + "bytes" + "fmt" + "os" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/interact" + "github.com/c9s/bbgo/pkg/types" + "github.com/wcharczuk/go-chart/v2" +) + +func (s *Strategy) InitDrawCommands(profit, cumProfit types.Series) { + bbgo.RegisterCommand("/pnl", "Draw PNL(%) per trade", func(reply interact.Reply) { + canvas := DrawPNL(s.InstanceID(), profit) + var buffer bytes.Buffer + if err := canvas.Render(chart.PNG, &buffer); err != nil { + log.WithError(err).Errorf("cannot render pnl in drift") + reply.Message(fmt.Sprintf("[error] cannot render pnl in ewo: %v", err)) + return + } + bbgo.SendPhoto(&buffer) + }) + bbgo.RegisterCommand("/cumpnl", "Draw Cummulative PNL(Quote)", func(reply interact.Reply) { + canvas := DrawCumPNL(s.InstanceID(), cumProfit) + var buffer bytes.Buffer + if err := canvas.Render(chart.PNG, &buffer); err != nil { + log.WithError(err).Errorf("cannot render cumpnl in drift") + reply.Message(fmt.Sprintf("[error] canot render cumpnl in drift: %v", err)) + return + } + bbgo.SendPhoto(&buffer) + }) +} + +func (s *Strategy) Draw(profit, cumProfit types.Series) error { + + canvas := DrawPNL(s.InstanceID(), profit) + fPnL, err := os.Create(s.GraphPNLPath) + if err != nil { + return fmt.Errorf("cannot create on path " + s.GraphPNLPath) + } + defer fPnL.Close() + if err = canvas.Render(chart.PNG, fPnL); err != nil { + return fmt.Errorf("cannot render pnl") + } + canvas = DrawCumPNL(s.InstanceID(), cumProfit) + fCumPnL, err := os.Create(s.GraphCumPNLPath) + if err != nil { + return fmt.Errorf("cannot create on path " + s.GraphCumPNLPath) + } + defer fCumPnL.Close() + if err = canvas.Render(chart.PNG, fCumPnL); err != nil { + return fmt.Errorf("cannot render cumpnl") + } + + return nil +} + +func DrawPNL(instanceID string, profit types.Series) *types.Canvas { + canvas := types.NewCanvas(instanceID) + length := profit.Length() + log.Infof("pnl Highest: %f, Lowest: %f", types.Highest(profit, length), types.Lowest(profit, length)) + canvas.PlotRaw("pnl %", profit, length) + canvas.YAxis = chart.YAxis{ + ValueFormatter: func(v interface{}) string { + if vf, isFloat := v.(float64); isFloat { + return fmt.Sprintf("%.4f", vf) + } + return "" + }, + } + canvas.PlotRaw("1", types.NumberSeries(1), length) + return canvas +} + +func DrawCumPNL(instanceID string, cumProfit types.Series) *types.Canvas { + canvas := types.NewCanvas(instanceID) + canvas.PlotRaw("cummulative pnl", cumProfit, cumProfit.Length()) + canvas.YAxis = chart.YAxis{ + ValueFormatter: func(v interface{}) string { + if vf, isFloat := v.(float64); isFloat { + return fmt.Sprintf("%.4f", vf) + } + return "" + }, + } + return canvas +} diff --git a/pkg/strategy/harmonic/shark.go b/pkg/strategy/harmonic/shark.go new file mode 100644 index 0000000000..630d4f4bf7 --- /dev/null +++ b/pkg/strategy/harmonic/shark.go @@ -0,0 +1,201 @@ +package harmonic + +import ( + "math" + "time" + + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +var zeroTime time.Time + +//go:generate callbackgen -type SHARK +type SHARK struct { + types.IntervalWindow + types.SeriesBase + + Lows floats.Slice + Highs floats.Slice + LongScores floats.Slice + ShortScores floats.Slice + + Values floats.Slice + + EndTime time.Time + + updateCallbacks []func(value float64) +} + +var _ types.SeriesExtend = &SHARK{} + +func (inc *SHARK) Update(high, low, price float64) { + if inc.SeriesBase.Series == nil { + inc.SeriesBase.Series = inc + } + inc.Highs.Update(high) + inc.Lows.Update(low) + + if inc.Highs.Length() < inc.Window || inc.Lows.Length() < inc.Window { + return + } + + longScore := inc.SharkLong(inc.Highs, inc.Lows, price, inc.Window) + shortScore := inc.SharkShort(inc.Highs, inc.Lows, price, inc.Window) + + inc.LongScores.Push(longScore) + inc.ShortScores.Push(shortScore) + + inc.Values.Push(longScore - shortScore) + +} + +func (inc *SHARK) Last() float64 { + if len(inc.Values) == 0 { + return 0 + } + + return inc.Values[len(inc.Values)-1] +} + +func (inc *SHARK) Index(i int) float64 { + if i >= len(inc.Values) { + return 0 + } + + return inc.Values[len(inc.Values)-1-i] +} + +func (inc *SHARK) Length() int { + return len(inc.Values) +} + +func (inc *SHARK) BindK(target indicator.KLineClosedEmitter, symbol string, interval types.Interval) { + target.OnKLineClosed(types.KLineWith(symbol, interval, inc.PushK)) +} + +func (inc *SHARK) PushK(k types.KLine) { + if inc.EndTime != zeroTime && k.EndTime.Before(inc.EndTime) { + return + } + + inc.Update(indicator.KLineHighPriceMapper(k), indicator.KLineLowPriceMapper(k), indicator.KLineClosePriceMapper(k)) + inc.EndTime = k.EndTime.Time() + inc.EmitUpdate(inc.Last()) +} + +func (inc *SHARK) LoadK(allKLines []types.KLine) { + for _, k := range allKLines { + inc.PushK(k) + } + inc.EmitUpdate(inc.Last()) +} + +func (inc SHARK) SharkLong(highs, lows floats.Slice, p float64, lookback int) float64 { + score := 0. + for x := 5; x < lookback; x++ { + if lows.Index(x-1) > lows.Index(x) && lows.Index(x) < lows.Index(x+1) { + X := lows.Index(x) + for a := 4; a < x; a++ { + if highs.Index(a-1) < highs.Index(a) && highs.Index(a) > highs.Index(a+1) { + A := highs.Index(a) + XA := math.Abs(X - A) + hB := A - 0.382*XA + lB := A - 0.618*XA + for b := 3; b < a; b++ { + if lows.Index(b-1) > lows.Index(b) && lows.Index(b) < lows.Index(b+1) { + B := lows.Index(b) + if hB > B && B > lB { + //log.Infof("got point B:%f", B) + AB := math.Abs(A - B) + hC := B + 1.618*AB + lC := B + 1.13*AB + for c := 2; c < b; c++ { + if highs.Index(c-1) < highs.Index(c) && highs.Index(c) > highs.Index(c+1) { + C := highs.Index(c) + if hC > C && C > lC { + //log.Infof("got point C:%f", C) + XC := math.Abs(X - C) + hD := C - 0.886*XC + lD := C - 1.13*XC + //for d := 1; d < c; d++ { + //if lows.Index(d-1) > lows.Index(d) && lows.Index(d) < lows.Index(d+1) { + D := p //lows.Index(d) + if hD > D && D > lD { + BC := math.Abs(B - C) + hD2 := C - 1.618*BC + lD2 := C - 2.24*BC + if hD2 > D && D > lD2 { + //log.Infof("got point D:%f", D) + score++ + } + } + //} + //} + } + } + } + } + } + } + } + } + } + } + return score +} + +func (inc SHARK) SharkShort(highs, lows floats.Slice, p float64, lookback int) float64 { + score := 0. + for x := 5; x < lookback; x++ { + if highs.Index(x-1) < highs.Index(x) && highs.Index(x) > highs.Index(x+1) { + X := highs.Index(x) + for a := 4; a < x; a++ { + if lows.Index(a-1) > lows.Index(a) && lows.Index(a) < lows.Index(a+1) { + A := lows.Index(a) + XA := math.Abs(X - A) + lB := A + 0.382*XA + hB := A + 0.618*XA + for b := 3; b < a; b++ { + if highs.Index(b-1) > highs.Index(b) && highs.Index(b) < highs.Index(b+1) { + B := highs.Index(b) + if hB > B && B > lB { + //log.Infof("got point B:%f", B) + AB := math.Abs(A - B) + lC := B - 1.618*AB + hC := B - 1.13*AB + for c := 2; c < b; c++ { + if lows.Index(c-1) < lows.Index(c) && lows.Index(c) > lows.Index(c+1) { + C := lows.Index(c) + if hC > C && C > lC { + //log.Infof("got point C:%f", C) + XC := math.Abs(X - C) + lD := C + 0.886*XC + hD := C + 1.13*XC + //for d := 1; d < c; d++ { + //if lows.Index(d-1) > lows.Index(d) && lows.Index(d) < lows.Index(d+1) { + D := p //lows.Index(d) + if hD > D && D > lD { + BC := math.Abs(B - C) + lD2 := C + 1.618*BC + hD2 := C + 2.24*BC + if hD2 > D && D > lD2 { + //log.Infof("got point D:%f", D) + score++ + } + } + //} + //} + } + } + } + } + } + } + } + } + } + } + return score +} diff --git a/pkg/strategy/harmonic/shark_callbacks.go b/pkg/strategy/harmonic/shark_callbacks.go new file mode 100644 index 0000000000..7d265267e5 --- /dev/null +++ b/pkg/strategy/harmonic/shark_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type SHARK"; DO NOT EDIT. + +package harmonic + +import () + +func (inc *SHARK) OnUpdate(cb func(value float64)) { + inc.updateCallbacks = append(inc.updateCallbacks, cb) +} + +func (inc *SHARK) EmitUpdate(value float64) { + for _, cb := range inc.updateCallbacks { + cb(value) + } +} diff --git a/pkg/strategy/harmonic/strategy.go b/pkg/strategy/harmonic/strategy.go new file mode 100644 index 0000000000..49469bddb0 --- /dev/null +++ b/pkg/strategy/harmonic/strategy.go @@ -0,0 +1,397 @@ +package harmonic + +import ( + "context" + "fmt" + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/data/tsv" + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" + + "github.com/sirupsen/logrus" +) + +const ID = "harmonic" + +var log = logrus.WithField("strategy", ID) + +func init() { + bbgo.RegisterStrategy(ID, &Strategy{}) +} + +type Strategy struct { + Environment *bbgo.Environment + Symbol string `json:"symbol"` + Market types.Market + + types.IntervalWindow + //bbgo.OpenPositionOptions + + // persistence fields + Position *types.Position `persistence:"position"` + ProfitStats *types.ProfitStats `persistence:"profit_stats"` + TradeStats *types.TradeStats `persistence:"trade_stats"` + + ExitMethods bbgo.ExitMethodSet `json:"exits"` + + session *bbgo.ExchangeSession + orderExecutor *bbgo.GeneralOrderExecutor + + bbgo.QuantityOrAmount + + // StrategyController + bbgo.StrategyController + + shark *SHARK + + AccountValueCalculator *bbgo.AccountValueCalculator + + // whether to draw graph or not by the end of backtest + DrawGraph bool `json:"drawGraph"` + GraphPNLPath string `json:"graphPNLPath"` + GraphCumPNLPath string `json:"graphCumPNLPath"` + + // for position + buyPrice float64 `persistence:"buy_price"` + sellPrice float64 `persistence:"sell_price"` + highestPrice float64 `persistence:"highest_price"` + lowestPrice float64 `persistence:"lowest_price"` + + // Accumulated profit report + AccumulatedProfitReport *AccumulatedProfitReport `json:"accumulatedProfitReport"` +} + +// AccumulatedProfitReport For accumulated profit report output +type AccumulatedProfitReport struct { + // AccumulatedProfitMAWindow Accumulated profit SMA window, in number of trades + AccumulatedProfitMAWindow int `json:"accumulatedProfitMAWindow"` + + // IntervalWindow interval window, in days + IntervalWindow int `json:"intervalWindow"` + + // NumberOfInterval How many intervals to output to TSV + NumberOfInterval int `json:"NumberOfInterval"` + + // TsvReportPath The path to output report to + TsvReportPath string `json:"tsvReportPath"` + + // AccumulatedDailyProfitWindow The window to sum up the daily profit, in days + AccumulatedDailyProfitWindow int `json:"accumulatedDailyProfitWindow"` + + // Accumulated profit + accumulatedProfit fixedpoint.Value + accumulatedProfitPerDay floats.Slice + previousAccumulatedProfit fixedpoint.Value + + // Accumulated profit MA + accumulatedProfitMA *indicator.SMA + accumulatedProfitMAPerDay floats.Slice + + // Daily profit + dailyProfit floats.Slice + + // Accumulated fee + accumulatedFee fixedpoint.Value + accumulatedFeePerDay floats.Slice + + // Win ratio + winRatioPerDay floats.Slice + + // Profit factor + profitFactorPerDay floats.Slice + + // Trade number + dailyTrades floats.Slice + accumulatedTrades int + previousAccumulatedTrades int +} + +func (r *AccumulatedProfitReport) Initialize() { + if r.AccumulatedProfitMAWindow <= 0 { + r.AccumulatedProfitMAWindow = 60 + } + if r.IntervalWindow <= 0 { + r.IntervalWindow = 7 + } + if r.AccumulatedDailyProfitWindow <= 0 { + r.AccumulatedDailyProfitWindow = 7 + } + if r.NumberOfInterval <= 0 { + r.NumberOfInterval = 1 + } + r.accumulatedProfitMA = &indicator.SMA{IntervalWindow: types.IntervalWindow{Interval: types.Interval1d, Window: r.AccumulatedProfitMAWindow}} +} + +func (r *AccumulatedProfitReport) RecordProfit(profit fixedpoint.Value) { + r.accumulatedProfit = r.accumulatedProfit.Add(profit) +} + +func (r *AccumulatedProfitReport) RecordTrade(fee fixedpoint.Value) { + r.accumulatedFee = r.accumulatedFee.Add(fee) + r.accumulatedTrades += 1 +} + +func (r *AccumulatedProfitReport) DailyUpdate(tradeStats *types.TradeStats) { + // Daily profit + r.dailyProfit.Update(r.accumulatedProfit.Sub(r.previousAccumulatedProfit).Float64()) + r.previousAccumulatedProfit = r.accumulatedProfit + + // Accumulated profit + r.accumulatedProfitPerDay.Update(r.accumulatedProfit.Float64()) + + // Accumulated profit MA + r.accumulatedProfitMA.Update(r.accumulatedProfit.Float64()) + r.accumulatedProfitMAPerDay.Update(r.accumulatedProfitMA.Last()) + + // Accumulated Fee + r.accumulatedFeePerDay.Update(r.accumulatedFee.Float64()) + + // Win ratio + r.winRatioPerDay.Update(tradeStats.WinningRatio.Float64()) + + // Profit factor + r.profitFactorPerDay.Update(tradeStats.ProfitFactor.Float64()) + + // Daily trades + r.dailyTrades.Update(float64(r.accumulatedTrades - r.previousAccumulatedTrades)) + r.previousAccumulatedTrades = r.accumulatedTrades +} + +// Output Accumulated profit report to a TSV file +func (r *AccumulatedProfitReport) Output(symbol string) { + if r.TsvReportPath != "" { + tsvwiter, err := tsv.AppendWriterFile(r.TsvReportPath) + if err != nil { + panic(err) + } + defer tsvwiter.Close() + // Output symbol, total acc. profit, acc. profit 60MA, interval acc. profit, fee, win rate, profit factor + _ = tsvwiter.Write([]string{"#", "Symbol", "accumulatedProfit", "accumulatedProfitMA", fmt.Sprintf("%dd profit", r.AccumulatedDailyProfitWindow), "accumulatedFee", "winRatio", "profitFactor", "60D trades"}) + for i := 0; i <= r.NumberOfInterval-1; i++ { + accumulatedProfit := r.accumulatedProfitPerDay.Index(r.IntervalWindow * i) + accumulatedProfitStr := fmt.Sprintf("%f", accumulatedProfit) + accumulatedProfitMA := r.accumulatedProfitMAPerDay.Index(r.IntervalWindow * i) + accumulatedProfitMAStr := fmt.Sprintf("%f", accumulatedProfitMA) + intervalAccumulatedProfit := r.dailyProfit.Tail(r.AccumulatedDailyProfitWindow+r.IntervalWindow*i).Sum() - r.dailyProfit.Tail(r.IntervalWindow*i).Sum() + intervalAccumulatedProfitStr := fmt.Sprintf("%f", intervalAccumulatedProfit) + accumulatedFee := fmt.Sprintf("%f", r.accumulatedFeePerDay.Index(r.IntervalWindow*i)) + winRatio := fmt.Sprintf("%f", r.winRatioPerDay.Index(r.IntervalWindow*i)) + profitFactor := fmt.Sprintf("%f", r.profitFactorPerDay.Index(r.IntervalWindow*i)) + trades := r.dailyTrades.Tail(60+r.IntervalWindow*i).Sum() - r.dailyTrades.Tail(r.IntervalWindow*i).Sum() + tradesStr := fmt.Sprintf("%f", trades) + + _ = tsvwiter.Write([]string{fmt.Sprintf("%d", i+1), symbol, accumulatedProfitStr, accumulatedProfitMAStr, intervalAccumulatedProfitStr, accumulatedFee, winRatio, profitFactor, tradesStr}) + } + } +} + +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) + + if !bbgo.IsBackTesting { + session.Subscribe(types.MarketTradeChannel, s.Symbol, types.SubscribeOptions{}) + } + + s.ExitMethods.SetAndSubscribe(session, s) +} + +func (s *Strategy) ID() string { + return ID +} + +func (s *Strategy) InstanceID() string { + return fmt.Sprintf("%s:%s", ID, s.Symbol) +} + +func (s *Strategy) CalcAssetValue(price fixedpoint.Value) fixedpoint.Value { + balances := s.session.GetAccount().Balances() + return balances[s.Market.BaseCurrency].Total().Mul(price).Add(balances[s.Market.QuoteCurrency].Total()) +} + +func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + var instanceID = s.InstanceID() + + if s.Position == nil { + s.Position = types.NewPositionFromMarket(s.Market) + } + + if s.ProfitStats == nil { + s.ProfitStats = types.NewProfitStats(s.Market) + } + + if s.TradeStats == nil { + s.TradeStats = types.NewTradeStats(s.Symbol) + } + + // StrategyController + s.Status = types.StrategyStatusRunning + + s.OnSuspend(func() { + // Cancel active orders + _ = s.orderExecutor.GracefulCancel(ctx) + }) + + s.OnEmergencyStop(func() { + // Cancel active orders + _ = s.orderExecutor.GracefulCancel(ctx) + // Close 100% position + //_ = s.ClosePosition(ctx, fixedpoint.One) + }) + + s.session = session + + // Set fee rate + if s.session.MakerFeeRate.Sign() > 0 || s.session.TakerFeeRate.Sign() > 0 { + s.Position.SetExchangeFeeRate(s.session.ExchangeName, types.ExchangeFee{ + MakerFeeRate: s.session.MakerFeeRate, + TakerFeeRate: s.session.TakerFeeRate, + }) + } + + s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) + s.orderExecutor.BindEnvironment(s.Environment) + s.orderExecutor.BindProfitStats(s.ProfitStats) + s.orderExecutor.BindTradeStats(s.TradeStats) + + // AccountValueCalculator + s.AccountValueCalculator = bbgo.NewAccountValueCalculator(s.session, s.Market.QuoteCurrency) + + // Accumulated profit report + if bbgo.IsBackTesting { + if s.AccumulatedProfitReport == nil { + s.AccumulatedProfitReport = &AccumulatedProfitReport{} + } + s.AccumulatedProfitReport.Initialize() + s.orderExecutor.TradeCollector().OnProfit(func(trade types.Trade, profit *types.Profit) { + if profit == nil { + return + } + + s.AccumulatedProfitReport.RecordProfit(profit.Profit) + }) + // s.orderExecutor.TradeCollector().OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { + // s.AccumulatedProfitReport.RecordTrade(trade.Fee) + // }) + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1d, func(kline types.KLine) { + s.AccumulatedProfitReport.DailyUpdate(s.TradeStats) + })) + } + + // For drawing + profitSlice := floats.Slice{1., 1.} + price, _ := session.LastPrice(s.Symbol) + initAsset := s.CalcAssetValue(price).Float64() + cumProfitSlice := floats.Slice{initAsset, initAsset} + + s.orderExecutor.TradeCollector().OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { + if bbgo.IsBackTesting { + s.AccumulatedProfitReport.RecordTrade(trade.Fee) + } + + // For drawing/charting + price := trade.Price.Float64() + if s.buyPrice > 0 { + profitSlice.Update(price / s.buyPrice) + cumProfitSlice.Update(s.CalcAssetValue(trade.Price).Float64()) + } else if s.sellPrice > 0 { + profitSlice.Update(s.sellPrice / price) + cumProfitSlice.Update(s.CalcAssetValue(trade.Price).Float64()) + } + if s.Position.IsDust(trade.Price) { + s.buyPrice = 0 + s.sellPrice = 0 + s.highestPrice = 0 + s.lowestPrice = 0 + } else if s.Position.IsLong() { + s.buyPrice = price + s.sellPrice = 0 + s.highestPrice = s.buyPrice + s.lowestPrice = 0 + } else { + s.sellPrice = price + s.buyPrice = 0 + s.highestPrice = 0 + s.lowestPrice = s.sellPrice + } + }) + + s.orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { + bbgo.Sync(ctx, s) + }) + s.orderExecutor.Bind() + + for _, method := range s.ExitMethods { + method.Bind(session, s.orderExecutor) + } + + kLineStore, _ := s.session.MarketDataStore(s.Symbol) + s.shark = &SHARK{IntervalWindow: types.IntervalWindow{Window: s.Window, Interval: s.Interval}} + s.shark.BindK(s.session.MarketDataStream, s.Symbol, s.shark.Interval) + if klines, ok := kLineStore.KLinesOfInterval(s.shark.Interval); ok { + s.shark.LoadK((*klines)[0:]) + } + s.session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { + + log.Infof("Shark Score: %f, Current Price: %f", s.shark.Last(), kline.Close.Float64()) + + //previousRegime := s.shark.Values.Tail(10).Mean() + //zeroThreshold := 5. + + if s.shark.Rank(s.Window).Last()/float64(s.Window) > 0.99 { // && ((previousRegime < zeroThreshold && previousRegime > -zeroThreshold) || s.shark.Index(1) < 0) + if s.Position.IsShort() { + _ = s.orderExecutor.GracefulCancel(ctx) + s.orderExecutor.ClosePosition(ctx, fixedpoint.One, "close short position") + } + _, err := s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeBuy, + Quantity: s.Quantity, + Type: types.OrderTypeMarket, + Tag: "shark long: buy in", + }) + if err == nil { + _, err = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeSell, + Quantity: s.Quantity, + Price: fixedpoint.NewFromFloat(s.shark.Highs.Tail(100).Max()), + Type: types.OrderTypeLimit, + Tag: "shark long: sell back", + }) + } + if err != nil { + log.Errorln(err) + } + + } else if s.shark.Rank(s.Window).Last()/float64(s.Window) < 0.01 { // && ((previousRegime < zeroThreshold && previousRegime > -zeroThreshold) || s.shark.Index(1) > 0) + if s.Position.IsLong() { + _ = s.orderExecutor.GracefulCancel(ctx) + s.orderExecutor.ClosePosition(ctx, fixedpoint.One, "close long position") + } + _, err := s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeSell, + Quantity: s.Quantity, + Type: types.OrderTypeMarket, + Tag: "shark short: sell in", + }) + if err == nil { + _, err = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeBuy, + Quantity: s.Quantity, + Price: fixedpoint.NewFromFloat(s.shark.Lows.Tail(100).Min()), + Type: types.OrderTypeLimit, + Tag: "shark short: buy back", + }) + } + if err != nil { + log.Errorln(err) + } + } + })) + + return nil +} diff --git a/pkg/strategy/irr/draw.go b/pkg/strategy/irr/draw.go new file mode 100644 index 0000000000..cf9f98bfa9 --- /dev/null +++ b/pkg/strategy/irr/draw.go @@ -0,0 +1,90 @@ +package irr + +import ( + "bytes" + "fmt" + "os" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/interact" + "github.com/c9s/bbgo/pkg/types" + "github.com/wcharczuk/go-chart/v2" +) + +func (s *Strategy) InitDrawCommands(profit, cumProfit types.Series) { + bbgo.RegisterCommand("/pnl", "Draw PNL(%) per trade", func(reply interact.Reply) { + canvas := DrawPNL(s.InstanceID(), profit) + var buffer bytes.Buffer + if err := canvas.Render(chart.PNG, &buffer); err != nil { + log.WithError(err).Errorf("cannot render pnl in drift") + reply.Message(fmt.Sprintf("[error] cannot render pnl in ewo: %v", err)) + return + } + bbgo.SendPhoto(&buffer) + }) + bbgo.RegisterCommand("/cumpnl", "Draw Cummulative PNL(Quote)", func(reply interact.Reply) { + canvas := DrawCumPNL(s.InstanceID(), cumProfit) + var buffer bytes.Buffer + if err := canvas.Render(chart.PNG, &buffer); err != nil { + log.WithError(err).Errorf("cannot render cumpnl in drift") + reply.Message(fmt.Sprintf("[error] canot render cumpnl in drift: %v", err)) + return + } + bbgo.SendPhoto(&buffer) + }) +} + +func (s *Strategy) Draw(profit, cumProfit types.Series) error { + + canvas := DrawPNL(s.InstanceID(), profit) + fPnL, err := os.Create(s.GraphPNLPath) + if err != nil { + return fmt.Errorf("cannot create on path " + s.GraphPNLPath) + } + defer fPnL.Close() + if err = canvas.Render(chart.PNG, fPnL); err != nil { + return fmt.Errorf("cannot render pnl") + } + canvas = DrawCumPNL(s.InstanceID(), cumProfit) + fCumPnL, err := os.Create(s.GraphCumPNLPath) + if err != nil { + return fmt.Errorf("cannot create on path " + s.GraphCumPNLPath) + } + defer fCumPnL.Close() + if err = canvas.Render(chart.PNG, fCumPnL); err != nil { + return fmt.Errorf("cannot render cumpnl") + } + + return nil +} + +func DrawPNL(instanceID string, profit types.Series) *types.Canvas { + canvas := types.NewCanvas(instanceID) + length := profit.Length() + log.Infof("pnl Highest: %f, Lowest: %f", types.Highest(profit, length), types.Lowest(profit, length)) + canvas.PlotRaw("pnl %", profit, length) + canvas.YAxis = chart.YAxis{ + ValueFormatter: func(v interface{}) string { + if vf, isFloat := v.(float64); isFloat { + return fmt.Sprintf("%.4f", vf) + } + return "" + }, + } + canvas.PlotRaw("1", types.NumberSeries(1), length) + return canvas +} + +func DrawCumPNL(instanceID string, cumProfit types.Series) *types.Canvas { + canvas := types.NewCanvas(instanceID) + canvas.PlotRaw("cummulative pnl", cumProfit, cumProfit.Length()) + canvas.YAxis = chart.YAxis{ + ValueFormatter: func(v interface{}) string { + if vf, isFloat := v.(float64); isFloat { + return fmt.Sprintf("%.4f", vf) + } + return "" + }, + } + return canvas +} diff --git a/pkg/strategy/irr/neg_return_rate.go b/pkg/strategy/irr/neg_return_rate.go new file mode 100644 index 0000000000..09cee398b7 --- /dev/null +++ b/pkg/strategy/irr/neg_return_rate.go @@ -0,0 +1,99 @@ +package irr + +import ( + "time" + + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +var zeroTime time.Time + +// simple negative internal return rate over certain timeframe(interval) + +//go:generate callbackgen -type NRR +type NRR struct { + types.IntervalWindow + types.SeriesBase + + RankingWindow int + + Prices *types.Queue + Values floats.Slice + RankedValues floats.Slice + + EndTime time.Time + + updateCallbacks []func(value float64) +} + +var _ types.SeriesExtend = &NRR{} + +func (inc *NRR) Update(price float64) { + if inc.SeriesBase.Series == nil { + inc.SeriesBase.Series = inc + inc.Prices = types.NewQueue(inc.Window) + } + inc.Prices.Update(price) + if inc.Prices.Length() < inc.Window { + return + } + irr := (inc.Prices.Last() / inc.Prices.Index(inc.Window-1)) - 1 + + inc.Values.Push(-irr) // neg ret here + inc.RankedValues.Push(inc.Rank(inc.RankingWindow).Last() / float64(inc.RankingWindow)) // ranked neg ret here + +} + +func (inc *NRR) Last() float64 { + if len(inc.Values) == 0 { + return 0 + } + + return inc.Values[len(inc.Values)-1] +} + +func (inc *NRR) Index(i int) float64 { + if i >= len(inc.Values) { + return 0 + } + + return inc.Values[len(inc.Values)-1-i] +} + +func (inc *NRR) Length() int { + return len(inc.Values) +} + +func (inc *NRR) BindK(target indicator.KLineClosedEmitter, symbol string, interval types.Interval) { + target.OnKLineClosed(types.KLineWith(symbol, interval, inc.PushK)) +} + +func (inc *NRR) PushK(k types.KLine) { + if inc.EndTime != zeroTime && k.EndTime.Before(inc.EndTime) { + return + } + + inc.Update(indicator.KLineClosePriceMapper(k)) + inc.EndTime = k.EndTime.Time() + inc.EmitUpdate(inc.Last()) +} + +func (inc *NRR) LoadK(allKLines []types.KLine) { + for _, k := range allKLines { + inc.PushK(k) + } + inc.EmitUpdate(inc.Last()) +} + +//func calculateReturn(klines []types.KLine, window int, val KLineValueMapper) (float64, error) { +// length := len(klines) +// if length == 0 || length < window { +// return 0.0, fmt.Errorf("insufficient elements for calculating VOL with window = %d", window) +// } +// +// rate := val(klines[length-1])/val(klines[length-2]) - 1 +// +// return rate, nil +//} diff --git a/pkg/strategy/irr/nrr_callbacks.go b/pkg/strategy/irr/nrr_callbacks.go new file mode 100644 index 0000000000..40552efab0 --- /dev/null +++ b/pkg/strategy/irr/nrr_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type NRR"; DO NOT EDIT. + +package irr + +import () + +func (inc *NRR) OnUpdate(cb func(value float64)) { + inc.updateCallbacks = append(inc.updateCallbacks, cb) +} + +func (inc *NRR) EmitUpdate(value float64) { + for _, cb := range inc.updateCallbacks { + cb(value) + } +} diff --git a/pkg/strategy/irr/strategy.go b/pkg/strategy/irr/strategy.go new file mode 100644 index 0000000000..75e5910d61 --- /dev/null +++ b/pkg/strategy/irr/strategy.go @@ -0,0 +1,400 @@ +package irr + +import ( + "context" + "fmt" + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/data/tsv" + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" + "os" + "sync" + + "github.com/sirupsen/logrus" +) + +const ID = "irr" + +var one = fixedpoint.One +var zero = fixedpoint.Zero +var Fee = 0.0008 // taker fee % * 2, for upper bound + +var log = logrus.WithField("strategy", ID) + +func init() { + bbgo.RegisterStrategy(ID, &Strategy{}) +} + +type Strategy struct { + Environment *bbgo.Environment + Symbol string `json:"symbol"` + Market types.Market + + types.IntervalWindow + + // persistence fields + Position *types.Position `persistence:"position"` + ProfitStats *types.ProfitStats `persistence:"profit_stats"` + TradeStats *types.TradeStats `persistence:"trade_stats"` + + activeOrders *bbgo.ActiveOrderBook + + ExitMethods bbgo.ExitMethodSet `json:"exits"` + + session *bbgo.ExchangeSession + orderExecutor *bbgo.GeneralOrderExecutor + + bbgo.QuantityOrAmount + nrr *NRR + + // StrategyController + bbgo.StrategyController + + AccountValueCalculator *bbgo.AccountValueCalculator + + // whether to draw graph or not by the end of backtest + DrawGraph bool `json:"drawGraph"` + GraphPNLPath string `json:"graphPNLPath"` + GraphCumPNLPath string `json:"graphCumPNLPath"` + + // for position + buyPrice float64 `persistence:"buy_price"` + sellPrice float64 `persistence:"sell_price"` + highestPrice float64 `persistence:"highest_price"` + lowestPrice float64 `persistence:"lowest_price"` + + // Accumulated profit report + AccumulatedProfitReport *AccumulatedProfitReport `json:"accumulatedProfitReport"` +} + +// AccumulatedProfitReport For accumulated profit report output +type AccumulatedProfitReport struct { + // AccumulatedProfitMAWindow Accumulated profit SMA window, in number of trades + AccumulatedProfitMAWindow int `json:"accumulatedProfitMAWindow"` + + // IntervalWindow interval window, in days + IntervalWindow int `json:"intervalWindow"` + + // NumberOfInterval How many intervals to output to TSV + NumberOfInterval int `json:"NumberOfInterval"` + + // TsvReportPath The path to output report to + TsvReportPath string `json:"tsvReportPath"` + + // AccumulatedDailyProfitWindow The window to sum up the daily profit, in days + AccumulatedDailyProfitWindow int `json:"accumulatedDailyProfitWindow"` + + // Accumulated profit + accumulatedProfit fixedpoint.Value + accumulatedProfitPerDay floats.Slice + previousAccumulatedProfit fixedpoint.Value + + // Accumulated profit MA + accumulatedProfitMA *indicator.SMA + accumulatedProfitMAPerDay floats.Slice + + // Daily profit + dailyProfit floats.Slice + + // Accumulated fee + accumulatedFee fixedpoint.Value + accumulatedFeePerDay floats.Slice + + // Win ratio + winRatioPerDay floats.Slice + + // Profit factor + profitFactorPerDay floats.Slice + + // Trade number + dailyTrades floats.Slice + accumulatedTrades int + previousAccumulatedTrades int +} + +func (r *AccumulatedProfitReport) Initialize() { + if r.AccumulatedProfitMAWindow <= 0 { + r.AccumulatedProfitMAWindow = 60 + } + if r.IntervalWindow <= 0 { + r.IntervalWindow = 7 + } + if r.AccumulatedDailyProfitWindow <= 0 { + r.AccumulatedDailyProfitWindow = 7 + } + if r.NumberOfInterval <= 0 { + r.NumberOfInterval = 1 + } + r.accumulatedProfitMA = &indicator.SMA{IntervalWindow: types.IntervalWindow{Interval: types.Interval1d, Window: r.AccumulatedProfitMAWindow}} +} + +func (r *AccumulatedProfitReport) RecordProfit(profit fixedpoint.Value) { + r.accumulatedProfit = r.accumulatedProfit.Add(profit) +} + +func (r *AccumulatedProfitReport) RecordTrade(fee fixedpoint.Value) { + r.accumulatedFee = r.accumulatedFee.Add(fee) + r.accumulatedTrades += 1 +} + +func (r *AccumulatedProfitReport) DailyUpdate(tradeStats *types.TradeStats) { + // Daily profit + r.dailyProfit.Update(r.accumulatedProfit.Sub(r.previousAccumulatedProfit).Float64()) + r.previousAccumulatedProfit = r.accumulatedProfit + + // Accumulated profit + r.accumulatedProfitPerDay.Update(r.accumulatedProfit.Float64()) + + // Accumulated profit MA + r.accumulatedProfitMA.Update(r.accumulatedProfit.Float64()) + r.accumulatedProfitMAPerDay.Update(r.accumulatedProfitMA.Last()) + + // Accumulated Fee + r.accumulatedFeePerDay.Update(r.accumulatedFee.Float64()) + + // Win ratio + r.winRatioPerDay.Update(tradeStats.WinningRatio.Float64()) + + // Profit factor + r.profitFactorPerDay.Update(tradeStats.ProfitFactor.Float64()) + + // Daily trades + r.dailyTrades.Update(float64(r.accumulatedTrades - r.previousAccumulatedTrades)) + r.previousAccumulatedTrades = r.accumulatedTrades +} + +// Output Accumulated profit report to a TSV file +func (r *AccumulatedProfitReport) Output(symbol string) { + if r.TsvReportPath != "" { + tsvwiter, err := tsv.AppendWriterFile(r.TsvReportPath) + if err != nil { + panic(err) + } + defer tsvwiter.Close() + // Output symbol, total acc. profit, acc. profit 60MA, interval acc. profit, fee, win rate, profit factor + _ = tsvwiter.Write([]string{"#", "Symbol", "accumulatedProfit", "accumulatedProfitMA", fmt.Sprintf("%dd profit", r.AccumulatedDailyProfitWindow), "accumulatedFee", "winRatio", "profitFactor", "60D trades"}) + for i := 0; i <= r.NumberOfInterval-1; i++ { + accumulatedProfit := r.accumulatedProfitPerDay.Index(r.IntervalWindow * i) + accumulatedProfitStr := fmt.Sprintf("%f", accumulatedProfit) + accumulatedProfitMA := r.accumulatedProfitMAPerDay.Index(r.IntervalWindow * i) + accumulatedProfitMAStr := fmt.Sprintf("%f", accumulatedProfitMA) + intervalAccumulatedProfit := r.dailyProfit.Tail(r.AccumulatedDailyProfitWindow+r.IntervalWindow*i).Sum() - r.dailyProfit.Tail(r.IntervalWindow*i).Sum() + intervalAccumulatedProfitStr := fmt.Sprintf("%f", intervalAccumulatedProfit) + accumulatedFee := fmt.Sprintf("%f", r.accumulatedFeePerDay.Index(r.IntervalWindow*i)) + winRatio := fmt.Sprintf("%f", r.winRatioPerDay.Index(r.IntervalWindow*i)) + profitFactor := fmt.Sprintf("%f", r.profitFactorPerDay.Index(r.IntervalWindow*i)) + trades := r.dailyTrades.Tail(60+r.IntervalWindow*i).Sum() - r.dailyTrades.Tail(r.IntervalWindow*i).Sum() + tradesStr := fmt.Sprintf("%f", trades) + + _ = tsvwiter.Write([]string{fmt.Sprintf("%d", i+1), symbol, accumulatedProfitStr, accumulatedProfitMAStr, intervalAccumulatedProfitStr, accumulatedFee, winRatio, profitFactor, tradesStr}) + } + } +} + +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) + + if !bbgo.IsBackTesting { + session.Subscribe(types.MarketTradeChannel, s.Symbol, types.SubscribeOptions{}) + } + + s.ExitMethods.SetAndSubscribe(session, s) +} + +func (s *Strategy) ID() string { + return ID +} + +func (s *Strategy) InstanceID() string { + return fmt.Sprintf("%s:%s", ID, s.Symbol) +} + +func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + var instanceID = s.InstanceID() + + if s.Position == nil { + s.Position = types.NewPositionFromMarket(s.Market) + } + + if s.ProfitStats == nil { + s.ProfitStats = types.NewProfitStats(s.Market) + } + + if s.TradeStats == nil { + s.TradeStats = types.NewTradeStats(s.Symbol) + } + + // StrategyController + s.Status = types.StrategyStatusRunning + + s.OnSuspend(func() { + // Cancel active orders + _ = s.orderExecutor.GracefulCancel(ctx) + }) + + s.OnEmergencyStop(func() { + // Cancel active orders + _ = s.orderExecutor.GracefulCancel(ctx) + // Close 100% position + // _ = s.ClosePosition(ctx, fixedpoint.One) + }) + + // initial required information + s.session = session + + // Set fee rate + if s.session.MakerFeeRate.Sign() > 0 || s.session.TakerFeeRate.Sign() > 0 { + s.Position.SetExchangeFeeRate(s.session.ExchangeName, types.ExchangeFee{ + MakerFeeRate: s.session.MakerFeeRate, + TakerFeeRate: s.session.TakerFeeRate, + }) + } + + s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) + s.orderExecutor.BindEnvironment(s.Environment) + s.orderExecutor.BindProfitStats(s.ProfitStats) + s.orderExecutor.BindTradeStats(s.TradeStats) + + // AccountValueCalculator + s.AccountValueCalculator = bbgo.NewAccountValueCalculator(s.session, s.Market.QuoteCurrency) + + // Accumulated profit report + if bbgo.IsBackTesting { + if s.AccumulatedProfitReport == nil { + s.AccumulatedProfitReport = &AccumulatedProfitReport{} + } + s.AccumulatedProfitReport.Initialize() + s.orderExecutor.TradeCollector().OnProfit(func(trade types.Trade, profit *types.Profit) { + if profit == nil { + return + } + + s.AccumulatedProfitReport.RecordProfit(profit.Profit) + }) + // s.orderExecutor.TradeCollector().OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { + // s.AccumulatedProfitReport.RecordTrade(trade.Fee) + // }) + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1d, func(kline types.KLine) { + s.AccumulatedProfitReport.DailyUpdate(s.TradeStats) + })) + } + + // For drawing + profitSlice := floats.Slice{1., 1.} + price, _ := session.LastPrice(s.Symbol) + initAsset := s.CalcAssetValue(price).Float64() + cumProfitSlice := floats.Slice{initAsset, initAsset} + + s.orderExecutor.TradeCollector().OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { + if bbgo.IsBackTesting { + s.AccumulatedProfitReport.RecordTrade(trade.Fee) + } + + // For drawing/charting + price := trade.Price.Float64() + if s.buyPrice > 0 { + profitSlice.Update(price / s.buyPrice) + cumProfitSlice.Update(s.CalcAssetValue(trade.Price).Float64()) + } else if s.sellPrice > 0 { + profitSlice.Update(s.sellPrice / price) + cumProfitSlice.Update(s.CalcAssetValue(trade.Price).Float64()) + } + if s.Position.IsDust(trade.Price) { + s.buyPrice = 0 + s.sellPrice = 0 + s.highestPrice = 0 + s.lowestPrice = 0 + } else if s.Position.IsLong() { + s.buyPrice = price + s.sellPrice = 0 + s.highestPrice = s.buyPrice + s.lowestPrice = 0 + } else { + s.sellPrice = price + s.buyPrice = 0 + s.highestPrice = 0 + s.lowestPrice = s.sellPrice + } + }) + + s.orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { + bbgo.Sync(ctx, s) + }) + s.orderExecutor.Bind() + s.activeOrders = bbgo.NewActiveOrderBook(s.Symbol) + + for _, method := range s.ExitMethods { + method.Bind(session, s.orderExecutor) + } + + kLineStore, _ := s.session.MarketDataStore(s.Symbol) + s.nrr = &NRR{IntervalWindow: types.IntervalWindow{Window: 2, Interval: s.Interval}, RankingWindow: s.Window} + s.nrr.BindK(s.session.MarketDataStream, s.Symbol, s.Interval) + if klines, ok := kLineStore.KLinesOfInterval(s.nrr.Interval); ok { + s.nrr.LoadK((*klines)[0:]) + } + + // startTime := s.Environment.StartTime() + // s.TradeStats.SetIntervalProfitCollector(types.NewIntervalProfitCollector(types.Interval1h, startTime)) + + s.session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { + + // ts_rank(): transformed to [0~1] which divided equally + // queued first signal as its initial process + // important: delayed signal in order to submit order at current kline close (a.k.a. next open while in production) + // instead of right in current kline open + + // alpha-weighted assets (inventory and capital) + targetBase := s.QuantityOrAmount.CalculateQuantity(kline.Close).Mul(fixedpoint.NewFromFloat(s.nrr.RankedValues.Index(1))) + diffQty := targetBase.Sub(s.Position.Base) + + log.Infof("decision alpah: %f, ranked negative return: %f, current position: %f, target position diff: %f", s.nrr.RankedValues.Index(1), s.nrr.RankedValues.Last(), s.Position.Base.Float64(), diffQty.Float64()) + + // use kline direction to prevent reversing position too soon + if diffQty.Sign() > 0 { // && kline.Direction() >= 0 + _, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeBuy, + Quantity: diffQty.Abs(), + Type: types.OrderTypeMarket, + Tag: "irr buy more", + }) + } else if diffQty.Sign() < 0 { // && kline.Direction() <= 0 + _, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeSell, + Quantity: diffQty.Abs(), + Type: types.OrderTypeMarket, + Tag: "irr sell more", + }) + } + + })) + + bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { + defer wg.Done() + + // Output accumulated profit report + if bbgo.IsBackTesting { + defer s.AccumulatedProfitReport.Output(s.Symbol) + + if s.DrawGraph { + if err := s.Draw(&profitSlice, &cumProfitSlice); err != nil { + log.WithError(err).Errorf("cannot draw graph") + } + } + } + + _, _ = fmt.Fprintln(os.Stderr, s.TradeStats.String()) + _ = s.orderExecutor.GracefulCancel(ctx) + }) + + return nil +} + +func (s *Strategy) CalcAssetValue(price fixedpoint.Value) fixedpoint.Value { + balances := s.session.GetAccount().Balances() + return balances[s.Market.BaseCurrency].Total().Mul(price).Add(balances[s.Market.QuoteCurrency].Total()) +} diff --git a/pkg/strategy/kline/strategy.go b/pkg/strategy/kline/strategy.go index 1dae69be4b..2c800efaa2 100644 --- a/pkg/strategy/kline/strategy.go +++ b/pkg/strategy/kline/strategy.go @@ -2,6 +2,7 @@ package kline import ( "context" + "github.com/sirupsen/logrus" "github.com/c9s/bbgo/pkg/bbgo" @@ -26,7 +27,7 @@ func (s *Strategy) ID() string { } func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { - session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: string(s.MovingAverage.Interval)}) + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.MovingAverage.Interval}) } func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { diff --git a/pkg/strategy/marketcap/strategy.go b/pkg/strategy/marketcap/strategy.go new file mode 100644 index 0000000000..83800f573b --- /dev/null +++ b/pkg/strategy/marketcap/strategy.go @@ -0,0 +1,262 @@ +package marketcap + +import ( + "context" + "fmt" + "os" + + "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/datasource/coinmarketcap" + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +const ID = "marketcap" + +var log = logrus.WithField("strategy", ID) + +func init() { + bbgo.RegisterStrategy(ID, &Strategy{}) +} + +type Strategy struct { + datasource *coinmarketcap.DataSource + + // interval to rebalance the portfolio + Interval types.Interval `json:"interval"` + QuoteCurrency string `json:"quoteCurrency"` + QuoteCurrencyWeight fixedpoint.Value `json:"quoteCurrencyWeight"` + BaseCurrencies []string `json:"baseCurrencies"` + Threshold fixedpoint.Value `json:"threshold"` + DryRun bool `json:"dryRun"` + // max amount to buy or sell per order + MaxAmount fixedpoint.Value `json:"maxAmount"` + // interval to query marketcap data from coinmarketcap + QueryInterval types.Interval `json:"queryInterval"` + + subscribeSymbol string + activeOrderBook *bbgo.ActiveOrderBook + targetWeights types.ValueMap +} + +func (s *Strategy) Initialize() error { + apiKey := os.Getenv("COINMARKETCAP_API_KEY") + s.datasource = coinmarketcap.New(apiKey) + + // select one symbol to subscribe + s.subscribeSymbol = s.BaseCurrencies[0] + s.QuoteCurrency + + s.activeOrderBook = bbgo.NewActiveOrderBook("") + s.targetWeights = types.ValueMap{} + return nil +} + +func (s *Strategy) ID() string { + return ID +} + +func (s *Strategy) Validate() error { + if len(s.BaseCurrencies) == 0 { + return fmt.Errorf("taretCurrencies should not be empty") + } + + for _, c := range s.BaseCurrencies { + if c == s.QuoteCurrency { + return fmt.Errorf("targetCurrencies contain baseCurrency") + } + } + + if s.Threshold.Sign() < 0 { + return fmt.Errorf("threshold should not less than 0") + } + + if s.MaxAmount.Sign() < 0 { + return fmt.Errorf("maxAmount shoud not less than 0") + } + + return nil +} + +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { + symbol := s.BaseCurrencies[0] + s.QuoteCurrency + session.Subscribe(types.KLineChannel, symbol, types.SubscribeOptions{Interval: s.Interval}) + session.Subscribe(types.KLineChannel, symbol, types.SubscribeOptions{Interval: s.QueryInterval}) +} + +func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + s.activeOrderBook.BindStream(session.UserDataStream) + + s.updateTargetWeights(ctx) + session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { + if kline.Interval == s.QueryInterval { + s.updateTargetWeights(ctx) + } + + if kline.Interval == s.Interval { + s.rebalance(ctx, orderExecutor, session) + } + }) + return nil +} + +func (s *Strategy) rebalance(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) { + if err := orderExecutor.CancelOrders(ctx, s.activeOrderBook.Orders()...); err != nil { + log.WithError(err).Error("failed to cancel orders") + } + + submitOrders := s.generateSubmitOrders(ctx, session) + for _, submitOrder := range submitOrders { + log.Infof("generated submit order: %s", submitOrder.String()) + } + + if s.DryRun { + return + } + + createdOrders, err := orderExecutor.SubmitOrders(ctx, submitOrders...) + if err != nil { + log.WithError(err).Error("failed to submit orders") + return + } + + s.activeOrderBook.Add(createdOrders...) +} + +func (s *Strategy) generateSubmitOrders(ctx context.Context, session *bbgo.ExchangeSession) (submitOrders []types.SubmitOrder) { + prices := s.prices(ctx, session) + marketValues := prices.Mul(s.quantities(session)) + currentWeights := marketValues.Normalize() + + for currency, targetWeight := range s.targetWeights { + if currency == s.QuoteCurrency { + continue + } + symbol := currency + s.QuoteCurrency + currentWeight := currentWeights[currency] + currentPrice := prices[currency] + + log.Infof("%s price: %v, current weight: %v, target weight: %v", + symbol, + currentPrice, + currentWeight, + targetWeight) + + // calculate the difference between current weight and target weight + // if the difference is less than threshold, then we will not create the order + weightDifference := targetWeight.Sub(currentWeight) + if weightDifference.Abs().Compare(s.Threshold) < 0 { + log.Infof("%s weight distance |%v - %v| = |%v| less than the threshold: %v", + symbol, + currentWeight, + targetWeight, + weightDifference, + s.Threshold) + continue + } + + quantity := weightDifference.Mul(marketValues.Sum()).Div(currentPrice) + + side := types.SideTypeBuy + if quantity.Sign() < 0 { + side = types.SideTypeSell + quantity = quantity.Abs() + } + + if s.MaxAmount.Sign() > 0 { + quantity = bbgo.AdjustQuantityByMaxAmount(quantity, currentPrice, s.MaxAmount) + log.Infof("adjust the quantity %v (%s %s @ %v) by max amount %v", + quantity, + symbol, + side.String(), + currentPrice, + s.MaxAmount) + } + + order := types.SubmitOrder{ + Symbol: symbol, + Side: side, + Type: types.OrderTypeLimit, + Quantity: quantity, + Price: currentPrice, + } + + submitOrders = append(submitOrders, order) + } + return submitOrders +} + +func (s *Strategy) updateTargetWeights(ctx context.Context) { + m := floats.Map{} + + // get marketcap from coinmarketcap + // set higher query limit to avoid target currency not in the list + marketcaps, err := s.datasource.QueryMarketCapInUSD(ctx, 100) + if err != nil { + log.WithError(err).Error("failed to query market cap") + } + + for _, currency := range s.BaseCurrencies { + m[currency] = marketcaps[currency] + } + + // normalize + m = m.Normalize() + + // rescale by 1 - baseWeight + m = m.MulScalar(1.0 - s.QuoteCurrencyWeight.Float64()) + + // append base weight + m[s.QuoteCurrency] = s.QuoteCurrencyWeight.Float64() + + // convert to types.ValueMap + for currency, weight := range m { + s.targetWeights[currency] = fixedpoint.NewFromFloat(weight) + } + + log.Infof("target weights: %v", s.targetWeights) +} + +func (s *Strategy) prices(ctx context.Context, session *bbgo.ExchangeSession) types.ValueMap { + tickers, err := session.Exchange.QueryTickers(ctx, s.symbols()...) + if err != nil { + log.WithError(err).Error("failed to query tickers") + return nil + } + + prices := types.ValueMap{} + for _, currency := range s.BaseCurrencies { + prices[currency] = tickers[currency+s.QuoteCurrency].Last + } + + // append base currency price + prices[s.QuoteCurrency] = fixedpoint.One + + return prices +} + +func (s *Strategy) quantities(session *bbgo.ExchangeSession) types.ValueMap { + balances := session.Account.Balances() + + quantities := types.ValueMap{} + for _, currency := range s.currencies() { + quantities[currency] = balances[currency].Total() + } + + return quantities +} + +func (s *Strategy) symbols() (symbols []string) { + for _, currency := range s.BaseCurrencies { + symbols = append(symbols, currency+s.QuoteCurrency) + } + return symbols +} + +func (s *Strategy) currencies() (currencies []string) { + currencies = append(currencies, s.BaseCurrencies...) + currencies = append(currencies, s.QuoteCurrency) + return currencies +} diff --git a/pkg/strategy/pivotshort/breaklow.go b/pkg/strategy/pivotshort/breaklow.go new file mode 100644 index 0000000000..91f9f0eed9 --- /dev/null +++ b/pkg/strategy/pivotshort/breaklow.go @@ -0,0 +1,286 @@ +package pivotshort + +import ( + "context" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +type FakeBreakStop struct { + types.IntervalWindow +} + +// BreakLow -- when price breaks the previous pivot low, we set a trade entry +type BreakLow struct { + Symbol string + Market types.Market + types.IntervalWindow + + // FastWindow is used for fast pivot (this is to to filter the nearest high/low) + FastWindow int `json:"fastWindow"` + + // Ratio is a number less than 1.0, price * ratio will be the price triggers the short order. + Ratio fixedpoint.Value `json:"ratio"` + + bbgo.OpenPositionOptions + + // BounceRatio is a ratio used for placing the limit order sell price + // limit sell price = breakLowPrice * (1 + BounceRatio) + BounceRatio fixedpoint.Value `json:"bounceRatio"` + + StopEMA *bbgo.StopEMA `json:"stopEMA"` + + TrendEMA *bbgo.TrendEMA `json:"trendEMA"` + + FakeBreakStop *FakeBreakStop `json:"fakeBreakStop"` + + lastLow, lastFastLow fixedpoint.Value + + lastLowInvalidated bool + + // lastBreakLow is the low that the price just break + lastBreakLow fixedpoint.Value + + pivotLow, fastPivotLow *indicator.PivotLow + pivotLowPrices []fixedpoint.Value + + orderExecutor *bbgo.GeneralOrderExecutor + session *bbgo.ExchangeSession + + // StrategyController + bbgo.StrategyController +} + +func (s *BreakLow) Subscribe(session *bbgo.ExchangeSession) { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: types.Interval1m}) + + if s.StopEMA != nil { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.StopEMA.Interval}) + } + + if s.TrendEMA != nil { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.TrendEMA.Interval}) + } + + if s.FakeBreakStop != nil { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.FakeBreakStop.Interval}) + } +} + +func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor) { + if s.FastWindow == 0 { + s.FastWindow = 3 + } + + s.session = session + s.orderExecutor = orderExecutor + + // StrategyController + s.Status = types.StrategyStatusRunning + + position := orderExecutor.Position() + symbol := position.Symbol + standardIndicator := session.StandardIndicatorSet(s.Symbol) + + s.lastLow = fixedpoint.Zero + s.pivotLow = standardIndicator.PivotLow(s.IntervalWindow) + s.fastPivotLow = standardIndicator.PivotLow(types.IntervalWindow{ + Interval: s.Interval, + Window: s.FastWindow, // make it faster + }) + + if s.StopEMA != nil { + s.StopEMA.Bind(session, orderExecutor) + } + + if s.TrendEMA != nil { + s.TrendEMA.Bind(session, orderExecutor) + } + + // update pivot low data + session.MarketDataStream.OnStart(func() { + if s.updatePivotLow() { + bbgo.Notify("%s new pivot low: %f", s.Symbol, s.pivotLow.Last()) + } + + s.pilotQuantityCalculation() + }) + + session.MarketDataStream.OnKLineClosed(types.KLineWith(symbol, s.Interval, func(kline types.KLine) { + if s.updatePivotLow() { + // when position is opened, do not send pivot low notify + if position.IsOpened(kline.Close) { + return + } + + bbgo.Notify("%s new pivot low: %f", s.Symbol, s.pivotLow.Last()) + } + })) + + if s.FakeBreakStop != nil { + // if the position is already opened, and we just break the low, this checks if the kline closed above the low, + // so that we can close the position earlier + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.FakeBreakStop.Interval, func(k types.KLine) { + // make sure the position is opened, and it's a short position + if !position.IsOpened(k.Close) || !position.IsShort() { + return + } + + // make sure we recorded the last break low + if s.lastBreakLow.IsZero() { + return + } + + // the kline opened below the last break low, and closed above the last break low + if k.Open.Compare(s.lastBreakLow) < 0 && k.Close.Compare(s.lastBreakLow) > 0 { + bbgo.Notify("kLine closed above the last break low, triggering stop earlier") + if err := s.orderExecutor.ClosePosition(context.Background(), one, "fakeBreakStop"); err != nil { + log.WithError(err).Error("position close error") + } + + // reset to zero + s.lastBreakLow = fixedpoint.Zero + } + })) + } + + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, func(kline types.KLine) { + if len(s.pivotLowPrices) == 0 || s.lastLow.IsZero() { + log.Infof("currently there is no pivot low prices, can not check break low...") + return + } + + if s.lastLowInvalidated { + log.Infof("the last low is invalidated, skip") + return + } + + previousLow := s.lastLow + ratio := fixedpoint.One.Add(s.Ratio) + breakPrice := previousLow.Mul(ratio) + + // StrategyController + if s.Status != types.StrategyStatusRunning { + return + } + + openPrice := kline.Open + closePrice := kline.Close + + // if the previous low is not break, or the kline is not strong enough to break it, skip + if closePrice.Compare(breakPrice) >= 0 { + return + } + + // we need the price cross the break line, or we do nothing: + // 1) open > break price > close price + // 2) high > break price > open price and close price + // v2 + if !((openPrice.Compare(breakPrice) > 0 && closePrice.Compare(breakPrice) < 0) || + (kline.High.Compare(breakPrice) > 0 && openPrice.Compare(breakPrice) < 0 && closePrice.Compare(breakPrice) < 0)) { + return + } + + // force direction to be down + if closePrice.Compare(openPrice) >= 0 { + bbgo.Notify("%s price %f is closed higher than the open price %f, skip this break", kline.Symbol, closePrice.Float64(), openPrice.Float64()) + // skip UP klines + return + } + + bbgo.Notify("%s breakLow signal detected, closed price %f < breakPrice %f", kline.Symbol, closePrice.Float64(), breakPrice.Float64(), kline) + + if s.lastBreakLow.IsZero() || previousLow.Compare(s.lastBreakLow) < 0 { + s.lastBreakLow = previousLow + } + + if position.IsOpened(kline.Close) { + bbgo.Notify("%s position is already opened, skip", s.Symbol) + return + } + + // trend EMA protection + if s.TrendEMA != nil && !s.TrendEMA.GradientAllowed() { + bbgo.Notify("trendEMA protection: %s close price %f, gradient %f", s.Symbol, kline.Close.Float64(), s.TrendEMA.Gradient()) + return + } + + // stop EMA protection + if s.StopEMA != nil { + if !s.StopEMA.Allowed(closePrice) { + return + } + } + + ctx := context.Background() + + // graceful cancel all active orders + _ = orderExecutor.GracefulCancel(ctx) + + bbgo.Notify("%s price %f breaks the previous low %f with ratio %f, opening short position", symbol, kline.Close.Float64(), previousLow.Float64(), s.Ratio.Float64()) + opts := s.OpenPositionOptions + opts.Short = true + opts.Price = closePrice + opts.Tags = []string{"breakLowMarket"} + if opts.LimitOrder && !s.BounceRatio.IsZero() { + opts.Price = previousLow.Mul(fixedpoint.One.Add(s.BounceRatio)) + } + + if _, err := s.orderExecutor.OpenPosition(ctx, opts); err != nil { + log.WithError(err).Errorf("failed to open short position") + } + })) +} + +func (s *BreakLow) pilotQuantityCalculation() { + if s.lastLow.IsZero() { + return + } + + log.Infof("pilot calculation for max position: last low = %f, quantity = %f, leverage = %f", + s.lastLow.Float64(), + s.Quantity.Float64(), + s.Leverage.Float64()) + + quantity, err := bbgo.CalculateBaseQuantity(s.session, s.Market, s.lastLow, s.Quantity, s.Leverage) + if err != nil { + log.WithError(err).Errorf("quantity calculation error") + } + + if quantity.IsZero() { + log.WithError(err).Errorf("quantity is zero, can not submit order") + return + } + + bbgo.Notify("%s %f quantity will be used for shorting", s.Symbol, quantity.Float64()) +} + +func (s *BreakLow) updatePivotLow() bool { + low := fixedpoint.NewFromFloat(s.pivotLow.Last()) + if low.IsZero() { + return false + } + + // if the last low is different + lastLowChanged := low.Compare(s.lastLow) != 0 + if lastLowChanged { + s.lastLow = low + s.lastLowInvalidated = false + s.pivotLowPrices = append(s.pivotLowPrices, low) + } + + fastLow := fixedpoint.NewFromFloat(s.fastPivotLow.Last()) + if !fastLow.IsZero() { + if fastLow.Compare(s.lastLow) < 0 { + s.lastLowInvalidated = true + lastLowChanged = false + } + s.lastFastLow = fastLow + } + + return lastLowChanged +} diff --git a/pkg/strategy/pivotshort/failedbreakhigh.go b/pkg/strategy/pivotshort/failedbreakhigh.go new file mode 100644 index 0000000000..eb193e994f --- /dev/null +++ b/pkg/strategy/pivotshort/failedbreakhigh.go @@ -0,0 +1,404 @@ +package pivotshort + +import ( + "context" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +type MACDDivergence struct { + *indicator.MACDConfig + PivotWindow int `json:"pivotWindow"` +} + +// FailedBreakHigh -- when price breaks the previous pivot low, we set a trade entry +type FailedBreakHigh struct { + Symbol string + Market types.Market + + // IntervalWindow is used for finding the pivot high + types.IntervalWindow + + FastWindow int + + bbgo.OpenPositionOptions + + // BreakInterval is used for checking failed break + BreakInterval types.Interval `json:"breakInterval"` + + Enabled bool `json:"enabled"` + + // Ratio is a number less than 1.0, price * ratio will be the price triggers the short order. + Ratio fixedpoint.Value `json:"ratio"` + + // EarlyStopRatio adjusts the break high price with the given ratio + // this is for stop loss earlier if the price goes above the previous price + EarlyStopRatio fixedpoint.Value `json:"earlyStopRatio"` + + VWMA *types.IntervalWindow `json:"vwma"` + + StopEMA *bbgo.StopEMA `json:"stopEMA"` + + TrendEMA *bbgo.TrendEMA `json:"trendEMA"` + + MACDDivergence *MACDDivergence `json:"macdDivergence"` + + macd *indicator.MACD + + macdTopDivergence bool + + lastFailedBreakHigh, lastHigh, lastFastHigh fixedpoint.Value + lastHighInvalidated bool + pivotHighPrices []fixedpoint.Value + + pivotHigh, fastPivotHigh *indicator.PivotHigh + vwma *indicator.VWMA + + orderExecutor *bbgo.GeneralOrderExecutor + session *bbgo.ExchangeSession + + // StrategyController + bbgo.StrategyController +} + +func (s *FailedBreakHigh) Subscribe(session *bbgo.ExchangeSession) { + if s.BreakInterval == "" { + s.BreakInterval = types.Interval1m + } + + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: types.Interval1m}) + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.BreakInterval}) + + if s.StopEMA != nil { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.StopEMA.Interval}) + } + + if s.TrendEMA != nil { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.TrendEMA.Interval}) + } + + if s.MACDDivergence != nil { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.MACDDivergence.Interval}) + } +} + +func (s *FailedBreakHigh) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor) { + s.session = session + s.orderExecutor = orderExecutor + + if !s.Enabled { + return + } + + // set default value for StrategyController + s.Status = types.StrategyStatusRunning + + if s.FastWindow == 0 { + s.FastWindow = 3 + } + + position := orderExecutor.Position() + symbol := position.Symbol + standardIndicator := session.StandardIndicatorSet(s.Symbol) + + s.lastHigh = fixedpoint.Zero + s.pivotHigh = standardIndicator.PivotHigh(s.IntervalWindow) + s.fastPivotHigh = standardIndicator.PivotHigh(types.IntervalWindow{ + Interval: s.IntervalWindow.Interval, + Window: s.FastWindow, + }) + + // Experimental: MACD divergence detection + if s.MACDDivergence != nil { + log.Infof("MACD divergence detection is enabled") + s.macd = standardIndicator.MACD(s.MACDDivergence.IntervalWindow, s.MACDDivergence.ShortPeriod, s.MACDDivergence.LongPeriod) + s.macd.OnUpdate(func(macd, signal, histogram float64) { + log.Infof("MACD %+v: macd: %f, signal: %f histogram: %f", s.macd.IntervalWindow, macd, signal, histogram) + s.detectMacdDivergence() + }) + s.detectMacdDivergence() + } + + if s.VWMA != nil { + s.vwma = standardIndicator.VWMA(types.IntervalWindow{ + Interval: s.BreakInterval, + Window: s.VWMA.Window, + }) + } + + if s.StopEMA != nil { + s.StopEMA.Bind(session, orderExecutor) + } + + if s.TrendEMA != nil { + s.TrendEMA.Bind(session, orderExecutor) + } + + // update pivot low data + session.MarketDataStream.OnStart(func() { + if s.updatePivotHigh() { + bbgo.Notify("%s new pivot high: %f", s.Symbol, s.pivotHigh.Last()) + } + + s.pilotQuantityCalculation() + }) + + session.MarketDataStream.OnKLineClosed(types.KLineWith(symbol, s.Interval, func(kline types.KLine) { + if s.updatePivotHigh() { + // when position is opened, do not send pivot low notify + if position.IsOpened(kline.Close) { + return + } + + bbgo.Notify("%s new pivot high: %f", s.Symbol, s.pivotHigh.Last()) + } + })) + + // if the position is already opened, and we just break the low, this checks if the kline closed above the low, + // so that we can close the position earlier + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.BreakInterval, func(k types.KLine) { + if !s.Enabled { + return + } + + // StrategyController + if s.Status != types.StrategyStatusRunning { + return + } + + // make sure the position is opened, and it's a short position + if !position.IsOpened(k.Close) || !position.IsShort() { + return + } + + // make sure we recorded the last break low + if s.lastFailedBreakHigh.IsZero() { + return + } + + lastHigh := s.lastFastHigh + + if !s.EarlyStopRatio.IsZero() { + lastHigh = lastHigh.Mul(one.Add(s.EarlyStopRatio)) + } + + // the kline opened below the last break low, and closed above the last break low + if k.Open.Compare(lastHigh) < 0 && k.Close.Compare(lastHigh) > 0 && k.Open.Compare(k.Close) > 0 { + bbgo.Notify("kLine closed %f above the last break high %f (ratio %f), triggering stop earlier", k.Close.Float64(), lastHigh.Float64(), s.EarlyStopRatio.Float64()) + + if err := s.orderExecutor.ClosePosition(context.Background(), one, "failedBreakHighStop"); err != nil { + log.WithError(err).Error("position close error") + } + + // reset to zero + s.lastFailedBreakHigh = fixedpoint.Zero + } + })) + + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.BreakInterval, func(kline types.KLine) { + if len(s.pivotHighPrices) == 0 || s.lastHigh.IsZero() { + log.Infof("%s currently there is no pivot high prices, can not check failed break high...", s.Symbol) + return + } + + if s.lastHighInvalidated { + log.Infof("%s last high %f is invalidated by the fast pivot", s.Symbol, s.lastHigh.Float64()) + return + } + + // StrategyController + if s.Status != types.StrategyStatusRunning { + return + } + + previousHigh := s.lastHigh + ratio := fixedpoint.One.Add(s.Ratio) + breakPrice := previousHigh.Mul(ratio) + + openPrice := kline.Open + closePrice := kline.Close + + // we need few conditions: + // 1) kline.High is higher than the previous high + // 2) kline.Close is lower than the previous high + if kline.High.Compare(breakPrice) < 0 || closePrice.Compare(breakPrice) >= 0 { + return + } + + // 3) kline.Close is lower than kline.Open + if closePrice.Compare(openPrice) > 0 { + bbgo.Notify("the %s closed price %f is higher than the open price %f, skip failed break high short", s.Symbol, closePrice.Float64(), openPrice.Float64()) + return + } + + if s.vwma != nil { + vma := fixedpoint.NewFromFloat(s.vwma.Last()) + if kline.Volume.Compare(vma) < 0 { + bbgo.Notify("%s %s kline volume %f is less than VMA %f, skip failed break high short", kline.Symbol, kline.Interval, kline.Volume.Float64(), vma.Float64()) + return + } + } + + bbgo.Notify("%s FailedBreakHigh signal detected, closed price %f < breakPrice %f", kline.Symbol, closePrice.Float64(), breakPrice.Float64()) + + if position.IsOpened(kline.Close) { + bbgo.Notify("%s position is already opened, skip", s.Symbol) + return + } + + // trend EMA protection + if s.TrendEMA != nil && !s.TrendEMA.GradientAllowed() { + bbgo.Notify("trendEMA protection: close price %f, gradient %f", kline.Close.Float64(), s.TrendEMA.Gradient()) + return + } + + // stop EMA protection + if s.StopEMA != nil { + if !s.StopEMA.Allowed(closePrice) { + bbgo.Notify("stopEMA protection: close price %f %s", kline.Close.Float64(), s.StopEMA.String()) + return + } + } + + if s.macd != nil && !s.macdTopDivergence { + bbgo.Notify("Detected MACD top divergence") + return + } + + if s.lastFailedBreakHigh.IsZero() || previousHigh.Compare(s.lastFailedBreakHigh) < 0 { + s.lastFailedBreakHigh = previousHigh + } + + ctx := context.Background() + + bbgo.Notify("%s price %f failed breaking the previous high %f with ratio %f, opening short position", + symbol, + kline.Close.Float64(), + previousHigh.Float64(), + s.Ratio.Float64()) + + // graceful cancel all active orders + _ = orderExecutor.GracefulCancel(ctx) + + opts := s.OpenPositionOptions + opts.Short = true + opts.Price = closePrice + opts.Tags = []string{"FailedBreakHighMarket"} + if _, err := s.orderExecutor.OpenPosition(ctx, opts); err != nil { + log.WithError(err).Errorf("failed to open short position") + } + })) +} + +func (s *FailedBreakHigh) pilotQuantityCalculation() { + if s.lastHigh.IsZero() { + return + } + + log.Infof("pilot calculation for max position: last low = %f, quantity = %f, leverage = %f", + s.lastHigh.Float64(), + s.Quantity.Float64(), + s.Leverage.Float64()) + + quantity, err := bbgo.CalculateBaseQuantity(s.session, s.Market, s.lastHigh, s.Quantity, s.Leverage) + if err != nil { + log.WithError(err).Errorf("quantity calculation error") + } + + if quantity.IsZero() { + log.WithError(err).Errorf("quantity is zero, can not submit order") + return + } + + bbgo.Notify("%s %f quantity will be used for failed break high short", s.Symbol, quantity.Float64()) +} + +func (s *FailedBreakHigh) detectMacdDivergence() { + if s.MACDDivergence == nil { + return + } + + // always reset the top divergence to false + s.macdTopDivergence = false + + histogramValues := s.macd.Histogram + + pivotWindow := s.MACDDivergence.PivotWindow + if pivotWindow == 0 { + pivotWindow = 3 + } + + if len(histogramValues) < pivotWindow*2 { + log.Warnf("histogram values is not enough for finding pivots, length=%d", len(histogramValues)) + return + } + + var histogramPivots floats.Slice + for i := pivotWindow; i > 0 && i < len(histogramValues); i++ { + // find positive histogram and the top + pivot, ok := floats.CalculatePivot(histogramValues[0:i], pivotWindow, pivotWindow, func(a, pivot float64) bool { + return pivot > 0 && pivot > a + }) + if ok { + histogramPivots = append(histogramPivots, pivot) + } + } + log.Infof("histogram pivots: %+v", histogramPivots) + + // take the last 2-3 pivots to check if there is a divergence + if len(histogramPivots) < 3 { + return + } + + histogramPivots = histogramPivots[len(histogramPivots)-3:] + minDiff := 0.01 + for i := len(histogramPivots) - 1; i > 0; i-- { + p1 := histogramPivots[i] + p2 := histogramPivots[i-1] + diff := p1 - p2 + + if diff > -minDiff || diff > minDiff { + continue + } + + // negative value = MACD top divergence + if diff < -minDiff { + log.Infof("MACD TOP DIVERGENCE DETECTED: diff %f", diff) + s.macdTopDivergence = true + } else { + s.macdTopDivergence = false + } + return + } +} + +func (s *FailedBreakHigh) updatePivotHigh() bool { + high := fixedpoint.NewFromFloat(s.pivotHigh.Last()) + if high.IsZero() { + return false + } + + lastHighChanged := high.Compare(s.lastHigh) != 0 + if lastHighChanged { + s.lastHigh = high + s.lastHighInvalidated = false + s.pivotHighPrices = append(s.pivotHighPrices, high) + } + + fastHigh := fixedpoint.NewFromFloat(s.fastPivotHigh.Last()) + if !fastHigh.IsZero() { + if fastHigh.Compare(s.lastHigh) > 0 { + // invalidate the last low + lastHighChanged = false + s.lastHighInvalidated = true + } + s.lastFastHigh = fastHigh + } + + return lastHighChanged +} diff --git a/pkg/strategy/pivotshort/resistance.go b/pkg/strategy/pivotshort/resistance.go new file mode 100644 index 0000000000..744abefba7 --- /dev/null +++ b/pkg/strategy/pivotshort/resistance.go @@ -0,0 +1,207 @@ +package pivotshort + +import ( + "context" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +type ResistanceShort struct { + Enabled bool `json:"enabled"` + Symbol string `json:"-"` + Market types.Market `json:"-"` + + types.IntervalWindow + + MinDistance fixedpoint.Value `json:"minDistance"` + GroupDistance fixedpoint.Value `json:"groupDistance"` + NumLayers int `json:"numLayers"` + LayerSpread fixedpoint.Value `json:"layerSpread"` + Quantity fixedpoint.Value `json:"quantity"` + Leverage fixedpoint.Value `json:"leverage"` + Ratio fixedpoint.Value `json:"ratio"` + + TrendEMA *bbgo.TrendEMA `json:"trendEMA"` + + session *bbgo.ExchangeSession + orderExecutor *bbgo.GeneralOrderExecutor + + resistancePivot *indicator.PivotLow + resistancePrices []float64 + currentResistancePrice fixedpoint.Value + + activeOrders *bbgo.ActiveOrderBook + + // StrategyController + bbgo.StrategyController +} + +func (s *ResistanceShort) Subscribe(session *bbgo.ExchangeSession) { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) + + if s.TrendEMA != nil { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.TrendEMA.Interval}) + } +} + +func (s *ResistanceShort) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor) { + if s.GroupDistance.IsZero() { + s.GroupDistance = fixedpoint.NewFromFloat(0.01) + } + + s.session = session + s.orderExecutor = orderExecutor + s.activeOrders = bbgo.NewActiveOrderBook(s.Symbol) + s.activeOrders.OnFilled(func(o types.Order) { + // reset resistance price + s.currentResistancePrice = fixedpoint.Zero + }) + s.activeOrders.BindStream(session.UserDataStream) + + // StrategyController + s.Status = types.StrategyStatusRunning + + if s.TrendEMA != nil { + s.TrendEMA.Bind(session, orderExecutor) + } + + s.resistancePivot = session.StandardIndicatorSet(s.Symbol).PivotLow(s.IntervalWindow) + + // use the last kline from the history before we get the next closed kline + s.updateResistanceOrders(fixedpoint.NewFromFloat(s.resistancePivot.Last())) + + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { + // StrategyController + if s.Status != types.StrategyStatusRunning { + return + } + + // trend EMA protection + if s.TrendEMA != nil && !s.TrendEMA.GradientAllowed() { + return + } + + position := s.orderExecutor.Position() + if position.IsOpened(kline.Close) { + return + } + + s.updateResistanceOrders(kline.Close) + })) +} + +// updateCurrentResistancePrice updates the current resistance price +// we should only update the resistance price when: +// 1) the close price is already above the current resistance price by (1 + minDistance) +// 2) the next resistance price is lower than the current resistance price. +func (s *ResistanceShort) updateCurrentResistancePrice(closePrice fixedpoint.Value) bool { + minDistance := s.MinDistance.Float64() + groupDistance := s.GroupDistance.Float64() + resistancePrices := findPossibleResistancePrices(closePrice.Float64()*(1.0+minDistance), groupDistance, s.resistancePivot.Values.Tail(6)) + if len(resistancePrices) == 0 { + return false + } + + log.Infof("%s close price: %f, min distance: %f, possible resistance prices: %+v", s.Symbol, closePrice.Float64(), minDistance, resistancePrices) + + nextResistancePrice := fixedpoint.NewFromFloat(resistancePrices[0]) + + if s.currentResistancePrice.IsZero() { + s.currentResistancePrice = nextResistancePrice + return true + } + + // if the current sell price is out-dated + // or + // the next resistance is lower than the current one. + minPriceToUpdate := s.currentResistancePrice.Mul(one.Add(s.MinDistance)) + if closePrice.Compare(minPriceToUpdate) > 0 || nextResistancePrice.Compare(s.currentResistancePrice) < 0 { + s.currentResistancePrice = nextResistancePrice + return true + } + + return false +} + +func (s *ResistanceShort) updateResistanceOrders(closePrice fixedpoint.Value) { + ctx := context.Background() + resistanceUpdated := s.updateCurrentResistancePrice(closePrice) + if resistanceUpdated { + s.placeResistanceOrders(ctx, s.currentResistancePrice) + } else if s.activeOrders.NumOfOrders() == 0 && !s.currentResistancePrice.IsZero() { + s.placeResistanceOrders(ctx, s.currentResistancePrice) + } +} + +func (s *ResistanceShort) placeResistanceOrders(ctx context.Context, resistancePrice fixedpoint.Value) { + totalQuantity, err := bbgo.CalculateBaseQuantity(s.session, s.Market, resistancePrice, s.Quantity, s.Leverage) + if err != nil { + log.WithError(err).Errorf("quantity calculation error") + } + + if totalQuantity.IsZero() { + return + } + + bbgo.Notify("Next %s resistance price at %f, updating resistance orders with total quantity %f", s.Symbol, s.currentResistancePrice.Float64(), totalQuantity.Float64()) + + numLayers := s.NumLayers + if numLayers == 0 { + numLayers = 1 + } + + numLayersF := fixedpoint.NewFromInt(int64(numLayers)) + layerSpread := s.LayerSpread + quantity := totalQuantity.Div(numLayersF) + + if s.activeOrders.NumOfOrders() > 0 { + if err := s.orderExecutor.GracefulCancelActiveOrderBook(ctx, s.activeOrders); err != nil { + log.WithError(err).Errorf("can not cancel resistance orders: %+v", s.activeOrders.Orders()) + } + } + + log.Infof("placing resistance orders: resistance price = %f, layer quantity = %f, num of layers = %d", resistancePrice.Float64(), quantity.Float64(), numLayers) + + var sellPriceStart = resistancePrice.Mul(fixedpoint.One.Add(s.Ratio)) + var orderForms []types.SubmitOrder + for i := 0; i < numLayers; i++ { + balances := s.session.GetAccount().Balances() + quoteBalance := balances[s.Market.QuoteCurrency] + baseBalance := balances[s.Market.BaseCurrency] + _ = quoteBalance + _ = baseBalance + + spread := layerSpread.Mul(fixedpoint.NewFromInt(int64(i))) + price := sellPriceStart.Mul(one.Add(spread)) + log.Infof("resistance sell price = %f", price.Float64()) + log.Infof("placing resistance short order #%d: price = %f, quantity = %f", i, price.Float64(), quantity.Float64()) + + orderForms = append(orderForms, types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeSell, + Type: types.OrderTypeLimitMaker, + Price: price, + Quantity: quantity, + Tag: "resistanceShort", + MarginSideEffect: types.SideEffectTypeMarginBuy, + }) + } + + createdOrders, err := s.orderExecutor.SubmitOrders(ctx, orderForms...) + if err != nil { + log.WithError(err).Errorf("can not place resistance order") + } + s.activeOrders.Add(createdOrders...) +} + +func findPossibleSupportPrices(closePrice float64, groupDistance float64, lows []float64) []float64 { + return floats.Group(floats.Lower(lows, closePrice), groupDistance) +} + +func findPossibleResistancePrices(closePrice float64, groupDistance float64, lows []float64) []float64 { + return floats.Group(floats.Higher(lows, closePrice), groupDistance) +} diff --git a/pkg/strategy/pivotshort/resistance_test.go b/pkg/strategy/pivotshort/resistance_test.go new file mode 100644 index 0000000000..6a46017719 --- /dev/null +++ b/pkg/strategy/pivotshort/resistance_test.go @@ -0,0 +1,28 @@ +package pivotshort + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_findPossibleResistancePrices(t *testing.T) { + prices := findPossibleResistancePrices(19000.0, 0.01, []float64{ + 23020.0, + 23040.0, + 23060.0, + + 24020.0, + 24040.0, + 24060.0, + }) + assert.Equal(t, []float64{23035, 24040}, prices) + + + prices = findPossibleResistancePrices(19000.0, 0.01, []float64{ + 23020.0, + 23040.0, + 23060.0, + }) + assert.Equal(t, []float64{23035}, prices) +} diff --git a/pkg/strategy/pivotshort/strategy.go b/pkg/strategy/pivotshort/strategy.go new file mode 100644 index 0000000000..8774b1ab8f --- /dev/null +++ b/pkg/strategy/pivotshort/strategy.go @@ -0,0 +1,194 @@ +package pivotshort + +import ( + "context" + "fmt" + "os" + "sync" + + "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/dynamic" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +const ID = "pivotshort" + +var one = fixedpoint.One + +var log = logrus.WithField("strategy", ID) + +func init() { + bbgo.RegisterStrategy(ID, &Strategy{}) +} + +type Strategy struct { + Environment *bbgo.Environment + Symbol string `json:"symbol"` + Market types.Market + + // pivot interval and window + types.IntervalWindow + + Leverage fixedpoint.Value `json:"leverage"` + Quantity fixedpoint.Value `json:"quantity"` + + // persistence fields + + Position *types.Position `persistence:"position"` + ProfitStats *types.ProfitStats `persistence:"profit_stats"` + TradeStats *types.TradeStats `persistence:"trade_stats"` + + // BreakLow is one of the entry method + BreakLow *BreakLow `json:"breakLow"` + FailedBreakHigh *FailedBreakHigh `json:"failedBreakHigh"` + + // ResistanceShort is one of the entry method + ResistanceShort *ResistanceShort `json:"resistanceShort"` + + ExitMethods bbgo.ExitMethodSet `json:"exits"` + + session *bbgo.ExchangeSession + orderExecutor *bbgo.GeneralOrderExecutor + + // StrategyController + bbgo.StrategyController +} + +func (s *Strategy) ID() string { + return ID +} + +func (s *Strategy) InstanceID() string { + return fmt.Sprintf("%s:%s", ID, s.Symbol) +} + +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: types.Interval1m}) + + if s.ResistanceShort != nil && s.ResistanceShort.Enabled { + dynamic.InheritStructValues(s.ResistanceShort, s) + s.ResistanceShort.Subscribe(session) + } + + if s.BreakLow != nil { + dynamic.InheritStructValues(s.BreakLow, s) + s.BreakLow.Subscribe(session) + } + + if s.FailedBreakHigh != nil { + dynamic.InheritStructValues(s.FailedBreakHigh, s) + s.FailedBreakHigh.Subscribe(session) + } + + if !bbgo.IsBackTesting { + session.Subscribe(types.MarketTradeChannel, s.Symbol, types.SubscribeOptions{}) + } + + s.ExitMethods.SetAndSubscribe(session, s) +} + +func (s *Strategy) CurrentPosition() *types.Position { + return s.Position +} + +func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Value) error { + return s.orderExecutor.ClosePosition(ctx, percentage) +} + +func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + var instanceID = s.InstanceID() + + if s.Position == nil { + s.Position = types.NewPositionFromMarket(s.Market) + } + + if s.ProfitStats == nil { + s.ProfitStats = types.NewProfitStats(s.Market) + } + + if s.TradeStats == nil { + s.TradeStats = types.NewTradeStats(s.Symbol) + } + + if s.Leverage.IsZero() { + // the default leverage is 3x + s.Leverage = fixedpoint.NewFromInt(3) + } + + // StrategyController + s.Status = types.StrategyStatusRunning + + s.OnSuspend(func() { + // Cancel active orders + _ = s.orderExecutor.GracefulCancel(ctx) + + if s.BreakLow != nil { + s.BreakLow.Suspend() + } + + if s.ResistanceShort != nil { + s.ResistanceShort.Suspend() + } + + if s.FailedBreakHigh != nil { + s.FailedBreakHigh.Suspend() + } + }) + + s.OnEmergencyStop(func() { + // Cancel active orders + _ = s.orderExecutor.GracefulCancel(ctx) + // Close 100% position + _ = s.ClosePosition(ctx, fixedpoint.One) + + if s.BreakLow != nil { + s.BreakLow.EmergencyStop() + } + + if s.ResistanceShort != nil { + s.ResistanceShort.EmergencyStop() + } + + if s.FailedBreakHigh != nil { + s.FailedBreakHigh.EmergencyStop() + } + }) + + // initial required information + s.session = session + s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) + s.orderExecutor.BindEnvironment(s.Environment) + s.orderExecutor.BindProfitStats(s.ProfitStats) + s.orderExecutor.BindTradeStats(s.TradeStats) + s.orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { + bbgo.Sync(ctx, s) + }) + s.orderExecutor.Bind() + + s.ExitMethods.Bind(session, s.orderExecutor) + + if s.ResistanceShort != nil && s.ResistanceShort.Enabled { + s.ResistanceShort.Bind(session, s.orderExecutor) + } + + if s.BreakLow != nil { + s.BreakLow.Bind(session, s.orderExecutor) + } + + if s.FailedBreakHigh != nil { + s.FailedBreakHigh.Bind(session, s.orderExecutor) + } + + bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { + defer wg.Done() + + _, _ = fmt.Fprintln(os.Stderr, s.TradeStats.String()) + _ = s.orderExecutor.GracefulCancel(ctx) + }) + + return nil +} diff --git a/pkg/strategy/pricealert/strategy.go b/pkg/strategy/pricealert/strategy.go index 0e484e52b5..f6b15c6eeb 100644 --- a/pkg/strategy/pricealert/strategy.go +++ b/pkg/strategy/pricealert/strategy.go @@ -15,12 +15,9 @@ func init() { } type Strategy struct { - // The notification system will be injected into the strategy automatically. - *bbgo.Notifiability - // These fields will be filled from the config file (it translates YAML to JSON) Symbol string `json:"symbol"` - Interval string `json:"interval"` + Interval types.Interval `json:"interval"` MinChange fixedpoint.Value `json:"minChange"` } @@ -40,10 +37,10 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } if kline.GetChange().Abs().Compare(s.MinChange) > 0 { - if channel, ok := s.RouteSymbol(s.Symbol); ok { - s.NotifyTo(channel, "%s hit price %s, change %v", s.Symbol, market.FormatPrice(kline.Close), kline.GetChange()) + if channel, ok := bbgo.Notification.RouteSymbol(s.Symbol); ok { + bbgo.NotifyTo(channel, "%s hit price %s, change %v", s.Symbol, market.FormatPrice(kline.Close), kline.GetChange()) } else { - s.Notify("%s hit price %s, change %v", s.Symbol, market.FormatPrice(kline.Close), kline.GetChange()) + bbgo.Notify("%s hit price %s, change %v", s.Symbol, market.FormatPrice(kline.Close), kline.GetChange()) } } }) diff --git a/pkg/strategy/pricedrop/strategy.go b/pkg/strategy/pricedrop/strategy.go index 915f7b59c0..c8a9b16648 100644 --- a/pkg/strategy/pricedrop/strategy.go +++ b/pkg/strategy/pricedrop/strategy.go @@ -35,7 +35,7 @@ func (s *Strategy) ID() string { } func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { - session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: string(s.Interval)}) + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) } func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { @@ -53,7 +53,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return fmt.Errorf("market %s is not defined", s.Symbol) } - standardIndicatorSet, ok := session.StandardIndicatorSet(s.Symbol) + standardIndicatorSet := session.StandardIndicatorSet(s.Symbol) if !ok { return fmt.Errorf("standardIndicatorSet is nil, symbol %s", s.Symbol) } diff --git a/pkg/strategy/rebalance/strategy.go b/pkg/strategy/rebalance/strategy.go index 64d1c6d5a8..fba79f2968 100644 --- a/pkg/strategy/rebalance/strategy.go +++ b/pkg/strategy/rebalance/strategy.go @@ -3,8 +3,6 @@ package rebalance import ( "context" "fmt" - "math" - "sort" "github.com/sirupsen/logrus" @@ -22,27 +20,18 @@ func init() { } type Strategy struct { - Notifiability *bbgo.Notifiability - - Interval types.Interval `json:"interval"` - BaseCurrency string `json:"baseCurrency"` - TargetWeights map[string]fixedpoint.Value `json:"targetWeights"` - Threshold fixedpoint.Value `json:"threshold"` - IgnoreLocked bool `json:"ignoreLocked"` - Verbose bool `json:"verbose"` - DryRun bool `json:"dryRun"` + Interval types.Interval `json:"interval"` + QuoteCurrency string `json:"quoteCurrency"` + TargetWeights types.ValueMap `json:"targetWeights"` + Threshold fixedpoint.Value `json:"threshold"` + DryRun bool `json:"dryRun"` // max amount to buy or sell per order MaxAmount fixedpoint.Value `json:"maxAmount"` - currencies []string + activeOrderBook *bbgo.ActiveOrderBook } func (s *Strategy) Initialize() error { - for currency := range s.TargetWeights { - s.currencies = append(s.currencies, currency) - } - - sort.Strings(s.currencies) return nil } @@ -55,6 +44,10 @@ func (s *Strategy) Validate() error { return fmt.Errorf("targetWeights should not be empty") } + if !s.TargetWeights.Sum().Eq(fixedpoint.One) { + return fmt.Errorf("the sum of targetWeights should be 1") + } + for currency, weight := range s.TargetWeights { if weight.Float64() < 0 { return fmt.Errorf("%s weight: %f should not less than 0", currency, weight.Float64()) @@ -73,30 +66,35 @@ func (s *Strategy) Validate() error { } func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { - for _, symbol := range s.getSymbols() { - session.Subscribe(types.KLineChannel, symbol, types.SubscribeOptions{Interval: s.Interval.String()}) - } + session.Subscribe(types.KLineChannel, s.symbols()[0], types.SubscribeOptions{Interval: s.Interval}) } func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + s.activeOrderBook = bbgo.NewActiveOrderBook("") + s.activeOrderBook.BindStream(session.UserDataStream) + + markets := session.Markets() + for _, symbol := range s.symbols() { + if _, ok := markets[symbol]; !ok { + return fmt.Errorf("exchange: %s does not supoort matket: %s", session.Exchange.Name(), symbol) + } + } + session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { s.rebalance(ctx, orderExecutor, session) }) + return nil } func (s *Strategy) rebalance(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) { - prices, err := s.getPrices(ctx, session) - if err != nil { - return + // cancel active orders before rebalance + if err := session.Exchange.CancelOrders(ctx, s.activeOrderBook.Orders()...); err != nil { + log.WithError(err).Errorf("failed to cancel orders") } - balances := session.GetAccount().Balances() - quantities := s.getQuantities(balances) - marketValues := prices.Mul(quantities) - - orders := s.generateSubmitOrders(prices, marketValues) - for _, order := range orders { + submitOrders := s.generateSubmitOrders(ctx, session) + for _, order := range submitOrders { log.Infof("generated submit order: %s", order.String()) } @@ -104,59 +102,59 @@ func (s *Strategy) rebalance(ctx context.Context, orderExecutor bbgo.OrderExecut return } - _, err = orderExecutor.SubmitOrders(ctx, orders...) + createdOrders, err := orderExecutor.SubmitOrders(ctx, submitOrders...) if err != nil { - log.WithError(err).Error("submit order error") + log.WithError(err).Error("failed to submit orders") return } + + s.activeOrderBook.Add(createdOrders...) } -func (s *Strategy) getPrices(ctx context.Context, session *bbgo.ExchangeSession) (prices types.Float64Slice, err error) { - for _, currency := range s.currencies { - if currency == s.BaseCurrency { - prices = append(prices, 1.0) - continue - } +func (s *Strategy) prices(ctx context.Context, session *bbgo.ExchangeSession) types.ValueMap { + m := make(types.ValueMap) - symbol := currency + s.BaseCurrency - ticker, err := session.Exchange.QueryTicker(ctx, symbol) - if err != nil { - s.Notifiability.Notify("query ticker error: %s", err.Error()) - log.WithError(err).Error("query ticker error") - return prices, err - } + tickers, err := session.Exchange.QueryTickers(ctx, s.symbols()...) + if err != nil { + log.WithError(err).Error("failed to query tickers") + return nil + } - prices = append(prices, ticker.Last.Float64()) + for currency := range s.TargetWeights { + if currency == s.QuoteCurrency { + m[s.QuoteCurrency] = fixedpoint.One + continue + } + m[currency] = tickers[currency+s.QuoteCurrency].Last } - return prices, nil + + return m } -func (s *Strategy) getQuantities(balances types.BalanceMap) (quantities types.Float64Slice) { - for _, currency := range s.currencies { - if s.IgnoreLocked { - quantities = append(quantities, balances[currency].Total().Float64()) - } else { - quantities = append(quantities, balances[currency].Available.Float64()) - } +func (s *Strategy) quantities(session *bbgo.ExchangeSession) types.ValueMap { + m := make(types.ValueMap) + + balances := session.GetAccount().Balances() + for currency := range s.TargetWeights { + m[currency] = balances[currency].Total() } - return quantities + + return m } -func (s *Strategy) generateSubmitOrders(prices, marketValues types.Float64Slice) (submitOrders []types.SubmitOrder) { +func (s *Strategy) generateSubmitOrders(ctx context.Context, session *bbgo.ExchangeSession) (submitOrders []types.SubmitOrder) { + prices := s.prices(ctx, session) + marketValues := prices.Mul(s.quantities(session)) currentWeights := marketValues.Normalize() - totalValue := marketValues.Sum() - log.Infof("total value: %f", totalValue) - - for i, currency := range s.currencies { - if currency == s.BaseCurrency { + for currency, targetWeight := range s.TargetWeights { + if currency == s.QuoteCurrency { continue } - symbol := currency + s.BaseCurrency - currentWeight := currentWeights[i] - currentPrice := prices[i] - targetWeight := s.TargetWeights[currency].Float64() + symbol := currency + s.QuoteCurrency + currentWeight := currentWeights[currency] + currentPrice := prices[currency] log.Infof("%s price: %v, current weight: %v, target weight: %v", symbol, @@ -166,8 +164,8 @@ func (s *Strategy) generateSubmitOrders(prices, marketValues types.Float64Slice) // calculate the difference between current weight and target weight // if the difference is less than threshold, then we will not create the order - weightDifference := targetWeight - currentWeight - if math.Abs(weightDifference) < s.Threshold.Float64() { + weightDifference := targetWeight.Sub(currentWeight) + if weightDifference.Abs().Compare(s.Threshold) < 0 { log.Infof("%s weight distance |%v - %v| = |%v| less than the threshold: %v", symbol, currentWeight, @@ -177,7 +175,7 @@ func (s *Strategy) generateSubmitOrders(prices, marketValues types.Float64Slice) continue } - quantity := fixedpoint.NewFromFloat((weightDifference * totalValue) / currentPrice) + quantity := weightDifference.Mul(marketValues.Sum()).Div(currentPrice) side := types.SideTypeBuy if quantity.Sign() < 0 { @@ -186,7 +184,7 @@ func (s *Strategy) generateSubmitOrders(prices, marketValues types.Float64Slice) } if s.MaxAmount.Sign() > 0 { - quantity = bbgo.AdjustQuantityByMaxAmount(quantity, fixedpoint.NewFromFloat(currentPrice), s.MaxAmount) + quantity = bbgo.AdjustQuantityByMaxAmount(quantity, currentPrice, s.MaxAmount) log.Infof("adjust the quantity %v (%s %s @ %v) by max amount %v", quantity, symbol, @@ -194,24 +192,29 @@ func (s *Strategy) generateSubmitOrders(prices, marketValues types.Float64Slice) currentPrice, s.MaxAmount) } + log.Debugf("symbol: %v, quantity: %v", symbol, quantity) + order := types.SubmitOrder{ Symbol: symbol, Side: side, - Type: types.OrderTypeMarket, - Quantity: quantity} + Type: types.OrderTypeLimit, + Quantity: quantity, + Price: currentPrice, + } submitOrders = append(submitOrders, order) } + return submitOrders } -func (s *Strategy) getSymbols() (symbols []string) { - for _, currency := range s.currencies { - if currency == s.BaseCurrency { +func (s *Strategy) symbols() (symbols []string) { + for currency := range s.TargetWeights { + if currency == s.QuoteCurrency { continue } - symbols = append(symbols, currency+s.BaseCurrency) + symbols = append(symbols, currency+s.QuoteCurrency) } return symbols } diff --git a/pkg/strategy/rsmaker/strategy.go b/pkg/strategy/rsmaker/strategy.go new file mode 100644 index 0000000000..2dd477a8fb --- /dev/null +++ b/pkg/strategy/rsmaker/strategy.go @@ -0,0 +1,470 @@ +package rsmaker + +import ( + "context" + "fmt" + "math" + + "github.com/c9s/bbgo/pkg/indicator" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + + "github.com/muesli/clusters" + "github.com/muesli/kmeans" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +const ID = "rsmaker" + +var notionModifier = fixedpoint.NewFromFloat(1.1) + +var log = logrus.WithField("strategy", ID) + +func init() { + bbgo.RegisterStrategy(ID, &Strategy{}) +} + +type Strategy struct { + Environment *bbgo.Environment + StandardIndicatorSet *bbgo.StandardIndicatorSet + Market types.Market + + // Symbol is the market symbol you want to trade + Symbol string `json:"symbol"` + + // Interval is how long do you want to update your order price and quantity + Interval types.Interval `json:"interval"` + + bbgo.QuantityOrAmount + + // Spread is the price spread from the middle price. + // For ask orders, the ask price is ((bestAsk + bestBid) / 2 * (1.0 + spread)) + // For bid orders, the bid price is ((bestAsk + bestBid) / 2 * (1.0 - spread)) + // Spread can be set by percentage or floating number. e.g., 0.1% or 0.001 + Spread fixedpoint.Value `json:"spread"` + + // BidSpread overrides the spread setting, this spread will be used for the buy order + BidSpread fixedpoint.Value `json:"bidSpread,omitempty"` + + // AskSpread overrides the spread setting, this spread will be used for the sell order + AskSpread fixedpoint.Value `json:"askSpread,omitempty"` + + // MinProfitSpread is the minimal order price spread from the current average cost. + // For long position, you will only place sell order above the price (= average cost * (1 + minProfitSpread)) + // For short position, you will only place buy order below the price (= average cost * (1 - minProfitSpread)) + MinProfitSpread fixedpoint.Value `json:"minProfitSpread"` + + // UseTickerPrice use the ticker api to get the mid price instead of the closed kline price. + // The back-test engine is kline-based, so the ticker price api is not supported. + // Turn this on if you want to do real trading. + UseTickerPrice bool `json:"useTickerPrice"` + + // MaxExposurePosition is the maximum position you can hold + // +10 means you can hold 10 ETH long position by maximum + // -10 means you can hold -10 ETH short position by maximum + MaxExposurePosition fixedpoint.Value `json:"maxExposurePosition"` + + // DynamicExposurePositionScale is used to define the exposure position range with the given percentage + // when DynamicExposurePositionScale is set, + // your MaxExposurePosition will be calculated dynamically according to the bollinger band you set. + DynamicExposurePositionScale *bbgo.PercentageScale `json:"dynamicExposurePositionScale"` + + // Long means your position will be long position + // Currently not used yet + Long *bool `json:"long,omitempty"` + + // Short means your position will be long position + // Currently not used yet + Short *bool `json:"short,omitempty"` + + // DisableShort means you can don't want short position during the market making + // Set to true if you want to hold more spot during market making. + DisableShort bool `json:"disableShort"` + + // BuyBelowNeutralSMA if true, the market maker will only place buy order when the current price is below the neutral band SMA. + BuyBelowNeutralSMA bool `json:"buyBelowNeutralSMA"` + + // NeutralBollinger is the smaller range of the bollinger band + // If price is in this band, it usually means the price is oscillating. + // If price goes out of this band, we tend to not place sell orders or buy orders + NeutralBollinger *types.BollingerSetting `json:"neutralBollinger"` + + // DefaultBollinger is the wide range of the bollinger band + // for controlling your exposure position + DefaultBollinger *types.BollingerSetting `json:"defaultBollinger"` + + // DowntrendSkew is the order quantity skew for normal downtrend band. + // The price is still in the default bollinger band. + // greater than 1.0 means when placing buy order, place sell order with less quantity + // less than 1.0 means when placing sell order, place buy order with less quantity + DowntrendSkew fixedpoint.Value `json:"downtrendSkew"` + + // UptrendSkew is the order quantity skew for normal uptrend band. + // The price is still in the default bollinger band. + // greater than 1.0 means when placing buy order, place sell order with less quantity + // less than 1.0 means when placing sell order, place buy order with less quantity + UptrendSkew fixedpoint.Value `json:"uptrendSkew"` + + // TradeInBand + // When this is on, places orders only when the current price is in the bollinger band. + TradeInBand bool `json:"tradeInBand"` + + // ShadowProtection is used to avoid placing bid order when price goes down strongly (without shadow) + ShadowProtection bool `json:"shadowProtection"` + ShadowProtectionRatio fixedpoint.Value `json:"shadowProtectionRatio"` + + Position *types.Position `persistence:"position"` + ProfitStats *types.ProfitStats `persistence:"profit_stats"` + TradeStats *types.TradeStats `persistence:"trade_stats"` + + session *bbgo.ExchangeSession + orderExecutor *bbgo.GeneralOrderExecutor + book *types.StreamOrderBook + + groupID uint32 + + // defaultBoll is the BOLLINGER indicator we used for predicting the price. + defaultBoll *indicator.BOLL + + // neutralBoll is the neutral price section + neutralBoll *indicator.BOLL + + // StrategyController + status types.StrategyStatus +} + +func (s *Strategy) ID() string { + return ID +} + +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ + Interval: s.Interval, + }) +} + +func (s *Strategy) Validate() error { + if len(s.Symbol) == 0 { + return errors.New("symbol is required") + } + + return nil +} + +func (s *Strategy) CurrentPosition() *types.Position { + return s.Position +} + +func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Value) error { + return s.orderExecutor.ClosePosition(ctx, percentage) +} + +// StrategyController +func (s *Strategy) GetStatus() types.StrategyStatus { + return s.status +} + +func (s *Strategy) Suspend(ctx context.Context) error { + s.status = types.StrategyStatusStopped + + if err := s.orderExecutor.GracefulCancel(ctx); err != nil { + log.WithError(err).Errorf("graceful cancel order error") + } + + bbgo.Sync(ctx, s) + return nil +} + +func (s *Strategy) Resume(ctx context.Context) error { + s.status = types.StrategyStatusRunning + + return nil +} + +func (s *Strategy) getCurrentAllowedExposurePosition(bandPercentage float64) (fixedpoint.Value, error) { + if s.DynamicExposurePositionScale != nil { + v, err := s.DynamicExposurePositionScale.Scale(bandPercentage) + if err != nil { + return fixedpoint.Zero, err + } + return fixedpoint.NewFromFloat(v), nil + } + + return s.MaxExposurePosition, nil +} + +func (s *Strategy) placeOrders(ctx context.Context, midPrice fixedpoint.Value, klines []*types.KLine) { + // preprocessing + max := 0. + min := 100000. + + mv := 0. + for x := 0; x < 50; x++ { + if klines[x].High.Float64() > max { + max = klines[x].High.Float64() + } + if klines[x].Low.Float64() < min { + min = klines[x].High.Float64() + } + + mv += klines[x].Volume.Float64() + } + mv = mv / 50 + + // logrus.Info(max, min) + // set up a random two-dimensional data set (float64 values between 0.0 and 1.0) + var d clusters.Observations + for x := 0; x < 50; x++ { + // if klines[x].High.Float64() < max || klines[x].Low.Float64() > min { + if klines[x].Volume.Float64() > mv*0.3 { + d = append(d, clusters.Coordinates{ + klines[x].High.Float64(), + klines[x].Low.Float64(), + // klines[x].Open.Float64(), + // klines[x].Close.Float64(), + // klines[x].Volume.Float64(), + }) + } + // } + + } + log.Info(len(d)) + + // Partition the data points into 2 clusters + km := kmeans.New() + clusters, err := km.Partition(d, 3) + + // for _, c := range clusters { + // fmt.Printf("Centered at x: %.2f y: %.2f\n", c.Center[0], c.Center[1]) + // fmt.Printf("Matching data points: %+v\n\n", c.Observations) + // } + // clustered virtual kline_1's mid price + // vk1mp := fixedpoint.NewFromFloat((clusters[0].Center[0] + clusters[0].Center[1]) / 2.) + // clustered virtual kline_2's mid price + // vk2mp := fixedpoint.NewFromFloat((clusters[1].Center[0] + clusters[1].Center[1]) / 2.) + // clustered virtual kline_3's mid price + // vk3mp := fixedpoint.NewFromFloat((clusters[2].Center[0] + clusters[2].Center[1]) / 2.) + + // clustered virtual kline_1's high price + vk1hp := fixedpoint.NewFromFloat(clusters[0].Center[0]) + // clustered virtual kline_2's high price + vk2hp := fixedpoint.NewFromFloat(clusters[1].Center[0]) + // clustered virtual kline_3's high price + vk3hp := fixedpoint.NewFromFloat(clusters[2].Center[0]) + + // clustered virtual kline_1's low price + vk1lp := fixedpoint.NewFromFloat(clusters[0].Center[1]) + // clustered virtual kline_2's low price + vk2lp := fixedpoint.NewFromFloat(clusters[1].Center[1]) + // clustered virtual kline_3's low price + vk3lp := fixedpoint.NewFromFloat(clusters[2].Center[1]) + + askPrice := fixedpoint.NewFromFloat(math.Max(math.Max(vk1hp.Float64(), vk2hp.Float64()), vk3hp.Float64())) // fixedpoint.NewFromFloat(math.Max(math.Max(vk1mp.Float64(), vk2mp.Float64()), vk3mp.Float64())) + bidPrice := fixedpoint.NewFromFloat(math.Min(math.Min(vk1lp.Float64(), vk2lp.Float64()), vk3lp.Float64())) // fixedpoint.NewFromFloat(math.Min(math.Min(vk1mp.Float64(), vk2mp.Float64()), vk3mp.Float64())) + + // if vk1mp.Compare(vk2mp) > 0 { + // askPrice = vk1mp //.Mul(fixedpoint.NewFromFloat(1.001)) + // bidPrice = vk2mp //.Mul(fixedpoint.NewFromFloat(0.999)) + // } else if vk1mp.Compare(vk2mp) < 0 { + // askPrice = vk2mp //.Mul(fixedpoint.NewFromFloat(1.001)) + // bidPrice = vk1mp //.Mul(fixedpoint.NewFromFloat(0.999)) + // } + // midPrice.Mul(fixedpoint.One.Add(askSpread)) + // midPrice.Mul(fixedpoint.One.Sub(bidSpread)) + base := s.Position.GetBase() + // balances := s.session.GetAccount().Balances() + + canSell := true + canBuy := true + + // predMidPrice := (askPrice + bidPrice) / 2. + + // if midPrice.Float64() > predMidPrice.Float64() { + // bidPrice = predMidPrice.Mul(fixedpoint.NewFromFloat(0.999)) + // } + // + // if midPrice.Float64() < predMidPrice.Float64() { + // askPrice = predMidPrice.Mul(fixedpoint.NewFromFloat(1.001)) + // } + // + // if midPrice.Float64() > askPrice.Float64() { + // canBuy = false + // askPrice = midPrice.Mul(fixedpoint.NewFromFloat(1.001)) + // } + // + // if midPrice.Float64() < bidPrice.Float64() { + // canSell = false + // bidPrice = midPrice.Mul(fixedpoint.NewFromFloat(0.999)) + // } + + sellQuantity := s.QuantityOrAmount.CalculateQuantity(askPrice) + buyQuantity := s.QuantityOrAmount.CalculateQuantity(bidPrice) + + sellOrder := types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeSell, + Type: types.OrderTypeLimitMaker, + Quantity: sellQuantity, + Price: askPrice, + Market: s.Market, + GroupID: s.groupID, + } + buyOrder := types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeLimitMaker, + Quantity: buyQuantity, + Price: bidPrice, + Market: s.Market, + GroupID: s.groupID, + } + + var submitBuyOrders []types.SubmitOrder + var submitSellOrders []types.SubmitOrder + + // baseBalance, hasBaseBalance := balances[s.Market.BaseCurrency] + // quoteBalance, hasQuoteBalance := balances[s.Market.QuoteCurrency] + + downBand := s.defaultBoll.DownBand.Last() + upBand := s.defaultBoll.UpBand.Last() + sma := s.defaultBoll.SMA.Last() + log.Infof("bollinger band: up %f sma %f down %f", upBand, sma, downBand) + + bandPercentage := calculateBandPercentage(upBand, downBand, sma, midPrice.Float64()) + log.Infof("mid price band percentage: %v", bandPercentage) + + maxExposurePosition, err := s.getCurrentAllowedExposurePosition(bandPercentage) + if err != nil { + log.WithError(err).Errorf("can not calculate CurrentAllowedExposurePosition") + return + } + + log.Infof("calculated max exposure position: %v", maxExposurePosition) + + if maxExposurePosition.Sign() > 0 && base.Compare(maxExposurePosition) > 0 { + canBuy = false + } + + if maxExposurePosition.Sign() > 0 { + if s.Long != nil && *s.Long && base.Sign() < 0 { + canSell = false + } else if base.Compare(maxExposurePosition.Neg()) < 0 { + canSell = false + } + } + + if canSell { + submitSellOrders = append(submitSellOrders, sellOrder) + } + if canBuy { + submitBuyOrders = append(submitBuyOrders, buyOrder) + } + + for i := range submitBuyOrders { + submitBuyOrders[i] = s.adjustOrderQuantity(submitBuyOrders[i]) + } + + for i := range submitSellOrders { + submitSellOrders[i] = s.adjustOrderQuantity(submitSellOrders[i]) + } + + if _, err := s.orderExecutor.SubmitOrders(ctx, submitBuyOrders...); err != nil { + log.WithError(err).Errorf("can not place orders") + } + if _, err := s.orderExecutor.SubmitOrders(ctx, submitSellOrders...); err != nil { + log.WithError(err).Errorf("can not place orders") + } +} + +func (s *Strategy) adjustOrderQuantity(submitOrder types.SubmitOrder) types.SubmitOrder { + if submitOrder.Quantity.Mul(submitOrder.Price).Compare(s.Market.MinNotional) < 0 { + submitOrder.Quantity = bbgo.AdjustFloatQuantityByMinAmount(submitOrder.Quantity, submitOrder.Price, s.Market.MinNotional.Mul(notionModifier)) + } + + if submitOrder.Quantity.Compare(s.Market.MinQuantity) < 0 { + submitOrder.Quantity = fixedpoint.Max(submitOrder.Quantity, s.Market.MinQuantity) + } + + return submitOrder +} + +func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + instanceID := fmt.Sprintf("%s-%s", ID, s.Symbol) + + s.status = types.StrategyStatusRunning + + if s.Position == nil { + s.Position = types.NewPositionFromMarket(s.Market) + } + + if s.ProfitStats == nil { + s.ProfitStats = types.NewProfitStats(s.Market) + } + + if s.TradeStats == nil { + s.TradeStats = types.NewTradeStats(s.Symbol) + } + + // initial required information + s.session = session + s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) + s.orderExecutor.BindEnvironment(s.Environment) + s.orderExecutor.BindProfitStats(s.ProfitStats) + s.orderExecutor.BindTradeStats(s.TradeStats) + s.orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { + bbgo.Sync(ctx, s) + }) + s.orderExecutor.Bind() + + s.neutralBoll = s.StandardIndicatorSet.BOLL(s.NeutralBollinger.IntervalWindow, s.NeutralBollinger.BandWidth) + s.defaultBoll = s.StandardIndicatorSet.BOLL(s.DefaultBollinger.IntervalWindow, s.DefaultBollinger.BandWidth) + + var klines []*types.KLine + session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { + // StrategyController + if s.status != types.StrategyStatusRunning { + return + } + + // if kline.Symbol != s.Symbol || kline.Interval != s.Interval { + // return + // } + + if kline.Interval == s.Interval { + klines = append(klines, &kline) + } + + if len(klines) > 50 { + if kline.Interval == s.Interval { + if err := s.orderExecutor.GracefulCancel(ctx); err != nil { + log.WithError(err).Errorf("graceful cancel order error") + } + + s.placeOrders(ctx, kline.Close, klines[len(klines)-50:]) + } + } + + }) + + return nil +} + +func calculateBandPercentage(up, down, sma, midPrice float64) float64 { + if midPrice < sma { + // should be negative percentage + return (midPrice - sma) / math.Abs(sma-down) + } else if midPrice > sma { + // should be positive percentage + return (midPrice - sma) / math.Abs(up-sma) + } + + return 0.0 +} + +func inBetween(x, a, b float64) bool { + return a < x && x < b +} diff --git a/pkg/strategy/schedule/strategy.go b/pkg/strategy/schedule/strategy.go index 4d34263b94..a647592373 100644 --- a/pkg/strategy/schedule/strategy.go +++ b/pkg/strategy/schedule/strategy.go @@ -2,11 +2,13 @@ package schedule import ( "context" + "fmt" "github.com/pkg/errors" log "github.com/sirupsen/logrus" "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) @@ -19,8 +21,6 @@ func init() { type Strategy struct { Market types.Market - Notifiability *bbgo.Notifiability - // StandardIndicatorSet contains the standard indicators of a market (symbol) // This field will be injected automatically since we defined the Symbol field. *bbgo.StandardIndicatorSet @@ -36,22 +36,33 @@ type Strategy struct { bbgo.QuantityOrAmount + MaxBaseBalance fixedpoint.Value `json:"maxBaseBalance"` + BelowMovingAverage *bbgo.MovingAverageSettings `json:"belowMovingAverage,omitempty"` AboveMovingAverage *bbgo.MovingAverageSettings `json:"aboveMovingAverage,omitempty"` + + Position *types.Position `persistence:"position"` + + session *bbgo.ExchangeSession + orderExecutor *bbgo.GeneralOrderExecutor } func (s *Strategy) ID() string { return ID } +func (s *Strategy) InstanceID() string { + return fmt.Sprintf("%s:%s", ID, s.Symbol) +} + func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { - session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval.String()}) + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) if s.BelowMovingAverage != nil { - session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.BelowMovingAverage.Interval.String()}) + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.BelowMovingAverage.Interval}) } if s.AboveMovingAverage != nil { - session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.AboveMovingAverage.Interval.String()}) + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.AboveMovingAverage.Interval}) } } @@ -64,10 +75,23 @@ func (s *Strategy) Validate() error { } func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + s.session = session + if s.StandardIndicatorSet == nil { return errors.New("StandardIndicatorSet can not be nil, injection failed?") } + if s.Position == nil { + s.Position = types.NewPositionFromMarket(s.Market) + } + + instanceID := s.InstanceID() + s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) + s.orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { + bbgo.Sync(ctx, s) + }) + s.orderExecutor.Bind() + var belowMA types.Float64Indicator var aboveMA types.Float64Indicator var err error @@ -129,7 +153,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } if !match { - s.Notifiability.Notify("skip, the %s closed price %v is below or above moving average", s.Symbol, closePrice) + bbgo.Notify("skip, the %s closed price %v is below or above moving average", s.Symbol, closePrice) return } } @@ -140,6 +164,16 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // execute orders switch side { case types.SideTypeBuy: + if !s.MaxBaseBalance.IsZero() { + if baseBalance, ok := session.GetAccount().Balance(s.Market.BaseCurrency); ok { + total := baseBalance.Total() + if total.Add(quantity).Compare(s.MaxBaseBalance) >= 0 { + quantity = s.MaxBaseBalance.Sub(total) + quoteQuantity = quantity.Mul(closePrice) + } + } + } + quoteBalance, ok := session.GetAccount().Balance(s.Market.QuoteCurrency) if !ok { log.Errorf("can not place scheduled %s order, quote balance %s is empty", s.Symbol, s.Market.QuoteCurrency) @@ -147,7 +181,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } if quoteBalance.Available.Compare(quoteQuantity) < 0 { - s.Notifiability.Notify("Can not place scheduled %s order: quote balance %s is not enough: %v < %v", s.Symbol, s.Market.QuoteCurrency, quoteBalance.Available, quoteQuantity) + bbgo.Notify("Can not place scheduled %s order: quote balance %s is not enough: %v < %v", s.Symbol, s.Market.QuoteCurrency, quoteBalance.Available, quoteQuantity) log.Errorf("can not place scheduled %s order: quote balance %s is not enough: %v < %v", s.Symbol, s.Market.QuoteCurrency, quoteBalance.Available, quoteQuantity) return } @@ -159,16 +193,17 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return } - if baseBalance.Available.Compare(quantity) < 0 { - s.Notifiability.Notify("Can not place scheduled %s order: base balance %s is not enough: %v < %v", s.Symbol, s.Market.QuoteCurrency, baseBalance.Available, quantity) - log.Errorf("can not place scheduled %s order: base balance %s is not enough: %v < %v", s.Symbol, s.Market.QuoteCurrency, baseBalance.Available, quantity) - return - } + quantity = fixedpoint.Min(quantity, baseBalance.Available) + quoteQuantity = quantity.Mul(closePrice) + } + if s.Market.IsDustQuantity(quantity, closePrice) { + log.Warnf("%s: quantity %f is too small", s.Symbol, quantity.Float64()) + return } - s.Notifiability.Notify("Submitting scheduled %s order with quantity %v at price %v", s.Symbol, quantity, closePrice) - _, err := orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + bbgo.Notify("Submitting scheduled %s order with quantity %s at price %s", s.Symbol, quantity.String(), closePrice.String()) + _, err := s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ Symbol: s.Symbol, Side: side, Type: types.OrderTypeMarket, @@ -176,7 +211,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se Market: s.Market, }) if err != nil { - s.Notifiability.Notify("Can not place scheduled %s order: submit error %s", s.Symbol, err.Error()) + bbgo.Notify("Can not place scheduled %s order: submit error %s", s.Symbol, err.Error()) log.WithError(err).Errorf("can not place scheduled %s order error", s.Symbol) } }) diff --git a/pkg/strategy/skeleton/strategy.go b/pkg/strategy/skeleton/strategy.go index 21079e7421..536aaae10b 100644 --- a/pkg/strategy/skeleton/strategy.go +++ b/pkg/strategy/skeleton/strategy.go @@ -2,6 +2,7 @@ package skeleton import ( "context" + "fmt" "github.com/sirupsen/logrus" @@ -10,54 +11,131 @@ import ( "github.com/c9s/bbgo/pkg/types" ) +// ID is the unique strategy ID, it needs to be in all lower case +// For example, grid strategy uses "grid" const ID = "skeleton" +// log is a logrus.Entry that will be reused. +// This line attaches the strategy field to the logger with our ID, so that the logs from this strategy will be tagged with our ID var log = logrus.WithField("strategy", ID) +var ten = fixedpoint.NewFromInt(10) + +// init is a special function of golang, it will be called when the program is started +// importing this package will trigger the init function call. func init() { + // Register our struct type to BBGO + // Note that you don't need to field the fields. + // BBGO uses reflect to parse your type information. bbgo.RegisterStrategy(ID, &Strategy{}) } +// State is a struct contains the information that we want to keep in the persistence layer, +// for example, redis or json file. +type State struct { + Counter int `json:"counter,omitempty"` +} + +// Strategy is a struct that contains the settings of your strategy. +// These settings will be loaded from the BBGO YAML config file "bbgo.yaml" automatically. type Strategy struct { Symbol string `json:"symbol"` + + // State is a state of your strategy + // When BBGO shuts down, everything in the memory will be dropped + // If you need to store something and restore this information back, + // Simply define the "persistence" tag + State *State `persistence:"state"` } +// ID should return the identity of this strategy func (s *Strategy) ID() string { return ID } +// InstanceID returns the identity of the current instance of this strategy. +// You may have multiple instance of a strategy, with different symbols and settings. +// This value will be used for persistence layer to separate the storage. +// +// Run: +// redis-cli KEYS "*" +// +// And you will see how this instance ID is used in redis. +func (s *Strategy) InstanceID() string { + return ID + ":" + s.Symbol +} + +// Subscribe method subscribes specific market data from the given session. +// Before BBGO is connected to the exchange, we need to collect what we want to subscribe. +// Here the strategy needs kline data, so it adds the kline subscription. func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { - log.Infof("subscribe %s", s.Symbol) + // We want 1m kline data of the symbol + // It will be BTCUSDT 1m if our s.Symbol is BTCUSDT session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: "1m"}) } -var Ten = fixedpoint.NewFromInt(10) - // This strategy simply spent all available quote currency to buy the symbol whenever kline gets closed func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + // Initialize the default value for state + if s.State == nil { + s.State = &State{Counter: 1} + } + + indicators := session.StandardIndicatorSet(s.Symbol) + atr := indicators.ATR(types.IntervalWindow{ + Interval: types.Interval1m, + Window: 14, + }) + + // To get the market information from the current session + // The market object provides the precision, MoQ (minimal of quantity) information market, ok := session.Market(s.Symbol) if !ok { - log.Warnf("fetch market fail %s", s.Symbol) - return nil + return fmt.Errorf("market %s not found", s.Symbol) } + + // here we define a kline callback + // when a kline is closed, we will do something callback := func(kline types.KLine) { + // get the latest ATR value from the indicator object that we just defined. + atrValue := atr.Last() + log.Infof("atr %f", atrValue) + + // Update our counter and sync the changes to the persistence layer on time + // If you don't do this, BBGO will sync it automatically when BBGO shuts down. + s.State.Counter++ + bbgo.Sync(ctx, s) + + // To check if we have the quote balance + // When symbol = "BTCUSDT", the quote currency is USDT + // We can get this information from the market object quoteBalance, ok := session.GetAccount().Balance(market.QuoteCurrency) if !ok { + // if not ok, it means we don't have this currency in the account return } + + // For each balance, we have Available and Locked balance. + // balance.Available is the balance you can use to place an order. + // Note that the available balance is a fixed-point object, so you can not compare it with integer directly. + // Instead, you should call valueA.Compare(valueB) quantityAmount := quoteBalance.Available - if quantityAmount.Sign() <= 0 || quantityAmount.Compare(Ten) < 0 { + if quantityAmount.Sign() <= 0 || quantityAmount.Compare(ten) < 0 { return } + // Call LastPrice(symbol) If you need to get the latest price + // Note this last price is updated by the closed kline currentPrice, ok := session.LastPrice(s.Symbol) if !ok { return } + // totalQuantity = quantityAmount / currentPrice totalQuantity := quantityAmount.Div(currentPrice) - _, err := orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + // Place a market order to the exchange + createdOrders, err := orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ Symbol: kline.Symbol, Side: types.SideTypeBuy, Type: types.OrderTypeMarket, @@ -68,12 +146,21 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se if err != nil { log.WithError(err).Error("submit order error") } + + log.Infof("createdOrders: %+v", createdOrders) + + // send notification to slack or telegram if you have configured it + bbgo.Notify("order created") } + + // register our kline event handler + session.MarketDataStream.OnKLineClosed(callback) + + // if you need to do something when the user data stream is ready + // note that you only receive order update, trade update, balance update when the user data stream is connect. session.UserDataStream.OnStart(func() { log.Infof("connected") }) - session.MarketDataStream.OnKLineClosed(callback) - return nil } diff --git a/pkg/strategy/supertrend/double_dema.go b/pkg/strategy/supertrend/double_dema.go new file mode 100644 index 0000000000..e711f89f42 --- /dev/null +++ b/pkg/strategy/supertrend/double_dema.go @@ -0,0 +1,67 @@ +package supertrend + +import ( + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +type DoubleDema struct { + Interval types.Interval `json:"interval"` + + // FastDEMAWindow DEMA window for checking breakout + FastDEMAWindow int `json:"fastDEMAWindow"` + // SlowDEMAWindow DEMA window for checking breakout + SlowDEMAWindow int `json:"slowDEMAWindow"` + fastDEMA *indicator.DEMA + slowDEMA *indicator.DEMA +} + +// getDemaSignal get current DEMA signal +func (dd *DoubleDema) getDemaSignal(openPrice float64, closePrice float64) types.Direction { + var demaSignal types.Direction = types.DirectionNone + + if closePrice > dd.fastDEMA.Last() && closePrice > dd.slowDEMA.Last() && !(openPrice > dd.fastDEMA.Last() && openPrice > dd.slowDEMA.Last()) { + demaSignal = types.DirectionUp + } else if closePrice < dd.fastDEMA.Last() && closePrice < dd.slowDEMA.Last() && !(openPrice < dd.fastDEMA.Last() && openPrice < dd.slowDEMA.Last()) { + demaSignal = types.DirectionDown + } + + return demaSignal +} + +// preloadDema preloads DEMA indicators +func (dd *DoubleDema) preloadDema(kLineStore *bbgo.MarketDataStore) { + if klines, ok := kLineStore.KLinesOfInterval(dd.fastDEMA.Interval); ok { + for i := 0; i < len(*klines); i++ { + dd.fastDEMA.Update((*klines)[i].GetClose().Float64()) + } + } + if klines, ok := kLineStore.KLinesOfInterval(dd.slowDEMA.Interval); ok { + for i := 0; i < len(*klines); i++ { + dd.slowDEMA.Update((*klines)[i].GetClose().Float64()) + } + } +} + +// newDoubleDema initializes double DEMA indicators +func newDoubleDema(kLineStore *bbgo.MarketDataStore, interval types.Interval, fastDEMAWindow int, slowDEMAWindow int) *DoubleDema { + dd := DoubleDema{Interval: interval, FastDEMAWindow: fastDEMAWindow, SlowDEMAWindow: slowDEMAWindow} + + // DEMA + if dd.FastDEMAWindow == 0 { + dd.FastDEMAWindow = 144 + } + dd.fastDEMA = &indicator.DEMA{IntervalWindow: types.IntervalWindow{Interval: dd.Interval, Window: dd.FastDEMAWindow}} + dd.fastDEMA.Bind(kLineStore) + + if dd.SlowDEMAWindow == 0 { + dd.SlowDEMAWindow = 169 + } + dd.slowDEMA = &indicator.DEMA{IntervalWindow: types.IntervalWindow{Interval: dd.Interval, Window: dd.SlowDEMAWindow}} + dd.slowDEMA.Bind(kLineStore) + + dd.preloadDema(kLineStore) + + return &dd +} diff --git a/pkg/strategy/supertrend/draw.go b/pkg/strategy/supertrend/draw.go new file mode 100644 index 0000000000..b6569410a7 --- /dev/null +++ b/pkg/strategy/supertrend/draw.go @@ -0,0 +1,90 @@ +package supertrend + +import ( + "bytes" + "fmt" + "os" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/interact" + "github.com/c9s/bbgo/pkg/types" + "github.com/wcharczuk/go-chart/v2" +) + +func (s *Strategy) InitDrawCommands(profit, cumProfit types.Series) { + bbgo.RegisterCommand("/pnl", "Draw PNL(%) per trade", func(reply interact.Reply) { + canvas := DrawPNL(s.InstanceID(), profit) + var buffer bytes.Buffer + if err := canvas.Render(chart.PNG, &buffer); err != nil { + log.WithError(err).Errorf("cannot render pnl in drift") + reply.Message(fmt.Sprintf("[error] cannot render pnl in ewo: %v", err)) + return + } + bbgo.SendPhoto(&buffer) + }) + bbgo.RegisterCommand("/cumpnl", "Draw Cummulative PNL(Quote)", func(reply interact.Reply) { + canvas := DrawCumPNL(s.InstanceID(), cumProfit) + var buffer bytes.Buffer + if err := canvas.Render(chart.PNG, &buffer); err != nil { + log.WithError(err).Errorf("cannot render cumpnl in drift") + reply.Message(fmt.Sprintf("[error] canot render cumpnl in drift: %v", err)) + return + } + bbgo.SendPhoto(&buffer) + }) +} + +func (s *Strategy) Draw(profit, cumProfit types.Series) error { + + canvas := DrawPNL(s.InstanceID(), profit) + f, err := os.Create(s.GraphPNLPath) + if err != nil { + return fmt.Errorf("cannot create on path " + s.GraphPNLPath) + } + defer f.Close() + if err = canvas.Render(chart.PNG, f); err != nil { + return fmt.Errorf("cannot render pnl") + } + canvas = DrawCumPNL(s.InstanceID(), cumProfit) + f, err = os.Create(s.GraphCumPNLPath) + if err != nil { + return fmt.Errorf("cannot create on path " + s.GraphCumPNLPath) + } + defer f.Close() + if err = canvas.Render(chart.PNG, f); err != nil { + return fmt.Errorf("cannot render cumpnl") + } + + return nil +} + +func DrawPNL(instanceID string, profit types.Series) *types.Canvas { + canvas := types.NewCanvas(instanceID) + length := profit.Length() + log.Infof("pnl Highest: %f, Lowest: %f", types.Highest(profit, length), types.Lowest(profit, length)) + canvas.PlotRaw("pnl %", profit, length) + canvas.YAxis = chart.YAxis{ + ValueFormatter: func(v interface{}) string { + if vf, isFloat := v.(float64); isFloat { + return fmt.Sprintf("%.4f", vf) + } + return "" + }, + } + canvas.PlotRaw("1", types.NumberSeries(1), length) + return canvas +} + +func DrawCumPNL(instanceID string, cumProfit types.Series) *types.Canvas { + canvas := types.NewCanvas(instanceID) + canvas.PlotRaw("cummulative pnl", cumProfit, cumProfit.Length()) + canvas.YAxis = chart.YAxis{ + ValueFormatter: func(v interface{}) string { + if vf, isFloat := v.(float64); isFloat { + return fmt.Sprintf("%.4f", vf) + } + return "" + }, + } + return canvas +} diff --git a/pkg/strategy/supertrend/linreg.go b/pkg/strategy/supertrend/linreg.go new file mode 100644 index 0000000000..eb47c2c7b3 --- /dev/null +++ b/pkg/strategy/supertrend/linreg.go @@ -0,0 +1,106 @@ +package supertrend + +import ( + "time" + + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +// LinReg is Linear Regression baseline +type LinReg struct { + types.SeriesBase + types.IntervalWindow + // Values are the slopes of linear regression baseline + Values floats.Slice + klines types.KLineWindow + EndTime time.Time +} + +// Last slope of linear regression baseline +func (lr *LinReg) Last() float64 { + if lr.Values.Length() == 0 { + return 0.0 + } + return lr.Values.Last() +} + +// Index returns the slope of specified index +func (lr *LinReg) Index(i int) float64 { + if i >= lr.Values.Length() { + return 0.0 + } + + return lr.Values.Index(i) +} + +// Length of the slope values +func (lr *LinReg) Length() int { + return lr.Values.Length() +} + +var _ types.SeriesExtend = &LinReg{} + +// Update Linear Regression baseline slope +func (lr *LinReg) Update(kline types.KLine) { + lr.klines.Add(kline) + lr.klines.Truncate(lr.Window) + if len(lr.klines) < lr.Window { + lr.Values.Push(0) + return + } + + var sumX, sumY, sumXSqr, sumXY float64 = 0, 0, 0, 0 + end := len(lr.klines) - 1 // The last kline + for i := end; i >= end-lr.Window+1; i-- { + val := lr.klines[i].GetClose().Float64() + per := float64(end - i + 1) + sumX += per + sumY += val + sumXSqr += per * per + sumXY += val * per + } + length := float64(lr.Window) + slope := (length*sumXY - sumX*sumY) / (length*sumXSqr - sumX*sumX) + average := sumY / length + endPrice := average - slope*sumX/length + slope + startPrice := endPrice + slope*(length-1) + lr.Values.Push((endPrice - startPrice) / (length - 1)) + + log.Debugf("linear regression baseline slope: %f", lr.Last()) +} + +func (lr *LinReg) BindK(target indicator.KLineClosedEmitter, symbol string, interval types.Interval) { + target.OnKLineClosed(types.KLineWith(symbol, interval, lr.PushK)) +} + +func (lr *LinReg) PushK(k types.KLine) { + var zeroTime = time.Time{} + if lr.EndTime != zeroTime && k.EndTime.Before(lr.EndTime) { + return + } + + lr.Update(k) + lr.EndTime = k.EndTime.Time() +} + +func (lr *LinReg) LoadK(allKLines []types.KLine) { + for _, k := range allKLines { + lr.PushK(k) + } +} + +// GetSignal get linear regression signal +func (lr *LinReg) GetSignal() types.Direction { + var lrSignal types.Direction = types.DirectionNone + + switch { + case lr.Last() > 0: + lrSignal = types.DirectionUp + case lr.Last() < 0: + lrSignal = types.DirectionDown + } + + return lrSignal +} diff --git a/pkg/strategy/supertrend/strategy.go b/pkg/strategy/supertrend/strategy.go new file mode 100644 index 0000000000..c40a018544 --- /dev/null +++ b/pkg/strategy/supertrend/strategy.go @@ -0,0 +1,665 @@ +package supertrend + +import ( + "context" + "fmt" + "os" + "sync" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/data/tsv" + "github.com/c9s/bbgo/pkg/datatype/floats" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +const ID = "supertrend" + +var log = logrus.WithField("strategy", ID) + +// TODO: limit order for ATR TP +func init() { + // Register the pointer of the strategy struct, + // so that bbgo knows what struct to be used to unmarshal the configs (YAML or JSON) + // Note: built-in strategies need to imported manually in the bbgo cmd package. + bbgo.RegisterStrategy(ID, &Strategy{}) +} + +// AccumulatedProfitReport For accumulated profit report output +type AccumulatedProfitReport struct { + // AccumulatedProfitMAWindow Accumulated profit SMA window, in number of trades + AccumulatedProfitMAWindow int `json:"accumulatedProfitMAWindow"` + + // IntervalWindow interval window, in days + IntervalWindow int `json:"intervalWindow"` + + // NumberOfInterval How many intervals to output to TSV + NumberOfInterval int `json:"NumberOfInterval"` + + // TsvReportPath The path to output report to + TsvReportPath string `json:"tsvReportPath"` + + // AccumulatedDailyProfitWindow The window to sum up the daily profit, in days + AccumulatedDailyProfitWindow int `json:"accumulatedDailyProfitWindow"` + + // Accumulated profit + accumulatedProfit fixedpoint.Value + accumulatedProfitPerDay floats.Slice + previousAccumulatedProfit fixedpoint.Value + + // Accumulated profit MA + accumulatedProfitMA *indicator.SMA + accumulatedProfitMAPerDay floats.Slice + + // Daily profit + dailyProfit floats.Slice + + // Accumulated fee + accumulatedFee fixedpoint.Value + accumulatedFeePerDay floats.Slice + + // Win ratio + winRatioPerDay floats.Slice + + // Profit factor + profitFactorPerDay floats.Slice + + // Trade number + dailyTrades floats.Slice + accumulatedTrades int + previousAccumulatedTrades int +} + +func (r *AccumulatedProfitReport) Initialize() { + if r.AccumulatedProfitMAWindow <= 0 { + r.AccumulatedProfitMAWindow = 60 + } + if r.IntervalWindow <= 0 { + r.IntervalWindow = 7 + } + if r.AccumulatedDailyProfitWindow <= 0 { + r.AccumulatedDailyProfitWindow = 7 + } + if r.NumberOfInterval <= 0 { + r.NumberOfInterval = 1 + } + r.accumulatedProfitMA = &indicator.SMA{IntervalWindow: types.IntervalWindow{Interval: types.Interval1d, Window: r.AccumulatedProfitMAWindow}} +} + +func (r *AccumulatedProfitReport) RecordProfit(profit fixedpoint.Value) { + r.accumulatedProfit = r.accumulatedProfit.Add(profit) +} + +func (r *AccumulatedProfitReport) RecordTrade(fee fixedpoint.Value) { + r.accumulatedFee = r.accumulatedFee.Add(fee) + r.accumulatedTrades += 1 +} + +func (r *AccumulatedProfitReport) DailyUpdate(tradeStats *types.TradeStats) { + // Daily profit + r.dailyProfit.Update(r.accumulatedProfit.Sub(r.previousAccumulatedProfit).Float64()) + r.previousAccumulatedProfit = r.accumulatedProfit + + // Accumulated profit + r.accumulatedProfitPerDay.Update(r.accumulatedProfit.Float64()) + + // Accumulated profit MA + r.accumulatedProfitMA.Update(r.accumulatedProfit.Float64()) + r.accumulatedProfitMAPerDay.Update(r.accumulatedProfitMA.Last()) + + // Accumulated Fee + r.accumulatedFeePerDay.Update(r.accumulatedFee.Float64()) + + // Win ratio + r.winRatioPerDay.Update(tradeStats.WinningRatio.Float64()) + + // Profit factor + r.profitFactorPerDay.Update(tradeStats.ProfitFactor.Float64()) + + // Daily trades + r.dailyTrades.Update(float64(r.accumulatedTrades - r.previousAccumulatedTrades)) + r.previousAccumulatedTrades = r.accumulatedTrades +} + +// Output Accumulated profit report to a TSV file +func (r *AccumulatedProfitReport) Output(symbol string) { + if r.TsvReportPath != "" { + tsvwiter, err := tsv.AppendWriterFile(r.TsvReportPath) + if err != nil { + panic(err) + } + defer tsvwiter.Close() + // Output symbol, total acc. profit, acc. profit 60MA, interval acc. profit, fee, win rate, profit factor + _ = tsvwiter.Write([]string{"#", "Symbol", "accumulatedProfit", "accumulatedProfitMA", fmt.Sprintf("%dd profit", r.AccumulatedDailyProfitWindow), "accumulatedFee", "winRatio", "profitFactor", "60D trades"}) + for i := 0; i <= r.NumberOfInterval-1; i++ { + accumulatedProfit := r.accumulatedProfitPerDay.Index(r.IntervalWindow * i) + accumulatedProfitStr := fmt.Sprintf("%f", accumulatedProfit) + accumulatedProfitMA := r.accumulatedProfitMAPerDay.Index(r.IntervalWindow * i) + accumulatedProfitMAStr := fmt.Sprintf("%f", accumulatedProfitMA) + intervalAccumulatedProfit := r.dailyProfit.Tail(r.AccumulatedDailyProfitWindow+r.IntervalWindow*i).Sum() - r.dailyProfit.Tail(r.IntervalWindow*i).Sum() + intervalAccumulatedProfitStr := fmt.Sprintf("%f", intervalAccumulatedProfit) + accumulatedFee := fmt.Sprintf("%f", r.accumulatedFeePerDay.Index(r.IntervalWindow*i)) + winRatio := fmt.Sprintf("%f", r.winRatioPerDay.Index(r.IntervalWindow*i)) + profitFactor := fmt.Sprintf("%f", r.profitFactorPerDay.Index(r.IntervalWindow*i)) + trades := r.dailyTrades.Tail(60+r.IntervalWindow*i).Sum() - r.dailyTrades.Tail(r.IntervalWindow*i).Sum() + tradesStr := fmt.Sprintf("%f", trades) + + _ = tsvwiter.Write([]string{fmt.Sprintf("%d", i+1), symbol, accumulatedProfitStr, accumulatedProfitMAStr, intervalAccumulatedProfitStr, accumulatedFee, winRatio, profitFactor, tradesStr}) + } + } +} + +type Strategy struct { + Environment *bbgo.Environment + Market types.Market + + // persistence fields + Position *types.Position `persistence:"position"` + ProfitStats *types.ProfitStats `persistence:"profit_stats"` + TradeStats *types.TradeStats `persistence:"trade_stats"` + + // Symbol is the market symbol you want to trade + Symbol string `json:"symbol"` + + types.IntervalWindow + + // Double DEMA + doubleDema *DoubleDema + // FastDEMAWindow DEMA window for checking breakout + FastDEMAWindow int `json:"fastDEMAWindow"` + // SlowDEMAWindow DEMA window for checking breakout + SlowDEMAWindow int `json:"slowDEMAWindow"` + + // SuperTrend indicator + Supertrend *indicator.Supertrend + // SupertrendMultiplier ATR multiplier for calculation of supertrend + SupertrendMultiplier float64 `json:"supertrendMultiplier"` + + // LinearRegression Use linear regression as trend confirmation + LinearRegression *LinReg `json:"linearRegression,omitempty"` + + // Leverage uses the account net value to calculate the order qty + Leverage fixedpoint.Value `json:"leverage"` + // Quantity sets the fixed order qty, takes precedence over Leverage + Quantity fixedpoint.Value `json:"quantity"` + AccountValueCalculator *bbgo.AccountValueCalculator + + // TakeProfitAtrMultiplier TP according to ATR multiple, 0 to disable this + TakeProfitAtrMultiplier float64 `json:"takeProfitAtrMultiplier"` + + // StopLossByTriggeringK Set SL price to the low/high of the triggering Kline + StopLossByTriggeringK bool `json:"stopLossByTriggeringK"` + + // StopByReversedSupertrend TP/SL by reversed supertrend signal + StopByReversedSupertrend bool `json:"stopByReversedSupertrend"` + + // StopByReversedDema TP/SL by reversed DEMA signal + StopByReversedDema bool `json:"stopByReversedDema"` + + // StopByReversedLinGre TP/SL by reversed linear regression signal + StopByReversedLinGre bool `json:"stopByReversedLinGre"` + + // ExitMethods Exit methods + ExitMethods bbgo.ExitMethodSet `json:"exits"` + + // whether to draw graph or not by the end of backtest + DrawGraph bool `json:"drawGraph"` + GraphPNLPath string `json:"graphPNLPath"` + GraphCumPNLPath string `json:"graphCumPNLPath"` + + // for position + buyPrice float64 `persistence:"buy_price"` + sellPrice float64 `persistence:"sell_price"` + highestPrice float64 `persistence:"highest_price"` + lowestPrice float64 `persistence:"lowest_price"` + + session *bbgo.ExchangeSession + orderExecutor *bbgo.GeneralOrderExecutor + currentTakeProfitPrice fixedpoint.Value + currentStopLossPrice fixedpoint.Value + + // StrategyController + bbgo.StrategyController + + // Accumulated profit report + AccumulatedProfitReport *AccumulatedProfitReport `json:"accumulatedProfitReport"` +} + +func (s *Strategy) ID() string { + return ID +} + +func (s *Strategy) InstanceID() string { + return fmt.Sprintf("%s:%s", ID, s.Symbol) +} + +func (s *Strategy) Validate() error { + if len(s.Symbol) == 0 { + return errors.New("symbol is required") + } + + if len(s.Interval) == 0 { + return errors.New("interval is required") + } + + return nil +} + +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.LinearRegression.Interval}) + + s.ExitMethods.SetAndSubscribe(session, s) + + // Accumulated profit report + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: types.Interval1d}) +} + +// Position control + +func (s *Strategy) CurrentPosition() *types.Position { + return s.Position +} + +func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Value) error { + base := s.Position.GetBase() + if base.IsZero() { + return fmt.Errorf("no opened %s position", s.Position.Symbol) + } + + // make it negative + quantity := base.Mul(percentage).Abs() + side := types.SideTypeBuy + if base.Sign() > 0 { + side = types.SideTypeSell + } + + if quantity.Compare(s.Market.MinQuantity) < 0 { + return fmt.Errorf("%s order quantity %v is too small, less than %v", s.Symbol, quantity, s.Market.MinQuantity) + } + + orderForm := s.generateOrderForm(side, quantity, types.SideEffectTypeAutoRepay) + + bbgo.Notify("submitting %s %s order to close position by %v", s.Symbol, side.String(), percentage, orderForm) + + _, err := s.orderExecutor.SubmitOrders(ctx, orderForm) + if err != nil { + log.WithError(err).Errorf("can not place %s position close order", s.Symbol) + bbgo.Notify("can not place %s position close order", s.Symbol) + } + + return err +} + +// setupIndicators initializes indicators +func (s *Strategy) setupIndicators() { + // K-line store for indicators + kLineStore, _ := s.session.MarketDataStore(s.Symbol) + + // Double DEMA + s.doubleDema = newDoubleDema(kLineStore, s.Interval, s.FastDEMAWindow, s.SlowDEMAWindow) + + // Supertrend + if s.Window == 0 { + s.Window = 39 + } + if s.SupertrendMultiplier == 0 { + s.SupertrendMultiplier = 3 + } + s.Supertrend = &indicator.Supertrend{IntervalWindow: types.IntervalWindow{Window: s.Window, Interval: s.Interval}, ATRMultiplier: s.SupertrendMultiplier} + s.Supertrend.AverageTrueRange = &indicator.ATR{IntervalWindow: types.IntervalWindow{Window: s.Window, Interval: s.Interval}} + s.Supertrend.BindK(s.session.MarketDataStream, s.Symbol, s.Supertrend.Interval) + if klines, ok := kLineStore.KLinesOfInterval(s.Supertrend.Interval); ok { + s.Supertrend.LoadK((*klines)[0:]) + } + + // Linear Regression + if s.LinearRegression != nil { + if s.LinearRegression.Window == 0 { + s.LinearRegression = nil + } else if s.LinearRegression.Interval == "" { + s.LinearRegression = nil + } else { + s.LinearRegression.BindK(s.session.MarketDataStream, s.Symbol, s.LinearRegression.Interval) + if klines, ok := kLineStore.KLinesOfInterval(s.LinearRegression.Interval); ok { + s.LinearRegression.LoadK((*klines)[0:]) + } + } + } +} + +func (s *Strategy) shouldStop(kline types.KLine, stSignal types.Direction, demaSignal types.Direction, lgSignal types.Direction) bool { + stopNow := false + base := s.Position.GetBase() + baseSign := base.Sign() + + if s.StopLossByTriggeringK && !s.currentStopLossPrice.IsZero() && ((baseSign < 0 && kline.GetClose().Compare(s.currentStopLossPrice) > 0) || (baseSign > 0 && kline.GetClose().Compare(s.currentStopLossPrice) < 0)) { + // SL by triggering Kline low/high + bbgo.Notify("%s stop loss by triggering the kline low/high", s.Symbol) + stopNow = true + } else if s.TakeProfitAtrMultiplier > 0 && !s.currentTakeProfitPrice.IsZero() && ((baseSign < 0 && kline.GetClose().Compare(s.currentTakeProfitPrice) < 0) || (baseSign > 0 && kline.GetClose().Compare(s.currentTakeProfitPrice) > 0)) { + // TP by multiple of ATR + bbgo.Notify("%s take profit by multiple of ATR", s.Symbol) + stopNow = true + } else if s.StopByReversedSupertrend && ((baseSign < 0 && stSignal == types.DirectionUp) || (baseSign > 0 && stSignal == types.DirectionDown)) { + // Use supertrend signal to TP/SL + bbgo.Notify("%s stop by the reversed signal of Supertrend", s.Symbol) + stopNow = true + } else if s.StopByReversedDema && ((baseSign < 0 && demaSignal == types.DirectionUp) || (baseSign > 0 && demaSignal == types.DirectionDown)) { + // Use DEMA signal to TP/SL + bbgo.Notify("%s stop by the reversed signal of DEMA", s.Symbol) + stopNow = true + } else if s.StopByReversedLinGre && ((baseSign < 0 && lgSignal == types.DirectionUp) || (baseSign > 0 && lgSignal == types.DirectionDown)) { + // Use linear regression signal to TP/SL + bbgo.Notify("%s stop by the reversed signal of linear regression", s.Symbol) + stopNow = true + } + + return stopNow +} + +func (s *Strategy) getSide(stSignal types.Direction, demaSignal types.Direction, lgSignal types.Direction) types.SideType { + var side types.SideType + + if stSignal == types.DirectionUp && demaSignal == types.DirectionUp && (s.LinearRegression == nil || lgSignal == types.DirectionUp) { + side = types.SideTypeBuy + } else if stSignal == types.DirectionDown && demaSignal == types.DirectionDown && (s.LinearRegression == nil || lgSignal == types.DirectionDown) { + side = types.SideTypeSell + } + + return side +} + +func (s *Strategy) generateOrderForm(side types.SideType, quantity fixedpoint.Value, marginOrderSideEffect types.MarginOrderSideEffectType) types.SubmitOrder { + orderForm := types.SubmitOrder{ + Symbol: s.Symbol, + Market: s.Market, + Side: side, + Type: types.OrderTypeMarket, + Quantity: quantity, + MarginSideEffect: marginOrderSideEffect, + } + + return orderForm +} + +// calculateQuantity returns leveraged quantity +func (s *Strategy) calculateQuantity(ctx context.Context, currentPrice fixedpoint.Value, side types.SideType) fixedpoint.Value { + // Quantity takes precedence + if !s.Quantity.IsZero() { + return s.Quantity + } + + usingLeverage := s.session.Margin || s.session.IsolatedMargin || s.session.Futures || s.session.IsolatedFutures + + if bbgo.IsBackTesting { // Backtesting + balance, ok := s.session.GetAccount().Balance(s.Market.QuoteCurrency) + if !ok { + log.Errorf("can not update %s quote balance from exchange", s.Symbol) + return fixedpoint.Zero + } + + return balance.Available.Mul(fixedpoint.Min(s.Leverage, fixedpoint.One)).Div(currentPrice) + } else if !usingLeverage && side == types.SideTypeSell { // Spot sell + balance, ok := s.session.GetAccount().Balance(s.Market.BaseCurrency) + if !ok { + log.Errorf("can not update %s base balance from exchange", s.Symbol) + return fixedpoint.Zero + } + + return balance.Available.Mul(fixedpoint.Min(s.Leverage, fixedpoint.One)) + } else { // Using leverage or spot buy + quoteQty, err := bbgo.CalculateQuoteQuantity(ctx, s.session, s.Market.QuoteCurrency, s.Leverage) + if err != nil { + log.WithError(err).Errorf("can not update %s quote balance from exchange", s.Symbol) + return fixedpoint.Zero + } + + return quoteQty.Div(currentPrice) + } +} + +func (s *Strategy) CalcAssetValue(price fixedpoint.Value) fixedpoint.Value { + balances := s.session.GetAccount().Balances() + return balances[s.Market.BaseCurrency].Total().Mul(price).Add(balances[s.Market.QuoteCurrency].Total()) +} + +func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + s.session = session + + s.currentStopLossPrice = fixedpoint.Zero + s.currentTakeProfitPrice = fixedpoint.Zero + + // calculate group id for orders + instanceID := s.InstanceID() + + // If position is nil, we need to allocate a new position for calculation + if s.Position == nil { + s.Position = types.NewPositionFromMarket(s.Market) + } + // Always update the position fields + s.Position.Strategy = ID + s.Position.StrategyInstanceID = s.InstanceID() + + // Profit stats + if s.ProfitStats == nil { + s.ProfitStats = types.NewProfitStats(s.Market) + } + + if s.TradeStats == nil { + s.TradeStats = types.NewTradeStats(s.Symbol) + } + + // Interval profit report + if bbgo.IsBackTesting { + startTime := s.Environment.StartTime() + s.TradeStats.SetIntervalProfitCollector(types.NewIntervalProfitCollector(types.Interval1d, startTime)) + s.TradeStats.SetIntervalProfitCollector(types.NewIntervalProfitCollector(types.Interval1w, startTime)) + s.TradeStats.SetIntervalProfitCollector(types.NewIntervalProfitCollector(types.Interval1mo, startTime)) + } + + // Set fee rate + if s.session.MakerFeeRate.Sign() > 0 || s.session.TakerFeeRate.Sign() > 0 { + s.Position.SetExchangeFeeRate(s.session.ExchangeName, types.ExchangeFee{ + MakerFeeRate: s.session.MakerFeeRate, + TakerFeeRate: s.session.TakerFeeRate, + }) + } + + // Setup order executor + s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) + s.orderExecutor.BindEnvironment(s.Environment) + s.orderExecutor.BindProfitStats(s.ProfitStats) + s.orderExecutor.BindTradeStats(s.TradeStats) + s.orderExecutor.Bind() + + // AccountValueCalculator + s.AccountValueCalculator = bbgo.NewAccountValueCalculator(s.session, s.Market.QuoteCurrency) + + // Accumulated profit report + if bbgo.IsBackTesting { + if s.AccumulatedProfitReport == nil { + s.AccumulatedProfitReport = &AccumulatedProfitReport{} + } + s.AccumulatedProfitReport.Initialize() + s.orderExecutor.TradeCollector().OnProfit(func(trade types.Trade, profit *types.Profit) { + if profit == nil { + return + } + + s.AccumulatedProfitReport.RecordProfit(profit.Profit) + }) + // s.orderExecutor.TradeCollector().OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { + // s.AccumulatedProfitReport.RecordTrade(trade.Fee) + // }) + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1d, func(kline types.KLine) { + s.AccumulatedProfitReport.DailyUpdate(s.TradeStats) + })) + } + + // For drawing + profitSlice := floats.Slice{1., 1.} + price, _ := session.LastPrice(s.Symbol) + initAsset := s.CalcAssetValue(price).Float64() + cumProfitSlice := floats.Slice{initAsset, initAsset} + + s.orderExecutor.TradeCollector().OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { + if bbgo.IsBackTesting { + s.AccumulatedProfitReport.RecordTrade(trade.Fee) + } + + // For drawing/charting + price := trade.Price.Float64() + if s.buyPrice > 0 { + profitSlice.Update(price / s.buyPrice) + cumProfitSlice.Update(s.CalcAssetValue(trade.Price).Float64()) + } else if s.sellPrice > 0 { + profitSlice.Update(s.sellPrice / price) + cumProfitSlice.Update(s.CalcAssetValue(trade.Price).Float64()) + } + if s.Position.IsDust(trade.Price) { + s.buyPrice = 0 + s.sellPrice = 0 + s.highestPrice = 0 + s.lowestPrice = 0 + } else if s.Position.IsLong() { + s.buyPrice = price + s.sellPrice = 0 + s.highestPrice = s.buyPrice + s.lowestPrice = 0 + } else { + s.sellPrice = price + s.buyPrice = 0 + s.highestPrice = 0 + s.lowestPrice = s.sellPrice + } + }) + + s.InitDrawCommands(&profitSlice, &cumProfitSlice) + + // Sync position to redis on trade + s.orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { + bbgo.Sync(ctx, s) + }) + + // StrategyController + s.Status = types.StrategyStatusRunning + s.OnSuspend(func() { + _ = s.orderExecutor.GracefulCancel(ctx) + bbgo.Sync(ctx, s) + }) + s.OnEmergencyStop(func() { + _ = s.orderExecutor.GracefulCancel(ctx) + // Close 100% position + _ = s.ClosePosition(ctx, fixedpoint.One) + }) + + // Setup indicators + s.setupIndicators() + + // Exit methods + for _, method := range s.ExitMethods { + method.Bind(session, s.orderExecutor) + } + + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { + // StrategyController + if s.Status != types.StrategyStatusRunning { + return + } + + closePrice := kline.GetClose() + openPrice := kline.GetOpen() + closePrice64 := closePrice.Float64() + openPrice64 := openPrice.Float64() + + // Supertrend signal + stSignal := s.Supertrend.GetSignal() + + // DEMA signal + demaSignal := s.doubleDema.getDemaSignal(openPrice64, closePrice64) + + // Linear Regression signal + var lgSignal types.Direction + if s.LinearRegression != nil { + lgSignal = s.LinearRegression.GetSignal() + } + + // TP/SL if there's non-dust position and meets the criteria + if !s.Market.IsDustQuantity(s.Position.GetBase().Abs(), closePrice) && s.shouldStop(kline, stSignal, demaSignal, lgSignal) { + if err := s.ClosePosition(ctx, fixedpoint.One); err == nil { + s.currentStopLossPrice = fixedpoint.Zero + s.currentTakeProfitPrice = fixedpoint.Zero + } + } + + // Get order side + side := s.getSide(stSignal, demaSignal, lgSignal) + // Set TP/SL price if needed + if side == types.SideTypeBuy { + if s.StopLossByTriggeringK { + s.currentStopLossPrice = kline.GetLow() + } + if s.TakeProfitAtrMultiplier > 0 { + s.currentTakeProfitPrice = closePrice.Add(fixedpoint.NewFromFloat(s.Supertrend.AverageTrueRange.Last() * s.TakeProfitAtrMultiplier)) + } + } else if side == types.SideTypeSell { + if s.StopLossByTriggeringK { + s.currentStopLossPrice = kline.GetHigh() + } + if s.TakeProfitAtrMultiplier > 0 { + s.currentTakeProfitPrice = closePrice.Sub(fixedpoint.NewFromFloat(s.Supertrend.AverageTrueRange.Last() * s.TakeProfitAtrMultiplier)) + } + } + + // Open position + // The default value of side is an empty string. Unless side is set by the checks above, the result of the following condition is false + if side == types.SideTypeSell || side == types.SideTypeBuy { + bbgo.Notify("open %s position for signal %v", s.Symbol, side) + // Close opposite position if any + if !s.Position.IsDust(closePrice) { + if (side == types.SideTypeSell && s.Position.IsLong()) || (side == types.SideTypeBuy && s.Position.IsShort()) { + bbgo.Notify("close existing %s position before open a new position", s.Symbol) + _ = s.ClosePosition(ctx, fixedpoint.One) + } else { + bbgo.Notify("existing %s position has the same direction with the signal", s.Symbol) + return + } + } + + orderForm := s.generateOrderForm(side, s.calculateQuantity(ctx, closePrice, side), types.SideEffectTypeMarginBuy) + log.Infof("submit open position order %v", orderForm) + _, err := s.orderExecutor.SubmitOrders(ctx, orderForm) + if err != nil { + log.WithError(err).Errorf("can not place %s open position order", s.Symbol) + bbgo.Notify("can not place %s open position order", s.Symbol) + } + } + })) + + // Graceful shutdown + bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { + defer wg.Done() + + // Output accumulated profit report + if bbgo.IsBackTesting { + defer s.AccumulatedProfitReport.Output(s.Symbol) + + if s.DrawGraph { + if err := s.Draw(&profitSlice, &cumProfitSlice); err != nil { + log.WithError(err).Errorf("cannot draw graph") + } + } + } + + _ = s.orderExecutor.GracefulCancel(ctx) + _, _ = fmt.Fprintln(os.Stderr, s.TradeStats.String()) + }) + + return nil +} diff --git a/pkg/strategy/support/strategy.go b/pkg/strategy/support/strategy.go index 91aaeabddf..d59788a3a0 100644 --- a/pkg/strategy/support/strategy.go +++ b/pkg/strategy/support/strategy.go @@ -7,18 +7,14 @@ import ( "github.com/sirupsen/logrus" - "github.com/c9s/bbgo/pkg/indicator" - "github.com/c9s/bbgo/pkg/service" - "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/indicator" "github.com/c9s/bbgo/pkg/types" ) const ID = "support" -const stateKey = "state-v1" - var log = logrus.WithField("strategy", ID) var zeroiw = types.IntervalWindow{} @@ -92,7 +88,13 @@ type TrailingStopControl struct { minimumProfitPercentage fixedpoint.Value CurrentHighestPrice fixedpoint.Value - OrderID uint64 + StopOrder *types.Order +} + +func (control *TrailingStopControl) UpdateCurrentHighestPrice(p fixedpoint.Value) bool { + orig := control.CurrentHighestPrice + control.CurrentHighestPrice = fixedpoint.Max(control.CurrentHighestPrice, p) + return orig.Compare(control.CurrentHighestPrice) == 0 } func (control *TrailingStopControl) IsHigherThanMin(minTargetPrice fixedpoint.Value) bool { @@ -122,17 +124,17 @@ func (control *TrailingStopControl) GenerateStopOrder(quantity fixedpoint.Value) // Not implemented yet // ResistanceStop is a kind of stop order by detecting resistance -//type ResistanceStop struct { +// type ResistanceStop struct { // Interval types.Interval `json:"interval"` // sensitivity fixedpoint.Value `json:"sensitivity"` // MinVolume fixedpoint.Value `json:"minVolume"` // TakerBuyRatio fixedpoint.Value `json:"takerBuyRatio"` -//} +// } type Strategy struct { - *bbgo.Notifiability `json:"-"` - *bbgo.Persistence - *bbgo.Graceful `json:"-"` + *bbgo.Environment `json:"-"` + + session *bbgo.ExchangeSession Symbol string `json:"symbol"` Market types.Market `json:"-"` @@ -156,7 +158,7 @@ type Strategy struct { // Not implemented yet // ResistanceStop *ResistanceStop `json:"resistanceStop"` // - //ResistanceTakerBuyRatio fixedpoint.Value `json:"resistanceTakerBuyRatio"` + // ResistanceTakerBuyRatio fixedpoint.Value `json:"resistanceTakerBuyRatio"` // Min BaseAsset balance to keep MinBaseAssetBalance fixedpoint.Value `json:"minBaseAssetBalance"` @@ -166,13 +168,12 @@ type Strategy struct { ScaleQuantity *bbgo.PriceVolumeScale `json:"scaleQuantity"` - orderExecutor bbgo.OrderExecutor + orderExecutor *bbgo.GeneralOrderExecutor - tradeCollector *bbgo.TradeCollector - - orderStore *bbgo.OrderStore - activeOrders *bbgo.LocalActiveOrderBook - state *State + Position *types.Position `persistence:"position"` + ProfitStats *types.ProfitStats `persistence:"profit_stats"` + TradeStats *types.TradeStats `persistence:"trade_stats"` + CurrentHighestPrice fixedpoint.Value `persistence:"current_highest_price"` triggerEMA *indicator.EWMA longTermEMA *indicator.EWMA @@ -189,6 +190,10 @@ func (s *Strategy) ID() string { return ID } +func (s *Strategy) InstanceID() string { + return fmt.Sprintf("%s:%s", ID, s.Symbol) +} + func (s *Strategy) Validate() error { if s.Quantity.IsZero() && s.ScaleQuantity == nil { return fmt.Errorf("quantity or scaleQuantity can not be zero") @@ -202,25 +207,25 @@ func (s *Strategy) Validate() error { } func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { - session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: string(s.Interval)}) + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) if s.TriggerMovingAverage != zeroiw { - session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: string(s.TriggerMovingAverage.Interval)}) + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.TriggerMovingAverage.Interval}) } if s.LongTermMovingAverage != zeroiw { - session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: string(s.LongTermMovingAverage.Interval)}) + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.LongTermMovingAverage.Interval}) } } func (s *Strategy) CurrentPosition() *types.Position { - return s.state.Position + return s.Position } func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Value) error { - base := s.state.Position.GetBase() + base := s.Position.GetBase() if base.IsZero() { - return fmt.Errorf("no opened %s position", s.state.Position.Symbol) + return fmt.Errorf("no opened %s position", s.Position.Symbol) } // make it negative @@ -242,90 +247,13 @@ func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Valu Market: s.Market, } - s.Notify("Submitting %s %s order to close position by %v", s.Symbol, side.String(), percentage, submitOrder) - - createdOrders, err := s.submitOrders(ctx, s.orderExecutor, submitOrder) - if err != nil { - log.WithError(err).Errorf("can not place position close order") - } - - s.orderStore.Add(createdOrders...) - s.activeOrders.Add(createdOrders...) + bbgo.Notify("Submitting %s %s order to close position by %v", s.Symbol, side.String(), percentage, submitOrder) + _, err := s.orderExecutor.SubmitOrders(ctx, submitOrder) return err } -func (s *Strategy) SaveState() error { - if err := s.Persistence.Save(s.state, ID, s.Symbol, stateKey); err != nil { - return err - } else { - log.Infof("state is saved => %+v", s.state) - } - return nil -} - -func (s *Strategy) LoadState() error { - var state State - - // load position - if err := s.Persistence.Load(&state, ID, s.Symbol, stateKey); err != nil { - if err != service.ErrPersistenceNotExists { - return err - } - - s.state = &State{} - } else { - s.state = &state - log.Infof("state is restored: %+v", s.state) - } - - if s.state.Position == nil { - s.state.Position = types.NewPositionFromMarket(s.Market) - } - - if s.trailingStopControl != nil { - if s.state.CurrentHighestPrice == nil { - s.trailingStopControl.CurrentHighestPrice = fixedpoint.Zero - } else { - s.trailingStopControl.CurrentHighestPrice = *s.state.CurrentHighestPrice - } - s.state.CurrentHighestPrice = &s.trailingStopControl.CurrentHighestPrice - } - - return nil -} - func (s *Strategy) submitOrders(ctx context.Context, orderExecutor bbgo.OrderExecutor, orderForms ...types.SubmitOrder) (types.OrderSlice, error) { - for _, o := range orderForms { - s.Notifiability.Notify(o) - } - - createdOrders, err := orderExecutor.SubmitOrders(ctx, orderForms...) - if err != nil { - return nil, err - } - - s.orderStore.Add(createdOrders...) - s.activeOrders.Add(createdOrders...) - s.tradeCollector.Emit() - return createdOrders, nil -} - -// Cancel order -func (s *Strategy) cancelOrder(orderID uint64, ctx context.Context, orderExecutor bbgo.OrderExecutor) error { - // Cancel the original order - order, ok := s.orderStore.Get(orderID) - if ok { - switch order.Status { - case types.OrderStatusCanceled, types.OrderStatusRejected, types.OrderStatusFilled: - // Do nothing - default: - if err := orderExecutor.CancelOrders(ctx, order); err != nil { - return err - } - } - } - - return nil + return s.orderExecutor.SubmitOrders(ctx, orderForms...) } var slippageModifier = fixedpoint.NewFromFloat(1.003) @@ -390,27 +318,35 @@ func (s *Strategy) calculateQuantity(session *bbgo.ExchangeSession, side types.S } func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { - s.orderExecutor = orderExecutor + s.session = session + instanceID := s.InstanceID() + + if s.Position == nil { + s.Position = types.NewPositionFromMarket(s.Market) + } + + if s.ProfitStats == nil { + s.ProfitStats = types.NewProfitStats(s.Market) + } + + // trade stats + if s.TradeStats == nil { + s.TradeStats = types.NewTradeStats(s.Symbol) + } + + s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) + s.orderExecutor.BindEnvironment(s.Environment) + s.orderExecutor.BindProfitStats(s.ProfitStats) + s.orderExecutor.BindTradeStats(s.TradeStats) + s.orderExecutor.Bind() // StrategyController s.Status = types.StrategyStatusRunning s.OnSuspend(func() { // Cancel all order - if err := s.activeOrders.GracefulCancel(ctx, session.Exchange); err != nil { - errMsg := fmt.Sprintf("Not all %s orders are cancelled! Please check again.", s.Symbol) - log.WithError(err).Errorf(errMsg) - s.Notify(errMsg) - } else { - s.Notify("All %s orders are cancelled.", s.Symbol) - } - - // Save state - if err := s.SaveState(); err != nil { - log.WithError(err).Errorf("can not save state: %+v", s.state) - } else { - log.Infof("%s state is saved.", s.Symbol) - } + _ = s.orderExecutor.GracefulCancel(ctx) + bbgo.Sync(ctx, s) }) s.OnEmergencyStop(func() { @@ -419,13 +355,13 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se if err := s.ClosePosition(context.Background(), percentage); err != nil { errMsg := "failed to close position" log.WithError(err).Errorf(errMsg) - s.Notify(errMsg) + bbgo.Notify(errMsg) } if err := s.Suspend(); err != nil { errMsg := "failed to suspend strategy" log.WithError(err).Errorf(errMsg) - s.Notify(errMsg) + bbgo.Notify(errMsg) } }) @@ -448,16 +384,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se log.Infof("adjusted minimal support volume to %s according to sensitivity %s", s.MinVolume.String(), s.Sensitivity.String()) } - market, ok := session.Market(s.Symbol) - if !ok { - return fmt.Errorf("market %s is not defined", s.Symbol) - } - s.Market = market - - standardIndicatorSet, ok := session.StandardIndicatorSet(s.Symbol) - if !ok { - return fmt.Errorf("standardIndicatorSet is nil, symbol %s", s.Symbol) - } + standardIndicatorSet := session.StandardIndicatorSet(s.Symbol) if s.TriggerMovingAverage != zeroiw { s.triggerEMA = standardIndicatorSet.EWMA(s.TriggerMovingAverage) @@ -472,85 +399,33 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.longTermEMA = standardIndicatorSet.EWMA(s.LongTermMovingAverage) } - s.orderStore = bbgo.NewOrderStore(s.Symbol) - s.orderStore.BindStream(session.UserDataStream) - - s.activeOrders = bbgo.NewLocalActiveOrderBook(s.Symbol) - s.activeOrders.BindStream(session.UserDataStream) - if !s.TrailingStopTarget.TrailingStopCallbackRatio.IsZero() { s.trailingStopControl = &TrailingStopControl{ symbol: s.Symbol, market: s.Market, marginSideEffect: s.MarginOrderSideEffect, - CurrentHighestPrice: fixedpoint.Zero, trailingStopCallbackRatio: s.TrailingStopTarget.TrailingStopCallbackRatio, minimumProfitPercentage: s.TrailingStopTarget.MinimumProfitPercentage, + CurrentHighestPrice: s.CurrentHighestPrice, } } - if err := s.LoadState(); err != nil { - return err - } else { - s.Notify("%s state is restored => %+v", s.Symbol, s.state) - } - - s.tradeCollector = bbgo.NewTradeCollector(s.Symbol, s.state.Position, s.orderStore) - if !s.TrailingStopTarget.TrailingStopCallbackRatio.IsZero() { // Update trailing stop when the position changes - s.tradeCollector.OnPositionUpdate(func(position *types.Position) { + s.orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { // StrategyController if s.Status != types.StrategyStatusRunning { return } - if position.Base.Compare(s.Market.MinQuantity) > 0 { // Update order if we have a position - // Cancel the original order - if err := s.cancelOrder(s.trailingStopControl.OrderID, ctx, orderExecutor); err != nil { - log.WithError(err).Errorf("Can not cancel the original trailing stop order!") - } - s.trailingStopControl.OrderID = 0 - - // Calculate minimum target price - var minTargetPrice = fixedpoint.Zero - if s.trailingStopControl.minimumProfitPercentage.Sign() > 0 { - minTargetPrice = position.AverageCost.Mul(fixedpoint.One.Add(s.trailingStopControl.minimumProfitPercentage)) - } - - // Place new order if the target price is higher than the minimum target price - if s.trailingStopControl.IsHigherThanMin(minTargetPrice) { - orderForm := s.trailingStopControl.GenerateStopOrder(position.Base) - orders, err := s.submitOrders(ctx, orderExecutor, orderForm) - if err != nil { - log.WithError(err).Error("submit profit trailing stop order error") - s.Notify("submit %s profit trailing stop order error", s.Symbol) - } else { - orderIds := orders.IDs() - if len(orderIds) > 0 { - s.trailingStopControl.OrderID = orderIds[0] - } else { - log.Error("submit profit trailing stop order error. unknown error") - s.Notify("submit %s profit trailing stop order error", s.Symbol) - s.trailingStopControl.OrderID = 0 - } - } - } - } - // Save state - if err := s.SaveState(); err != nil { - log.WithError(err).Errorf("can not save state: %+v", s.state) - } else { - s.Notify("%s position is saved", s.Symbol, s.state.Position) + if !position.IsLong() || position.IsDust(position.AverageCost) { + return } + + s.updateStopOrder(ctx) }) } - s.tradeCollector.BindStream(session.UserDataStream) - - // s.tradeCollector.BindStreamForBackground(session.UserDataStream) - // go s.tradeCollector.Run(ctx) - session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { // StrategyController if s.Status != types.StrategyStatusRunning { @@ -568,49 +443,15 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se closePrice := kline.GetClose() highPrice := kline.GetHigh() + // check our trailing stop if s.TrailingStopTarget.TrailingStopCallbackRatio.Sign() > 0 { - if s.state.Position.Base.Compare(s.Market.MinQuantity) <= 0 { // Without a position - // Update trailing orders with current high price - s.trailingStopControl.CurrentHighestPrice = highPrice - } else if s.trailingStopControl.CurrentHighestPrice.Compare(highPrice) < 0 || s.trailingStopControl.OrderID == 0 { // With a position or no trailing stop order yet - // Update trailing orders with current high price if it's higher - s.trailingStopControl.CurrentHighestPrice = highPrice - - // Cancel the original order - if err := s.cancelOrder(s.trailingStopControl.OrderID, ctx, orderExecutor); err != nil { - log.WithError(err).Errorf("Can not cancel the original trailing stop order!") - } - s.trailingStopControl.OrderID = 0 - - // Calculate minimum target price - var minTargetPrice = fixedpoint.Zero - if s.trailingStopControl.minimumProfitPercentage.Sign() > 0 { - minTargetPrice = s.state.Position.AverageCost.Mul(fixedpoint.One.Add(s.trailingStopControl.minimumProfitPercentage)) - } - - // Place new order if the target price is higher than the minimum target price - if s.trailingStopControl.IsHigherThanMin(minTargetPrice) { - orderForm := s.trailingStopControl.GenerateStopOrder(s.state.Position.Base) - orders, err := s.submitOrders(ctx, orderExecutor, orderForm) - if err != nil || orders == nil { - log.WithError(err).Errorf("submit %s profit trailing stop order error", s.Symbol) - s.Notify("submit %s profit trailing stop order error", s.Symbol) - } else { - orderIds := orders.IDs() - if len(orderIds) > 0 { - s.trailingStopControl.OrderID = orderIds[0] - } else { - log.Error("submit profit trailing stop order error. unknown error") - s.Notify("submit %s profit trailing stop order error", s.Symbol) - s.trailingStopControl.OrderID = 0 - } - } + if s.Position.IsLong() && !s.Position.IsDust(closePrice) { + changed := s.trailingStopControl.UpdateCurrentHighestPrice(highPrice) + if changed { + // Cancel the original order + s.updateStopOrder(ctx) } } - // Save state - if err := s.SaveState(); err != nil { - log.WithError(err).Errorf("can not save state: %+v", s.state) - } } // check support volume @@ -623,7 +464,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se takerBuyRatio := kline.TakerBuyBaseAssetVolume.Div(kline.Volume) takerBuyBaseVolumeThreshold := kline.Volume.Mul(s.TakerBuyRatio) if takerBuyRatio.Compare(s.TakerBuyRatio) < 0 { - s.Notify("%s: taker buy base volume %s (volume ratio %s) is less than %s (volume ratio %s)", + bbgo.Notify("%s: taker buy base volume %s (volume ratio %s) is less than %s (volume ratio %s)", s.Symbol, kline.TakerBuyBaseAssetVolume.String(), takerBuyRatio.String(), @@ -637,7 +478,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } if s.longTermEMA != nil && closePrice.Float64() < s.longTermEMA.Last() { - s.Notify("%s: closed price is below the long term moving average line %f, skipping this support", + bbgo.Notify("%s: closed price is below the long term moving average line %f, skipping this support", s.Symbol, s.longTermEMA.Last(), kline, @@ -646,7 +487,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } if s.triggerEMA != nil && closePrice.Float64() > s.triggerEMA.Last() { - s.Notify("%s: closed price is above the trigger moving average line %f, skipping this support", + bbgo.Notify("%s: closed price is above the trigger moving average line %f, skipping this support", s.Symbol, s.triggerEMA.Last(), kline, @@ -655,7 +496,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } if s.triggerEMA != nil && s.longTermEMA != nil { - s.Notify("Found %s support: the close price %s is below trigger EMA %f and above long term EMA %f and volume %s > minimum volume %s", + bbgo.Notify("Found %s support: the close price %s is below trigger EMA %f and above long term EMA %f and volume %s > minimum volume %s", s.Symbol, closePrice.String(), s.triggerEMA.Last(), @@ -664,7 +505,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.MinVolume.String(), kline) } else { - s.Notify("Found %s support: the close price %s and volume %s > minimum volume %s", + bbgo.Notify("Found %s support: the close price %s and volume %s > minimum volume %s", s.Symbol, closePrice.String(), kline.Volume.String(), @@ -680,14 +521,14 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se orderForm := types.SubmitOrder{ Symbol: s.Symbol, - Market: market, + Market: s.Market, Side: types.SideTypeBuy, Type: types.OrderTypeMarket, Quantity: quantity, MarginSideEffect: s.MarginOrderSideEffect, } - s.Notify("Submitting %s market order buy with quantity %s according to the base volume %s, taker buy base volume %s", + bbgo.Notify("Submitting %s market order buy with quantity %s according to the base volume %s, taker buy base volume %s", s.Symbol, quantity.String(), kline.Volume.String(), @@ -698,12 +539,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se log.WithError(err).Error("submit order error") return } - // Save state - if err := s.SaveState(); err != nil { - log.WithError(err).Errorf("can not save state: %+v", s.state) - } else { - s.Notify("%s position is saved", s.Symbol, s.state.Position) - } if s.TrailingStopTarget.TrailingStopCallbackRatio.IsZero() { // submit fixed target orders var targetOrders []types.SubmitOrder @@ -712,17 +547,17 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se targetQuantity := quantity.Mul(target.QuantityPercentage) targetQuoteQuantity := targetPrice.Mul(targetQuantity) - if targetQuoteQuantity.Compare(market.MinNotional) <= 0 { + if targetQuoteQuantity.Compare(s.Market.MinNotional) <= 0 { continue } - if targetQuantity.Compare(market.MinQuantity) <= 0 { + if targetQuantity.Compare(s.Market.MinQuantity) <= 0 { continue } targetOrders = append(targetOrders, types.SubmitOrder{ Symbol: kline.Symbol, - Market: market, + Market: s.Market, Type: types.OrderTypeLimit, Side: types.SideTypeSell, Price: targetPrice, @@ -733,39 +568,55 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se }) } - if _, err := s.submitOrders(ctx, orderExecutor, targetOrders...); err != nil { - log.WithError(err).Error("submit profit target order error") - s.Notify("submit %s profit trailing stop order error", s.Symbol) - return + _, err = s.orderExecutor.SubmitOrders(ctx, targetOrders...) + if err != nil { + bbgo.Notify("submit %s profit trailing stop order error: %s", s.Symbol, err.Error()) } } - - s.tradeCollector.Process() }) - s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() // Cancel trailing stop order if s.TrailingStopTarget.TrailingStopCallbackRatio.Sign() > 0 { - // Cancel all orders - if err := s.activeOrders.GracefulCancel(ctx, session.Exchange); err != nil { - errMsg := "Not all {s.Symbol} orders are cancelled! Please check again." - log.WithError(err).Errorf(errMsg) - s.Notify(errMsg) - } else { - s.Notify("All {s.Symbol} orders are cancelled.") - } + _ = s.orderExecutor.GracefulCancel(ctx) + } + }) + + return nil +} - s.trailingStopControl.OrderID = 0 +func (s *Strategy) updateStopOrder(ctx context.Context) { + // cancel the original stop order + if s.trailingStopControl.StopOrder != nil { + if err := s.session.Exchange.CancelOrders(ctx, *s.trailingStopControl.StopOrder); err != nil { + log.WithError(err).Error("cancel order error") } + s.trailingStopControl.StopOrder = nil + s.orderExecutor.TradeCollector().Process() + } - if err := s.SaveState(); err != nil { - log.WithError(err).Errorf("can not save state: %+v", s.state) - } else { - s.Notify("%s position is saved", s.Symbol, s.state.Position) + // Calculate minimum target price + var minTargetPrice = fixedpoint.Zero + if s.trailingStopControl.minimumProfitPercentage.Sign() > 0 { + minTargetPrice = s.Position.AverageCost.Mul(fixedpoint.One.Add(s.trailingStopControl.minimumProfitPercentage)) + } + + // Place new order if the target price is higher than the minimum target price + if s.trailingStopControl.IsHigherThanMin(minTargetPrice) { + orderForm := s.trailingStopControl.GenerateStopOrder(s.Position.Base) + orders, err := s.orderExecutor.SubmitOrders(ctx, orderForm) + if err != nil { + bbgo.Notify("failed to submit the trailing stop order on %s", s.Symbol) + log.WithError(err).Error("submit profit trailing stop order error") } - }) - return nil + if len(orders) == 0 { + log.Error("unexpected error: len(createdOrders) = 0") + return + } + + s.trailingStopControl.StopOrder = &orders[0] + } } diff --git a/pkg/strategy/swing/strategy.go b/pkg/strategy/swing/strategy.go index 2d8d9e55b3..da85591ce2 100644 --- a/pkg/strategy/swing/strategy.go +++ b/pkg/strategy/swing/strategy.go @@ -26,10 +26,6 @@ func init() { } type Strategy struct { - // The notification system will be injected into the strategy automatically. - // This field will be injected automatically since it's a single exchange strategy. - *bbgo.Notifiability - // OrderExecutor is an interface for submitting order. // This field will be injected automatically since it's a single exchange strategy. bbgo.OrderExecutor @@ -55,7 +51,7 @@ type Strategy struct { // Interval is the interval of the kline channel we want to subscribe, // the kline event will trigger the strategy to check if we need to submit order. - Interval string `json:"interval"` + Interval types.Interval `json:"interval"` // MinChange filters out the k-lines with small changes. so that our strategy will only be triggered // in specific events. @@ -164,9 +160,9 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } func (s *Strategy) notify(format string, args ...interface{}) { - if channel, ok := s.RouteSymbol(s.Symbol); ok { - s.NotifyTo(channel, format, args...) + if channel, ok := bbgo.Notification.RouteSymbol(s.Symbol); ok { + bbgo.NotifyTo(channel, format, args...) } else { - s.Notify(format, args...) + bbgo.Notify(format, args...) } } diff --git a/pkg/strategy/techsignal/strategy.go b/pkg/strategy/techsignal/strategy.go index 814af8474a..bf3326dd37 100644 --- a/pkg/strategy/techsignal/strategy.go +++ b/pkg/strategy/techsignal/strategy.go @@ -3,13 +3,13 @@ package techsignal import ( "context" "errors" - "fmt" "strings" "time" + "github.com/sirupsen/logrus" + "github.com/c9s/bbgo/pkg/exchange/binance" "github.com/c9s/bbgo/pkg/fixedpoint" - "github.com/sirupsen/logrus" "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/types" @@ -27,8 +27,6 @@ func init() { } type Strategy struct { - *bbgo.Notifiability - // These fields will be filled from the config file (it translates YAML to JSON) Symbol string `json:"symbol"` Market types.Market `json:"-"` @@ -69,11 +67,11 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { // session.Subscribe(types.BookChannel, s.Symbol, types.SubscribeOptions{}) for _, detection := range s.SupportDetection { session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ - Interval: string(detection.Interval), + Interval: detection.Interval, }) session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ - Interval: string(detection.MovingAverageInterval), + Interval: detection.MovingAverageInterval, }) } } @@ -107,7 +105,7 @@ func (s *Strategy) listenToFundingRate(ctx context.Context, exchange *binance.Ex fundingRate := index.LastFundingRate if fundingRate.Compare(s.FundingRate.High) >= 0 { - s.Notifiability.Notify("%s funding rate %s is too high! threshold %s", + bbgo.Notify("%s funding rate %s is too high! threshold %s", s.Symbol, fundingRate.Percentage(), s.FundingRate.High.Percentage(), @@ -121,7 +119,7 @@ func (s *Strategy) listenToFundingRate(ctx context.Context, exchange *binance.Ex diff := fundingRate.Sub(previousIndex.LastFundingRate) if diff.Abs().Compare(s.FundingRate.DiffThreshold) > 0 { - s.Notifiability.Notify("%s funding rate changed %s, current funding rate %s", + bbgo.Notify("%s funding rate changed %s, current funding rate %s", s.Symbol, diff.SignedPercentage(), fundingRate.Percentage(), @@ -146,10 +144,7 @@ func (s *Strategy) listenToFundingRate(ctx context.Context, exchange *binance.Ex } func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { - standardIndicatorSet, ok := session.StandardIndicatorSet(s.Symbol) - if !ok { - return fmt.Errorf("standardIndicatorSet is nil, symbol %s", s.Symbol) - } + standardIndicatorSet := session.StandardIndicatorSet(s.Symbol) if s.FundingRate != nil { if binanceExchange, ok := session.Exchange.(*binance.Exchange); ok { @@ -204,21 +199,21 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se prettyQuoteVolume := s.Market.QuoteCurrencyFormatter() if detection.MinVolume.Sign() > 0 && kline.Volume.Compare(detection.MinVolume) > 0 { - s.Notifiability.Notify("Detected %s %s support base volume %s > min base volume %s, quote volume %s", + bbgo.Notify("Detected %s %s support base volume %s > min base volume %s, quote volume %s", s.Symbol, detection.Interval.String(), prettyBaseVolume.FormatMoney(kline.Volume.Trunc()), prettyBaseVolume.FormatMoney(detection.MinVolume.Trunc()), prettyQuoteVolume.FormatMoney(kline.QuoteVolume.Trunc()), ) - s.Notifiability.Notify(kline) + bbgo.Notify(kline) } else if detection.MinQuoteVolume.Sign() > 0 && kline.QuoteVolume.Compare(detection.MinQuoteVolume) > 0 { - s.Notifiability.Notify("Detected %s %s support quote volume %s > min quote volume %s, base volume %s", + bbgo.Notify("Detected %s %s support quote volume %s > min quote volume %s, base volume %s", s.Symbol, detection.Interval.String(), prettyQuoteVolume.FormatMoney(kline.QuoteVolume.Trunc()), prettyQuoteVolume.FormatMoney(detection.MinQuoteVolume.Trunc()), prettyBaseVolume.FormatMoney(kline.Volume.Trunc()), ) - s.Notifiability.Notify(kline) + bbgo.Notify(kline) } } }) diff --git a/pkg/strategy/trendtrader/strategy.go b/pkg/strategy/trendtrader/strategy.go new file mode 100644 index 0000000000..3feedfa754 --- /dev/null +++ b/pkg/strategy/trendtrader/strategy.go @@ -0,0 +1,136 @@ +package trendtrader + +import ( + "context" + "fmt" + "os" + "sync" + + "github.com/c9s/bbgo/pkg/dynamic" + + "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +const ID = "trendtrader" + +var one = fixedpoint.One +var zero = fixedpoint.Zero + +var log = logrus.WithField("strategy", ID) + +func init() { + bbgo.RegisterStrategy(ID, &Strategy{}) +} + +type IntervalWindowSetting struct { + types.IntervalWindow +} + +type Strategy struct { + Environment *bbgo.Environment + Symbol string `json:"symbol"` + Market types.Market + + types.IntervalWindow + + // persistence fields + Position *types.Position `persistence:"position"` + ProfitStats *types.ProfitStats `persistence:"profit_stats"` + TradeStats *types.TradeStats `persistence:"trade_stats"` + + activeOrders *bbgo.ActiveOrderBook + + TrendLine *TrendLine `json:"trendLine"` + + ExitMethods bbgo.ExitMethodSet `json:"exits"` + + session *bbgo.ExchangeSession + orderExecutor *bbgo.GeneralOrderExecutor + + // StrategyController + bbgo.StrategyController +} + +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { + // session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Trend.Interval}) + // session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: types.Interval1m}) + + if s.TrendLine != nil { + dynamic.InheritStructValues(s.TrendLine, s) + s.TrendLine.Subscribe(session) + } + s.ExitMethods.SetAndSubscribe(session, s) +} + +func (s *Strategy) ID() string { + return ID +} + +func (s *Strategy) InstanceID() string { + return fmt.Sprintf("%s:%s", ID, s.Symbol) +} + +func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + var instanceID = s.InstanceID() + + if s.Position == nil { + s.Position = types.NewPositionFromMarket(s.Market) + } + + if s.ProfitStats == nil { + s.ProfitStats = types.NewProfitStats(s.Market) + } + + if s.TradeStats == nil { + s.TradeStats = types.NewTradeStats(s.Symbol) + } + + // StrategyController + s.Status = types.StrategyStatusRunning + + s.OnSuspend(func() { + // Cancel active orders + _ = s.orderExecutor.GracefulCancel(ctx) + }) + + s.OnEmergencyStop(func() { + // Cancel active orders + _ = s.orderExecutor.GracefulCancel(ctx) + // Close 100% position + // _ = s.ClosePosition(ctx, fixedpoint.One) + }) + + // initial required information + s.session = session + + s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) + s.orderExecutor.BindEnvironment(s.Environment) + s.orderExecutor.BindProfitStats(s.ProfitStats) + s.orderExecutor.BindTradeStats(s.TradeStats) + s.orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { + bbgo.Sync(ctx, s) + }) + s.orderExecutor.Bind() + s.activeOrders = bbgo.NewActiveOrderBook(s.Symbol) + + for _, method := range s.ExitMethods { + method.Bind(session, s.orderExecutor) + } + + if s.TrendLine != nil { + s.TrendLine.Bind(session, s.orderExecutor) + } + + bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { + defer wg.Done() + + _, _ = fmt.Fprintln(os.Stderr, s.TradeStats.String()) + _ = s.orderExecutor.GracefulCancel(ctx) + }) + + return nil +} diff --git a/pkg/strategy/trendtrader/trend.go b/pkg/strategy/trendtrader/trend.go new file mode 100644 index 0000000000..fb27c8568d --- /dev/null +++ b/pkg/strategy/trendtrader/trend.go @@ -0,0 +1,156 @@ +package trendtrader + +import ( + "context" + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +type TrendLine struct { + Symbol string + Market types.Market `json:"-"` + types.IntervalWindow + + PivotRightWindow fixedpoint.Value `json:"pivotRightWindow"` + + // MarketOrder is the option to enable market order short. + MarketOrder bool `json:"marketOrder"` + + Quantity fixedpoint.Value `json:"quantity"` + + orderExecutor *bbgo.GeneralOrderExecutor + session *bbgo.ExchangeSession + activeOrders *bbgo.ActiveOrderBook + + pivotHigh *indicator.PivotHigh + pivotLow *indicator.PivotLow + + bbgo.QuantityOrAmount +} + +func (s *TrendLine) Subscribe(session *bbgo.ExchangeSession) { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: types.Interval1m}) + + //if s.pivot != nil { + // session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) + //} +} + +func (s *TrendLine) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor) { + s.session = session + s.orderExecutor = orderExecutor + + position := orderExecutor.Position() + symbol := position.Symbol + standardIndicator := session.StandardIndicatorSet(s.Symbol) + s.pivotHigh = standardIndicator.PivotHigh(types.IntervalWindow{s.Interval, int(3. * s.PivotRightWindow.Float64()), int(s.PivotRightWindow.Float64())}) + s.pivotLow = standardIndicator.PivotLow(types.IntervalWindow{s.Interval, int(3. * s.PivotRightWindow.Float64()), int(s.PivotRightWindow.Float64())}) + + resistancePrices := types.NewQueue(3) + pivotHighDurationCounter := 0. + resistanceDuration := types.NewQueue(2) + supportPrices := types.NewQueue(3) + pivotLowDurationCounter := 0. + supportDuration := types.NewQueue(2) + + resistanceSlope := 0. + resistanceSlope1 := 0. + resistanceSlope2 := 0. + supportSlope := 0. + supportSlope1 := 0. + supportSlope2 := 0. + + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { + if s.pivotHigh.Last() != resistancePrices.Last() { + resistancePrices.Update(s.pivotHigh.Last()) + resistanceDuration.Update(pivotHighDurationCounter) + pivotHighDurationCounter = 0 + } else { + pivotHighDurationCounter++ + } + if s.pivotLow.Last() != supportPrices.Last() { + supportPrices.Update(s.pivotLow.Last()) + supportDuration.Update(pivotLowDurationCounter) + pivotLowDurationCounter = 0 + } else { + pivotLowDurationCounter++ + } + + if line(resistancePrices.Index(2), resistancePrices.Index(1), resistancePrices.Index(0)) < 0 { + resistanceSlope1 = (resistancePrices.Index(1) - resistancePrices.Index(2)) / resistanceDuration.Index(1) + resistanceSlope2 = (resistancePrices.Index(0) - resistancePrices.Index(1)) / resistanceDuration.Index(0) + + resistanceSlope = (resistanceSlope1 + resistanceSlope2) / 2. + } + if line(supportPrices.Index(2), supportPrices.Index(1), supportPrices.Index(0)) > 0 { + supportSlope1 = (supportPrices.Index(1) - supportPrices.Index(2)) / supportDuration.Index(1) + supportSlope2 = (supportPrices.Index(0) - supportPrices.Index(1)) / supportDuration.Index(0) + + supportSlope = (supportSlope1 + supportSlope2) / 2. + } + + if converge(resistanceSlope, supportSlope) { + // y = mx+b + currentResistance := resistanceSlope*pivotHighDurationCounter + resistancePrices.Last() + currentSupport := supportSlope*pivotLowDurationCounter + supportPrices.Last() + log.Info(currentResistance, currentSupport, kline.Close) + + if kline.High.Float64() > currentResistance { + if position.IsShort() { + s.orderExecutor.ClosePosition(context.Background(), one) + } + if position.IsDust(kline.Close) || position.IsClosed() { + s.placeOrder(context.Background(), types.SideTypeBuy, s.Quantity, symbol) // OrAmount.CalculateQuantity(kline.Close) + } + + } else if kline.Low.Float64() < currentSupport { + if position.IsLong() { + s.orderExecutor.ClosePosition(context.Background(), one) + } + if position.IsDust(kline.Close) || position.IsClosed() { + s.placeOrder(context.Background(), types.SideTypeSell, s.Quantity, symbol) // OrAmount.CalculateQuantity(kline.Close) + } + } + } + })) + + if !bbgo.IsBackTesting { + session.MarketDataStream.OnMarketTrade(func(trade types.Trade) { + }) + } +} + +func (s *TrendLine) placeOrder(ctx context.Context, side types.SideType, quantity fixedpoint.Value, symbol string) error { + market, _ := s.session.Market(symbol) + _, err := s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: symbol, + Market: market, + Side: side, + Type: types.OrderTypeMarket, + Quantity: quantity, + Tag: "trend-break", + }) + if err != nil { + log.WithError(err).Errorf("can not place market order") + } + return err +} + +func line(p1, p2, p3 float64) int64 { + if p1 >= p2 && p2 >= p3 { + return -1 + } else if p1 <= p2 && p2 <= p3 { + return +1 + } + return 0 +} + +func converge(mr, ms float64) bool { + if ms > mr { + return true + } + return false +} diff --git a/pkg/strategy/wall/strategy.go b/pkg/strategy/wall/strategy.go new file mode 100644 index 0000000000..3babbcda80 --- /dev/null +++ b/pkg/strategy/wall/strategy.go @@ -0,0 +1,402 @@ +package wall + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/c9s/bbgo/pkg/util" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +const ID = "wall" + +const stateKey = "state-v1" + +var defaultFeeRate = fixedpoint.NewFromFloat(0.001) +var two = fixedpoint.NewFromInt(2) + +var log = logrus.WithField("strategy", ID) + +func init() { + bbgo.RegisterStrategy(ID, &Strategy{}) +} + +type Strategy struct { + Environment *bbgo.Environment + StandardIndicatorSet *bbgo.StandardIndicatorSet + Market types.Market + + // Symbol is the market symbol you want to trade + Symbol string `json:"symbol"` + + Side types.SideType `json:"side"` + + // Interval is how long do you want to update your order price and quantity + Interval types.Interval `json:"interval"` + + FixedPrice fixedpoint.Value `json:"fixedPrice"` + + bbgo.QuantityOrAmount + + NumLayers int `json:"numLayers"` + + // LayerSpread is the price spread between each layer + LayerSpread fixedpoint.Value `json:"layerSpread"` + + // QuantityScale helps user to define the quantity by layer scale + QuantityScale *bbgo.LayerScale `json:"quantityScale,omitempty"` + + AdjustmentMinSpread fixedpoint.Value `json:"adjustmentMinSpread"` + AdjustmentQuantity fixedpoint.Value `json:"adjustmentQuantity"` + + session *bbgo.ExchangeSession + + // persistence fields + Position *types.Position `json:"position,omitempty" persistence:"position"` + ProfitStats *types.ProfitStats `json:"profitStats,omitempty" persistence:"profit_stats"` + + activeAdjustmentOrders *bbgo.ActiveOrderBook + activeWallOrders *bbgo.ActiveOrderBook + orderStore *bbgo.OrderStore + tradeCollector *bbgo.TradeCollector + + groupID uint32 + + stopC chan struct{} +} + +func (s *Strategy) ID() string { + return ID +} + +func (s *Strategy) InstanceID() string { + return fmt.Sprintf("%s:%s", ID, s.Symbol) +} + +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { + session.Subscribe(types.BookChannel, s.Symbol, types.SubscribeOptions{ + Depth: types.DepthLevelFull, + }) +} + +func (s *Strategy) Validate() error { + if len(s.Symbol) == 0 { + return errors.New("symbol is required") + } + + if len(s.Side) == 0 { + return errors.New("side is required") + } + + if s.FixedPrice.IsZero() { + return errors.New("fixedPrice can not be zero") + } + + return nil +} + +func (s *Strategy) CurrentPosition() *types.Position { + return s.Position +} + +func (s *Strategy) placeAdjustmentOrders(ctx context.Context, orderExecutor bbgo.OrderExecutor) error { + var submitOrders []types.SubmitOrder + // position adjustment orders + base := s.Position.GetBase() + if base.IsZero() { + return nil + } + + ticker, err := s.session.Exchange.QueryTicker(ctx, s.Symbol) + if err != nil { + return err + } + + if s.Market.IsDustQuantity(base, ticker.Last) { + return nil + } + + switch s.Side { + case types.SideTypeBuy: + askPrice := ticker.Sell.Mul(s.AdjustmentMinSpread.Add(fixedpoint.One)) + + if s.Position.AverageCost.Compare(askPrice) <= 0 { + return nil + } + + if base.Sign() < 0 { + return nil + } + + quantity := base.Abs() + if quantity.Compare(s.AdjustmentQuantity) >= 0 { + quantity = s.AdjustmentQuantity + } + + submitOrders = append(submitOrders, types.SubmitOrder{ + Symbol: s.Symbol, + Side: s.Side.Reverse(), + Type: types.OrderTypeLimitMaker, + Price: askPrice, + Quantity: quantity, + Market: s.Market, + GroupID: s.groupID, + }) + + case types.SideTypeSell: + bidPrice := ticker.Sell.Mul(fixedpoint.One.Sub(s.AdjustmentMinSpread)) + + if s.Position.AverageCost.Compare(bidPrice) >= 0 { + return nil + } + + if base.Sign() > 0 { + return nil + } + + quantity := base.Abs() + if quantity.Compare(s.AdjustmentQuantity) >= 0 { + quantity = s.AdjustmentQuantity + } + + submitOrders = append(submitOrders, types.SubmitOrder{ + Symbol: s.Symbol, + Side: s.Side.Reverse(), + Type: types.OrderTypeLimitMaker, + Price: bidPrice, + Quantity: quantity, + Market: s.Market, + GroupID: s.groupID, + }) + } + + // condition for lower the average cost + if len(submitOrders) == 0 { + return nil + } + + createdOrders, err := orderExecutor.SubmitOrders(ctx, submitOrders...) + if err != nil { + return err + } + + s.orderStore.Add(createdOrders...) + s.activeAdjustmentOrders.Add(createdOrders...) + return nil +} + +func (s *Strategy) placeWallOrders(ctx context.Context, orderExecutor bbgo.OrderExecutor) error { + var submitOrders []types.SubmitOrder + var startPrice = s.FixedPrice + for i := 0; i < s.NumLayers; i++ { + var price = startPrice + var quantity fixedpoint.Value + if s.QuantityOrAmount.IsSet() { + quantity = s.QuantityOrAmount.CalculateQuantity(price) + } else if s.QuantityScale != nil { + qf, err := s.QuantityScale.Scale(i + 1) + if err != nil { + return err + } + quantity = fixedpoint.NewFromFloat(qf) + } + + order := types.SubmitOrder{ + Symbol: s.Symbol, + Side: s.Side, + Type: types.OrderTypeLimitMaker, + Price: price, + Quantity: quantity, + Market: s.Market, + GroupID: s.groupID, + } + submitOrders = append(submitOrders, order) + switch s.Side { + case types.SideTypeSell: + startPrice = startPrice.Add(s.LayerSpread) + + case types.SideTypeBuy: + startPrice = startPrice.Sub(s.LayerSpread) + + } + } + + // condition for lower the average cost + if len(submitOrders) == 0 { + return nil + } + + createdOrders, err := orderExecutor.SubmitOrders(ctx, submitOrders...) + if err != nil { + return err + } + + s.orderStore.Add(createdOrders...) + s.activeWallOrders.Add(createdOrders...) + return err +} + +func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + // initial required information + s.session = session + + // calculate group id for orders + instanceID := s.InstanceID() + s.groupID = util.FNV32(instanceID) + + // If position is nil, we need to allocate a new position for calculation + if s.Position == nil { + s.Position = types.NewPositionFromMarket(s.Market) + } + + if s.ProfitStats == nil { + s.ProfitStats = types.NewProfitStats(s.Market) + } + + // Always update the position fields + s.Position.Strategy = ID + s.Position.StrategyInstanceID = instanceID + + s.stopC = make(chan struct{}) + + s.activeWallOrders = bbgo.NewActiveOrderBook(s.Symbol) + s.activeWallOrders.BindStream(session.UserDataStream) + + s.activeAdjustmentOrders = bbgo.NewActiveOrderBook(s.Symbol) + s.activeAdjustmentOrders.BindStream(session.UserDataStream) + + s.orderStore = bbgo.NewOrderStore(s.Symbol) + s.orderStore.BindStream(session.UserDataStream) + + s.tradeCollector = bbgo.NewTradeCollector(s.Symbol, s.Position, s.orderStore) + + s.tradeCollector.OnTrade(func(trade types.Trade, profit, netProfit fixedpoint.Value) { + bbgo.Notify(trade) + s.ProfitStats.AddTrade(trade) + + if profit.Compare(fixedpoint.Zero) == 0 { + s.Environment.RecordPosition(s.Position, trade, nil) + } else { + log.Infof("%s generated profit: %v", s.Symbol, profit) + p := s.Position.NewProfit(trade, profit, netProfit) + p.Strategy = ID + p.StrategyInstanceID = instanceID + bbgo.Notify(&p) + + s.ProfitStats.AddProfit(p) + bbgo.Notify(&s.ProfitStats) + + s.Environment.RecordPosition(s.Position, trade, &p) + } + }) + + s.tradeCollector.OnPositionUpdate(func(position *types.Position) { + log.Infof("position changed: %s", s.Position) + bbgo.Notify(s.Position) + }) + + s.tradeCollector.BindStream(session.UserDataStream) + + session.UserDataStream.OnStart(func() { + if err := s.placeWallOrders(ctx, orderExecutor); err != nil { + log.WithError(err).Errorf("can not place order") + } + }) + + s.activeAdjustmentOrders.OnFilled(func(o types.Order) { + if err := s.activeAdjustmentOrders.GracefulCancel(ctx, s.session.Exchange); err != nil { + log.WithError(err).Errorf("graceful cancel order error") + } + + // check if there is a canceled order had partially filled. + s.tradeCollector.Process() + + if err := s.placeAdjustmentOrders(ctx, orderExecutor); err != nil { + log.WithError(err).Errorf("can not place order") + } + }) + + s.activeWallOrders.OnFilled(func(o types.Order) { + if err := s.activeWallOrders.GracefulCancel(ctx, s.session.Exchange); err != nil { + log.WithError(err).Errorf("graceful cancel order error") + } + + // check if there is a canceled order had partially filled. + s.tradeCollector.Process() + + if err := s.placeWallOrders(ctx, orderExecutor); err != nil { + log.WithError(err).Errorf("can not place order") + } + + if err := s.activeAdjustmentOrders.GracefulCancel(ctx, s.session.Exchange); err != nil { + log.WithError(err).Errorf("graceful cancel order error") + } + + // check if there is a canceled order had partially filled. + s.tradeCollector.Process() + + if err := s.placeAdjustmentOrders(ctx, orderExecutor); err != nil { + log.WithError(err).Errorf("can not place order") + } + }) + + ticker := time.NewTicker(s.Interval.Duration()) + go func() { + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + + case <-ticker.C: + orders := s.activeWallOrders.Orders() + if anyOrderFilled(orders) { + if err := s.activeWallOrders.GracefulCancel(ctx, s.session.Exchange); err != nil { + log.WithError(err).Errorf("graceful cancel order error") + } + + // check if there is a canceled order had partially filled. + s.tradeCollector.Process() + + if err := s.placeWallOrders(ctx, orderExecutor); err != nil { + log.WithError(err).Errorf("can not place order") + } + } + } + } + }() + + bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { + defer wg.Done() + close(s.stopC) + + if err := s.activeWallOrders.GracefulCancel(ctx, s.session.Exchange); err != nil { + log.WithError(err).Errorf("graceful cancel order error") + } + + if err := s.activeAdjustmentOrders.GracefulCancel(ctx, s.session.Exchange); err != nil { + log.WithError(err).Errorf("graceful cancel order error") + } + + s.tradeCollector.Process() + }) + + return nil +} + +func anyOrderFilled(orders []types.Order) bool { + for _, o := range orders { + if o.ExecutedQuantity.Sign() > 0 { + return true + } + } + return false +} diff --git a/pkg/strategy/xbalance/strategy.go b/pkg/strategy/xbalance/strategy.go index 46ea06171a..7175382467 100644 --- a/pkg/strategy/xbalance/strategy.go +++ b/pkg/strategy/xbalance/strategy.go @@ -13,9 +13,9 @@ import ( "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/fixedpoint" - "github.com/c9s/bbgo/pkg/service" "github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/util" + "github.com/c9s/bbgo/pkg/util/templateutil" ) const ID = "xbalance" @@ -36,11 +36,11 @@ type State struct { } func (s *State) IsOver24Hours() bool { - return time.Now().Sub(time.Unix(s.Since, 0)) >= 24*time.Hour + return time.Since(time.Unix(s.Since, 0)) >= 24*time.Hour } func (s *State) PlainText() string { - return util.Render(`{{ .Asset }} transfer stats: + return templateutil.Render(`{{ .Asset }} transfer stats: daily number of transfers: {{ .DailyNumberOfTransfers }} daily amount of transfers {{ .DailyAmountOfTransfers.Float64 }}`, s) } @@ -54,12 +54,12 @@ func (s *State) SlackAttachment() slack.Attachment { {Title: "Total Number of Transfers", Value: fmt.Sprintf("%d", s.DailyNumberOfTransfers), Short: true}, {Title: "Total Amount of Transfers", Value: util.FormatFloat(s.DailyAmountOfTransfers.Float64(), 4), Short: true}, }, - Footer: util.Render("Since {{ . }}", time.Unix(s.Since, 0).Format(time.RFC822)), + Footer: templateutil.Render("Since {{ . }}", time.Unix(s.Since, 0).Format(time.RFC822)), } } func (s *State) Reset() { - var beginningOfTheDay = util.BeginningOfTheDay(time.Now().Local()) + var beginningOfTheDay = types.BeginningOfTheDay(time.Now().Local()) *s = State{ DailyNumberOfTransfers: 0, DailyAmountOfTransfers: fixedpoint.Zero, @@ -84,7 +84,7 @@ func (r *WithdrawalRequest) String() string { } func (r *WithdrawalRequest) PlainText() string { - return fmt.Sprintf("Withdrawal request: sending %s %s from %s -> %s", + return fmt.Sprintf("Withdraw request: sending %s %s from %s -> %s", r.Amount.FormatString(4), r.Asset, r.FromSession, @@ -94,7 +94,7 @@ func (r *WithdrawalRequest) PlainText() string { func (r *WithdrawalRequest) SlackAttachment() slack.Attachment { var color = "#DC143C" - title := util.Render(`Withdrawal Request {{ .Asset }}`, r) + title := templateutil.Render(`Withdraw Request {{ .Asset }}`, r) return slack.Attachment{ // Pretext: "", // Text: text, @@ -106,7 +106,7 @@ func (r *WithdrawalRequest) SlackAttachment() slack.Attachment { {Title: "From", Value: r.FromSession}, {Title: "To", Value: r.ToSession}, }, - Footer: util.Render("Time {{ . }}", time.Now().Format(time.RFC822)), + Footer: templateutil.Render("Time {{ . }}", time.Now().Format(time.RFC822)), // FooterIcon: "", } } @@ -136,10 +136,6 @@ func (a *Address) UnmarshalJSON(body []byte) error { } type Strategy struct { - Notifiability *bbgo.Notifiability - *bbgo.Graceful - *bbgo.Persistence - Interval types.Duration `json:"interval"` Addresses map[string]Address `json:"addresses"` @@ -159,7 +155,7 @@ type Strategy struct { Verbose bool `json:"verbose"` - state *State + State *State `persistence:"state"` } func (s *Strategy) ID() string { @@ -170,7 +166,7 @@ func (s *Strategy) CrossSubscribe(sessions map[string]*bbgo.ExchangeSession) {} func (s *Strategy) checkBalance(ctx context.Context, sessions map[string]*bbgo.ExchangeSession) { if s.Verbose { - s.Notifiability.Notify("📝 Checking %s low balance level exchange session...", s.Asset) + bbgo.Notify("📝 Checking %s low balance level exchange session...", s.Asset) } var total fixedpoint.Value @@ -182,33 +178,33 @@ func (s *Strategy) checkBalance(ctx context.Context, sessions map[string]*bbgo.E lowLevelSession, lowLevelBalance, err := s.findLowBalanceLevelSession(sessions) if err != nil { - s.Notifiability.Notify("Can not find low balance level session: %s", err.Error()) + bbgo.Notify("Can not find low balance level session: %s", err.Error()) log.WithError(err).Errorf("Can not find low balance level session") return } if lowLevelSession == nil { if s.Verbose { - s.Notifiability.Notify("✅ All %s balances are looking good, total value: %v", s.Asset, total) + bbgo.Notify("✅ All %s balances are looking good, total value: %v", s.Asset, total) } return } - s.Notifiability.Notify("⚠ Found low level %s balance from session %s: %v", s.Asset, lowLevelSession.Name, lowLevelBalance) + bbgo.Notify("⚠ Found low level %s balance from session %s: %v", s.Asset, lowLevelSession.Name, lowLevelBalance) middle := s.Middle if middle.IsZero() { middle = total.Div(fixedpoint.NewFromInt(int64(len(sessions)))).Mul(priceFixer) - s.Notifiability.Notify("Total value %v %s, setting middle to %v", total, s.Asset, middle) + bbgo.Notify("Total value %v %s, setting middle to %v", total, s.Asset, middle) } requiredAmount := middle.Sub(lowLevelBalance.Available) - s.Notifiability.Notify("Need %v %s to satisfy the middle balance level %v", requiredAmount, s.Asset, middle) + bbgo.Notify("Need %v %s to satisfy the middle balance level %v", requiredAmount, s.Asset, middle) fromSession, _, err := s.findHighestBalanceLevelSession(sessions, requiredAmount) if err != nil || fromSession == nil { - s.Notifiability.Notify("Can not find session with enough balance") + bbgo.Notify("Can not find session with enough balance") log.WithError(err).Errorf("can not find session with enough balance") return } @@ -220,7 +216,7 @@ func (s *Strategy) checkBalance(ctx context.Context, sessions map[string]*bbgo.E } if !fromSession.Withdrawal { - s.Notifiability.Notify("The withdrawal function exchange session %s is not enabled", fromSession.Name) + bbgo.Notify("The withdrawal function exchange session %s is not enabled", fromSession.Name) log.Errorf("The withdrawal function of exchange session %s is not enabled", fromSession.Name) return } @@ -228,7 +224,7 @@ func (s *Strategy) checkBalance(ctx context.Context, sessions map[string]*bbgo.E toAddress, ok := s.Addresses[lowLevelSession.Name] if !ok { log.Errorf("%s address of session %s not found", s.Asset, lowLevelSession.Name) - s.Notifiability.Notify("%s address of session %s not found", s.Asset, lowLevelSession.Name) + bbgo.Notify("%s address of session %s not found", s.Asset, lowLevelSession.Name) return } @@ -236,54 +232,54 @@ func (s *Strategy) checkBalance(ctx context.Context, sessions map[string]*bbgo.E requiredAmount = requiredAmount.Add(toAddress.ForeignFee) } - if s.state != nil { + if s.State != nil { if s.MaxDailyNumberOfTransfer > 0 { - if s.state.DailyNumberOfTransfers >= s.MaxDailyNumberOfTransfer { - s.Notifiability.Notify("⚠ Exceeded %s max daily number of transfers %d (current %d), skipping transfer...", + if s.State.DailyNumberOfTransfers >= s.MaxDailyNumberOfTransfer { + bbgo.Notify("⚠ Exceeded %s max daily number of transfers %d (current %d), skipping transfer...", s.Asset, s.MaxDailyNumberOfTransfer, - s.state.DailyNumberOfTransfers) + s.State.DailyNumberOfTransfers) return } } if s.MaxDailyAmountOfTransfer.Sign() > 0 { - if s.state.DailyAmountOfTransfers.Compare(s.MaxDailyAmountOfTransfer) >= 0 { - s.Notifiability.Notify("⚠ Exceeded %s max daily amount of transfers %v (current %v), skipping transfer...", + if s.State.DailyAmountOfTransfers.Compare(s.MaxDailyAmountOfTransfer) >= 0 { + bbgo.Notify("⚠ Exceeded %s max daily amount of transfers %v (current %v), skipping transfer...", s.Asset, s.MaxDailyAmountOfTransfer, - s.state.DailyAmountOfTransfers) + s.State.DailyAmountOfTransfers) return } } } - s.Notifiability.Notify(&WithdrawalRequest{ + bbgo.Notify(&WithdrawalRequest{ FromSession: fromSession.Name, ToSession: lowLevelSession.Name, Asset: s.Asset, Amount: requiredAmount, }) - if err := withdrawalService.Withdrawal(ctx, s.Asset, requiredAmount, toAddress.Address, &types.WithdrawalOptions{ + if err := withdrawalService.Withdraw(ctx, s.Asset, requiredAmount, toAddress.Address, &types.WithdrawalOptions{ Network: toAddress.Network, AddressTag: toAddress.AddressTag, }); err != nil { log.WithError(err).Errorf("withdrawal failed") - s.Notifiability.Notify("withdrawal request failed, error: %v", err) + bbgo.Notify("withdrawal request failed, error: %v", err) return } - s.Notifiability.Notify("%s withdrawal request sent", s.Asset) + bbgo.Notify("%s withdrawal request sent", s.Asset) - if s.state != nil { - if s.state.IsOver24Hours() { - s.state.Reset() + if s.State != nil { + if s.State.IsOver24Hours() { + s.State.Reset() } - s.state.DailyNumberOfTransfers += 1 - s.state.DailyAmountOfTransfers = s.state.DailyAmountOfTransfers.Add(requiredAmount) - s.SaveState() + s.State.DailyNumberOfTransfers += 1 + s.State.DailyAmountOfTransfers = s.State.DailyAmountOfTransfers.Add(requiredAmount) + bbgo.Sync(ctx, s) } } @@ -328,15 +324,6 @@ func (s *Strategy) findLowBalanceLevelSession(sessions map[string]*bbgo.Exchange return nil, balance, nil } -func (s *Strategy) SaveState() { - if err := s.Persistence.Save(s.state, ID, s.Asset, stateKey); err != nil { - log.WithError(err).Errorf("can not save state: %+v", s.state) - } else { - log.Infof("%s %s state is saved: %+v", ID, s.Asset, s.state) - s.Notifiability.Notify("%s %s state is saved", ID, s.Asset, s.state) - } -} - func (s *Strategy) newDefaultState() *State { return &State{ Asset: s.Asset, @@ -345,42 +332,17 @@ func (s *Strategy) newDefaultState() *State { } } -func (s *Strategy) LoadState() error { - var state State - if err := s.Persistence.Load(&state, ID, s.Asset, stateKey); err != nil { - if err != service.ErrPersistenceNotExists { - return err - } - - s.state = s.newDefaultState() - s.state.Reset() - } else { - // we loaded it successfully - s.state = &state - - // update Asset name for legacy caches - s.state.Asset = s.Asset - - log.Infof("%s %s state is restored: %+v", ID, s.Asset, s.state) - s.Notifiability.Notify("%s %s state is restored", ID, s.Asset, s.state) - } - - return nil -} - func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, sessions map[string]*bbgo.ExchangeSession) error { if s.Interval == 0 { return errors.New("interval can not be zero") } - if err := s.LoadState(); err != nil { - return err + if s.State == nil { + s.State = s.newDefaultState() } - s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() - - s.SaveState() }) if s.CheckOnStart { diff --git a/pkg/strategy/xgap/strategy.go b/pkg/strategy/xgap/strategy.go index 77a5df3cea..fd0b67f604 100644 --- a/pkg/strategy/xgap/strategy.go +++ b/pkg/strategy/xgap/strategy.go @@ -11,9 +11,7 @@ import ( "github.com/sirupsen/logrus" "github.com/c9s/bbgo/pkg/bbgo" - "github.com/c9s/bbgo/pkg/exchange/max" "github.com/c9s/bbgo/pkg/fixedpoint" - "github.com/c9s/bbgo/pkg/service" "github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/util" ) @@ -43,7 +41,7 @@ type State struct { } func (s *State) IsOver24Hours() bool { - return time.Now().Sub(s.AccumulatedFeeStartedAt) >= 24*time.Hour + return time.Since(s.AccumulatedFeeStartedAt) >= 24*time.Hour } func (s *State) Reset() { @@ -58,10 +56,6 @@ func (s *State) Reset() { } type Strategy struct { - *bbgo.Graceful - *bbgo.Notifiability - *bbgo.Persistence - Symbol string `json:"symbol"` SourceExchange string `json:"sourceExchange"` TradingExchange string `json:"tradingExchange"` @@ -76,7 +70,7 @@ type Strategy struct { sourceSession, tradingSession *bbgo.ExchangeSession sourceMarket, tradingMarket types.Market - state *State + State *State `persistence:"state"` mu sync.Mutex lastSourceKLine, lastTradingKLine types.KLine @@ -91,12 +85,12 @@ func (s *Strategy) isBudgetAllowed() bool { return true } - if s.state.AccumulatedFees == nil { + if s.State.AccumulatedFees == nil { return true } for asset, budget := range s.DailyFeeBudgets { - if fee, ok := s.state.AccumulatedFees[asset]; ok { + if fee, ok := s.State.AccumulatedFees[asset]; ok { if fee.Compare(budget) >= 0 { log.Warnf("accumulative fee %s exceeded the fee budget %s, skipping...", fee.String(), budget.String()) return false @@ -114,18 +108,18 @@ func (s *Strategy) handleTradeUpdate(trade types.Trade) { return } - if s.state.IsOver24Hours() { - s.state.Reset() + if s.State.IsOver24Hours() { + s.State.Reset() } // safe check - if s.state.AccumulatedFees == nil { - s.state.AccumulatedFees = make(map[string]fixedpoint.Value) + if s.State.AccumulatedFees == nil { + s.State.AccumulatedFees = make(map[string]fixedpoint.Value) } - s.state.AccumulatedFees[trade.FeeCurrency] = s.state.AccumulatedFees[trade.FeeCurrency].Add(trade.Fee) - s.state.AccumulatedVolume = s.state.AccumulatedVolume.Add(trade.Quantity) - log.Infof("accumulated fee: %s %s", s.state.AccumulatedFees[trade.FeeCurrency].String(), trade.FeeCurrency) + s.State.AccumulatedFees[trade.FeeCurrency] = s.State.AccumulatedFees[trade.FeeCurrency].Add(trade.Fee) + s.State.AccumulatedVolume = s.State.AccumulatedVolume.Add(trade.Quantity) + log.Infof("accumulated fee: %s %s", s.State.AccumulatedFees[trade.FeeCurrency].String(), trade.FeeCurrency) } func (s *Strategy) CrossSubscribe(sessions map[string]*bbgo.ExchangeSession) { @@ -175,36 +169,20 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se s.stopC = make(chan struct{}) - var state State - // load position - if err := s.Persistence.Load(&state, ID, stateKey); err != nil { - if err != service.ErrPersistenceNotExists { - return err - } - - s.state = &State{} - s.state.Reset() - } else { - // loaded successfully - s.state = &state - log.Infof("state is restored: %+v", s.state) + if s.State == nil { + s.State = &State{} + s.State.Reset() + } - if s.state.IsOver24Hours() { - log.Warn("state is over 24 hours, resetting to zero") - s.state.Reset() - } + if s.State.IsOver24Hours() { + log.Warn("state is over 24 hours, resetting to zero") + s.State.Reset() } - s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() - close(s.stopC) - - if err := s.Persistence.Save(&s.state, ID, stateKey); err != nil { - log.WithError(err).Errorf("can not save state: %+v", s.state) - } else { - log.Infof("state is saved => %+v", s.state) - } + bbgo.Sync(context.Background(), s) }) // from here, set data binding @@ -232,7 +210,7 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se s.tradingSession.UserDataStream.OnTradeUpdate(s.handleTradeUpdate) instanceID := fmt.Sprintf("%s-%s", ID, s.Symbol) - s.groupID = max.GenerateGroupID(instanceID) + s.groupID = util.FNV32(instanceID) log.Infof("using group id %d from fnv32(%s)", s.groupID, instanceID) go func() { @@ -353,7 +331,7 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se s.tradingMarket.MinNotional.Mul(NotionModifier).Div(price)) } - createdOrders, err := tradingSession.Exchange.SubmitOrders(ctx, types.SubmitOrder{ + createdOrders, _, err := bbgo.BatchPlaceOrder(ctx, tradingSession.Exchange, types.SubmitOrder{ Symbol: s.Symbol, Side: types.SideTypeBuy, Type: types.OrderTypeLimit, @@ -372,6 +350,7 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se // TimeInForce: types.TimeInForceGTC, GroupID: s.groupID, }) + if err != nil { log.WithError(err).Error("order submit error") } diff --git a/pkg/strategy/xmaker/state.go b/pkg/strategy/xmaker/state.go index ea82bc96f3..ec55d41014 100644 --- a/pkg/strategy/xmaker/state.go +++ b/pkg/strategy/xmaker/state.go @@ -2,6 +2,7 @@ package xmaker import ( "sync" + "time" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" @@ -9,12 +10,17 @@ import ( type State struct { CoveredPosition fixedpoint.Value `json:"coveredPosition,omitempty"` - Position *types.Position `json:"position,omitempty"` - ProfitStats ProfitStats `json:"profitStats,omitempty"` + + // Deprecated: + Position *types.Position `json:"position,omitempty"` + + // Deprecated: + ProfitStats ProfitStats `json:"profitStats,omitempty"` } type ProfitStats struct { - types.ProfitStats + *types.ProfitStats + lock sync.Mutex MakerExchange types.ExchangeName `json:"makerExchange"` @@ -52,7 +58,7 @@ func (s *ProfitStats) AddTrade(trade types.Trade) { } func (s *ProfitStats) ResetToday() { - s.ProfitStats.ResetToday() + s.ProfitStats.ResetToday(time.Now()) s.lock.Lock() s.TodayMakerVolume = fixedpoint.Zero diff --git a/pkg/strategy/xmaker/strategy.go b/pkg/strategy/xmaker/strategy.go index 590a689f0a..7ff87ce99e 100644 --- a/pkg/strategy/xmaker/strategy.go +++ b/pkg/strategy/xmaker/strategy.go @@ -11,10 +11,8 @@ import ( "golang.org/x/time/rate" "github.com/c9s/bbgo/pkg/bbgo" - "github.com/c9s/bbgo/pkg/exchange/max" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/indicator" - "github.com/c9s/bbgo/pkg/service" "github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/util" ) @@ -26,8 +24,6 @@ const priceUpdateTimeout = 30 * time.Second const ID = "xmaker" -const stateKey = "state-v1" - var log = logrus.WithField("strategy", ID) func init() { @@ -35,9 +31,6 @@ func init() { } type Strategy struct { - *bbgo.Graceful - *bbgo.Notifiability - *bbgo.Persistence Environment *bbgo.Environment Symbol string `json:"symbol"` @@ -99,8 +92,13 @@ type Strategy struct { state *State + // persistence fields + Position *types.Position `json:"position,omitempty" persistence:"position"` + ProfitStats *ProfitStats `json:"profitStats,omitempty" persistence:"profit_stats"` + CoveredPosition fixedpoint.Value `json:"coveredPosition,omitempty" persistence:"covered_position"` + book *types.StreamOrderBook - activeMakerOrders *bbgo.LocalActiveOrderBook + activeMakerOrders *bbgo.ActiveOrderBook hedgeErrorLimiter *rate.Limiter hedgeErrorRateReservation *rate.Reservation @@ -120,6 +118,10 @@ func (s *Strategy) ID() string { return ID } +func (s *Strategy) InstanceID() string { + return fmt.Sprintf("%s:%s", ID, s.Symbol) +} + func (s *Strategy) CrossSubscribe(sessions map[string]*bbgo.ExchangeSession) { sourceSession, ok := sessions[s.SourceExchange] if !ok { @@ -169,7 +171,7 @@ func (s *Strategy) updateQuote(ctx context.Context, orderExecutionRouter bbgo.Or return } - if s.activeMakerOrders.NumOfAsks() > 0 || s.activeMakerOrders.NumOfBids() > 0 { + if s.activeMakerOrders.NumOfOrders() > 0 { return } @@ -271,7 +273,7 @@ func (s *Strategy) updateQuote(ctx context.Context, orderExecutionRouter bbgo.Or // 1. place bid orders when we already bought too much // 2. place ask orders when we already sold too much if s.MaxExposurePosition.Sign() > 0 { - pos := s.state.Position.GetBase() + pos := s.Position.GetBase() if pos.Compare(s.MaxExposurePosition.Neg()) > 0 { // stop sell if we over-sell @@ -300,14 +302,21 @@ func (s *Strategy) updateQuote(ctx context.Context, orderExecutionRouter bbgo.Or var pips = s.Pips if s.EnableBollBandMargin { - lastDownBand := s.boll.LastDownBand() - lastUpBand := s.boll.LastUpBand() + lastDownBand := fixedpoint.NewFromFloat(s.boll.DownBand.Last()) + lastUpBand := fixedpoint.NewFromFloat(s.boll.UpBand.Last()) + + if lastUpBand.IsZero() || lastDownBand.IsZero() { + log.Warnf("bollinger band value is zero, skipping") + return + } + + log.Infof("bollinger band: up/down = %f/%f", lastUpBand.Float64(), lastDownBand.Float64()) // when bid price is lower than the down band, then it's in the downtrend // when ask price is higher than the up band, then it's in the uptrend - if bestBidPrice.Float64() < lastDownBand { + if bestBidPrice.Compare(lastDownBand) < 0 { // ratio here should be greater than 1.00 - ratio := fixedpoint.NewFromFloat(lastDownBand).Div(bestBidPrice) + ratio := lastDownBand.Div(bestBidPrice) // so that the original bid margin can be multiplied by 1.x bollMargin := s.BollBandMargin.Mul(ratio).Mul(s.BollBandMarginFactor) @@ -322,9 +331,9 @@ func (s *Strategy) updateQuote(ctx context.Context, orderExecutionRouter bbgo.Or pips = pips.Mul(ratio) } - if bestAskPrice.Float64() > lastUpBand { + if bestAskPrice.Compare(lastUpBand) > 0 { // ratio here should be greater than 1.00 - ratio := bestAskPrice.Div(fixedpoint.NewFromFloat(lastUpBand)) + ratio := bestAskPrice.Div(lastUpBand) // so that the original bid margin can be multiplied by 1.x bollMargin := s.BollBandMargin.Mul(ratio).Mul(s.BollBandMarginFactor) @@ -542,13 +551,13 @@ func (s *Strategy) Hedge(ctx context.Context, pos fixedpoint.Value) { if !s.hedgeErrorRateReservation.OK() { return } - s.Notify("Hit hedge error rate limit, waiting...") + bbgo.Notify("Hit hedge error rate limit, waiting...") time.Sleep(s.hedgeErrorRateReservation.Delay()) s.hedgeErrorRateReservation = nil } log.Infof("submitting %s hedge order %s %v", s.Symbol, side.String(), quantity) - s.Notifiability.Notify("Submitting %s hedge order %s %v", s.Symbol, side.String(), quantity) + bbgo.Notify("Submitting %s hedge order %s %v", s.Symbol, side.String(), quantity) orderExecutor := &bbgo.ExchangeOrderExecutor{Session: s.sourceSession} returnOrders, err := orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ Market: s.sourceMarket, @@ -566,9 +575,9 @@ func (s *Strategy) Hedge(ctx context.Context, pos fixedpoint.Value) { // if it's selling, than we should add positive position if side == types.SideTypeSell { - s.state.CoveredPosition = s.state.CoveredPosition.Add(quantity) + s.CoveredPosition = s.CoveredPosition.Add(quantity) } else { - s.state.CoveredPosition = s.state.CoveredPosition.Add(quantity.Neg()) + s.CoveredPosition = s.CoveredPosition.Add(quantity.Neg()) } s.orderStore.Add(returnOrders...) @@ -590,46 +599,6 @@ func (s *Strategy) Validate() error { return nil } -func (s *Strategy) LoadState() error { - var state State - - // load position - if err := s.Persistence.Load(&state, ID, s.Symbol, stateKey); err != nil { - if err != service.ErrPersistenceNotExists { - return err - } - - s.state = &State{} - } else { - s.state = &state - } - - // if position is nil, we need to allocate a new position for calculation - if s.state.Position == nil { - s.state.Position = types.NewPositionFromMarket(s.makerMarket) - } - s.state.Position.Market = s.makerMarket - - s.state.ProfitStats.Symbol = s.makerMarket.Symbol - s.state.ProfitStats.BaseCurrency = s.makerMarket.BaseCurrency - s.state.ProfitStats.QuoteCurrency = s.makerMarket.QuoteCurrency - s.state.ProfitStats.MakerExchange = s.makerSession.ExchangeName - if s.state.ProfitStats.AccumulatedSince == 0 { - s.state.ProfitStats.AccumulatedSince = time.Now().Unix() - } - - return nil -} - -func (s *Strategy) SaveState() error { - if err := s.Persistence.Save(s.state, ID, s.Symbol, stateKey); err != nil { - return err - } else { - log.Infof("%s state is saved => %+v", ID, s.state) - } - return nil -} - func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.OrderExecutionRouter, sessions map[string]*bbgo.ExchangeSession) error { if s.BollBandInterval == "" { s.BollBandInterval = types.Interval1m @@ -698,7 +667,7 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order return fmt.Errorf("maker session market %s is not defined", s.Symbol) } - standardIndicatorSet, ok := s.sourceSession.StandardIndicatorSet(s.Symbol) + standardIndicatorSet := s.sourceSession.StandardIndicatorSet(s.Symbol) if !ok { return fmt.Errorf("%s standard indicator set not found", s.Symbol) } @@ -708,26 +677,50 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order Window: 21, }, 1.0) + if store, ok := s.sourceSession.MarketDataStore(s.Symbol); ok { + if klines, ok2 := store.KLinesOfInterval(s.BollBandInterval); ok2 { + for i := 0; i < len(*klines); i++ { + s.boll.CalculateAndUpdate((*klines)[0 : i+1]) + } + } + } + // restore state - instanceID := fmt.Sprintf("%s-%s", ID, s.Symbol) - s.groupID = max.GenerateGroupID(instanceID) + instanceID := s.InstanceID() + s.groupID = util.FNV32(instanceID) log.Infof("using group id %d from fnv(%s)", s.groupID, instanceID) - if err := s.LoadState(); err != nil { - return err - } else { - s.Notify("xmaker: %s position is restored", s.Symbol, s.state.Position) + if s.Position == nil { + s.Position = types.NewPositionFromMarket(s.makerMarket) + + // force update for legacy code + s.Position.Market = s.makerMarket + } + + bbgo.Notify("xmaker: %s position is restored", s.Symbol, s.Position) + + if s.ProfitStats == nil { + s.ProfitStats = &ProfitStats{ + ProfitStats: types.NewProfitStats(s.makerMarket), + MakerExchange: s.makerSession.ExchangeName, + } + } + + if s.CoveredPosition.IsZero() { + if s.state != nil && !s.CoveredPosition.IsZero() { + s.CoveredPosition = s.state.CoveredPosition + } } if s.makerSession.MakerFeeRate.Sign() > 0 || s.makerSession.TakerFeeRate.Sign() > 0 { - s.state.Position.SetExchangeFeeRate(types.ExchangeName(s.MakerExchange), types.ExchangeFee{ + s.Position.SetExchangeFeeRate(types.ExchangeName(s.MakerExchange), types.ExchangeFee{ MakerFeeRate: s.makerSession.MakerFeeRate, TakerFeeRate: s.makerSession.TakerFeeRate, }) } if s.sourceSession.MakerFeeRate.Sign() > 0 || s.sourceSession.TakerFeeRate.Sign() > 0 { - s.state.Position.SetExchangeFeeRate(types.ExchangeName(s.SourceExchange), types.ExchangeFee{ + s.Position.SetExchangeFeeRate(types.ExchangeName(s.SourceExchange), types.ExchangeFee{ MakerFeeRate: s.sourceSession.MakerFeeRate, TakerFeeRate: s.sourceSession.TakerFeeRate, }) @@ -736,53 +729,49 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order s.book = types.NewStreamBook(s.Symbol) s.book.BindStream(s.sourceSession.MarketDataStream) - s.activeMakerOrders = bbgo.NewLocalActiveOrderBook(s.Symbol) + s.activeMakerOrders = bbgo.NewActiveOrderBook(s.Symbol) s.activeMakerOrders.BindStream(s.makerSession.UserDataStream) s.orderStore = bbgo.NewOrderStore(s.Symbol) s.orderStore.BindStream(s.sourceSession.UserDataStream) s.orderStore.BindStream(s.makerSession.UserDataStream) - s.tradeCollector = bbgo.NewTradeCollector(s.Symbol, s.state.Position, s.orderStore) + s.tradeCollector = bbgo.NewTradeCollector(s.Symbol, s.Position, s.orderStore) if s.NotifyTrade { s.tradeCollector.OnTrade(func(trade types.Trade, profit, netProfit fixedpoint.Value) { - s.Notifiability.Notify(trade) + bbgo.Notify(trade) }) } s.tradeCollector.OnTrade(func(trade types.Trade, profit, netProfit fixedpoint.Value) { c := trade.PositionChange() if trade.Exchange == s.sourceSession.ExchangeName { - s.state.CoveredPosition = s.state.CoveredPosition.Add(c) + s.CoveredPosition = s.CoveredPosition.Add(c) } - s.state.ProfitStats.AddTrade(trade) + s.ProfitStats.AddTrade(trade) if profit.Compare(fixedpoint.Zero) == 0 { - s.Environment.RecordPosition(s.state.Position, trade, nil) + s.Environment.RecordPosition(s.Position, trade, nil) } else { log.Infof("%s generated profit: %v", s.Symbol, profit) - p := s.state.Position.NewProfit(trade, profit, netProfit) + p := s.Position.NewProfit(trade, profit, netProfit) p.Strategy = ID p.StrategyInstanceID = instanceID - s.Notify(&p) - s.state.ProfitStats.AddProfit(p) - - s.Environment.RecordPosition(s.state.Position, trade, &p) - } + bbgo.Notify(&p) + s.ProfitStats.AddProfit(p) - if err := s.SaveState(); err != nil { - log.WithError(err).Error("save state error") + s.Environment.RecordPosition(s.Position, trade, &p) } }) s.tradeCollector.OnPositionUpdate(func(position *types.Position) { - s.Notifiability.Notify(position) + bbgo.Notify(position) }) s.tradeCollector.OnRecover(func(trade types.Trade) { - s.Notifiability.Notify("Recover trade", trade) + bbgo.Notify("Recover trade", trade) }) s.tradeCollector.BindStream(s.sourceSession.UserDataStream) s.tradeCollector.BindStream(s.makerSession.UserDataStream) @@ -804,8 +793,7 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order defer tradeScanTicker.Stop() defer func() { - if err := s.activeMakerOrders.GracefulCancel(context.Background(), - s.makerSession.Exchange); err != nil { + if err := s.activeMakerOrders.GracefulCancel(context.Background(), s.makerSession.Exchange); err != nil { log.WithError(err).Errorf("can not cancel %s orders", s.Symbol) } }() @@ -825,7 +813,7 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order s.updateQuote(ctx, orderExecutionRouter) case <-reportTicker.C: - s.Notifiability.Notify(&s.state.ProfitStats) + bbgo.Notify(s.ProfitStats) case <-tradeScanTicker.C: log.Infof("scanning trades from %s ago...", tradeScanInterval) @@ -847,15 +835,15 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order // uncover position = -5 - -3 (covered position) = -2 s.tradeCollector.Process() - position := s.state.Position.GetBase() + position := s.Position.GetBase() - uncoverPosition := position.Sub(s.state.CoveredPosition) + uncoverPosition := position.Sub(s.CoveredPosition) absPos := uncoverPosition.Abs() if !s.DisableHedge && absPos.Compare(s.sourceMarket.MinQuantity) > 0 { log.Infof("%s base position %v coveredPosition: %v uncoverPosition: %v", s.Symbol, position, - s.state.CoveredPosition, + s.CoveredPosition, uncoverPosition, ) @@ -865,7 +853,7 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order } }() - s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() close(s.stopC) @@ -880,11 +868,7 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order log.WithError(err).Errorf("graceful cancel error") } - if err := s.SaveState(); err != nil { - log.WithError(err).Errorf("can not save state: %+v", s.state) - } else { - s.Notify("%s: %s position is saved", ID, s.Symbol, s.state.Position) - } + bbgo.Notify("%s: %s position", ID, s.Symbol, s.Position) }) return nil diff --git a/pkg/strategy/xnav/strategy.go b/pkg/strategy/xnav/strategy.go index 38b1d0fa28..b0514d9467 100644 --- a/pkg/strategy/xnav/strategy.go +++ b/pkg/strategy/xnav/strategy.go @@ -6,13 +6,13 @@ import ( "time" "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/util/templateutil" "github.com/pkg/errors" - log "github.com/sirupsen/logrus" + "github.com/sirupsen/logrus" "github.com/slack-go/slack" "github.com/c9s/bbgo/pkg/bbgo" - "github.com/c9s/bbgo/pkg/service" "github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/util" ) @@ -21,6 +21,8 @@ const ID = "xnav" const stateKey = "state-v1" +var log = logrus.WithField("strategy", ID) + func init() { bbgo.RegisterStrategy(ID, &Strategy{}) } @@ -30,11 +32,11 @@ type State struct { } func (s *State) IsOver24Hours() bool { - return util.Over24Hours(time.Unix(s.Since, 0)) + return types.Over24Hours(time.Unix(s.Since, 0)) } func (s *State) PlainText() string { - return util.Render(`{{ .Asset }} transfer stats: + return templateutil.Render(`{{ .Asset }} transfer stats: daily number of transfers: {{ .DailyNumberOfTransfers }} daily amount of transfers {{ .DailyAmountOfTransfers.Float64 }}`, s) } @@ -44,26 +46,25 @@ func (s *State) SlackAttachment() slack.Attachment { // Pretext: "", // Text: text, Fields: []slack.AttachmentField{}, - Footer: util.Render("Since {{ . }}", time.Unix(s.Since, 0).Format(time.RFC822)), + Footer: templateutil.Render("Since {{ . }}", time.Unix(s.Since, 0).Format(time.RFC822)), } } func (s *State) Reset() { - var beginningOfTheDay = util.BeginningOfTheDay(time.Now().Local()) + var beginningOfTheDay = types.BeginningOfTheDay(time.Now().Local()) *s = State{ Since: beginningOfTheDay.Unix(), } } type Strategy struct { - Notifiability *bbgo.Notifiability - *bbgo.Graceful - *bbgo.Persistence + *bbgo.Environment - Interval types.Duration `json:"interval"` + Interval types.Interval `json:"interval"` ReportOnStart bool `json:"reportOnStart"` IgnoreDusts bool `json:"ignoreDusts"` - state *State + + State *State `persistence:"state"` } func (s *Strategy) ID() string { @@ -75,120 +76,92 @@ var Ten = fixedpoint.NewFromInt(10) func (s *Strategy) CrossSubscribe(sessions map[string]*bbgo.ExchangeSession) {} func (s *Strategy) recordNetAssetValue(ctx context.Context, sessions map[string]*bbgo.ExchangeSession) { - totalAssets := types.AssetMap{} totalBalances := types.BalanceMap{} - totalBorrowed := map[string]fixedpoint.Value{} - lastPrices := map[string]fixedpoint.Value{} - for _, session := range sessions { - if err := session.UpdateAccount(ctx) ; err != nil { + allPrices := map[string]fixedpoint.Value{} + sessionBalances := map[string]types.BalanceMap{} + priceTime := time.Now() + + // iterate the sessions and record them + for sessionName, session := range sessions { + // update the account balances and the margin information + if _, err := session.UpdateAccount(ctx); err != nil { log.WithError(err).Errorf("can not update account") return } account := session.GetAccount() balances := account.Balances() - if err := session.UpdatePrices(ctx); err != nil { + if err := session.UpdatePrices(ctx, balances.Currencies(), "USDT"); err != nil { log.WithError(err).Error("price update failed") return } - for _, b := range balances { - if tb, ok := totalBalances[b.Currency]; ok { - tb.Available = tb.Available.Add(b.Available) - tb.Locked = tb.Locked.Add(b.Locked) - totalBalances[b.Currency] = tb - - if b.Borrowed.Sign() > 0 { - totalBorrowed[b.Currency] = totalBorrowed[b.Currency].Add(b.Borrowed) - } - } else { - totalBalances[b.Currency] = b - totalBorrowed[b.Currency] = b.Borrowed - } - } + sessionBalances[sessionName] = balances + totalBalances = totalBalances.Add(balances) prices := session.LastPrices() + assets := balances.Assets(prices, priceTime) + + // merge prices for m, p := range prices { - lastPrices[m] = p + allPrices[m] = p } + + s.Environment.RecordAsset(priceTime, session, assets) } - assets := totalBalances.Assets(lastPrices) - for currency, asset := range assets { + displayAssets := types.AssetMap{} + totalAssets := totalBalances.Assets(allPrices, priceTime) + s.Environment.RecordAsset(priceTime, &bbgo.ExchangeSession{Name: "ALL"}, totalAssets) + + for currency, asset := range totalAssets { // calculated if it's dust only when InUSD (usd value) is defined. - if s.IgnoreDusts && !asset.InUSD.IsZero() && asset.InUSD.Compare(Ten) < 0 { + if s.IgnoreDusts && !asset.InUSD.IsZero() && asset.InUSD.Compare(Ten) < 0 && asset.InUSD.Compare(Ten.Neg()) > 0 { continue } - totalAssets[currency] = asset + displayAssets[currency] = asset } - s.Notifiability.Notify(totalAssets) + bbgo.Notify(displayAssets) - if s.state != nil { - if s.state.IsOver24Hours() { - s.state.Reset() + if s.State != nil { + if s.State.IsOver24Hours() { + s.State.Reset() } - - s.SaveState() - } -} - -func (s *Strategy) SaveState() { - if err := s.Persistence.Save(s.state, ID, stateKey); err != nil { - log.WithError(err).Errorf("%s can not save state: %+v", ID, s.state) - } else { - log.Infof("%s state is saved: %+v", ID, s.state) - // s.Notifiability.Notify("%s %s state is saved", ID, s.Asset, s.state) + bbgo.Sync(ctx, s) } } -func (s *Strategy) newDefaultState() *State { - return &State{} -} - -func (s *Strategy) LoadState() error { - var state State - if err := s.Persistence.Load(&state, ID, stateKey); err != nil { - if err != service.ErrPersistenceNotExists { - return err - } - - s.state = s.newDefaultState() - s.state.Reset() - } else { - // we loaded it successfully - s.state = &state - - // update Asset name for legacy caches - // s.state.Asset = s.Asset - - log.Infof("%s state is restored: %+v", ID, s.state) - s.Notifiability.Notify("%s state is restored", ID, s.state) - } - - return nil -} - func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, sessions map[string]*bbgo.ExchangeSession) error { - if s.Interval == 0 { - return errors.New("interval can not be zero") + if s.Interval == "" { + return errors.New("interval can not be empty") } - if err := s.LoadState(); err != nil { - return err + if s.State == nil { + s.State = &State{} + s.State.Reset() } - s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() - s.SaveState() + bbgo.Sync(ctx, s) }) if s.ReportOnStart { s.recordNetAssetValue(ctx, sessions) } + if s.Environment.BacktestService != nil { + log.Warnf("xnav does not support backtesting") + } + + // TODO: if interval is supported, we can use kline as the ticker + if _, ok := types.SupportedIntervals[s.Interval]; ok { + + } + go func() { ticker := time.NewTicker(util.MillisecondsJitter(s.Interval.Duration(), 1000)) defer ticker.Stop() diff --git a/pkg/strategy/xpuremaker/strategy.go b/pkg/strategy/xpuremaker/strategy.go index c6cb53bf80..a5d441bdc1 100644 --- a/pkg/strategy/xpuremaker/strategy.go +++ b/pkg/strategy/xpuremaker/strategy.go @@ -113,7 +113,7 @@ func (s *Strategy) update(orderExecutor bbgo.OrderExecutor, session *bbgo.Exchan func (s *Strategy) updateOrders(orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession, side types.SideType) { var book = s.book.Copy() var pvs = book.SideBook(side) - if pvs == nil || len(pvs) == 0 { + if len(pvs) == 0 { log.Warnf("empty side: %s", side) return } diff --git a/pkg/types/color.go b/pkg/style/colors.go similarity index 85% rename from pkg/types/color.go rename to pkg/style/colors.go index ac8324aa2d..f9447fcd05 100644 --- a/pkg/types/color.go +++ b/pkg/style/colors.go @@ -1,4 +1,4 @@ -package types +package style const GreenColor = "#228B22" const RedColor = "#800000" diff --git a/pkg/style/pnl.go b/pkg/style/pnl.go new file mode 100644 index 0000000000..8f9a68edd9 --- /dev/null +++ b/pkg/style/pnl.go @@ -0,0 +1,61 @@ +package style + +import ( + "github.com/c9s/bbgo/pkg/fixedpoint" +) + +var LossEmoji = "đŸ”„" +var ProfitEmoji = "💰" +var DefaultPnLLevelResolution = fixedpoint.NewFromFloat(0.001) + +func PnLColor(pnl fixedpoint.Value) string { + if pnl.Sign() > 0 { + return GreenColor + } + return RedColor +} + +func PnLSignString(pnl fixedpoint.Value) string { + if pnl.Sign() > 0 { + return "+" + pnl.String() + } + return pnl.String() +} + +func PnLEmojiSimple(pnl fixedpoint.Value) string { + if pnl.Sign() < 0 { + return LossEmoji + } + + if pnl.IsZero() { + return "" + } + + return ProfitEmoji +} + +func PnLEmojiMargin(pnl, margin, resolution fixedpoint.Value) (out string) { + if margin.IsZero() { + return PnLEmojiSimple(pnl) + } + + if pnl.Sign() < 0 { + out = LossEmoji + level := (margin.Neg()).Div(resolution).Int() + for i := 1; i < level; i++ { + out += LossEmoji + } + return out + } + + if pnl.IsZero() { + return out + } + + out = ProfitEmoji + level := margin.Div(resolution).Int() + for i := 1; i < level; i++ { + out += ProfitEmoji + } + return out +} diff --git a/pkg/style/table.go b/pkg/style/table.go new file mode 100644 index 0000000000..15f426d178 --- /dev/null +++ b/pkg/style/table.go @@ -0,0 +1,21 @@ +package style + +import ( + "github.com/jedib0t/go-pretty/v6/table" + "github.com/jedib0t/go-pretty/v6/text" +) + +func NewDefaultTableStyle() *table.Style { + style := table.Style{ + Name: "StyleRounded", + Box: table.StyleBoxRounded, + Format: table.FormatOptionsDefault, + HTML: table.DefaultHTMLOptions, + Options: table.OptionsDefault, + Title: table.TitleOptionsDefault, + Color: table.ColorOptionsYellowWhiteOnBlack, + } + style.Color.Row = text.Colors{text.FgHiYellow, text.BgHiBlack} + style.Color.RowAlternate = text.Colors{text.FgYellow, text.BgBlack} + return &style +} diff --git a/pkg/testutil/auth.go b/pkg/testutil/auth.go new file mode 100644 index 0000000000..164207e295 --- /dev/null +++ b/pkg/testutil/auth.go @@ -0,0 +1,25 @@ +package testutil + +import ( + "os" + "regexp" + "testing" +) + +func maskSecret(s string) string { + re := regexp.MustCompile(`\b(\w{4})\w+\b`) + s = re.ReplaceAllString(s, "$1******") + return s +} + +func IntegrationTestConfigured(t *testing.T, prefix string) (key, secret string, ok bool) { + var hasKey, hasSecret bool + key, hasKey = os.LookupEnv(prefix + "_API_KEY") + secret, hasSecret = os.LookupEnv(prefix + "_API_SECRET") + ok = hasKey && hasSecret && os.Getenv("TEST_"+prefix) == "1" + if ok { + t.Logf(prefix+" api integration test enabled, key = %s, secret = %s", maskSecret(key), maskSecret(secret)) + } + + return key, secret, ok +} diff --git a/pkg/types/account.go b/pkg/types/account.go index 6d03406bc1..cd590f5300 100644 --- a/pkg/types/account.go +++ b/pkg/types/account.go @@ -2,12 +2,7 @@ package types import ( "fmt" - "sort" - "strings" "sync" - "time" - - "github.com/slack-go/slack" "github.com/sirupsen/logrus" "github.com/spf13/viper" @@ -21,219 +16,12 @@ func init() { debugBalance = viper.GetBool("debug-balance") } -type Balance struct { - Currency string `json:"currency"` - Available fixedpoint.Value `json:"available"` - Locked fixedpoint.Value `json:"locked,omitempty"` - - // margin related fields - Borrowed fixedpoint.Value `json:"borrowed,omitempty"` - Interest fixedpoint.Value `json:"interest,omitempty"` - - // NetAsset = (Available + Locked) - Borrowed - Interest - NetAsset fixedpoint.Value `json:"net,omitempty"` -} - -func (b Balance) Total() fixedpoint.Value { - return b.Available.Add(b.Locked) -} - -func (b Balance) String() (o string) { - - o = fmt.Sprintf("%s: %s", b.Currency, b.Available.String()) - - if b.Locked.Sign() > 0 { - o += fmt.Sprintf(" (locked %v)", b.Locked) - } - - if b.Borrowed.Sign() > 0 { - o += fmt.Sprintf(" (borrowed: %v)", b.Borrowed) - } - - return o -} - -type Asset struct { - Currency string `json:"currency" db:"currency"` - Total fixedpoint.Value `json:"total" db:"total"` - InUSD fixedpoint.Value `json:"inUSD" db:"inUSD"` - InBTC fixedpoint.Value `json:"inBTC" db:"inBTC"` - Time time.Time `json:"time" db:"time"` - Locked fixedpoint.Value `json:"lock" db:"lock" ` - Available fixedpoint.Value `json:"available" db:"available"` -} - -type AssetMap map[string]Asset - -func (m AssetMap) PlainText() (o string) { - var assets = m.Slice() - - // sort assets - sort.Slice(assets, func(i, j int) bool { - return assets[i].InUSD.Compare(assets[j].InUSD) > 0 - }) - - sumUsd := fixedpoint.Zero - sumBTC := fixedpoint.Zero - for _, a := range assets { - usd := a.InUSD - btc := a.InBTC - if !a.InUSD.IsZero() { - o += fmt.Sprintf(" %s: %s (≈ %s) (≈ %s)", - a.Currency, - a.Total.String(), - USD.FormatMoney(usd), - BTC.FormatMoney(btc), - ) + "\n" - sumUsd = sumUsd.Add(usd) - sumBTC = sumBTC.Add(btc) - } else { - o += fmt.Sprintf(" %s: %s", - a.Currency, - a.Total.String(), - ) + "\n" - } - } - o += fmt.Sprintf(" Summary: (≈ %s) (≈ %s)", - USD.FormatMoney(sumUsd), - BTC.FormatMoney(sumBTC), - ) + "\n" - return o -} - -func (m AssetMap) Slice() (assets []Asset) { - for _, a := range m { - assets = append(assets, a) - } - return assets -} - -func (m AssetMap) SlackAttachment() slack.Attachment { - var fields []slack.AttachmentField - var totalBTC, totalUSD fixedpoint.Value - - var assets = m.Slice() - - // sort assets - sort.Slice(assets, func(i, j int) bool { - return assets[i].InUSD.Compare(assets[j].InUSD) > 0 - }) - - for _, a := range assets { - totalUSD = totalUSD.Add(a.InUSD) - totalBTC = totalBTC.Add(a.InBTC) - } - - for _, a := range assets { - if !a.InUSD.IsZero() { - fields = append(fields, slack.AttachmentField{ - Title: a.Currency, - Value: fmt.Sprintf("%s (≈ %s) (≈ %s) (%s)", - a.Total.String(), - USD.FormatMoney(a.InUSD), - BTC.FormatMoney(a.InBTC), - a.InUSD.Div(totalUSD).FormatPercentage(2), - ), - Short: false, - }) - } else { - fields = append(fields, slack.AttachmentField{ - Title: a.Currency, - Value: fmt.Sprintf("%s", a.Total.String()), - Short: false, - }) - } - } - - return slack.Attachment{ - Title: fmt.Sprintf("Net Asset Value %s (≈ %s)", - USD.FormatMoney(totalUSD), - BTC.FormatMoney(totalBTC), - ), - Fields: fields, - } -} - -type BalanceMap map[string]Balance type PositionMap map[string]Position type IsolatedMarginAssetMap map[string]IsolatedMarginAsset type MarginAssetMap map[string]MarginUserAsset type FuturesAssetMap map[string]FuturesUserAsset type FuturesPositionMap map[string]FuturesPosition -func (m BalanceMap) String() string { - var ss []string - for _, b := range m { - ss = append(ss, b.String()) - } - - return "BalanceMap[" + strings.Join(ss, ", ") + "]" -} - -func (m BalanceMap) Copy() (d BalanceMap) { - d = make(BalanceMap) - for c, b := range m { - d[c] = b - } - return d -} - -func (m BalanceMap) Assets(prices map[string]fixedpoint.Value) AssetMap { - assets := make(AssetMap) - - now := time.Now() - for currency, b := range m { - if b.Locked.IsZero() && b.Available.IsZero() { - continue - } - - asset := Asset{ - Currency: currency, - Total: b.Available.Add(b.Locked), - Time: now, - Locked: b.Locked, - Available: b.Available, - } - - btcusdt, hasBtcPrice := prices["BTCUSDT"] - - usdMarkets := []string{currency + "USDT", currency + "USDC", currency + "USD", "USDT" + currency} - - for _, market := range usdMarkets { - if val, ok := prices[market]; ok { - - if strings.HasPrefix(market, "USD") { - asset.InUSD = asset.Total.Div(val) - } else { - asset.InUSD = asset.Total.Mul(val) - } - - if hasBtcPrice { - asset.InBTC = asset.InUSD.Div(btcusdt) - } - } - } - - assets[currency] = asset - } - - return assets -} - -func (m BalanceMap) Print() { - for _, balance := range m { - if balance.Available.IsZero() && balance.Locked.IsZero() { - continue - } - - if balance.Locked.Sign() > 0 { - logrus.Infof(" %s: %v (locked %v)", balance.Currency, balance.Available, balance.Locked) - } else { - logrus.Infof(" %s: %v", balance.Currency, balance.Available) - } - } -} - type AccountType string const ( @@ -357,13 +145,18 @@ func (a *Account) UseLockedBalance(currency string, fund fixedpoint.Value) error defer a.Unlock() balance, ok := a.balances[currency] - if ok && balance.Locked.Compare(fund) >= 0 { + if !ok { + return fmt.Errorf("account balance %s does not exist", currency) + } + + // simple case, using fund less than locked + if balance.Locked.Compare(fund) >= 0 { balance.Locked = balance.Locked.Sub(fund) a.balances[currency] = balance return nil } - return fmt.Errorf("trying to use more than locked: locked %v < want to use %v", balance.Locked, fund) + return fmt.Errorf("trying to use more than locked: locked %v < want to use %v diff %v", balance.Locked, fund, balance.Locked.Sub(fund)) } var QuantityDelta = fixedpoint.MustNewFromString("0.00000000001") diff --git a/pkg/types/active_book.go b/pkg/types/active_book.go deleted file mode 100644 index ab1254f4c2..0000000000 --- a/pkg/types/active_book.go +++ /dev/null @@ -1 +0,0 @@ -package types diff --git a/pkg/types/asset.go b/pkg/types/asset.go new file mode 100644 index 0000000000..c4865838be --- /dev/null +++ b/pkg/types/asset.go @@ -0,0 +1,148 @@ +package types + +import ( + "fmt" + "sort" + "time" + + "github.com/slack-go/slack" + + "github.com/c9s/bbgo/pkg/fixedpoint" +) + +type Asset struct { + Currency string `json:"currency" db:"currency"` + + Total fixedpoint.Value `json:"total" db:"total"` + + NetAsset fixedpoint.Value `json:"netAsset" db:"net_asset"` + + Interest fixedpoint.Value `json:"interest" db:"interest"` + + // InUSD is net asset in USD + InUSD fixedpoint.Value `json:"inUSD" db:"net_asset_in_usd"` + + // InBTC is net asset in BTC + InBTC fixedpoint.Value `json:"inBTC" db:"net_asset_in_btc"` + + Time time.Time `json:"time" db:"time"` + Locked fixedpoint.Value `json:"lock" db:"lock" ` + Available fixedpoint.Value `json:"available" db:"available"` + Borrowed fixedpoint.Value `json:"borrowed" db:"borrowed"` + PriceInUSD fixedpoint.Value `json:"priceInUSD" db:"price_in_usd"` +} + +type AssetMap map[string]Asset + +func (m AssetMap) InUSD() (total fixedpoint.Value) { + for _, a := range m { + if a.InUSD.IsZero() { + continue + } + + total = total.Add(a.InUSD) + } + return total +} + +func (m AssetMap) PlainText() (o string) { + var assets = m.Slice() + + // sort assets + sort.Slice(assets, func(i, j int) bool { + return assets[i].InUSD.Compare(assets[j].InUSD) > 0 + }) + + sumUsd := fixedpoint.Zero + sumBTC := fixedpoint.Zero + for _, a := range assets { + usd := a.InUSD + btc := a.InBTC + if !a.InUSD.IsZero() { + o += fmt.Sprintf(" %s: %s (≈ %s) (≈ %s)", + a.Currency, + a.NetAsset.String(), + USD.FormatMoney(usd), + BTC.FormatMoney(btc), + ) + "\n" + sumUsd = sumUsd.Add(usd) + sumBTC = sumBTC.Add(btc) + } else { + o += fmt.Sprintf(" %s: %s", + a.Currency, + a.NetAsset.String(), + ) + "\n" + } + } + + o += fmt.Sprintf("Net Asset Value: (≈ %s) (≈ %s)", + USD.FormatMoney(sumUsd), + BTC.FormatMoney(sumBTC), + ) + return o +} + +func (m AssetMap) Slice() (assets []Asset) { + for _, a := range m { + assets = append(assets, a) + } + return assets +} + +func (m AssetMap) SlackAttachment() slack.Attachment { + var fields []slack.AttachmentField + var netAssetInBTC, netAssetInUSD fixedpoint.Value + + var assets = m.Slice() + + // sort assets + sort.Slice(assets, func(i, j int) bool { + return assets[i].InUSD.Compare(assets[j].InUSD) > 0 + }) + + for _, a := range assets { + netAssetInUSD = netAssetInUSD.Add(a.InUSD) + netAssetInBTC = netAssetInBTC.Add(a.InBTC) + } + + for _, a := range assets { + if !a.InUSD.IsZero() { + text := fmt.Sprintf("%s (≈ %s) (≈ %s) (%s)", + a.NetAsset.String(), + USD.FormatMoney(a.InUSD), + BTC.FormatMoney(a.InBTC), + a.InUSD.Div(netAssetInUSD).FormatPercentage(2), + ) + + if !a.Borrowed.IsZero() { + text += fmt.Sprintf(" Borrowed: %s", a.Borrowed.String()) + } + + fields = append(fields, slack.AttachmentField{ + Title: a.Currency, + Value: text, + Short: false, + }) + } else { + text := a.NetAsset.String() + + if !a.Borrowed.IsZero() { + text += fmt.Sprintf(" Borrowed: %s", a.Borrowed.String()) + } + + fields = append(fields, slack.AttachmentField{ + Title: a.Currency, + Value: text, + Short: false, + }) + } + } + + return slack.Attachment{ + Title: fmt.Sprintf("Net Asset Value %s (≈ %s)", + USD.FormatMoney(netAssetInUSD), + BTC.FormatMoney(netAssetInBTC), + ), + Fields: fields, + } +} diff --git a/pkg/types/backtest_stream.go b/pkg/types/backtest_stream.go new file mode 100644 index 0000000000..ee46d31fcc --- /dev/null +++ b/pkg/types/backtest_stream.go @@ -0,0 +1,19 @@ +package types + +import ( + "context" +) + +type BacktestStream struct { + StandardStreamEmitter +} + +func (s *BacktestStream) Connect(ctx context.Context) error { + s.EmitConnect() + s.EmitStart() + return nil +} + +func (s *BacktestStream) Close() error { + return nil +} diff --git a/pkg/types/balance.go b/pkg/types/balance.go new file mode 100644 index 0000000000..94c7bc47b0 --- /dev/null +++ b/pkg/types/balance.go @@ -0,0 +1,265 @@ +package types + +import ( + "fmt" + "strconv" + "strings" + "time" + + log "github.com/sirupsen/logrus" + "github.com/slack-go/slack" + + "github.com/c9s/bbgo/pkg/fixedpoint" +) + +type PriceMap map[string]fixedpoint.Value + +type Balance struct { + Currency string `json:"currency"` + Available fixedpoint.Value `json:"available"` + Locked fixedpoint.Value `json:"locked,omitempty"` + + // margin related fields + Borrowed fixedpoint.Value `json:"borrowed,omitempty"` + Interest fixedpoint.Value `json:"interest,omitempty"` + + // NetAsset = (Available + Locked) - Borrowed - Interest + NetAsset fixedpoint.Value `json:"net,omitempty"` +} + +func (b Balance) Add(b2 Balance) Balance { + var newB = b + newB.Available = b.Available.Add(b2.Available) + newB.Locked = b.Locked.Add(b2.Locked) + newB.Borrowed = b.Borrowed.Add(b2.Borrowed) + newB.NetAsset = b.NetAsset.Add(b2.NetAsset) + newB.Interest = b.Interest.Add(b2.Interest) + return newB +} + +func (b Balance) Total() fixedpoint.Value { + return b.Available.Add(b.Locked) +} + +// Net returns the net asset value (total - debt) +func (b Balance) Net() fixedpoint.Value { + total := b.Total() + return total.Sub(b.Debt()) +} + +func (b Balance) Debt() fixedpoint.Value { + return b.Borrowed.Add(b.Interest) +} + +func (b Balance) ValueString() (o string) { + o = b.Net().String() + + if b.Locked.Sign() > 0 { + o += fmt.Sprintf(" (locked %v)", b.Locked) + } + + if b.Borrowed.Sign() > 0 { + o += fmt.Sprintf(" (borrowed: %v)", b.Borrowed) + } + + return o +} + +func (b Balance) String() (o string) { + o = fmt.Sprintf("%s: %s", b.Currency, b.Net().String()) + + if b.Locked.Sign() > 0 { + o += fmt.Sprintf(" (locked %v)", b.Locked) + } + + if b.Borrowed.Sign() > 0 { + o += fmt.Sprintf(" (borrowed: %v)", b.Borrowed) + } + + if b.Interest.Sign() > 0 { + o += fmt.Sprintf(" (interest: %v)", b.Interest) + } + + return o +} + +type BalanceSnapshot struct { + Balances BalanceMap `json:"balances"` + Session string `json:"session"` + Time time.Time `json:"time"` +} + +func (m BalanceSnapshot) CsvHeader() []string { + return []string{"time", "session", "currency", "available", "locked", "borrowed"} +} + +func (m BalanceSnapshot) CsvRecords() [][]string { + var records [][]string + + for cur, b := range m.Balances { + records = append(records, []string{ + strconv.FormatInt(m.Time.Unix(), 10), + m.Session, + cur, + b.Available.String(), + b.Locked.String(), + b.Borrowed.String(), + }) + } + + return records +} + +type BalanceMap map[string]Balance + +func (m BalanceMap) Debts() BalanceMap { + bm := make(BalanceMap) + for c, b := range m { + if b.Borrowed.Sign() > 0 || b.Interest.Sign() > 0 { + bm[c] = b + } + } + return bm +} + +func (m BalanceMap) Currencies() (currencies []string) { + for _, b := range m { + currencies = append(currencies, b.Currency) + } + return currencies +} + +func (m BalanceMap) Add(bm BalanceMap) BalanceMap { + var total = m.Copy() + for _, b := range bm { + tb, ok := total[b.Currency] + if ok { + tb = tb.Add(b) + } else { + tb = b + } + total[b.Currency] = tb + } + return total +} + +func (m BalanceMap) String() string { + var ss []string + for _, b := range m { + ss = append(ss, b.String()) + } + + return "BalanceMap[" + strings.Join(ss, ", ") + "]" +} + +func (m BalanceMap) Copy() (d BalanceMap) { + d = make(BalanceMap) + for c, b := range m { + d[c] = b + } + return d +} + +// Assets converts balances into assets with the given prices +func (m BalanceMap) Assets(prices PriceMap, priceTime time.Time) AssetMap { + assets := make(AssetMap) + + _, btcInUSD, hasBtcPrice := findUSDMarketPrice("BTC", prices) + + for currency, b := range m { + total := b.Total() + netAsset := b.Net() + + if total.IsZero() && netAsset.IsZero() { + continue + } + + asset := Asset{ + Currency: currency, + Total: total, + Time: priceTime, + Locked: b.Locked, + Available: b.Available, + Borrowed: b.Borrowed, + Interest: b.Interest, + NetAsset: netAsset, + } + + if IsUSDFiatCurrency(currency) { // for usd + asset.InUSD = netAsset + asset.PriceInUSD = fixedpoint.One + if hasBtcPrice && !asset.InUSD.IsZero() { + asset.InBTC = asset.InUSD.Div(btcInUSD) + } + } else { // for crypto + if market, usdPrice, ok := findUSDMarketPrice(currency, prices); ok { + // this includes USDT, USD, USDC and so on + if strings.HasPrefix(market, "USD") || strings.HasPrefix(market, "BUSD") { // for prices like USDT/TWD, BUSD/USDT + if !asset.NetAsset.IsZero() { + asset.InUSD = asset.NetAsset.Div(usdPrice) + } + asset.PriceInUSD = fixedpoint.One.Div(usdPrice) + } else { // for prices like BTC/USDT + if !asset.NetAsset.IsZero() { + asset.InUSD = asset.NetAsset.Mul(usdPrice) + } + asset.PriceInUSD = usdPrice + } + + if hasBtcPrice && !asset.InUSD.IsZero() { + asset.InBTC = asset.InUSD.Div(btcInUSD) + } + } + } + + assets[currency] = asset + } + + return assets +} + +func (m BalanceMap) Print() { + for _, balance := range m { + if balance.Net().IsZero() { + continue + } + + o := fmt.Sprintf(" %s: %v", balance.Currency, balance.Available) + if balance.Locked.Sign() > 0 { + o += fmt.Sprintf(" (locked %v)", balance.Locked) + } + + if balance.Borrowed.Sign() > 0 { + o += fmt.Sprintf(" (borrowed %v)", balance.Borrowed) + } + + log.Infoln(o) + } +} + +func (m BalanceMap) SlackAttachment() slack.Attachment { + var fields []slack.AttachmentField + + for _, b := range m { + fields = append(fields, slack.AttachmentField{ + Title: b.Currency, + Value: b.ValueString(), + Short: true, + }) + } + + return slack.Attachment{ + Color: "#CCA33F", + Fields: fields, + } +} + +func findUSDMarketPrice(currency string, prices map[string]fixedpoint.Value) (string, fixedpoint.Value, bool) { + usdMarkets := []string{currency + "USDT", currency + "USDC", currency + "USD", "USDT" + currency} + for _, market := range usdMarkets { + if usdPrice, ok := prices[market]; ok { + return market, usdPrice, ok + } + } + return "", fixedpoint.Zero, false +} diff --git a/pkg/types/balance_test.go b/pkg/types/balance_test.go new file mode 100644 index 0000000000..d3b69aeddf --- /dev/null +++ b/pkg/types/balance_test.go @@ -0,0 +1,98 @@ +package types + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/fixedpoint" +) + +func TestBalanceMap_Add(t *testing.T) { + var bm = BalanceMap{} + var bm2 = bm.Add(BalanceMap{ + "BTC": Balance{ + Currency: "BTC", + Available: fixedpoint.MustNewFromString("10.0"), + Locked: fixedpoint.MustNewFromString("0"), + NetAsset: fixedpoint.MustNewFromString("10.0"), + }, + }) + assert.Len(t, bm2, 1) + + var bm3 = bm2.Add(BalanceMap{ + "BTC": Balance{ + Currency: "BTC", + Available: fixedpoint.MustNewFromString("1.0"), + Locked: fixedpoint.MustNewFromString("0"), + NetAsset: fixedpoint.MustNewFromString("1.0"), + }, + "LTC": Balance{ + Currency: "LTC", + Available: fixedpoint.MustNewFromString("20.0"), + Locked: fixedpoint.MustNewFromString("0"), + NetAsset: fixedpoint.MustNewFromString("20.0"), + }, + }) + assert.Len(t, bm3, 2) + assert.Equal(t, fixedpoint.MustNewFromString("11.0"), bm3["BTC"].Available) +} + +func TestBalanceMap_Assets(t *testing.T) { + type args struct { + prices PriceMap + priceTime time.Time + } + tests := []struct { + name string + m BalanceMap + args args + want AssetMap + }{ + { + m: BalanceMap{ + "USDT": Balance{Currency: "USDT", Available: number(100.0)}, + "BTC": Balance{Currency: "BTC", Borrowed: number(2.0)}, + }, + args: args{ + prices: PriceMap{ + "BTCUSDT": number(19000.0), + }, + }, + want: AssetMap{ + "USDT": { + Currency: "USDT", + Total: number(100), + NetAsset: number(100.0), + Interest: number(0), + InUSD: number(100.0), + InBTC: number(100.0 / 19000.0), + Time: time.Time{}, + Locked: number(0), + Available: number(100.0), + Borrowed: number(0), + PriceInUSD: number(1.0), + }, + "BTC": { + Currency: "BTC", + Total: number(0), + NetAsset: number(-2), + Interest: number(0), + InUSD: number(-2 * 19000.0), + InBTC: number(-2), + Time: time.Time{}, + Locked: number(0), + Available: number(0), + Borrowed: number(2), + PriceInUSD: number(19000.0), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, tt.m.Assets(tt.args.prices, tt.args.priceTime), "Assets(%v, %v)", tt.args.prices, tt.args.priceTime) + }) + } +} diff --git a/pkg/types/bollinger.go b/pkg/types/bollinger.go new file mode 100644 index 0000000000..9e7f4b82c0 --- /dev/null +++ b/pkg/types/bollinger.go @@ -0,0 +1,8 @@ +package types + +// BollingerSetting contains the bollinger indicator setting propers +// Interval, Window and BandWidth +type BollingerSetting struct { + IntervalWindow + BandWidth float64 `json:"bandWidth"` +} diff --git a/pkg/types/channel.go b/pkg/types/channel.go index 23a90fc1e6..8b9b48e0f6 100644 --- a/pkg/types/channel.go +++ b/pkg/types/channel.go @@ -6,3 +6,4 @@ var BookChannel = Channel("book") var KLineChannel = Channel("kline") var BookTickerChannel = Channel("bookticker") var MarketTradeChannel = Channel("trade") +var AggTradeChannel = Channel("aggTrade") diff --git a/pkg/types/csv.go b/pkg/types/csv.go new file mode 100644 index 0000000000..46a0263bfc --- /dev/null +++ b/pkg/types/csv.go @@ -0,0 +1,7 @@ +package types + +// CsvFormatter is an interface used for dumping object into csv file +type CsvFormatter interface { + CsvHeader() []string + CsvRecords() [][]string +} diff --git a/pkg/types/currencies.go b/pkg/types/currencies.go index 3be262926a..4e24c3a830 100644 --- a/pkg/types/currencies.go +++ b/pkg/types/currencies.go @@ -1,10 +1,12 @@ package types -import "math/big" +import ( + "math/big" -import "github.com/leekchan/accounting" + "github.com/leekchan/accounting" -import "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/fixedpoint" +) type Acc = accounting.Accounting @@ -24,6 +26,17 @@ var BNB = wrapper{accounting.Accounting{Symbol: "BNB ", Precision: 4}} var FiatCurrencies = []string{"USDC", "USDT", "USD", "TWD", "EUR", "GBP", "BUSD"} +var USDFiatCurrencies = []string{"USDT", "USDC", "USD", "BUSD"} + +func IsUSDFiatCurrency(currency string) bool { + for _, c := range USDFiatCurrencies { + if c == currency { + return true + } + } + return false +} + func IsFiatCurrency(currency string) bool { for _, c := range FiatCurrencies { if c == currency { diff --git a/pkg/types/deposit.go b/pkg/types/deposit.go index d3340ff8d3..2414da8fba 100644 --- a/pkg/types/deposit.go +++ b/pkg/types/deposit.go @@ -1,8 +1,10 @@ package types import ( - "github.com/c9s/bbgo/pkg/fixedpoint" + "fmt" "time" + + "github.com/c9s/bbgo/pkg/fixedpoint" ) type DepositStatus string @@ -37,3 +39,22 @@ type Deposit struct { func (d Deposit) EffectiveTime() time.Time { return d.Time.Time() } + +func (d Deposit) String() (o string) { + o = fmt.Sprintf("%s deposit %s %v <- ", d.Exchange, d.Asset, d.Amount) + + if len(d.AddressTag) > 0 { + o += fmt.Sprintf("%s (tag: %s) at %s", d.Address, d.AddressTag, d.Time.Time()) + } else { + o += fmt.Sprintf("%s at %s", d.Address, d.Time.Time()) + } + + if len(d.TransactionID) > 0 { + o += fmt.Sprintf("txID: %s", cutstr(d.TransactionID, 12, 4, 4)) + } + if len(d.Status) > 0 { + o += "status: " + string(d.Status) + } + + return o +} diff --git a/pkg/types/error.go b/pkg/types/error.go index 48ce67fac0..5d58b57b8a 100644 --- a/pkg/types/error.go +++ b/pkg/types/error.go @@ -21,3 +21,13 @@ func NewOrderError(e error, o Order) error { order: o, } } + +type ZeroAssetError struct { + error +} + +func NewZeroAssetError(e error) ZeroAssetError { + return ZeroAssetError{ + error: e, + } +} diff --git a/pkg/types/exchange.go b/pkg/types/exchange.go index 2abbfec3ea..cb6708f94c 100644 --- a/pkg/types/exchange.go +++ b/pkg/types/exchange.go @@ -40,15 +40,22 @@ func (n ExchangeName) String() string { } const ( - ExchangeMax = ExchangeName("max") - ExchangeBinance = ExchangeName("binance") - ExchangeFTX = ExchangeName("ftx") - ExchangeOKEx = ExchangeName("okex") - ExchangeKucoin = ExchangeName("kucoin") - ExchangeBacktest = ExchangeName("backtest") + ExchangeMax ExchangeName = "max" + ExchangeBinance ExchangeName = "binance" + ExchangeFTX ExchangeName = "ftx" + ExchangeOKEx ExchangeName = "okex" + ExchangeKucoin ExchangeName = "kucoin" + ExchangeBacktest ExchangeName = "backtest" ) -var SupportedExchanges = []ExchangeName{"binance", "max", "ftx", "okex", "kucoin"} +var SupportedExchanges = []ExchangeName{ + ExchangeMax, + ExchangeBinance, + ExchangeFTX, + ExchangeOKEx, + ExchangeKucoin, + // note: we are not using "backtest" +} func ValidExchangeName(a string) (ExchangeName, error) { switch strings.ToLower(a) { @@ -67,6 +74,7 @@ func ValidExchangeName(a string) (ExchangeName, error) { return "", fmt.Errorf("invalid exchange name: %s", a) } +//go:generate mockgen -destination=mocks/mock_exchange.go -package=mocks . Exchange type Exchange interface { Name() ExchangeName @@ -80,6 +88,7 @@ type Exchange interface { // ExchangeOrderQueryService provides an interface for querying the order status via order ID or client order ID type ExchangeOrderQueryService interface { QueryOrder(ctx context.Context, q OrderQuery) (*Order, error) + QueryOrderTrades(ctx context.Context, q OrderQuery) ([]Trade, error) } type ExchangeTradeService interface { @@ -87,13 +96,21 @@ type ExchangeTradeService interface { QueryAccountBalances(ctx context.Context) (BalanceMap, error) - SubmitOrders(ctx context.Context, orders ...SubmitOrder) (createdOrders OrderSlice, err error) + SubmitOrder(ctx context.Context, order SubmitOrder) (createdOrder *Order, err error) QueryOpenOrders(ctx context.Context, symbol string) (orders []Order, err error) CancelOrders(ctx context.Context, orders ...Order) error } +type ExchangeDefaultFeeRates interface { + DefaultFeeRates() ExchangeFee +} + +type ExchangeAmountFeeProtect interface { + SetModifyOrderAmountForFee(ExchangeFee) +} + type ExchangeTradeHistoryService interface { QueryTrades(ctx context.Context, symbol string, options *TradeQueryOptions) ([]Trade, error) QueryClosedOrders(ctx context.Context, symbol string, since, until time.Time, lastOrderID uint64) (orders []Order, err error) @@ -122,7 +139,7 @@ type ExchangeTransferService interface { } type ExchangeWithdrawalService interface { - Withdrawal(ctx context.Context, asset string, amount fixedpoint.Value, address string, options *WithdrawalOptions) error + Withdraw(ctx context.Context, asset string, amount fixedpoint.Value, address string, options *WithdrawalOptions) error } type ExchangeRewardService interface { diff --git a/pkg/types/exchange_icon.go b/pkg/types/exchange_icon.go new file mode 100644 index 0000000000..a85624a134 --- /dev/null +++ b/pkg/types/exchange_icon.go @@ -0,0 +1,20 @@ +package types + +func ExchangeFooterIcon(exName ExchangeName) string { + footerIcon := "" + + switch exName { + case ExchangeBinance: + footerIcon = "https://bin.bnbstatic.com/static/images/common/favicon.ico" + case ExchangeMax: + footerIcon = "https://max.maicoin.com/favicon-16x16.png" + case ExchangeFTX: + footerIcon = "https://ftx.com/favicon.ico?v=2" + case ExchangeOKEx: + footerIcon = "https://static.okex.com/cdn/assets/imgs/MjAxODg/D91A7323087D31A588E0D2A379DD7747.png" + case ExchangeKucoin: + footerIcon = "https://assets.staticimg.com/cms/media/7AV75b9jzr9S8H3eNuOuoqj8PwdUjaDQGKGczGqTS.png" + } + + return footerIcon +} diff --git a/pkg/types/filter.go b/pkg/types/filter.go new file mode 100644 index 0000000000..58e0a966ec --- /dev/null +++ b/pkg/types/filter.go @@ -0,0 +1,51 @@ +package types + +type FilterResult struct { + a Series + b func(int, float64) bool + length int + c []int +} + +func (f *FilterResult) Last() float64 { + return f.Index(0) +} + +func (f *FilterResult) Index(j int) float64 { + if j >= f.length { + return 0 + } + if len(f.c) > j { + return f.a.Index(f.c[j]) + } + l := f.a.Length() + k := len(f.c) + i := 0 + if k > 0 { + i = f.c[k-1] + 1 + } + for ; i < l; i++ { + tmp := f.a.Index(i) + if f.b(i, tmp) { + f.c = append(f.c, i) + if j == k { + return tmp + } + k++ + } + } + return 0 +} + +func (f *FilterResult) Length() int { + return f.length +} + +// Filter function filters Series by using a boolean function. +// When the boolean function returns true, the Series value at index i will be included in the returned Series. +// The returned Series will find at most `length` latest matching elements from the input Series. +// Query index larger or equal than length from the returned Series will return 0 instead. +// Notice that any Update on the input Series will make the previously returned Series outdated. +func Filter(a Series, b func(i int, value float64) bool, length int) SeriesExtend { + return NewSeries(&FilterResult{a, b, length, nil}) +} diff --git a/pkg/types/float_map.go b/pkg/types/float_map.go new file mode 100644 index 0000000000..8a05391c9f --- /dev/null +++ b/pkg/types/float_map.go @@ -0,0 +1,5 @@ +package types + +import "github.com/c9s/bbgo/pkg/datatype/floats" + +var _ Series = floats.Slice([]float64{}).Addr() diff --git a/pkg/types/float_slice.go b/pkg/types/float_slice.go deleted file mode 100644 index f1f3b954f4..0000000000 --- a/pkg/types/float_slice.go +++ /dev/null @@ -1,144 +0,0 @@ -package types - -import ( - "math" - - "gonum.org/v1/gonum/floats" -) - -type Float64Slice []float64 - -func (s *Float64Slice) Push(v float64) { - *s = append(*s, v) -} - -func (s *Float64Slice) Pop(i int64) (v float64) { - v = (*s)[i] - *s = append((*s)[:i], (*s)[i+1:]...) - return v -} - -func (s Float64Slice) Max() float64 { - return floats.Max(s) -} - -func (s Float64Slice) Min() float64 { - return floats.Min(s) -} - -func (s Float64Slice) Sum() (sum float64) { - return floats.Sum(s) -} - -func (s Float64Slice) Mean() (mean float64) { - length := len(s) - if length == 0 { - panic("zero length slice") - } - return s.Sum() / float64(length) -} - -func (s Float64Slice) Tail(size int) Float64Slice { - length := len(s) - if length <= size { - win := make(Float64Slice, length) - copy(win, s) - return win - } - - win := make(Float64Slice, size) - copy(win, s[length-size:]) - return win -} - -func (s Float64Slice) Diff() (values Float64Slice) { - for i, v := range s { - if i == 0 { - values.Push(0) - continue - } - values.Push(v - s[i-1]) - } - return values -} - -func (s Float64Slice) PositiveValuesOrZero() (values Float64Slice) { - for _, v := range s { - values.Push(math.Max(v, 0)) - } - return values -} - -func (s Float64Slice) NegativeValuesOrZero() (values Float64Slice) { - for _, v := range s { - values.Push(math.Min(v, 0)) - } - return values -} - -func (s Float64Slice) Abs() (values Float64Slice) { - for _, v := range s { - values.Push(math.Abs(v)) - } - return values -} - -func (s Float64Slice) MulScalar(x float64) (values Float64Slice) { - for _, v := range s { - values.Push(v * x) - } - return values -} - -func (s Float64Slice) DivScalar(x float64) (values Float64Slice) { - for _, v := range s { - values.Push(v / x) - } - return values -} - -func (s Float64Slice) Mul(other Float64Slice) (values Float64Slice) { - if len(s) != len(other) { - panic("slice lengths do not match") - } - - for i, v := range s { - values.Push(v * other[i]) - } - - return values -} - -func (s Float64Slice) Dot(other Float64Slice) float64 { - return floats.Dot(s, other) -} - -func (s Float64Slice) Normalize() Float64Slice { - return s.DivScalar(s.Sum()) -} - -func (a *Float64Slice) Last() float64 { - length := len(*a) - if length > 0 { - return (*a)[length-1] - } - return 0.0 -} - -func (a *Float64Slice) Index(i int) float64 { - length := len(*a) - if length-i < 0 || i < 0 { - return 0.0 - } - return (*a)[length-i-1] -} - -func (a *Float64Slice) Length() int { - return len(*a) -} - -func (a Float64Slice) Addr() *Float64Slice { - return &a -} - -var _ Series = Float64Slice([]float64{}).Addr() diff --git a/pkg/types/heikinashi_stream.go b/pkg/types/heikinashi_stream.go new file mode 100644 index 0000000000..f3cc351a09 --- /dev/null +++ b/pkg/types/heikinashi_stream.go @@ -0,0 +1,72 @@ +package types + +import ( + "github.com/c9s/bbgo/pkg/fixedpoint" +) + +var Four fixedpoint.Value = fixedpoint.NewFromInt(4) + +type HeikinAshiStream struct { + StandardStreamEmitter + lastAshi map[string]map[Interval]*KLine + LastOrigin map[string]map[Interval]*KLine +} + +func (s *HeikinAshiStream) EmitKLineClosed(kline KLine) { + ashi := kline + if s.lastAshi == nil { + s.lastAshi = make(map[string]map[Interval]*KLine) + s.LastOrigin = make(map[string]map[Interval]*KLine) + } + if s.lastAshi[kline.Symbol] == nil { + s.lastAshi[kline.Symbol] = make(map[Interval]*KLine) + s.LastOrigin[kline.Symbol] = make(map[Interval]*KLine) + } + lastAshi := s.lastAshi[kline.Symbol][kline.Interval] + if lastAshi == nil { + ashi.Close = kline.Close.Add(kline.High). + Add(kline.Low). + Add(kline.Open). + Div(Four) + // High and Low are the same + s.lastAshi[kline.Symbol][kline.Interval] = &ashi + s.LastOrigin[kline.Symbol][kline.Interval] = &kline + } else { + ashi.Close = kline.Close.Add(kline.High). + Add(kline.Low). + Add(kline.Open). + Div(Four) + ashi.Open = lastAshi.Open.Add(lastAshi.Close).Div(Two) + // High and Low are the same + s.lastAshi[kline.Symbol][kline.Interval] = &ashi + s.LastOrigin[kline.Symbol][kline.Interval] = &kline + } + s.StandardStreamEmitter.EmitKLineClosed(ashi) +} + +// No writeback to lastAshi +func (s *HeikinAshiStream) EmitKLine(kline KLine) { + ashi := kline + if s.lastAshi == nil { + s.lastAshi = make(map[string]map[Interval]*KLine) + } + if s.lastAshi[kline.Symbol] == nil { + s.lastAshi[kline.Symbol] = make(map[Interval]*KLine) + } + lastAshi := s.lastAshi[kline.Symbol][kline.Interval] + if lastAshi == nil { + ashi.Close = kline.Close.Add(kline.High). + Add(kline.Low). + Add(kline.Open). + Div(Four) + } else { + ashi.Close = kline.Close.Add(kline.High). + Add(kline.Low). + Add(kline.Open). + Div(Four) + ashi.Open = lastAshi.Open.Add(lastAshi.Close).Div(Two) + } + s.StandardStreamEmitter.EmitKLine(ashi) +} + +var _ StandardStreamEmitter = &HeikinAshiStream{} diff --git a/pkg/types/indicator.go b/pkg/types/indicator.go index 1873894a92..f5263ac2fd 100644 --- a/pkg/types/indicator.go +++ b/pkg/types/indicator.go @@ -4,10 +4,67 @@ import ( "fmt" "math" "reflect" + "time" + "github.com/wcharczuk/go-chart/v2" "gonum.org/v1/gonum/stat" + + "github.com/c9s/bbgo/pkg/datatype/floats" ) +// Super basic Series type that simply holds the float64 data +// with size limit (the only difference compare to float64slice) +type Queue struct { + SeriesBase + arr []float64 + size int +} + +func NewQueue(size int) *Queue { + out := &Queue{ + arr: make([]float64, 0, size), + size: size, + } + out.SeriesBase.Series = out + return out +} + +func (inc *Queue) Last() float64 { + if len(inc.arr) == 0 { + return 0 + } + return inc.arr[len(inc.arr)-1] +} + +func (inc *Queue) Index(i int) float64 { + if len(inc.arr)-i-1 < 0 { + return 0 + } + return inc.arr[len(inc.arr)-i-1] +} + +func (inc *Queue) Length() int { + return len(inc.arr) +} + +func (inc *Queue) Clone() *Queue { + out := &Queue{ + arr: inc.arr[:], + size: inc.size, + } + out.SeriesBase.Series = out + return out +} + +func (inc *Queue) Update(v float64) { + inc.arr = append(inc.arr, v) + if len(inc.arr) > inc.size { + inc.arr = inc.arr[len(inc.arr)-inc.size:] + } +} + +var _ UpdatableSeriesExtend = &Queue{} + // Float64Indicator is the indicators (SMA and EWMA) that we want to use are returning float64 data. type Float64Indicator interface { Last() float64 @@ -22,6 +79,80 @@ type Series interface { Length() int } +type SeriesExtend interface { + Series + Sum(limit ...int) float64 + Mean(limit ...int) float64 + Abs() SeriesExtend + Predict(lookback int, offset ...int) float64 + NextCross(b Series, lookback int) (int, float64, bool) + CrossOver(b Series) BoolSeries + CrossUnder(b Series) BoolSeries + Highest(lookback int) float64 + Lowest(lookback int) float64 + Add(b interface{}) SeriesExtend + Minus(b interface{}) SeriesExtend + Div(b interface{}) SeriesExtend + Mul(b interface{}) SeriesExtend + Dot(b interface{}, limit ...int) float64 + Array(limit ...int) (result []float64) + Reverse(limit ...int) (result floats.Slice) + Change(offset ...int) SeriesExtend + PercentageChange(offset ...int) SeriesExtend + Stdev(params ...int) float64 + Rolling(window int) *RollingResult + Shift(offset int) SeriesExtend + Skew(length int) float64 + Variance(length int) float64 + Covariance(b Series, length int) float64 + Correlation(b Series, length int, method ...CorrFunc) float64 + AutoCorrelation(length int, lag ...int) float64 + Rank(length int) SeriesExtend + Sigmoid() SeriesExtend + Softmax(window int) SeriesExtend + Entropy(window int) float64 + CrossEntropy(b Series, window int) float64 + Filter(b func(i int, value float64) bool, length int) SeriesExtend +} + +type SeriesBase struct { + Series +} + +func NewSeries(a Series) SeriesExtend { + return &SeriesBase{ + Series: a, + } +} + +type UpdatableSeries interface { + Series + Update(float64) +} + +type UpdatableSeriesExtend interface { + SeriesExtend + Update(float64) +} + +func Clone(u UpdatableSeriesExtend) UpdatableSeriesExtend { + method, ok := reflect.TypeOf(u).MethodByName("Clone") + if ok { + out := method.Func.Call([]reflect.Value{reflect.ValueOf(u)}) + return out[0].Interface().(UpdatableSeriesExtend) + } + panic("method Clone not exist") +} + +func TestUpdate(u UpdatableSeriesExtend, input float64) UpdatableSeriesExtend { + method, ok := reflect.TypeOf(u).MethodByName("TestUpdate") + if ok { + out := method.Func.Call([]reflect.Value{reflect.ValueOf(u), reflect.ValueOf(input)}) + return out[0].Interface().(UpdatableSeriesExtend) + } + panic("method TestUpdate not exist") +} + // The interface maps to pinescript basic type `series` for bool type // Access the internal historical data from the latest to the oldest // Index(0) always maps to Last() @@ -35,13 +166,10 @@ type BoolSeries interface { // if limit is given, will only sum first limit numbers (a.Index[0..limit]) // otherwise will sum all elements func Sum(a Series, limit ...int) (sum float64) { - l := -1 - if len(limit) > 0 { + l := a.Length() + if len(limit) > 0 && limit[0] < l { l = limit[0] } - if l < a.Length() { - l = a.Length() - } for i := 0; i < l; i++ { sum += a.Index(i) } @@ -52,12 +180,12 @@ func Sum(a Series, limit ...int) (sum float64) { // if limit is given, will only calculate the average of first limit numbers (a.Index[0..limit]) // otherwise will operate on all elements func Mean(a Series, limit ...int) (mean float64) { - l := -1 - if len(limit) > 0 { - l = limit[0] + l := a.Length() + if l == 0 { + return 0 } - if l < a.Length() { - l = a.Length() + if len(limit) > 0 && limit[0] < l { + l = limit[0] } return Sum(a, l) / float64(l) } @@ -79,24 +207,29 @@ func (a *AbsResult) Length() int { } // Return series that having all the elements positive -func Abs(a Series) Series { - return &AbsResult{a} +func Abs(a Series) SeriesExtend { + return NewSeries(&AbsResult{a}) } var _ Series = &AbsResult{} -func Predict(a Series, lookback int, offset ...int) float64 { +func LinearRegression(a Series, lookback int) (alpha float64, beta float64) { if a.Length() < lookback { lookback = a.Length() } - x := make([]float64, lookback, lookback) - y := make([]float64, lookback, lookback) + x := make([]float64, lookback) + y := make([]float64, lookback) var weights []float64 for i := 0; i < lookback; i++ { x[i] = float64(i) y[i] = a.Index(i) } - alpha, beta := stat.LinearRegression(x, y, weights, false) + alpha, beta = stat.LinearRegression(x, y, weights, false) + return +} + +func Predict(a Series, lookback int, offset ...int) float64 { + alpha, beta := LinearRegression(a, lookback) o := -1.0 if len(offset) > 0 { o = -float64(offset[0]) @@ -117,9 +250,9 @@ func NextCross(a Series, b Series, lookback int) (int, float64, bool) { if b.Length() < lookback { lookback = b.Length() } - x := make([]float64, lookback, lookback) - y1 := make([]float64, lookback, lookback) - y2 := make([]float64, lookback, lookback) + x := make([]float64, lookback) + y1 := make([]float64, lookback) + y2 := make([]float64, lookback) var weights []float64 for i := 0; i < lookback; i++ { x[i] = float64(i) @@ -237,6 +370,10 @@ func (a NumberSeries) Length() int { return math.MaxInt32 } +func (a NumberSeries) Clone() NumberSeries { + return a +} + var _ Series = NumberSeries(0) type AddSeriesResult struct { @@ -245,29 +382,29 @@ type AddSeriesResult struct { } // Add two series, result[i] = a[i] + b[i] -func Add(a interface{}, b interface{}) Series { +func Add(a interface{}, b interface{}) SeriesExtend { var aa Series var bb Series - switch a.(type) { + switch tp := a.(type) { case float64: - aa = NumberSeries(a.(float64)) + aa = NumberSeries(tp) case Series: - aa = a.(Series) + aa = tp default: panic("input should be either *Series or float64") } - switch b.(type) { + switch tp := b.(type) { case float64: - bb = NumberSeries(b.(float64)) + bb = NumberSeries(tp) case Series: - bb = b.(Series) + bb = tp default: panic("input should be either *Series or float64") } - return &AddSeriesResult{aa, bb} + return NewSeries(&AddSeriesResult{aa, bb}) } func (a *AddSeriesResult) Last() float64 { @@ -295,10 +432,10 @@ type MinusSeriesResult struct { } // Minus two series, result[i] = a[i] - b[i] -func Minus(a interface{}, b interface{}) Series { +func Minus(a interface{}, b interface{}) SeriesExtend { aa := switchIface(a) bb := switchIface(b) - return &MinusSeriesResult{aa, bb} + return NewSeries(&MinusSeriesResult{aa, bb}) } func (a *MinusSeriesResult) Last() float64 { @@ -321,19 +458,19 @@ func (a *MinusSeriesResult) Length() int { var _ Series = &MinusSeriesResult{} func switchIface(b interface{}) Series { - switch b.(type) { + switch tp := b.(type) { case float64: - return NumberSeries(b.(float64)) + return NumberSeries(tp) case int32: - return NumberSeries(float64(b.(int32))) + return NumberSeries(float64(tp)) case int64: - return NumberSeries(float64(b.(int64))) + return NumberSeries(float64(tp)) case float32: - return NumberSeries(float64(b.(float32))) + return NumberSeries(float64(tp)) case int: - return NumberSeries(float64(b.(int))) + return NumberSeries(float64(tp)) case Series: - return b.(Series) + return tp default: fmt.Println(reflect.TypeOf(b)) panic("input should be either *Series or float64") @@ -342,13 +479,13 @@ func switchIface(b interface{}) Series { } // Divid two series, result[i] = a[i] / b[i] -func Div(a interface{}, b interface{}) Series { +func Div(a interface{}, b interface{}) SeriesExtend { aa := switchIface(a) if 0 == b { panic("Divid by zero exception") } bb := switchIface(b) - return &DivSeriesResult{aa, bb} + return NewSeries(&DivSeriesResult{aa, bb}) } @@ -377,28 +514,28 @@ func (a *DivSeriesResult) Length() int { var _ Series = &DivSeriesResult{} // Multiple two series, result[i] = a[i] * b[i] -func Mul(a interface{}, b interface{}) Series { +func Mul(a interface{}, b interface{}) SeriesExtend { var aa Series var bb Series - switch a.(type) { + switch tp := a.(type) { case float64: - aa = NumberSeries(a.(float64)) + aa = NumberSeries(tp) case Series: - aa = a.(Series) + aa = tp default: panic("input should be either Series or float64") } - switch b.(type) { + switch tp := b.(type) { case float64: - bb = NumberSeries(b.(float64)) + bb = NumberSeries(tp) case Series: - bb = b.(Series) + bb = tp default: panic("input should be either Series or float64") } - return &MulSeriesResult{aa, bb} + return NewSeries(&MulSeriesResult{aa, bb}) } @@ -430,41 +567,100 @@ var _ Series = &MulSeriesResult{} // if limit is given, will only calculate the first limit numbers (a.Index[0..limit]) // otherwise will operate on all elements func Dot(a interface{}, b interface{}, limit ...int) float64 { - return Sum(Mul(a, b), limit...) + var aaf float64 + var aas Series + var bbf float64 + var bbs Series + var isaf, isbf bool + + switch tp := a.(type) { + case float64: + aaf = tp + isaf = true + case Series: + aas = tp + isaf = false + default: + panic("input should be either Series or float64") + } + switch tp := b.(type) { + case float64: + bbf = tp + isbf = true + case Series: + bbs = tp + isbf = false + default: + panic("input should be either Series or float64") + + } + l := 1 + if len(limit) > 0 { + l = limit[0] + } else if isaf && isbf { + l = 1 + } else { + if !isaf { + l = aas.Length() + } + if !isbf { + if l > bbs.Length() { + l = bbs.Length() + } + } + } + if isaf && isbf { + return aaf * bbf * float64(l) + } else if isaf && !isbf { + sum := 0. + for i := 0; i < l; i++ { + sum += aaf * bbs.Index(i) + } + return sum + } else if !isaf && isbf { + sum := 0. + for i := 0; i < l; i++ { + sum += aas.Index(i) * bbf + } + return sum + } else { + sum := 0. + for i := 0; i < l; i++ { + sum += aas.Index(i) * bbs.Index(i) + } + return sum + } } // Extract elements from the Series to a float64 array, following the order of Index(0..limit) // if limit is given, will only take the first limit numbers (a.Index[0..limit]) // otherwise will operate on all elements -func ToArray(a Series, limit ...int) (result []float64) { - l := -1 - if len(limit) > 0 { +func Array(a Series, limit ...int) (result []float64) { + l := a.Length() + if len(limit) > 0 && l > limit[0] { l = limit[0] } - if l < a.Length() { + if l > a.Length() { l = a.Length() } - result = make([]float64, l, l) + result = make([]float64, l) for i := 0; i < l; i++ { result[i] = a.Index(i) } return } -// Similar to ToArray but in reverse order. +// Similar to Array but in reverse order. // Useful when you want to cache series' calculated result as float64 array // the then reuse the result in multiple places (so that no recalculation will be triggered) // // notice that the return type is a Float64Slice, which implements the Series interface -func ToReverseArray(a Series, limit ...int) (result Float64Slice) { - l := -1 - if len(limit) > 0 { +func Reverse(a Series, limit ...int) (result floats.Slice) { + l := a.Length() + if len(limit) > 0 && l > limit[0] { l = limit[0] } - if l < a.Length() { - l = a.Length() - } - result = make([]float64, l, l) + result = make([]float64, l) for i := 0; i < l; i++ { result[l-i-1] = a.Index(i) } @@ -500,13 +696,561 @@ func (c *ChangeResult) Length() int { // Difference between current value and previous, a - a[offset] // offset: if not given, offset is 1. -func Change(a Series, offset ...int) Series { +func Change(a Series, offset ...int) SeriesExtend { o := 1 if len(offset) > 0 { o = offset[0] } - return &ChangeResult{a, o} + return NewSeries(&ChangeResult{a, o}) +} + +type PercentageChangeResult struct { + a Series + offset int +} + +func (c *PercentageChangeResult) Last() float64 { + if c.offset >= c.a.Length() { + return 0 + } + return c.a.Last()/c.a.Index(c.offset) - 1 +} + +func (c *PercentageChangeResult) Index(i int) float64 { + if i+c.offset >= c.a.Length() { + return 0 + } + return c.a.Index(i)/c.a.Index(i+c.offset) - 1 +} + +func (c *PercentageChangeResult) Length() int { + length := c.a.Length() + if length >= c.offset { + return length - c.offset + } + return 0 +} + +// Percentage change between current and a prior element, a / a[offset] - 1. +// offset: if not give, offset is 1. +func PercentageChange(a Series, offset ...int) SeriesExtend { + o := 1 + if len(offset) > 0 { + o = offset[0] + } + + return NewSeries(&PercentageChangeResult{a, o}) +} + +func Stdev(a Series, params ...int) float64 { + length := a.Length() + if length == 0 { + return 0 + } + if len(params) > 0 && params[0] < length { + length = params[0] + } + ddof := 0 + if len(params) > 1 { + ddof = params[1] + } + avg := Mean(a, length) + s := .0 + for i := 0; i < length; i++ { + diff := a.Index(i) - avg + s += diff * diff + } + if length-ddof == 0 { + return 0 + } + return math.Sqrt(s / float64(length-ddof)) +} + +type CorrFunc func(Series, Series, int) float64 + +func Kendall(a, b Series, length int) float64 { + if a.Length() < length { + length = a.Length() + } + if b.Length() < length { + length = b.Length() + } + aRanks := Rank(a, length) + bRanks := Rank(b, length) + concordant, discordant := 0, 0 + for i := 0; i < length; i++ { + for j := i + 1; j < length; j++ { + value := (aRanks.Index(i) - aRanks.Index(j)) * (bRanks.Index(i) - bRanks.Index(j)) + if value > 0 { + concordant++ + } else { + discordant++ + } + } + } + return float64(concordant-discordant) * 2.0 / float64(length*(length-1)) +} + +func Rank(a Series, length int) SeriesExtend { + if length > a.Length() { + length = a.Length() + } + rank := make([]float64, length) + mapper := make([]float64, length+1) + for i := length - 1; i >= 0; i-- { + ii := a.Index(i) + counter := 0. + for j := 0; j < length; j++ { + if a.Index(j) <= ii { + counter += 1. + } + } + rank[i] = counter + mapper[int(counter)] += 1. + } + output := NewQueue(length) + for i := length - 1; i >= 0; i-- { + output.Update(rank[i] - (mapper[int(rank[i])]-1.)/2) + } + return output +} + +func Pearson(a, b Series, length int) float64 { + if a.Length() < length { + length = a.Length() + } + if b.Length() < length { + length = b.Length() + } + x := make([]float64, length) + y := make([]float64, length) + for i := 0; i < length; i++ { + x[i] = a.Index(i) + y[i] = b.Index(i) + } + return stat.Correlation(x, y, nil) +} + +func Spearman(a, b Series, length int) float64 { + if a.Length() < length { + length = a.Length() + } + if b.Length() < length { + length = b.Length() + } + aRank := Rank(a, length) + bRank := Rank(b, length) + return Pearson(aRank, bRank, length) +} + +// similar to pandas.Series.corr() function. +// +// method could either be `types.Pearson`, `types.Spearman` or `types.Kendall` +func Correlation(a Series, b Series, length int, method ...CorrFunc) float64 { + var runner CorrFunc + if len(method) == 0 { + runner = Pearson + } else { + runner = method[0] + } + return runner(a, b, length) +} + +// similar to pandas.Series.autocorr() function. +// +// The method computes the Pearson correlation between Series and shifted itself +func AutoCorrelation(a Series, length int, lags ...int) float64 { + lag := 1 + if len(lags) > 0 { + lag = lags[0] + } + return Pearson(a, Shift(a, lag), length) +} + +// similar to pandas.Series.cov() function with ddof=0 +// +// Compute covariance with Series +func Covariance(a Series, b Series, length int) float64 { + if a.Length() < length { + length = a.Length() + } + if b.Length() < length { + length = b.Length() + } + + meana := Mean(a, length) + meanb := Mean(b, length) + sum := 0.0 + for i := 0; i < length; i++ { + sum += (a.Index(i) - meana) * (b.Index(i) - meanb) + } + sum /= float64(length) + return sum +} + +func Variance(a Series, length int) float64 { + return Covariance(a, a, length) +} + +// similar to pandas.Series.skew() function. +// +// Return unbiased skew over input series +func Skew(a Series, length int) float64 { + if length > a.Length() { + length = a.Length() + } + mean := Mean(a, length) + sum2 := 0.0 + sum3 := 0.0 + for i := 0; i < length; i++ { + diff := a.Index(i) - mean + sum2 += diff * diff + sum3 += diff * diff * diff + } + if length <= 2 || sum2 == 0 { + return math.NaN() + } + l := float64(length) + return l * math.Sqrt(l-1) / (l - 2) * sum3 / math.Pow(sum2, 1.5) +} + +type ShiftResult struct { + a Series + offset int +} + +func (inc *ShiftResult) Last() float64 { + if inc.offset < 0 { + return 0 + } + if inc.offset > inc.a.Length() { + return 0 + } + return inc.a.Index(inc.offset) +} +func (inc *ShiftResult) Index(i int) float64 { + if inc.offset+i < 0 { + return 0 + } + if inc.offset+i > inc.a.Length() { + return 0 + } + return inc.a.Index(inc.offset + i) +} + +func (inc *ShiftResult) Length() int { + return inc.a.Length() - inc.offset +} + +func Shift(a Series, offset int) SeriesExtend { + return NewSeries(&ShiftResult{a, offset}) +} + +type RollingResult struct { + a Series + window int +} + +type SliceView struct { + a Series + start int + length int +} + +func (s *SliceView) Last() float64 { + return s.a.Index(s.start) +} +func (s *SliceView) Index(i int) float64 { + if i >= s.length { + return 0 + } + return s.a.Index(i + s.start) +} + +func (s *SliceView) Length() int { + return s.length +} + +var _ Series = &SliceView{} + +func (r *RollingResult) Last() SeriesExtend { + return NewSeries(&SliceView{r.a, 0, r.window}) +} + +func (r *RollingResult) Index(i int) SeriesExtend { + if i*r.window > r.a.Length() { + return nil + } + return NewSeries(&SliceView{r.a, i * r.window, r.window}) +} + +func (r *RollingResult) Length() int { + mod := r.a.Length() % r.window + if mod > 0 { + return r.a.Length()/r.window + 1 + } else { + return r.a.Length() / r.window + } +} + +func Rolling(a Series, window int) *RollingResult { + return &RollingResult{a, window} +} + +type SigmoidResult struct { + a Series +} + +func (s *SigmoidResult) Last() float64 { + return 1. / (1. + math.Exp(-s.a.Last())) +} + +func (s *SigmoidResult) Index(i int) float64 { + return 1. / (1. + math.Exp(-s.a.Index(i))) +} + +func (s *SigmoidResult) Length() int { + return s.a.Length() +} + +// Sigmoid returns the input values in range of -1 to 1 +// along the sigmoid or s-shaped curve. +// Commonly used in machine learning while training neural networks +// as an activation function. +func Sigmoid(a Series) SeriesExtend { + return NewSeries(&SigmoidResult{a}) +} + +// SoftMax returns the input value in the range of 0 to 1 +// with sum of all the probabilities being equal to one. +// It is commonly used in machine learning neural networks. +// Will return Softmax SeriesExtend result based in latest [window] numbers from [a] Series +func Softmax(a Series, window int) SeriesExtend { + s := 0.0 + max := Highest(a, window) + for i := 0; i < window; i++ { + s += math.Exp(a.Index(i) - max) + } + out := NewQueue(window) + for i := window - 1; i >= 0; i-- { + out.Update(math.Exp(a.Index(i)-max) / s) + } + return out +} + +// Entropy computes the Shannon entropy of a distribution or the distance between +// two distributions. The natural logarithm is used. +// - sum(v * ln(v)) +func Entropy(a Series, window int) (e float64) { + for i := 0; i < window; i++ { + v := a.Index(i) + if v != 0 { + e -= v * math.Log(v) + } + } + return e +} + +// CrossEntropy computes the cross-entropy between the two distributions +func CrossEntropy(a, b Series, window int) (e float64) { + for i := 0; i < window; i++ { + v := a.Index(i) + if v != 0 { + e -= v * math.Log(b.Index(i)) + } + } + return e +} + +func sigmoid(z float64) float64 { + return 1. / (1. + math.Exp(-z)) +} + +func propagate(w []float64, gradient float64, x [][]float64, y []float64) (float64, []float64, float64) { + logloss_epoch := 0.0 + var activations []float64 + var dw []float64 + m := len(y) + db := 0.0 + for i, xx := range x { + result := 0.0 + for j, ww := range w { + result += ww * xx[j] + } + a := sigmoid(result + gradient) + activations = append(activations, a) + logloss := a*math.Log1p(y[i]) + (1.-a)*math.Log1p(1-y[i]) + logloss_epoch += logloss + + db += a - y[i] + } + for j := range w { + err := 0.0 + for i, xx := range x { + err_i := activations[i] - y[i] + err += err_i * xx[j] + } + err /= float64(m) + dw = append(dw, err) + } + + cost := -(logloss_epoch / float64(len(x))) + db /= float64(m) + return cost, dw, db +} + +func LogisticRegression(x []Series, y Series, lookback, iterations int, learningRate float64) *LogisticRegressionModel { + features := len(x) + if features == 0 { + panic("no feature to train") + } + w := make([]float64, features) + if lookback > x[0].Length() { + lookback = x[0].Length() + } + xx := make([][]float64, lookback) + for i := 0; i < lookback; i++ { + for j := 0; j < features; j++ { + xx[i] = append(xx[i], x[j].Index(lookback-i-1)) + } + } + yy := Reverse(y, lookback) + + b := 0. + for i := 0; i < iterations; i++ { + _, dw, db := propagate(w, b, xx, yy) + for j := range w { + w[j] = w[j] - (learningRate * dw[j]) + } + b -= learningRate * db + } + return &LogisticRegressionModel{ + Weight: w, + Gradient: b, + LearningRate: learningRate, + } +} + +type LogisticRegressionModel struct { + Weight []float64 + Gradient float64 + LearningRate float64 +} + +/* +// Might not be correct. +// Please double check before uncomment this +func (l *LogisticRegressionModel) Update(x []float64, y float64) { + z := 0.0 + for i, w := l.Weight { + z += w * x[i] + } + a := sigmoid(z + l.Gradient) + //logloss := a * math.Log1p(y) + (1.-a)*math.Log1p(1-y) + db = a - y + var dw []float64 + for j, ww := range l.Weight { + err := db * x[j] + dw = append(dw, err) + } + for i := range l.Weight { + l.Weight[i] -= l.LearningRate * dw[i] + } + l.Gradient -= l.LearningRate * db +} +*/ + +func (l *LogisticRegressionModel) Predict(x []float64) float64 { + z := 0.0 + for i, w := range l.Weight { + z += w * x[i] + } + return sigmoid(z + l.Gradient) +} + +type Canvas struct { + chart.Chart + Interval Interval +} + +func NewCanvas(title string, intervals ...Interval) *Canvas { + valueFormatter := chart.TimeValueFormatter + interval := Interval1m + if len(intervals) > 0 { + interval = intervals[0] + if interval.Seconds() > 24*60*60 { + valueFormatter = chart.TimeDateValueFormatter + } else if interval.Seconds() > 60*60 { + valueFormatter = chart.TimeHourValueFormatter + } else { + valueFormatter = chart.TimeMinuteValueFormatter + } + } else { + valueFormatter = chart.IntValueFormatter + } + out := &Canvas{ + Chart: chart.Chart{ + Title: title, + XAxis: chart.XAxis{ + ValueFormatter: valueFormatter, + }, + }, + Interval: interval, + } + out.Chart.Elements = []chart.Renderable{ + chart.LegendLeft(&out.Chart), + } + return out +} + +func expand(a []float64, length int, defaultVal float64) []float64 { + l := len(a) + if l >= length { + return a + } + for i := 0; i < length-l; i++ { + a = append([]float64{defaultVal}, a...) + } + return a +} + +func (canvas *Canvas) Plot(tag string, a Series, endTime Time, length int, intervals ...Interval) { + var timeline []time.Time + e := endTime.Time() + if a.Length() == 0 { + return + } + oldest := a.Index(a.Length() - 1) + interval := canvas.Interval + if len(intervals) > 0 { + interval = intervals[0] + } + for i := length - 1; i >= 0; i-- { + shiftedT := e.Add(-time.Duration(i*interval.Seconds()) * time.Second) + timeline = append(timeline, shiftedT) + } + canvas.Series = append(canvas.Series, chart.TimeSeries{ + Name: tag, + YValues: expand(Reverse(a, length), length, oldest), + XValues: timeline, + }) +} + +func (canvas *Canvas) PlotRaw(tag string, a Series, length int) { + var x []float64 + for i := 0; i < length; i++ { + x = append(x, float64(i)) + } + if a.Length() == 0 { + return + } + oldest := a.Index(a.Length() - 1) + canvas.Series = append(canvas.Series, chart.ContinuousSeries{ + Name: tag, + XValues: x, + YValues: expand(Reverse(a, length), length, oldest), + }) } // TODO: ta.linreg diff --git a/pkg/types/indicator_test.go b/pkg/types/indicator_test.go index 8919c78661..7da2c17c5b 100644 --- a/pkg/types/indicator_test.go +++ b/pkg/types/indicator_test.go @@ -1,8 +1,15 @@ package types import ( - "github.com/stretchr/testify/assert" + //"os" "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/wcharczuk/go-chart/v2" + "gonum.org/v1/gonum/stat" + + "github.com/c9s/bbgo/pkg/datatype/floats" ) func TestFloat(t *testing.T) { @@ -14,7 +21,7 @@ func TestFloat(t *testing.T) { func TestNextCross(t *testing.T) { var a Series = NumberSeries(1.2) - var b Series = &Float64Slice{100., 80., 60.} + var b Series = &floats.Slice{100., 80., 60.} // index 2 1 0 // predicted 40 20 0 // offset 1 2 3 @@ -26,10 +33,150 @@ func TestNextCross(t *testing.T) { } func TestFloat64Slice(t *testing.T) { - var a = Float64Slice{1.0, 2.0, 3.0} - var b = Float64Slice{1.0, 2.0, 3.0} + var a = floats.Slice{1.0, 2.0, 3.0} + var b = floats.Slice{1.0, 2.0, 3.0} var c Series = Minus(&a, &b) a = append(a, 4.0) b = append(b, 3.0) assert.Equal(t, c.Last(), 1.) } + +/* +python + +import pandas as pd +s1 = pd.Series([.2, 0., .6, .2, .2]) +s2 = pd.Series([.3, .6, .0, .1]) +print(s1.corr(s2, method='pearson')) +print(s1.corr(s2, method='spearman') +print(s1.corr(s2, method='kendall')) +print(s1.rank()) +*/ +func TestCorr(t *testing.T) { + var a = floats.Slice{.2, .0, .6, .2} + var b = floats.Slice{.3, .6, .0, .1} + corr := Correlation(&a, &b, 4, Pearson) + assert.InDelta(t, corr, -0.8510644, 0.001) + out := Rank(&a, 4) + assert.Equal(t, out.Index(0), 2.5) + assert.Equal(t, out.Index(1), 4.0) + corr = Correlation(&a, &b, 4, Spearman) + assert.InDelta(t, corr, -0.94868, 0.001) +} + +/* +python + +import pandas as pd +s1 = pd.Series([.2, 0., .6, .2, .2]) +s2 = pd.Series([.3, .6, .0, .1]) +print(s1.cov(s2, ddof=0)) +*/ +func TestCov(t *testing.T) { + var a = floats.Slice{.2, .0, .6, .2} + var b = floats.Slice{.3, .6, .0, .1} + cov := Covariance(&a, &b, 4) + assert.InDelta(t, cov, -0.042499, 0.001) +} + +/* +python + +import pandas as pd +s1 = pd.Series([.2, 0., .6, .2, .2]) +print(s1.skew()) +*/ +func TestSkew(t *testing.T) { + var a = floats.Slice{.2, .0, .6, .2} + sk := Skew(&a, 4) + assert.InDelta(t, sk, 1.129338, 0.001) +} + +func TestEntropy(t *testing.T) { + var a = floats.Slice{.2, .0, .6, .2} + e := stat.Entropy(a) + assert.InDelta(t, e, Entropy(&a, a.Length()), 0.0001) +} + +func TestCrossEntropy(t *testing.T) { + var a = floats.Slice{.2, .0, .6, .2} + var b = floats.Slice{.3, .6, .0, .1} + e := stat.CrossEntropy(a, b) + assert.InDelta(t, e, CrossEntropy(&a, &b, a.Length()), 0.0001) +} + +func TestSoftmax(t *testing.T) { + var a = floats.Slice{3.0, 1.0, 0.2} + out := Softmax(&a, a.Length()) + r := floats.Slice{0.8360188027814407, 0.11314284146556013, 0.05083835575299916} + for i := 0; i < out.Length(); i++ { + assert.InDelta(t, r.Index(i), out.Index(i), 0.001) + } +} + +func TestSigmoid(t *testing.T) { + a := floats.Slice{3.0, 1.0, 2.1} + out := Sigmoid(&a) + r := floats.Slice{0.9525741268224334, 0.7310585786300049, 0.8909031788043871} + for i := 0; i < out.Length(); i++ { + assert.InDelta(t, r.Index(i), out.Index(i), 0.001) + } +} + +// from https://en.wikipedia.org/wiki/Logistic_regression +func TestLogisticRegression(t *testing.T) { + a := []floats.Slice{{0.5, 0.75, 1., 1.25, 1.5, 1.75, 1.75, 2.0, 2.25, 2.5, 2.75, 3., 3.25, 3.5, 4., 4.25, 4.5, 4.75, 5., 5.5}} + b := floats.Slice{0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1} + var x []Series + x = append(x, &a[0]) + + model := LogisticRegression(x, &b, a[0].Length(), 90000, 0.0018) + inputs := []float64{1., 2., 2.7, 3., 4., 5.} + results := []bool{false, false, true, true, true, true} + for i, x := range inputs { + input := []float64{x} + pred := model.Predict(input) + assert.Equal(t, pred >= 0.5, results[i]) + } +} + +func TestDot(t *testing.T) { + a := floats.Slice{7, 6, 5, 4, 3, 2, 1, 0} + b := floats.Slice{200., 201., 203., 204., 203., 199.} + out1 := Dot(&a, &b, 3) + assert.InDelta(t, out1, 611., 0.001) + out2 := Dot(&a, 3., 2) + assert.InDelta(t, out2, 3., 0.001) + out3 := Dot(3., &a, 2) + assert.InDelta(t, out2, out3, 0.001) +} + +func TestClone(t *testing.T) { + a := NewQueue(3) + a.Update(3.) + b := Clone(a) + b.Update(4.) + assert.Equal(t, a.Last(), 3.) + assert.Equal(t, b.Last(), 4.) +} + +func TestPlot(t *testing.T) { + ct := NewCanvas("test", Interval5m) + a := floats.Slice{200., 205., 230., 236} + ct.Plot("test", &a, Time(time.Now()), 4) + assert.Equal(t, ct.Interval, Interval5m) + assert.Equal(t, ct.Series[0].(chart.TimeSeries).Len(), 4) + //f, _ := os.Create("output.png") + //defer f.Close() + //ct.Render(chart.PNG, f) +} + +func TestFilter(t *testing.T) { + a := floats.Slice{200., -200, 0, 1000, -100} + b := Filter(&a, func(i int, val float64) bool { + return val > 0 + }, 4) + assert.Equal(t, b.Length(), 4) + assert.Equal(t, b.Last(), 1000.) + assert.Equal(t, b.Sum(3), 1200.) +} diff --git a/pkg/types/instance.go b/pkg/types/instance.go new file mode 100644 index 0000000000..ac4f26e29b --- /dev/null +++ b/pkg/types/instance.go @@ -0,0 +1,5 @@ +package types + +type InstanceIDProvider interface { + InstanceID() string +} diff --git a/pkg/types/interval.go b/pkg/types/interval.go index a06e083e57..405f64939d 100644 --- a/pkg/types/interval.go +++ b/pkg/types/interval.go @@ -3,17 +3,30 @@ package types import ( "encoding/json" "fmt" + "strings" "time" ) type Interval string func (i Interval) Minutes() int { - return SupportedIntervals[i] + m, ok := SupportedIntervals[i] + if !ok { + return ParseInterval(i) / 60 + } + return m / 60 +} + +func (i Interval) Seconds() int { + m, ok := SupportedIntervals[i] + if !ok { + return ParseInterval(i) + } + return m } func (i Interval) Duration() time.Duration { - return time.Duration(i.Minutes()) * time.Minute + return time.Duration(i.Seconds()) * time.Second } func (i *Interval) UnmarshalJSON(b []byte) (err error) { @@ -40,7 +53,9 @@ func (s IntervalSlice) StringSlice() (slice []string) { return slice } +var Interval1s = Interval("1s") var Interval1m = Interval("1m") +var Interval3m = Interval("3m") var Interval5m = Interval("5m") var Interval15m = Interval("15m") var Interval30m = Interval("30m") @@ -51,19 +66,57 @@ var Interval6h = Interval("6h") var Interval12h = Interval("12h") var Interval1d = Interval("1d") var Interval3d = Interval("3d") +var Interval1w = Interval("1w") +var Interval2w = Interval("2w") +var Interval1mo = Interval("1mo") + +func ParseInterval(input Interval) int { + t := 0 + index := 0 + for i, rn := range string(input) { + if rn >= '0' && rn <= '9' { + t = t*10 + int(rn-'0') + } else { + index = i + break + } + } + switch strings.ToLower(string(input[index:])) { + case "s": + return t + case "m": + t *= 60 + case "h": + t *= 60 * 60 + case "d": + t *= 60 * 60 * 24 + case "w": + t *= 60 * 60 * 24 * 7 + case "mo": + t *= 60 * 60 * 24 * 30 + default: + panic("unknown interval input: " + input) + } + return t +} var SupportedIntervals = map[Interval]int{ - Interval1m: 1, - Interval5m: 5, - Interval15m: 15, - Interval30m: 30, - Interval1h: 60, - Interval2h: 60 * 2, - Interval4h: 60 * 4, - Interval6h: 60 * 6, - Interval12h: 60 * 12, - Interval1d: 60 * 24, - Interval3d: 60 * 24 * 3, + Interval1s: 1, + Interval1m: 1 * 60, + Interval3m: 3 * 60, + Interval5m: 5 * 60, + Interval15m: 15 * 60, + Interval30m: 30 * 60, + Interval1h: 60 * 60, + Interval2h: 60 * 60 * 2, + Interval4h: 60 * 60 * 4, + Interval6h: 60 * 60 * 6, + Interval12h: 60 * 60 * 12, + Interval1d: 60 * 60 * 24, + Interval3d: 60 * 60 * 24 * 3, + Interval1w: 60 * 60 * 24 * 7, + Interval2w: 60 * 60 * 24 * 14, + Interval1mo: 60 * 60 * 24 * 30, } // IntervalWindow is used by the indicators @@ -71,8 +124,11 @@ type IntervalWindow struct { // The interval of kline Interval Interval `json:"interval"` - // The windows size of the indicator (EWMA and SMA) + // The windows size of the indicator (for example, EWMA and SMA) Window int `json:"window"` + + // RightWindow is used by the pivot indicator + RightWindow int `json:"rightWindow"` } type IntervalWindowBandWidth struct { diff --git a/pkg/types/interval_test.go b/pkg/types/interval_test.go new file mode 100644 index 0000000000..e87ab1575d --- /dev/null +++ b/pkg/types/interval_test.go @@ -0,0 +1,15 @@ +package types + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseInterval(t *testing.T) { + assert.Equal(t, ParseInterval("1s"), 1) + assert.Equal(t, ParseInterval("3m"), 3*60) + assert.Equal(t, ParseInterval("15h"), 15*60*60) + assert.Equal(t, ParseInterval("72d"), 72*24*60*60) + assert.Equal(t, ParseInterval("3Mo"), 3*30*24*60*60) +} diff --git a/pkg/types/json.go b/pkg/types/json.go new file mode 100644 index 0000000000..efc54d3cae --- /dev/null +++ b/pkg/types/json.go @@ -0,0 +1,14 @@ +package types + +type JsonStruct struct { + Key string + Json string + Type string + Value interface{} +} + +type JsonArr []JsonStruct + +func (a JsonArr) Len() int { return len(a) } +func (a JsonArr) Less(i, j int) bool { return a[i].Key < a[j].Key } +func (a JsonArr) Swap(i, j int) { a[i], a[j] = a[j], a[i] } diff --git a/pkg/types/kline.go b/pkg/types/kline.go index 396a53c8db..0ca77696e3 100644 --- a/pkg/types/kline.go +++ b/pkg/types/kline.go @@ -7,7 +7,7 @@ import ( "github.com/slack-go/slack" "github.com/c9s/bbgo/pkg/fixedpoint" - "github.com/c9s/bbgo/pkg/util" + "github.com/c9s/bbgo/pkg/style" ) type Direction int @@ -71,37 +71,71 @@ type KLine struct { Closed bool `json:"closed" db:"closed"` } -func (k KLine) GetStartTime() Time { +func (k *KLine) Set(o *KLine) { + k.GID = o.GID + k.Exchange = o.Exchange + k.Symbol = o.Symbol + k.StartTime = o.StartTime + k.EndTime = o.EndTime + k.Interval = o.Interval + k.Open = o.Open + k.Close = o.Close + k.High = o.High + k.Low = o.Low + k.Volume = o.Volume + k.QuoteVolume = o.QuoteVolume + k.TakerBuyBaseAssetVolume = o.TakerBuyBaseAssetVolume + k.TakerBuyQuoteAssetVolume = o.TakerBuyQuoteAssetVolume + k.LastTradeID = o.LastTradeID + k.NumberOfTrades = o.NumberOfTrades + k.Closed = o.Closed +} + +func (k *KLine) Merge(o *KLine) { + k.EndTime = o.EndTime + k.Close = o.Close + k.High = fixedpoint.Max(k.High, o.High) + k.Low = fixedpoint.Min(k.Low, o.Low) + k.Volume = k.Volume.Add(o.Volume) + k.QuoteVolume = k.QuoteVolume.Add(o.QuoteVolume) + k.TakerBuyBaseAssetVolume = k.TakerBuyBaseAssetVolume.Add(o.TakerBuyBaseAssetVolume) + k.TakerBuyQuoteAssetVolume = k.TakerBuyQuoteAssetVolume.Add(o.TakerBuyQuoteAssetVolume) + k.LastTradeID = o.LastTradeID + k.NumberOfTrades += o.NumberOfTrades + k.Closed = o.Closed +} + +func (k *KLine) GetStartTime() Time { return k.StartTime } -func (k KLine) GetEndTime() Time { +func (k *KLine) GetEndTime() Time { return k.EndTime } -func (k KLine) GetInterval() Interval { +func (k *KLine) GetInterval() Interval { return k.Interval } -func (k KLine) Mid() fixedpoint.Value { +func (k *KLine) Mid() fixedpoint.Value { return k.High.Add(k.Low).Div(Two) } // green candle with open and close near high price -func (k KLine) BounceUp() bool { +func (k *KLine) BounceUp() bool { mid := k.Mid() trend := k.Direction() return trend > 0 && k.Open.Compare(mid) > 0 && k.Close.Compare(mid) > 0 } // red candle with open and close near low price -func (k KLine) BounceDown() bool { +func (k *KLine) BounceDown() bool { mid := k.Mid() trend := k.Direction() return trend > 0 && k.Open.Compare(mid) < 0 && k.Close.Compare(mid) < 0 } -func (k KLine) Direction() Direction { +func (k *KLine) Direction() Direction { o := k.GetOpen() c := k.GetClose() @@ -113,32 +147,32 @@ func (k KLine) Direction() Direction { return DirectionNone } -func (k KLine) GetHigh() fixedpoint.Value { +func (k *KLine) GetHigh() fixedpoint.Value { return k.High } -func (k KLine) GetLow() fixedpoint.Value { +func (k *KLine) GetLow() fixedpoint.Value { return k.Low } -func (k KLine) GetOpen() fixedpoint.Value { +func (k *KLine) GetOpen() fixedpoint.Value { return k.Open } -func (k KLine) GetClose() fixedpoint.Value { +func (k *KLine) GetClose() fixedpoint.Value { return k.Close } -func (k KLine) GetMaxChange() fixedpoint.Value { +func (k *KLine) GetMaxChange() fixedpoint.Value { return k.GetHigh().Sub(k.GetLow()) } -func (k KLine) GetAmplification() fixedpoint.Value { +func (k *KLine) GetAmplification() fixedpoint.Value { return k.GetMaxChange().Div(k.GetLow()) } // GetThickness returns the thickness of the kline. 1 => thick, 0.1 => thin -func (k KLine) GetThickness() fixedpoint.Value { +func (k *KLine) GetThickness() fixedpoint.Value { out := k.GetChange().Div(k.GetMaxChange()) if out.Sign() < 0 { return out.Neg() @@ -146,7 +180,7 @@ func (k KLine) GetThickness() fixedpoint.Value { return out } -func (k KLine) GetUpperShadowRatio() fixedpoint.Value { +func (k *KLine) GetUpperShadowRatio() fixedpoint.Value { out := k.GetUpperShadowHeight().Div(k.GetMaxChange()) if out.Sign() < 0 { return out.Neg() @@ -154,7 +188,7 @@ func (k KLine) GetUpperShadowRatio() fixedpoint.Value { return out } -func (k KLine) GetUpperShadowHeight() fixedpoint.Value { +func (k *KLine) GetUpperShadowHeight() fixedpoint.Value { high := k.GetHigh() open := k.GetOpen() clos := k.GetClose() @@ -164,7 +198,7 @@ func (k KLine) GetUpperShadowHeight() fixedpoint.Value { return high.Sub(clos) } -func (k KLine) GetLowerShadowRatio() fixedpoint.Value { +func (k *KLine) GetLowerShadowRatio() fixedpoint.Value { out := k.GetLowerShadowHeight().Div(k.GetMaxChange()) if out.Sign() < 0 { return out.Neg() @@ -172,7 +206,7 @@ func (k KLine) GetLowerShadowRatio() fixedpoint.Value { return out } -func (k KLine) GetLowerShadowHeight() fixedpoint.Value { +func (k *KLine) GetLowerShadowHeight() fixedpoint.Value { low := k.Low if k.Open.Compare(k.Close) < 0 { // uptrend return k.Open.Sub(low) @@ -183,63 +217,63 @@ func (k KLine) GetLowerShadowHeight() fixedpoint.Value { } // GetBody returns the height of the candle real body -func (k KLine) GetBody() fixedpoint.Value { +func (k *KLine) GetBody() fixedpoint.Value { return k.GetChange() } // GetChange returns Close price - Open price. -func (k KLine) GetChange() fixedpoint.Value { +func (k *KLine) GetChange() fixedpoint.Value { return k.Close.Sub(k.Open) } -func (k KLine) Color() string { +func (k *KLine) Color() string { if k.Direction() > 0 { - return GreenColor + return style.GreenColor } else if k.Direction() < 0 { - return RedColor + return style.RedColor } - return GrayColor + return style.GrayColor } -func (k KLine) String() string { +func (k *KLine) String() string { return fmt.Sprintf("%s %s %s %s O: %.4f H: %.4f L: %.4f C: %.4f CHG: %.4f MAXCHG: %.4f V: %.4f QV: %.2f TBBV: %.2f", k.Exchange.String(), k.StartTime.Time().Format("2006-01-02 15:04"), k.Symbol, k.Interval, k.Open.Float64(), k.High.Float64(), k.Low.Float64(), k.Close.Float64(), k.GetChange().Float64(), k.GetMaxChange().Float64(), k.Volume.Float64(), k.QuoteVolume.Float64(), k.TakerBuyBaseAssetVolume.Float64()) } -func (k KLine) PlainText() string { +func (k *KLine) PlainText() string { return k.String() } -func (k KLine) SlackAttachment() slack.Attachment { +func (k *KLine) SlackAttachment() slack.Attachment { return slack.Attachment{ Text: fmt.Sprintf("*%s* KLine %s", k.Symbol, k.Interval), Color: k.Color(), Fields: []slack.AttachmentField{ - {Title: "Open", Value: util.FormatValue(k.Open, 2), Short: true}, - {Title: "High", Value: util.FormatValue(k.High, 2), Short: true}, - {Title: "Low", Value: util.FormatValue(k.Low, 2), Short: true}, - {Title: "Close", Value: util.FormatValue(k.Close, 2), Short: true}, - {Title: "Mid", Value: util.FormatValue(k.Mid(), 2), Short: true}, - {Title: "Change", Value: util.FormatValue(k.GetChange(), 2), Short: true}, - {Title: "Volume", Value: util.FormatValue(k.Volume, 2), Short: true}, - {Title: "Taker Buy Base Volume", Value: util.FormatValue(k.TakerBuyBaseAssetVolume, 2), Short: true}, - {Title: "Taker Buy Quote Volume", Value: util.FormatValue(k.TakerBuyQuoteAssetVolume, 2), Short: true}, - {Title: "Max Change", Value: util.FormatValue(k.GetMaxChange(), 2), Short: true}, + {Title: "Open", Value: k.Open.FormatString(2), Short: true}, + {Title: "High", Value: k.High.FormatString(2), Short: true}, + {Title: "Low", Value: k.Low.FormatString(2), Short: true}, + {Title: "Close", Value: k.Close.FormatString(2), Short: true}, + {Title: "Mid", Value: k.Mid().FormatString(2), Short: true}, + {Title: "Change", Value: k.GetChange().FormatString(2), Short: true}, + {Title: "Volume", Value: k.Volume.FormatString(2), Short: true}, + {Title: "Taker Buy Base Volume", Value: k.TakerBuyBaseAssetVolume.FormatString(2), Short: true}, + {Title: "Taker Buy Quote Volume", Value: k.TakerBuyQuoteAssetVolume.FormatString(2), Short: true}, + {Title: "Max Change", Value: k.GetMaxChange().FormatString(2), Short: true}, { Title: "Thickness", - Value: util.FormatValue(k.GetThickness(), 4), + Value: k.GetThickness().FormatString(4), Short: true, }, { Title: "UpperShadowRatio", - Value: util.FormatValue(k.GetUpperShadowRatio(), 4), + Value: k.GetUpperShadowRatio().FormatString(4), Short: true, }, { Title: "LowerShadowRatio", - Value: util.FormatValue(k.GetLowerShadowRatio(), 4), + Value: k.GetLowerShadowRatio().FormatString(4), Short: true, }, }, @@ -278,7 +312,8 @@ func (k KLineWindow) GetInterval() Interval { } func (k KLineWindow) GetOpen() fixedpoint.Value { - return k.First().GetOpen() + first := k.First() + return first.GetOpen() } func (k KLineWindow) GetClose() fixedpoint.Value { @@ -287,7 +322,8 @@ func (k KLineWindow) GetClose() fixedpoint.Value { } func (k KLineWindow) GetHigh() fixedpoint.Value { - high := k.First().GetHigh() + first := k.First() + high := first.GetHigh() for _, line := range k { high = fixedpoint.Max(high, line.GetHigh()) } @@ -296,7 +332,8 @@ func (k KLineWindow) GetHigh() fixedpoint.Value { } func (k KLineWindow) GetLow() fixedpoint.Value { - low := k.First().GetLow() + first := k.First() + low := first.GetLow() for _, line := range k { low = fixedpoint.Min(low, line.GetLow()) } @@ -348,11 +385,11 @@ func (k KLineWindow) GetTrend() int { func (k KLineWindow) Color() string { if k.GetTrend() > 0 { - return GreenColor + return style.GreenColor } else if k.GetTrend() < 0 { - return RedColor + return style.RedColor } - return GrayColor + return style.GrayColor } // Mid price @@ -471,34 +508,34 @@ func (k KLineWindow) SlackAttachment() slack.Attachment { Text: fmt.Sprintf("*%s* KLineWindow %s x %d", first.Symbol, first.Interval, windowSize), Color: k.Color(), Fields: []slack.AttachmentField{ - {Title: "Open", Value: util.FormatValue(k.GetOpen(), 2), Short: true}, - {Title: "High", Value: util.FormatValue(k.GetHigh(), 2), Short: true}, - {Title: "Low", Value: util.FormatValue(k.GetLow(), 2), Short: true}, - {Title: "Close", Value: util.FormatValue(k.GetClose(), 2), Short: true}, - {Title: "Mid", Value: util.FormatValue(k.Mid(), 2), Short: true}, + {Title: "Open", Value: k.GetOpen().FormatString(2), Short: true}, + {Title: "High", Value: k.GetHigh().FormatString(2), Short: true}, + {Title: "Low", Value: k.GetLow().FormatString(2), Short: true}, + {Title: "Close", Value: k.GetClose().FormatString(2), Short: true}, + {Title: "Mid", Value: k.Mid().FormatPercentage(2), Short: true}, { Title: "Change", - Value: util.FormatValue(k.GetChange(), 2), + Value: k.GetChange().FormatString(2), Short: true, }, { Title: "Max Change", - Value: util.FormatValue(k.GetMaxChange(), 2), + Value: k.GetMaxChange().FormatString(2), Short: true, }, { Title: "Thickness", - Value: util.FormatValue(k.GetThickness(), 4), + Value: k.GetThickness().FormatString(4), Short: true, }, { Title: "UpperShadowRatio", - Value: util.FormatValue(k.GetUpperShadowRatio(), 4), + Value: k.GetUpperShadowRatio().FormatString(4), Short: true, }, { Title: "LowerShadowRatio", - Value: util.FormatValue(k.GetLowerShadowRatio(), 4), + Value: k.GetLowerShadowRatio().FormatString(4), Short: true, }, }, @@ -563,6 +600,8 @@ type KLineSeries struct { func (k *KLineSeries) Last() float64 { length := len(*k.lines) switch k.kv { + case kOpUnknown: + panic("kline series operator unknown") case kOpenValue: return (*k.lines)[length-1].GetOpen().Float64() case kCloseValue: @@ -602,3 +641,14 @@ func (k *KLineSeries) Length() int { } var _ Series = &KLineSeries{} + +type KLineCallBack func(k KLine) + +func KLineWith(symbol string, interval Interval, callback KLineCallBack) KLineCallBack { + return func(k KLine) { + if k.Symbol != symbol || (k.Interval != "" && k.Interval != interval) { + return + } + callback(k) + } +} diff --git a/pkg/types/margin.go b/pkg/types/margin.go index 10e24e9e98..fbec9e2a0f 100644 --- a/pkg/types/margin.go +++ b/pkg/types/margin.go @@ -2,6 +2,7 @@ package types import ( "context" + "time" "github.com/c9s/bbgo/pkg/fixedpoint" ) @@ -51,12 +52,67 @@ type MarginExchange interface { GetMarginSettings() MarginSettings } -type MarginBorrowRepay interface { +// MarginBorrowRepayService provides repay and borrow actions of an crypto exchange +type MarginBorrowRepayService interface { RepayMarginAsset(ctx context.Context, asset string, amount fixedpoint.Value) error BorrowMarginAsset(ctx context.Context, asset string, amount fixedpoint.Value) error QueryMarginAssetMaxBorrowable(ctx context.Context, asset string) (amount fixedpoint.Value, err error) } +type MarginInterest struct { + GID uint64 `json:"gid" db:"gid"` + Exchange ExchangeName `json:"exchange" db:"exchange"` + Asset string `json:"asset" db:"asset"` + Principle fixedpoint.Value `json:"principle" db:"principle"` + Interest fixedpoint.Value `json:"interest" db:"interest"` + InterestRate fixedpoint.Value `json:"interestRate" db:"interest_rate"` + IsolatedSymbol string `json:"isolatedSymbol" db:"isolated_symbol"` + Time Time `json:"time" db:"time"` +} + +type MarginLoan struct { + GID uint64 `json:"gid" db:"gid"` + Exchange ExchangeName `json:"exchange" db:"exchange"` + TransactionID uint64 `json:"transactionID" db:"transaction_id"` + Asset string `json:"asset" db:"asset"` + Principle fixedpoint.Value `json:"principle" db:"principle"` + Time Time `json:"time" db:"time"` + IsolatedSymbol string `json:"isolatedSymbol" db:"isolated_symbol"` +} + +type MarginRepay struct { + GID uint64 `json:"gid" db:"gid"` + Exchange ExchangeName `json:"exchange" db:"exchange"` + TransactionID uint64 `json:"transactionID" db:"transaction_id"` + Asset string `json:"asset" db:"asset"` + Principle fixedpoint.Value `json:"principle" db:"principle"` + Time Time `json:"time" db:"time"` + IsolatedSymbol string `json:"isolatedSymbol" db:"isolated_symbol"` +} + +type MarginLiquidation struct { + GID uint64 `json:"gid" db:"gid"` + Exchange ExchangeName `json:"exchange" db:"exchange"` + AveragePrice fixedpoint.Value `json:"averagePrice" db:"average_price"` + ExecutedQuantity fixedpoint.Value `json:"executedQuantity" db:"executed_quantity"` + OrderID uint64 `json:"orderID" db:"order_id"` + Price fixedpoint.Value `json:"price" db:"price"` + Quantity fixedpoint.Value `json:"quantity" db:"quantity"` + Side SideType `json:"side" db:"side"` + Symbol string `json:"symbol" db:"symbol"` + TimeInForce TimeInForce `json:"timeInForce" db:"time_in_force"` + IsIsolated bool `json:"isIsolated" db:"is_isolated"` + UpdatedTime Time `json:"updatedTime" db:"time"` +} + +// MarginHistory provides the service of querying loan history and repay history +type MarginHistory interface { + QueryLoanHistory(ctx context.Context, asset string, startTime, endTime *time.Time) ([]MarginLoan, error) + QueryRepayHistory(ctx context.Context, asset string, startTime, endTime *time.Time) ([]MarginRepay, error) + QueryLiquidationHistory(ctx context.Context, startTime, endTime *time.Time) ([]MarginLiquidation, error) + QueryInterestHistory(ctx context.Context, asset string, startTime, endTime *time.Time) ([]MarginInterest, error) +} + type MarginSettings struct { IsMargin bool IsIsolatedMargin bool diff --git a/pkg/types/market.go b/pkg/types/market.go index b50bbef768..1092b441ef 100644 --- a/pkg/types/market.go +++ b/pkg/types/market.go @@ -94,10 +94,17 @@ type Market struct { TickSize fixedpoint.Value `json:"tickSize,omitempty"` } +func (m Market) IsDustQuantity(quantity, price fixedpoint.Value) bool { + return quantity.Compare(m.MinQuantity) <= 0 || quantity.Mul(price).Compare(m.MinNotional) <= 0 +} + // TruncateQuantity uses the step size to truncate floating number, in order to avoid the rounding issue func (m Market) TruncateQuantity(quantity fixedpoint.Value) fixedpoint.Value { - stepRound := math.Pow10(-int(math.Log10(m.StepSize.Float64()))) - return fixedpoint.NewFromFloat(math.Trunc(quantity.Float64()*stepRound) / stepRound) + return fixedpoint.MustNewFromString(m.FormatQuantity(quantity)) +} + +func (m Market) TruncatePrice(price fixedpoint.Value) fixedpoint.Value { + return fixedpoint.MustNewFromString(m.FormatPrice(price)) } func (m Market) BaseCurrencyFormatter() *accounting.Accounting { diff --git a/pkg/types/market_test.go b/pkg/types/market_test.go index 5ecb707724..d0544e9ba3 100644 --- a/pkg/types/market_test.go +++ b/pkg/types/market_test.go @@ -91,7 +91,7 @@ func Test_formatPrice(t *testing.T) { { name: "no fraction", args: args{ - price: fixedpoint.NewFromFloat(10.0), + price: fixedpoint.NewFromFloat(10.0), tickSize: fixedpoint.NewFromFloat(0.001), }, want: "10.000", @@ -99,7 +99,7 @@ func Test_formatPrice(t *testing.T) { { name: "fraction truncate", args: args{ - price: fixedpoint.NewFromFloat(2.334), + price: fixedpoint.NewFromFloat(2.334), tickSize: fixedpoint.NewFromFloat(0.01), }, want: "2.33", @@ -107,7 +107,7 @@ func Test_formatPrice(t *testing.T) { { name: "fraction", args: args{ - price: fixedpoint.NewFromFloat(2.334), + price: fixedpoint.NewFromFloat(2.334), tickSize: fixedpoint.NewFromFloat(0.0001), }, want: "2.3340", @@ -115,7 +115,7 @@ func Test_formatPrice(t *testing.T) { { name: "more fraction", args: args{ - price: fixedpoint.MustNewFromString("2.1234567898888"), + price: fixedpoint.MustNewFromString("2.1234567898888"), tickSize: fixedpoint.NewFromFloat(0.0001), }, want: "2.1234", @@ -125,7 +125,7 @@ func Test_formatPrice(t *testing.T) { binanceFormatRE := regexp.MustCompile("^([0-9]{1,20})(.[0-9]{1,20})?$") for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := formatPrice(tt.args.price, tt.args.tickSize); + got := formatPrice(tt.args.price, tt.args.tickSize) if got != tt.want { t.Errorf("formatPrice() = %v, want %v", got, tt.want) } @@ -135,10 +135,9 @@ func Test_formatPrice(t *testing.T) { } } - func Test_formatQuantity(t *testing.T) { type args struct { - quantity fixedpoint.Value + quantity fixedpoint.Value tickSize fixedpoint.Value } tests := []struct { @@ -183,7 +182,7 @@ func Test_formatQuantity(t *testing.T) { binanceFormatRE := regexp.MustCompile("^([0-9]{1,20})(.[0-9]{1,20})?$") for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := formatQuantity(tt.args.quantity, tt.args.tickSize); + got := formatQuantity(tt.args.quantity, tt.args.tickSize) if got != tt.want { t.Errorf("formatQuantity() = %v, want %v", got, tt.want) } diff --git a/pkg/types/mocks/mock_exchange.go b/pkg/types/mocks/mock_exchange.go new file mode 100644 index 0000000000..d1836c01d9 --- /dev/null +++ b/pkg/types/mocks/mock_exchange.go @@ -0,0 +1,222 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/c9s/bbgo/pkg/types (interfaces: Exchange) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + types "github.com/c9s/bbgo/pkg/types" + gomock "github.com/golang/mock/gomock" +) + +// MockExchange is a mock of Exchange interface. +type MockExchange struct { + ctrl *gomock.Controller + recorder *MockExchangeMockRecorder +} + +// MockExchangeMockRecorder is the mock recorder for MockExchange. +type MockExchangeMockRecorder struct { + mock *MockExchange +} + +// NewMockExchange creates a new mock instance. +func NewMockExchange(ctrl *gomock.Controller) *MockExchange { + mock := &MockExchange{ctrl: ctrl} + mock.recorder = &MockExchangeMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockExchange) EXPECT() *MockExchangeMockRecorder { + return m.recorder +} + +// CancelOrders mocks base method. +func (m *MockExchange) CancelOrders(arg0 context.Context, arg1 ...types.Order) error { + m.ctrl.T.Helper() + varargs := []interface{}{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "CancelOrders", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// CancelOrders indicates an expected call of CancelOrders. +func (mr *MockExchangeMockRecorder) CancelOrders(arg0 interface{}, arg1 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CancelOrders", reflect.TypeOf((*MockExchange)(nil).CancelOrders), varargs...) +} + +// Name mocks base method. +func (m *MockExchange) Name() types.ExchangeName { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Name") + ret0, _ := ret[0].(types.ExchangeName) + return ret0 +} + +// Name indicates an expected call of Name. +func (mr *MockExchangeMockRecorder) Name() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Name", reflect.TypeOf((*MockExchange)(nil).Name)) +} + +// NewStream mocks base method. +func (m *MockExchange) NewStream() types.Stream { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewStream") + ret0, _ := ret[0].(types.Stream) + return ret0 +} + +// NewStream indicates an expected call of NewStream. +func (mr *MockExchangeMockRecorder) NewStream() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewStream", reflect.TypeOf((*MockExchange)(nil).NewStream)) +} + +// PlatformFeeCurrency mocks base method. +func (m *MockExchange) PlatformFeeCurrency() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PlatformFeeCurrency") + ret0, _ := ret[0].(string) + return ret0 +} + +// PlatformFeeCurrency indicates an expected call of PlatformFeeCurrency. +func (mr *MockExchangeMockRecorder) PlatformFeeCurrency() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PlatformFeeCurrency", reflect.TypeOf((*MockExchange)(nil).PlatformFeeCurrency)) +} + +// QueryAccount mocks base method. +func (m *MockExchange) QueryAccount(arg0 context.Context) (*types.Account, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryAccount", arg0) + ret0, _ := ret[0].(*types.Account) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryAccount indicates an expected call of QueryAccount. +func (mr *MockExchangeMockRecorder) QueryAccount(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryAccount", reflect.TypeOf((*MockExchange)(nil).QueryAccount), arg0) +} + +// QueryAccountBalances mocks base method. +func (m *MockExchange) QueryAccountBalances(arg0 context.Context) (types.BalanceMap, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryAccountBalances", arg0) + ret0, _ := ret[0].(types.BalanceMap) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryAccountBalances indicates an expected call of QueryAccountBalances. +func (mr *MockExchangeMockRecorder) QueryAccountBalances(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryAccountBalances", reflect.TypeOf((*MockExchange)(nil).QueryAccountBalances), arg0) +} + +// QueryKLines mocks base method. +func (m *MockExchange) QueryKLines(arg0 context.Context, arg1 string, arg2 types.Interval, arg3 types.KLineQueryOptions) ([]types.KLine, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryKLines", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].([]types.KLine) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryKLines indicates an expected call of QueryKLines. +func (mr *MockExchangeMockRecorder) QueryKLines(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryKLines", reflect.TypeOf((*MockExchange)(nil).QueryKLines), arg0, arg1, arg2, arg3) +} + +// QueryMarkets mocks base method. +func (m *MockExchange) QueryMarkets(arg0 context.Context) (types.MarketMap, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryMarkets", arg0) + ret0, _ := ret[0].(types.MarketMap) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryMarkets indicates an expected call of QueryMarkets. +func (mr *MockExchangeMockRecorder) QueryMarkets(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryMarkets", reflect.TypeOf((*MockExchange)(nil).QueryMarkets), arg0) +} + +// QueryOpenOrders mocks base method. +func (m *MockExchange) QueryOpenOrders(arg0 context.Context, arg1 string) ([]types.Order, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryOpenOrders", arg0, arg1) + ret0, _ := ret[0].([]types.Order) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryOpenOrders indicates an expected call of QueryOpenOrders. +func (mr *MockExchangeMockRecorder) QueryOpenOrders(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryOpenOrders", reflect.TypeOf((*MockExchange)(nil).QueryOpenOrders), arg0, arg1) +} + +// QueryTicker mocks base method. +func (m *MockExchange) QueryTicker(arg0 context.Context, arg1 string) (*types.Ticker, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryTicker", arg0, arg1) + ret0, _ := ret[0].(*types.Ticker) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryTicker indicates an expected call of QueryTicker. +func (mr *MockExchangeMockRecorder) QueryTicker(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryTicker", reflect.TypeOf((*MockExchange)(nil).QueryTicker), arg0, arg1) +} + +// QueryTickers mocks base method. +func (m *MockExchange) QueryTickers(arg0 context.Context, arg1 ...string) (map[string]types.Ticker, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "QueryTickers", varargs...) + ret0, _ := ret[0].(map[string]types.Ticker) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryTickers indicates an expected call of QueryTickers. +func (mr *MockExchangeMockRecorder) QueryTickers(arg0 interface{}, arg1 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryTickers", reflect.TypeOf((*MockExchange)(nil).QueryTickers), varargs...) +} + +// SubmitOrder mocks base method. +func (m *MockExchange) SubmitOrder(arg0 context.Context, arg1 types.SubmitOrder) (*types.Order, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SubmitOrder", arg0, arg1) + ret0, _ := ret[0].(*types.Order) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SubmitOrder indicates an expected call of SubmitOrder. +func (mr *MockExchangeMockRecorder) SubmitOrder(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubmitOrder", reflect.TypeOf((*MockExchange)(nil).SubmitOrder), arg0, arg1) +} diff --git a/pkg/types/omega.go b/pkg/types/omega.go new file mode 100644 index 0000000000..a89649e4f1 --- /dev/null +++ b/pkg/types/omega.go @@ -0,0 +1,28 @@ +package types + +// Determines the Omega ratio of a strategy +// See https://en.wikipedia.org/wiki/Omega_ratio for more details +// +// @param returns (Series): Series of profit/loss percentage every specific interval +// @param returnThresholds(float64): threshold for returns filtering +// @return Omega ratio for give return series and threshold +func Omega(returns Series, returnThresholds ...float64) float64 { + threshold := 0.0 + if len(returnThresholds) > 0 { + threshold = returnThresholds[0] + } else { + threshold = Mean(returns) + } + length := returns.Length() + win := 0.0 + loss := 0.0 + for i := 0; i < length; i++ { + out := threshold - returns.Index(i) + if out > 0 { + win += out + } else { + loss -= out + } + } + return win / loss +} diff --git a/pkg/types/omega_test.go b/pkg/types/omega_test.go new file mode 100644 index 0000000000..6c6c6dabdc --- /dev/null +++ b/pkg/types/omega_test.go @@ -0,0 +1,15 @@ +package types + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/datatype/floats" +) + +func TestOmega(t *testing.T) { + var a Series = &floats.Slice{0.08, 0.09, 0.07, 0.15, 0.02, 0.03, 0.04, 0.05, 0.06, 0.01} + output := Omega(a) + assert.InDelta(t, output, 1, 0.0001) +} diff --git a/pkg/types/order.go b/pkg/types/order.go index d733010103..b206ace821 100644 --- a/pkg/types/order.go +++ b/pkg/types/order.go @@ -11,7 +11,7 @@ import ( "github.com/slack-go/slack" "github.com/c9s/bbgo/pkg/fixedpoint" - "github.com/c9s/bbgo/pkg/util" + "github.com/c9s/bbgo/pkg/util/templateutil" ) func init() { @@ -116,8 +116,12 @@ type SubmitOrder struct { Side SideType `json:"side" db:"side"` Type OrderType `json:"orderType" db:"order_type"` - Quantity fixedpoint.Value `json:"quantity" db:"quantity"` - Price fixedpoint.Value `json:"price" db:"price"` + Quantity fixedpoint.Value `json:"quantity" db:"quantity"` + Price fixedpoint.Value `json:"price" db:"price"` + + // AveragePrice is only used in back-test currently + AveragePrice fixedpoint.Value `json:"averagePrice"` + StopPrice fixedpoint.Value `json:"stopPrice,omitempty" db:"stop_price"` Market Market `json:"-" db:"-"` @@ -128,13 +132,46 @@ type SubmitOrder struct { MarginSideEffect MarginOrderSideEffectType `json:"marginSideEffect,omitempty"` // AUTO_REPAY = repay, MARGIN_BUY = borrow, defaults to NO_SIDE_EFFECT - // futures order fields - IsFutures bool `json:"is_futures" db:"is_futures"` ReduceOnly bool `json:"reduceOnly" db:"reduce_only"` ClosePosition bool `json:"closePosition" db:"close_position"` + + Tag string `json:"tag" db:"-"` +} + +func (o *SubmitOrder) In() (fixedpoint.Value, string) { + switch o.Side { + case SideTypeBuy: + if o.AveragePrice.IsZero() { + return o.Quantity.Mul(o.Price), o.Market.QuoteCurrency + } else { + return o.Quantity.Mul(o.AveragePrice), o.Market.QuoteCurrency + } + + case SideTypeSell: + return o.Quantity, o.Market.BaseCurrency + + } + + return fixedpoint.Zero, "" } -func (o SubmitOrder) String() string { +func (o *SubmitOrder) Out() (fixedpoint.Value, string) { + switch o.Side { + case SideTypeBuy: + return o.Quantity, o.Market.BaseCurrency + + case SideTypeSell: + if o.AveragePrice.IsZero() { + return o.Quantity.Mul(o.Price), o.Market.QuoteCurrency + } else { + return o.Quantity.Mul(o.AveragePrice), o.Market.QuoteCurrency + } + } + + return fixedpoint.Zero, "" +} + +func (o *SubmitOrder) String() string { switch o.Type { case OrderTypeMarket: return fmt.Sprintf("SubmitOrder %s %s %s %s", o.Symbol, o.Type, o.Side, o.Quantity.String()) @@ -143,7 +180,7 @@ func (o SubmitOrder) String() string { return fmt.Sprintf("SubmitOrder %s %s %s %s @ %s", o.Symbol, o.Type, o.Side, o.Quantity.String(), o.Price.String()) } -func (o SubmitOrder) PlainText() string { +func (o *SubmitOrder) PlainText() string { switch o.Type { case OrderTypeMarket: return fmt.Sprintf("SubmitOrder %s %s %s %s", o.Symbol, o.Type, o.Side, o.Quantity.String()) @@ -152,7 +189,7 @@ func (o SubmitOrder) PlainText() string { return fmt.Sprintf("SubmitOrder %s %s %s %s @ %s", o.Symbol, o.Type, o.Side, o.Quantity.String(), o.Price.String()) } -func (o SubmitOrder) SlackAttachment() slack.Attachment { +func (o *SubmitOrder) SlackAttachment() slack.Attachment { var fields = []slack.AttachmentField{ {Title: "Symbol", Value: o.Symbol, Short: true}, {Title: "Side", Value: string(o.Side), Short: true}, @@ -214,10 +251,43 @@ type Order struct { CreationTime Time `json:"creationTime" db:"created_at"` UpdateTime Time `json:"updateTime" db:"updated_at"` + IsFutures bool `json:"isFutures" db:"is_futures"` IsMargin bool `json:"isMargin" db:"is_margin"` IsIsolated bool `json:"isIsolated" db:"is_isolated"` } +func (o Order) CsvHeader() []string { + return []string{ + "order_id", + "symbol", + "side", + "order_type", + "status", + "price", + "quantity", + "creation_time", + "update_time", + "tag", + } +} + +func (o Order) CsvRecords() [][]string { + return [][]string{ + { + strconv.FormatUint(o.OrderID, 10), + o.Symbol, + string(o.Side), + string(o.Type), + string(o.Status), + o.Price.String(), + o.Quantity.String(), + o.CreationTime.Time().Local().Format(time.RFC1123), + o.UpdateTime.Time().Local().Format(time.RFC1123), + o.Tag, + }, + } +} + // Backup backs up the current order quantity to a SubmitOrder object // so that we can post the order later when we want to restore the orders. func (o Order) Backup() SubmitOrder { @@ -237,16 +307,22 @@ func (o Order) String() string { orderID = strconv.FormatUint(o.OrderID, 10) } - return fmt.Sprintf("ORDER %s | %s | %s | %s %-4s | %s/%s @ %s | %s", + desc := fmt.Sprintf("ORDER %s | %s | %s | %s | %s %-4s | %s/%s @ %s", o.Exchange.String(), o.CreationTime.Time().Local().Format(time.RFC1123), orderID, o.Symbol, + o.Type, o.Side, o.ExecutedQuantity.String(), o.Quantity.String(), - o.Price.String(), - o.Status) + o.Price.String()) + + if o.Type == OrderTypeStopLimit { + desc += " Stop @ " + o.StopPrice.String() + } + + return desc + " | " + string(o.Status) } // PlainText is used for telegram-styled messages @@ -300,7 +376,7 @@ func (o Order) SlackAttachment() slack.Attachment { Short: true, }) - footerIcon := exchangeFooterIcon(o.Exchange) + footerIcon := ExchangeFooterIcon(o.Exchange) return slack.Attachment{ Color: SideToColorName(o.Side), @@ -308,6 +384,6 @@ func (o Order) SlackAttachment() slack.Attachment { // Text: "", Fields: fields, FooterIcon: footerIcon, - Footer: strings.ToLower(o.Exchange.String()) + util.Render(" creation time {{ . }}", o.CreationTime.Time().Format(time.StampMilli)), + Footer: strings.ToLower(o.Exchange.String()) + templateutil.Render(" creation time {{ . }}", o.CreationTime.Time().Format(time.StampMilli)), } } diff --git a/pkg/types/orderbook.go b/pkg/types/orderbook.go index 8bae186c80..78d14ced38 100644 --- a/pkg/types/orderbook.go +++ b/pkg/types/orderbook.go @@ -114,12 +114,16 @@ func (b *MutexOrderBook) Update(update SliceOrderBook) { b.Unlock() } +//go:generate callbackgen -type StreamOrderBook // StreamOrderBook receives streaming data from websocket connection and // update the order book with mutex lock, so you can safely access it. type StreamOrderBook struct { *MutexOrderBook C sigchan.Chan + + updateCallbacks []func(update SliceOrderBook) + snapshotCallbacks []func(snapshot SliceOrderBook) } func NewStreamBook(symbol string) *StreamOrderBook { @@ -136,6 +140,7 @@ func (sb *StreamOrderBook) BindStream(stream Stream) { } sb.Load(book) + sb.EmitSnapshot(book) sb.C.Emit() }) @@ -145,6 +150,7 @@ func (sb *StreamOrderBook) BindStream(stream Stream) { } sb.Update(book) + sb.EmitUpdate(book) sb.C.Emit() }) } diff --git a/pkg/types/ordermap.go b/pkg/types/ordermap.go index e476778598..651f3d8c7b 100644 --- a/pkg/types/ordermap.go +++ b/pkg/types/ordermap.go @@ -205,10 +205,3 @@ func (m *SyncOrderMap) Orders() (slice OrderSlice) { } type OrderSlice []Order - -func (s OrderSlice) IDs() (ids []uint64) { - for _, o := range s { - ids = append(ids, o.OrderID) - } - return ids -} diff --git a/pkg/types/pca.go b/pkg/types/pca.go new file mode 100644 index 0000000000..9ecc00a6c2 --- /dev/null +++ b/pkg/types/pca.go @@ -0,0 +1,58 @@ +package types + +import ( + "fmt" + "gonum.org/v1/gonum/mat" +) + +type PCA struct { + svd *mat.SVD +} + +func (pca *PCA) FitTransform(x []SeriesExtend, lookback, feature int) ([]SeriesExtend, error) { + if err := pca.Fit(x, lookback); err != nil { + return nil, err + } + return pca.Transform(x, lookback, feature), nil +} + +func (pca *PCA) Fit(x []SeriesExtend, lookback int) error { + vec := make([]float64, lookback*len(x)) + for i, xx := range x { + mean := xx.Mean(lookback) + for j := 0; j < lookback; j++ { + vec[i+j*i] = xx.Index(j) - mean + } + } + pca.svd = &mat.SVD{} + diffMatrix := mat.NewDense(lookback, len(x), vec) + if ok := pca.svd.Factorize(diffMatrix, mat.SVDThin); !ok { + return fmt.Errorf("Unable to factorize") + } + return nil +} + +func (pca *PCA) Transform(x []SeriesExtend, lookback int, features int) (result []SeriesExtend) { + result = make([]SeriesExtend, features) + vTemp := new(mat.Dense) + pca.svd.VTo(vTemp) + var ret mat.Dense + vec := make([]float64, lookback*len(x)) + for i, xx := range x { + for j := 0; j < lookback; j++ { + vec[i+j*i] = xx.Index(j) + } + } + newX := mat.NewDense(lookback, len(x), vec) + ret.Mul(newX, vTemp) + newMatrix := mat.NewDense(lookback, features, nil) + newMatrix.Copy(&ret) + for i := 0; i < features; i++ { + queue := NewQueue(lookback) + for j := 0; j < lookback; j++ { + queue.Update(newMatrix.At(lookback-j-1, i)) + } + result[i] = queue + } + return result +} diff --git a/pkg/types/position.go b/pkg/types/position.go index 05c066fb8a..3fd5c85a14 100644 --- a/pkg/types/position.go +++ b/pkg/types/position.go @@ -8,7 +8,7 @@ import ( "github.com/slack-go/slack" "github.com/c9s/bbgo/pkg/fixedpoint" - "github.com/c9s/bbgo/pkg/util" + "github.com/c9s/bbgo/pkg/util/templateutil" ) type PositionType string @@ -50,6 +50,7 @@ type Position struct { // TotalFee stores the fee currency -> total fee quantity TotalFee map[string]fixedpoint.Value `json:"totalFee" db:"-"` + OpenedAt time.Time `json:"openedAt,omitempty" db:"-"` ChangedAt time.Time `json:"changedAt,omitempty" db:"changed_at"` Strategy string `json:"strategy,omitempty" db:"strategy"` @@ -58,6 +59,37 @@ type Position struct { AccumulatedProfit fixedpoint.Value `json:"accumulatedProfit,omitempty" db:"accumulated_profit"` sync.Mutex + + // Modify position callbacks + modifyCallbacks []func(baseQty fixedpoint.Value, quoteQty fixedpoint.Value, price fixedpoint.Value) +} + +func (p *Position) CsvHeader() []string { + return []string{ + "symbol", + "time", + "average_cost", + "base", + "quote", + "accumulated_profit", + } +} + +func (p *Position) CsvRecords() [][]string { + if p.AverageCost.IsZero() && p.Base.IsZero() { + return nil + } + + return [][]string{ + { + p.Symbol, + p.ChangedAt.UTC().Format(time.RFC1123), + p.AverageCost.String(), + p.Base.String(), + p.Quote.String(), + p.AccumulatedProfit.String(), + }, + } } // NewProfit generates the profit object from the current position @@ -72,8 +104,11 @@ func (p *Position) NewProfit(trade Trade, profit, netProfit fixedpoint.Value) Pr NetProfit: netProfit, ProfitMargin: profit.Div(trade.QuoteQuantity), NetProfitMargin: netProfit.Div(trade.QuoteQuantity), + // trade related fields + Trade: &trade, TradeID: trade.ID, + OrderID: trade.OrderID, Side: trade.Side, IsBuyer: trade.IsBuyer, IsMaker: trade.IsMaker, @@ -84,17 +119,35 @@ func (p *Position) NewProfit(trade Trade, profit, netProfit fixedpoint.Value) Pr Fee: trade.Fee, FeeCurrency: trade.FeeCurrency, - Exchange: trade.Exchange, - IsMargin: trade.IsMargin, - IsFutures: trade.IsFutures, - IsIsolated: trade.IsIsolated, - TradedAt: trade.Time.Time(), + Exchange: trade.Exchange, + IsMargin: trade.IsMargin, + IsFutures: trade.IsFutures, + IsIsolated: trade.IsIsolated, + TradedAt: trade.Time.Time(), + Strategy: p.Strategy, + StrategyInstanceID: p.StrategyInstanceID, + + PositionOpenedAt: p.OpenedAt, } } -func (p *Position) NewClosePositionOrder(percentage fixedpoint.Value) *SubmitOrder { +// ROI -- Return on investment (ROI) is a performance measure used to evaluate the efficiency or profitability of an investment +// or compare the efficiency of a number of different investments. +// ROI tries to directly measure the amount of return on a particular investment, relative to the investment's cost. +func (p *Position) ROI(price fixedpoint.Value) fixedpoint.Value { + unrealizedProfit := p.UnrealizedProfit(price) + cost := p.AverageCost.Mul(p.Base.Abs()) + return unrealizedProfit.Div(cost) +} + +func (p *Position) NewMarketCloseOrder(percentage fixedpoint.Value) *SubmitOrder { base := p.GetBase() - quantity := base.Mul(percentage) + + quantity := base.Abs() + if percentage.Compare(fixedpoint.One) < 0 { + quantity = quantity.Mul(percentage) + } + if quantity.Compare(p.Market.MinQuantity) < 0 { return nil } @@ -108,14 +161,22 @@ func (p *Position) NewClosePositionOrder(percentage fixedpoint.Value) *SubmitOrd } return &SubmitOrder{ - Symbol: p.Symbol, - Market: p.Market, - Type: OrderTypeMarket, - Side: side, - Quantity: quantity, + Symbol: p.Symbol, + Market: p.Market, + Type: OrderTypeMarket, + Side: side, + Quantity: quantity, + MarginSideEffect: SideEffectTypeAutoRepay, } } +func (p *Position) IsDust(price fixedpoint.Value) bool { + base := p.Base.Abs() + return p.Market.IsDustQuantity(base, price) +} + +// GetBase locks the mutex and return the base quantity +// The base quantity can be negative func (p *Position) GetBase() (base fixedpoint.Value) { p.Lock() base = p.Base @@ -123,6 +184,60 @@ func (p *Position) GetBase() (base fixedpoint.Value) { return base } +func (p *Position) GetQuantity() fixedpoint.Value { + base := p.GetBase() + return base.Abs() +} + +func (p *Position) UnrealizedProfit(price fixedpoint.Value) fixedpoint.Value { + quantity := p.GetBase().Abs() + + if p.IsLong() { + return price.Sub(p.AverageCost).Mul(quantity) + } else if p.IsShort() { + return p.AverageCost.Sub(price).Mul(quantity) + } + + return fixedpoint.Zero +} + +func (p *Position) OnModify(cb func(baseQty fixedpoint.Value, quoteQty fixedpoint.Value, price fixedpoint.Value)) { + p.modifyCallbacks = append(p.modifyCallbacks, cb) +} + +func (p *Position) EmitModify(baseQty fixedpoint.Value, quoteQty fixedpoint.Value, price fixedpoint.Value) { + for _, cb := range p.modifyCallbacks { + cb(baseQty, quoteQty, price) + } +} + +// ModifyBase modifies position base quantity with `qty` +func (p *Position) ModifyBase(qty fixedpoint.Value) error { + p.Base = qty + + p.EmitModify(p.Base, p.Quote, p.AverageCost) + + return nil +} + +// ModifyQuote modifies position quote quantity with `qty` +func (p *Position) ModifyQuote(qty fixedpoint.Value) error { + p.Quote = qty + + p.EmitModify(p.Base, p.Quote, p.AverageCost) + + return nil +} + +// ModifyAverageCost modifies position average cost with `price` +func (p *Position) ModifyAverageCost(price fixedpoint.Value) error { + p.AverageCost = price + + p.EmitModify(p.Base, p.Quote, p.AverageCost) + + return nil +} + type FuturesPosition struct { Symbol string `json:"symbol"` BaseCurrency string `json:"baseCurrency"` @@ -145,11 +260,13 @@ type FuturesPosition struct { Isolated bool `json:"isolated"` UpdateTime int64 `json:"updateTime"` PositionRisk *PositionRisk - - sync.Mutex } func NewPositionFromMarket(market Market) *Position { + if len(market.BaseCurrency) == 0 || len(market.QuoteCurrency) == 0 { + panic("logical exception: missing market information, base currency or quote currency is empty") + } + return &Position{ Symbol: market.Symbol, BaseCurrency: market.BaseCurrency, @@ -193,6 +310,22 @@ func (p *Position) SetExchangeFeeRate(ex ExchangeName, exchangeFee ExchangeFee) p.ExchangeFeeRates[ex] = exchangeFee } +func (p *Position) IsShort() bool { + return p.Base.Sign() < 0 +} + +func (p *Position) IsLong() bool { + return p.Base.Sign() > 0 +} + +func (p *Position) IsClosed() bool { + return p.Base.Sign() == 0 +} + +func (p *Position) IsOpened(currentPrice fixedpoint.Value) bool { + return !p.IsClosed() && !p.IsDust(currentPrice) +} + func (p *Position) Type() PositionType { if p.Base.Sign() > 0 { return PositionLong @@ -222,7 +355,7 @@ func (p *Position) SlackAttachment() slack.Attachment { color = "#DC143C" } - title := util.Render(string(posType)+` Position {{ .Symbol }} `, p) + title := templateutil.Render(string(posType)+` Position {{ .Symbol }} `, p) fields := []slack.AttachmentField{ {Title: "Average Cost", Value: averageCost.String() + " " + p.QuoteCurrency, Short: true}, @@ -248,7 +381,7 @@ func (p *Position) SlackAttachment() slack.Attachment { Title: title, Color: color, Fields: fields, - Footer: util.Render("update time {{ . }}", time.Now().Format(time.RFC822)), + Footer: templateutil.Render("update time {{ . }}", time.Now().Format(time.RFC822)), // FooterIcon: "", } } @@ -309,31 +442,37 @@ func (p *Position) AddTrade(td Trade) (profit fixedpoint.Value, netProfit fixedp // calculated fee in quote (some exchange accounts may enable platform currency fee discount, like BNB) // convert platform fee token into USD values - var feeInQuote fixedpoint.Value = fixedpoint.Zero + var feeInQuote = fixedpoint.Zero switch td.FeeCurrency { case p.BaseCurrency: - quantity = quantity.Sub(fee) + if !td.IsFutures { + quantity = quantity.Sub(fee) + } case p.QuoteCurrency: - quoteQuantity = quoteQuantity.Sub(fee) + if !td.IsFutures { + quoteQuantity = quoteQuantity.Sub(fee) + } default: - if p.ExchangeFeeRates != nil { - if exchangeFee, ok := p.ExchangeFeeRates[td.Exchange]; ok { + if !td.Fee.IsZero() { + if p.ExchangeFeeRates != nil { + if exchangeFee, ok := p.ExchangeFeeRates[td.Exchange]; ok { + if td.IsMaker { + feeInQuote = feeInQuote.Add(exchangeFee.MakerFeeRate.Mul(quoteQuantity)) + } else { + feeInQuote = feeInQuote.Add(exchangeFee.TakerFeeRate.Mul(quoteQuantity)) + } + } + } else if p.FeeRate != nil { if td.IsMaker { - feeInQuote = feeInQuote.Add(exchangeFee.MakerFeeRate.Mul(quoteQuantity)) + feeInQuote = feeInQuote.Add(p.FeeRate.MakerFeeRate.Mul(quoteQuantity)) } else { - feeInQuote = feeInQuote.Add(exchangeFee.TakerFeeRate.Mul(quoteQuantity)) + feeInQuote = feeInQuote.Add(p.FeeRate.TakerFeeRate.Mul(quoteQuantity)) } } - } else if p.FeeRate != nil { - if td.IsMaker { - feeInQuote = feeInQuote.Add(p.FeeRate.MakerFeeRate.Mul(quoteQuantity)) - } else { - feeInQuote = feeInQuote.Add(p.FeeRate.TakerFeeRate.Mul(quoteQuantity)) - } } } @@ -352,6 +491,7 @@ func (p *Position) AddTrade(td Trade) (profit fixedpoint.Value, netProfit fixedp switch td.Side { case SideTypeBuy: + // was short position, now trade buy should cover the position if p.Base.Sign() < 0 { // convert short position to long position if p.Base.Add(quantity).Sign() > 0 { @@ -362,9 +502,10 @@ func (p *Position) AddTrade(td Trade) (profit fixedpoint.Value, netProfit fixedp p.AverageCost = price p.ApproximateAverageCost = price p.AccumulatedProfit = p.AccumulatedProfit.Add(profit) + p.OpenedAt = td.Time.Time() return profit, netProfit, true } else { - // covering short position + // after adding quantity it's still short position p.Base = p.Base.Add(quantity) p.Quote = p.Quote.Sub(quoteQuantity) profit = p.AverageCost.Sub(price).Mul(quantity) @@ -374,6 +515,13 @@ func (p *Position) AddTrade(td Trade) (profit fixedpoint.Value, netProfit fixedp } } + // before adding the quantity, it's already a dust position + // then we should set the openedAt time + if p.IsDust(td.Price) { + p.OpenedAt = td.Time.Time() + } + + // here the case is: base == 0 or base > 0 divisor := p.Base.Add(quantity) p.ApproximateAverageCost = p.ApproximateAverageCost.Mul(p.Base). Add(quoteQuantity). @@ -382,10 +530,10 @@ func (p *Position) AddTrade(td Trade) (profit fixedpoint.Value, netProfit fixedp p.AverageCost = p.AverageCost.Mul(p.Base).Add(quoteQuantity).Div(divisor) p.Base = p.Base.Add(quantity) p.Quote = p.Quote.Sub(quoteQuantity) - return fixedpoint.Zero, fixedpoint.Zero, false case SideTypeSell: + // was long position, the sell trade should reduce the base amount if p.Base.Sign() > 0 { // convert long position to short position if p.Base.Compare(quantity) < 0 { @@ -396,6 +544,7 @@ func (p *Position) AddTrade(td Trade) (profit fixedpoint.Value, netProfit fixedp p.AverageCost = price p.ApproximateAverageCost = price p.AccumulatedProfit = p.AccumulatedProfit.Add(profit) + p.OpenedAt = td.Time.Time() return profit, netProfit, true } else { p.Base = p.Base.Sub(quantity) @@ -407,6 +556,12 @@ func (p *Position) AddTrade(td Trade) (profit fixedpoint.Value, netProfit fixedp } } + // before subtracting the quantity, it's already a dust position + // then we should set the openedAt time + if p.IsDust(td.Price) { + p.OpenedAt = td.Time.Time() + } + // handling short position, since Base here is negative we need to reverse the sign divisor := quantity.Sub(p.Base) p.ApproximateAverageCost = p.ApproximateAverageCost.Mul(p.Base.Neg()). diff --git a/pkg/types/position_test.go b/pkg/types/position_test.go index 904f3bb8d2..0b9b983200 100644 --- a/pkg/types/position_test.go +++ b/pkg/types/position_test.go @@ -10,6 +10,46 @@ import ( const Delta = 1e-9 +func TestPosition_ROI(t *testing.T) { + t.Run("short position", func(t *testing.T) { + // Long position + pos := &Position{ + Symbol: "BTCUSDT", + BaseCurrency: "BTC", + QuoteCurrency: "USDT", + Base: fixedpoint.NewFromFloat(-10.0), + AverageCost: fixedpoint.NewFromFloat(8000.0), + Quote: fixedpoint.NewFromFloat(8000.0 * 10.0), + } + + assert.True(t, pos.IsShort(), "should be a short position") + + currentPrice := fixedpoint.NewFromFloat(5000.0) + roi := pos.ROI(currentPrice) + assert.Equal(t, "0.375", roi.String()) + assert.Equal(t, "37.5%", roi.Percentage()) + }) + + t.Run("long position", func(t *testing.T) { + // Long position + pos := &Position{ + Symbol: "BTCUSDT", + BaseCurrency: "BTC", + QuoteCurrency: "USDT", + Base: fixedpoint.NewFromFloat(10.0), + AverageCost: fixedpoint.NewFromFloat(8000.0), + Quote: fixedpoint.NewFromFloat(-8000.0 * 10.0), + } + + assert.True(t, pos.IsLong(), "should be a long position") + + currentPrice := fixedpoint.NewFromFloat(10000.0) + roi := pos.ROI(currentPrice) + assert.Equal(t, "0.25", roi.String()) + assert.Equal(t, "25%", roi.Percentage()) + }) +} + func TestPosition_ExchangeFeeRate_Short(t *testing.T) { pos := &Position{ Symbol: "BTCUSDT", diff --git a/pkg/types/price_volume_slice.go b/pkg/types/price_volume_slice.go index 7c0c8a60fd..a7863702fe 100644 --- a/pkg/types/price_volume_slice.go +++ b/pkg/types/price_volume_slice.go @@ -38,13 +38,13 @@ func (slice PriceVolumeSlice) CopyDepth(depth int) PriceVolumeSlice { return slice.Copy() } - var s = make(PriceVolumeSlice, depth, depth) + var s = make(PriceVolumeSlice, depth) copy(s, slice[:depth]) return s } func (slice PriceVolumeSlice) Copy() PriceVolumeSlice { - var s = make(PriceVolumeSlice, len(slice), len(slice)) + var s = make(PriceVolumeSlice, len(slice)) copy(s, slice) return s } diff --git a/pkg/types/profit.go b/pkg/types/profit.go index a86f92737c..121e4bc88c 100644 --- a/pkg/types/profit.go +++ b/pkg/types/profit.go @@ -7,7 +7,7 @@ import ( "github.com/slack-go/slack" "github.com/c9s/bbgo/pkg/fixedpoint" - "github.com/c9s/bbgo/pkg/util" + "github.com/c9s/bbgo/pkg/style" ) // Profit struct stores the PnL information @@ -37,7 +37,9 @@ type Profit struct { // trade related fields // -------------------------------------------- // TradeID is the exchange trade id of that trade + Trade *Trade `json:"trade,omitempty" db:"-"` TradeID uint64 `json:"tradeID" db:"trade_id"` + OrderID uint64 `json:"orderID,omitempty"` Side SideType `json:"side" db:"side"` IsBuyer bool `json:"isBuyer" db:"is_buyer"` IsMaker bool `json:"isMaker" db:"is_maker"` @@ -56,23 +58,25 @@ type Profit struct { IsIsolated bool `json:"isIsolated" db:"is_isolated"` TradedAt time.Time `json:"tradedAt" db:"traded_at"` + PositionOpenedAt time.Time `json:"positionOpenedAt" db:"-"` + // strategy related fields Strategy string `json:"strategy" db:"strategy"` StrategyInstanceID string `json:"strategyInstanceID" db:"strategy_instance_id"` } func (p *Profit) SlackAttachment() slack.Attachment { - var color = pnlColor(p.Profit) + var color = style.PnLColor(p.Profit) var title = fmt.Sprintf("%s PnL ", p.Symbol) - title += pnlEmojiMargin(p.Profit, p.ProfitMargin, defaultPnlLevelResolution) + " " - title += pnlSignString(p.Profit) + " " + p.QuoteCurrency + title += style.PnLEmojiMargin(p.Profit, p.ProfitMargin, style.DefaultPnLLevelResolution) + " " + title += style.PnLSignString(p.Profit) + " " + p.QuoteCurrency var fields []slack.AttachmentField if !p.NetProfit.IsZero() { fields = append(fields, slack.AttachmentField{ Title: "Net Profit", - Value: pnlSignString(p.NetProfit) + " " + p.QuoteCurrency, + Value: style.PnLSignString(p.NetProfit) + " " + p.QuoteCurrency, Short: true, }) } @@ -128,9 +132,9 @@ func (p *Profit) SlackAttachment() slack.Attachment { func (p *Profit) PlainText() string { var emoji string if !p.ProfitMargin.IsZero() { - emoji = pnlEmojiMargin(p.Profit, p.ProfitMargin, defaultPnlLevelResolution) + emoji = style.PnLEmojiMargin(p.Profit, p.ProfitMargin, style.DefaultPnLLevelResolution) } else { - emoji = pnlEmojiSimple(p.Profit) + emoji = style.PnLEmojiSimple(p.Profit) } return fmt.Sprintf("%s trade profit %s %s %s (%s), net profit =~ %s %s (%s)", @@ -143,81 +147,46 @@ func (p *Profit) PlainText() string { ) } -var lossEmoji = "đŸ”„" -var profitEmoji = "💰" -var defaultPnlLevelResolution = fixedpoint.NewFromFloat(0.001) - -func pnlColor(pnl fixedpoint.Value) string { - if pnl.Sign() > 0 { - return GreenColor - } - return RedColor -} - -func pnlSignString(pnl fixedpoint.Value) string { - if pnl.Sign() > 0 { - return "+" + pnl.String() - } - return pnl.String() -} - -func pnlEmojiSimple(pnl fixedpoint.Value) string { - if pnl.Sign() < 0 { - return lossEmoji - } - - if pnl.IsZero() { - return "" - } - - return profitEmoji -} - -func pnlEmojiMargin(pnl, margin, resolution fixedpoint.Value) (out string) { - if margin.IsZero() { - return pnlEmojiSimple(pnl) - } - - if pnl.Sign() < 0 { - out = lossEmoji - level := (margin.Neg()).Div(resolution).Int() - for i := 1; i < level; i++ { - out += lossEmoji - } - return out - } - - if pnl.IsZero() { - return out - } - - out = profitEmoji - level := margin.Div(resolution).Int() - for i := 1; i < level; i++ { - out += profitEmoji - } - return out -} - type ProfitStats struct { Symbol string `json:"symbol"` QuoteCurrency string `json:"quoteCurrency"` BaseCurrency string `json:"baseCurrency"` - AccumulatedPnL fixedpoint.Value `json:"accumulatedPnL,omitempty"` - AccumulatedNetProfit fixedpoint.Value `json:"accumulatedNetProfit,omitempty"` - AccumulatedProfit fixedpoint.Value `json:"accumulatedProfit,omitempty"` - AccumulatedLoss fixedpoint.Value `json:"accumulatedLoss,omitempty"` - AccumulatedVolume fixedpoint.Value `json:"accumulatedVolume,omitempty"` - AccumulatedSince int64 `json:"accumulatedSince,omitempty"` - - TodayPnL fixedpoint.Value `json:"todayPnL,omitempty"` - TodayNetProfit fixedpoint.Value `json:"todayNetProfit,omitempty"` - TodayProfit fixedpoint.Value `json:"todayProfit,omitempty"` - TodayLoss fixedpoint.Value `json:"todayLoss,omitempty"` - TodaySince int64 `json:"todaySince,omitempty"` + AccumulatedPnL fixedpoint.Value `json:"accumulatedPnL,omitempty"` + AccumulatedNetProfit fixedpoint.Value `json:"accumulatedNetProfit,omitempty"` + AccumulatedGrossProfit fixedpoint.Value `json:"accumulatedGrossProfit,omitempty"` + AccumulatedGrossLoss fixedpoint.Value `json:"accumulatedGrossLoss,omitempty"` + AccumulatedVolume fixedpoint.Value `json:"accumulatedVolume,omitempty"` + AccumulatedSince int64 `json:"accumulatedSince,omitempty"` + + TodayPnL fixedpoint.Value `json:"todayPnL,omitempty"` + TodayNetProfit fixedpoint.Value `json:"todayNetProfit,omitempty"` + TodayGrossProfit fixedpoint.Value `json:"todayGrossProfit,omitempty"` + TodayGrossLoss fixedpoint.Value `json:"todayGrossLoss,omitempty"` + TodaySince int64 `json:"todaySince,omitempty"` +} + +func NewProfitStats(market Market) *ProfitStats { + return &ProfitStats{ + Symbol: market.Symbol, + QuoteCurrency: market.QuoteCurrency, + BaseCurrency: market.BaseCurrency, + AccumulatedPnL: fixedpoint.Zero, + AccumulatedNetProfit: fixedpoint.Zero, + AccumulatedGrossProfit: fixedpoint.Zero, + AccumulatedGrossLoss: fixedpoint.Zero, + AccumulatedVolume: fixedpoint.Zero, + AccumulatedSince: 0, + TodayPnL: fixedpoint.Zero, + TodayNetProfit: fixedpoint.Zero, + TodayGrossProfit: fixedpoint.Zero, + TodayGrossLoss: fixedpoint.Zero, + TodaySince: 0, + } } +// Init +// Deprecated: use NewProfitStats instead func (s *ProfitStats) Init(market Market) { s.Symbol = market.Symbol s.BaseCurrency = market.BaseCurrency @@ -228,40 +197,54 @@ func (s *ProfitStats) Init(market Market) { } func (s *ProfitStats) AddProfit(profit Profit) { + if s.IsOver24Hours() { + s.ResetToday(profit.TradedAt) + } + + // since field guard + if s.AccumulatedSince == 0 { + s.AccumulatedSince = profit.TradedAt.Unix() + } + + if s.TodaySince == 0 { + var beginningOfTheDay = BeginningOfTheDay(profit.TradedAt.Local()) + s.TodaySince = beginningOfTheDay.Unix() + } + s.AccumulatedPnL = s.AccumulatedPnL.Add(profit.Profit) s.AccumulatedNetProfit = s.AccumulatedNetProfit.Add(profit.NetProfit) - s.TodayPnL = s.TodayPnL.Add(profit.Profit) s.TodayNetProfit = s.TodayNetProfit.Add(profit.NetProfit) - if profit.Profit.Sign() < 0 { - s.AccumulatedLoss = s.AccumulatedLoss.Add(profit.Profit) - s.TodayLoss = s.TodayLoss.Add(profit.Profit) - } else if profit.Profit.Sign() > 0 { - s.AccumulatedProfit = s.AccumulatedLoss.Add(profit.Profit) - s.TodayProfit = s.TodayProfit.Add(profit.Profit) + if profit.Profit.Sign() > 0 { + s.AccumulatedGrossProfit = s.AccumulatedGrossProfit.Add(profit.Profit) + s.TodayGrossProfit = s.TodayGrossProfit.Add(profit.Profit) + } else if profit.Profit.Sign() < 0 { + s.AccumulatedGrossLoss = s.AccumulatedGrossLoss.Add(profit.Profit) + s.TodayGrossLoss = s.TodayGrossLoss.Add(profit.Profit) } } func (s *ProfitStats) AddTrade(trade Trade) { if s.IsOver24Hours() { - s.ResetToday() + s.ResetToday(trade.Time.Time()) } s.AccumulatedVolume = s.AccumulatedVolume.Add(trade.Quantity) } +// IsOver24Hours checks if the since time is over 24 hours func (s *ProfitStats) IsOver24Hours() bool { - return time.Since(time.Unix(s.TodaySince, 0)) > 24*time.Hour + return time.Since(time.Unix(s.TodaySince, 0)) >= 24*time.Hour } -func (s *ProfitStats) ResetToday() { +func (s *ProfitStats) ResetToday(t time.Time) { s.TodayPnL = fixedpoint.Zero s.TodayNetProfit = fixedpoint.Zero - s.TodayProfit = fixedpoint.Zero - s.TodayLoss = fixedpoint.Zero + s.TodayGrossProfit = fixedpoint.Zero + s.TodayGrossLoss = fixedpoint.Zero - var beginningOfTheDay = util.BeginningOfTheDay(time.Now().Local()) + var beginningOfTheDay = BeginningOfTheDay(t.Local()) s.TodaySince = beginningOfTheDay.Unix() } @@ -270,26 +253,26 @@ func (s *ProfitStats) PlainText() string { return fmt.Sprintf("%s Profit Today\n"+ "Profit %s %s\n"+ "Net profit %s %s\n"+ - "Trade Loss %s %s\n"+ + "Gross Loss %s %s\n"+ "Summary:\n"+ "Accumulated Profit %s %s\n"+ "Accumulated Net Profit %s %s\n"+ - "Accumulated Trade Loss %s %s\n"+ + "Accumulated Gross Loss %s %s\n"+ "Since %s", s.Symbol, s.TodayPnL.String(), s.QuoteCurrency, s.TodayNetProfit.String(), s.QuoteCurrency, - s.TodayLoss.String(), s.QuoteCurrency, + s.TodayGrossLoss.String(), s.QuoteCurrency, s.AccumulatedPnL.String(), s.QuoteCurrency, s.AccumulatedNetProfit.String(), s.QuoteCurrency, - s.AccumulatedLoss.String(), s.QuoteCurrency, + s.AccumulatedGrossLoss.String(), s.QuoteCurrency, since.Format(time.RFC822), ) } func (s *ProfitStats) SlackAttachment() slack.Attachment { - var color = pnlColor(s.AccumulatedPnL) - var title = fmt.Sprintf("%s Accumulated PnL %s %s", s.Symbol, pnlSignString(s.AccumulatedPnL), s.QuoteCurrency) + var color = style.PnLColor(s.AccumulatedPnL) + var title = fmt.Sprintf("%s Accumulated PnL %s %s", s.Symbol, style.PnLSignString(s.AccumulatedPnL), s.QuoteCurrency) since := time.Unix(s.AccumulatedSince, 0).Local() title += " Since " + since.Format(time.RFC822) @@ -299,31 +282,31 @@ func (s *ProfitStats) SlackAttachment() slack.Attachment { if !s.TodayPnL.IsZero() { fields = append(fields, slack.AttachmentField{ Title: "P&L Today", - Value: pnlSignString(s.TodayPnL) + " " + s.QuoteCurrency, + Value: style.PnLSignString(s.TodayPnL) + " " + s.QuoteCurrency, Short: true, }) } - if !s.TodayProfit.IsZero() { + if !s.TodayNetProfit.IsZero() { fields = append(fields, slack.AttachmentField{ - Title: "Profit Today", - Value: pnlSignString(s.TodayProfit) + " " + s.QuoteCurrency, + Title: "Net Profit Today", + Value: style.PnLSignString(s.TodayNetProfit) + " " + s.QuoteCurrency, Short: true, }) } - if !s.TodayNetProfit.IsZero() { + if !s.TodayGrossProfit.IsZero() { fields = append(fields, slack.AttachmentField{ - Title: "Net Profit Today", - Value: pnlSignString(s.TodayNetProfit) + " " + s.QuoteCurrency, + Title: "Gross Profit Today", + Value: style.PnLSignString(s.TodayGrossProfit) + " " + s.QuoteCurrency, Short: true, }) } - if !s.TodayLoss.IsZero() { + if !s.TodayGrossLoss.IsZero() { fields = append(fields, slack.AttachmentField{ - Title: "Loss Today", - Value: pnlSignString(s.TodayLoss) + " " + s.QuoteCurrency, + Title: "Gross Loss Today", + Value: style.PnLSignString(s.TodayGrossLoss) + " " + s.QuoteCurrency, Short: true, }) } @@ -331,28 +314,28 @@ func (s *ProfitStats) SlackAttachment() slack.Attachment { if !s.AccumulatedPnL.IsZero() { fields = append(fields, slack.AttachmentField{ Title: "Accumulated P&L", - Value: pnlSignString(s.AccumulatedPnL) + " " + s.QuoteCurrency, + Value: style.PnLSignString(s.AccumulatedPnL) + " " + s.QuoteCurrency, }) } - if !s.AccumulatedProfit.IsZero() { + if !s.AccumulatedGrossProfit.IsZero() { fields = append(fields, slack.AttachmentField{ - Title: "Accumulated Profit", - Value: pnlSignString(s.AccumulatedProfit) + " " + s.QuoteCurrency, + Title: "Accumulated Gross Profit", + Value: style.PnLSignString(s.AccumulatedGrossProfit) + " " + s.QuoteCurrency, }) } - if !s.AccumulatedNetProfit.IsZero() { + if !s.AccumulatedGrossLoss.IsZero() { fields = append(fields, slack.AttachmentField{ - Title: "Accumulated Net Profit", - Value: pnlSignString(s.AccumulatedNetProfit) + " " + s.QuoteCurrency, + Title: "Accumulated Gross Loss", + Value: style.PnLSignString(s.AccumulatedGrossLoss) + " " + s.QuoteCurrency, }) } - if !s.AccumulatedLoss.IsZero() { + if !s.AccumulatedNetProfit.IsZero() { fields = append(fields, slack.AttachmentField{ - Title: "Accumulated Loss", - Value: pnlSignString(s.AccumulatedLoss) + " " + s.QuoteCurrency, + Title: "Accumulated Net Profit", + Value: style.PnLSignString(s.AccumulatedNetProfit) + " " + s.QuoteCurrency, }) } diff --git a/pkg/types/rbtree.go b/pkg/types/rbtree.go index fc1c84b2af..3eeae0d888 100644 --- a/pkg/types/rbtree.go +++ b/pkg/types/rbtree.go @@ -11,11 +11,9 @@ type RBTree struct { size int } -var neel = &RBNode{color: Black} - func NewRBTree() *RBTree { - var root = neel - root.parent = neel + var root = NewNil() + root.parent = NewNil() return &RBTree{ Root: root, } @@ -35,7 +33,7 @@ func (tree *RBTree) Delete(key fixedpoint.Value) bool { // the deleting node has only one child, it's easy, // we just connect the child the parent of the deleting node - if deleting.left == neel || deleting.right == neel { + if deleting.left.isNil() || deleting.right.isNil() { y = deleting // fmt.Printf("y = deleting = %+v\n", y) } else { @@ -47,16 +45,16 @@ func (tree *RBTree) Delete(key fixedpoint.Value) bool { } // y.left or y.right could be neel - if y.left != neel { - x = y.left - } else { + if y.left.isNil() { x = y.right + } else { + x = y.left } // fmt.Printf("x = %+v\n", y) x.parent = y.parent - if y.parent == neel { + if y.parent.isNil() { tree.Root = x } else if y == y.parent.left { y.parent.left = x @@ -144,18 +142,18 @@ func (tree *RBTree) DeleteFixup(current *RBNode) { } func (tree *RBTree) Upsert(key, val fixedpoint.Value) { - var y = neel + var y = NewNil() var x = tree.Root var node = &RBNode{ key: key, value: val, color: Red, - left: neel, - right: neel, - parent: neel, + left: NewNil(), + right: NewNil(), + parent: NewNil(), } - for x != neel { + for !x.isNil() { y = x if node.key == x.key { @@ -171,7 +169,7 @@ func (tree *RBTree) Upsert(key, val fixedpoint.Value) { node.parent = y - if y == neel { + if y.isNil() { tree.Root = node } else if node.key.Compare(y.key) < 0 { y.left = node @@ -183,18 +181,18 @@ func (tree *RBTree) Upsert(key, val fixedpoint.Value) { } func (tree *RBTree) Insert(key, val fixedpoint.Value) { - var y = neel + var y = NewNil() var x = tree.Root var node = &RBNode{ key: key, value: val, color: Red, - left: neel, - right: neel, - parent: neel, + left: NewNil(), + right: NewNil(), + parent: NewNil(), } - for x != neel { + for !x.isNil() { y = x if node.key.Compare(x.key) < 0 { @@ -206,7 +204,7 @@ func (tree *RBTree) Insert(key, val fixedpoint.Value) { node.parent = y - if y == neel { + if y.isNil() { tree.Root = node } else if node.key.Compare(y.key) < 0 { y.left = node @@ -220,7 +218,7 @@ func (tree *RBTree) Insert(key, val fixedpoint.Value) { func (tree *RBTree) Search(key fixedpoint.Value) *RBNode { var current = tree.Root - for current != neel && key != current.key { + for !current.isNil() && key != current.key { if key.Compare(current.key) < 0 { current = current.left } else { @@ -228,7 +226,7 @@ func (tree *RBTree) Search(key fixedpoint.Value) *RBNode { } } - if current == neel { + if current.isNil() { return nil } @@ -293,13 +291,13 @@ func (tree *RBTree) RotateLeft(x *RBNode) { var y = x.right x.right = y.left - if y.left != neel { + if !y.left.isNil() { y.left.parent = x } y.parent = x.parent - if x.parent == neel { + if x.parent.isNil() { tree.Root = y } else if x == x.parent.left { x.parent.left = y @@ -315,13 +313,16 @@ func (tree *RBTree) RotateRight(y *RBNode) { x := y.left y.left = x.right - if x.right != neel { + if !x.right.isNil() { + if x.right == nil { + panic(fmt.Errorf("x.right is nil: node = %+v, left = %+v, right = %+v, parent = %+v", x, x.left, x.right, x.parent)) + } x.right.parent = y } x.parent = y.parent - if y.parent == neel { + if y.parent.isNil() { tree.Root = x } else if y == y.parent.left { y.parent.left = x @@ -338,11 +339,11 @@ func (tree *RBTree) Rightmost() *RBNode { } func (tree *RBTree) RightmostOf(current *RBNode) *RBNode { - if current == neel || current == nil { + if current.isNil() || current == nil { return nil } - for current.right != neel { + for !current.right.isNil() { current = current.right } @@ -354,11 +355,11 @@ func (tree *RBTree) Leftmost() *RBNode { } func (tree *RBTree) LeftmostOf(current *RBNode) *RBNode { - if current == neel || current == nil { + if current.isNil() || current == nil { return nil } - for current.left != neel { + for !current.left.isNil() { current = current.left } @@ -366,12 +367,12 @@ func (tree *RBTree) LeftmostOf(current *RBNode) *RBNode { } func (tree *RBTree) Successor(current *RBNode) *RBNode { - if current.right != neel { + if !current.right.isNil() { return tree.LeftmostOf(current.right) } var newNode = current.parent - for newNode != neel && current == newNode.right { + for !newNode.isNil() && current == newNode.right { current = newNode newNode = newNode.parent } @@ -384,7 +385,7 @@ func (tree *RBTree) Preorder(cb func(n *RBNode)) { } func (tree *RBTree) PreorderOf(current *RBNode, cb func(n *RBNode)) { - if current != neel && current != nil { + if !current.isNil() && current != nil { cb(current) tree.PreorderOf(current.left, cb) tree.PreorderOf(current.right, cb) @@ -397,7 +398,7 @@ func (tree *RBTree) Inorder(cb func(n *RBNode) bool) { } func (tree *RBTree) InorderOf(current *RBNode, cb func(n *RBNode) bool) { - if current != neel && current != nil { + if !current.isNil() && current != nil { tree.InorderOf(current.left, cb) if !cb(current) { return @@ -412,7 +413,7 @@ func (tree *RBTree) InorderReverse(cb func(n *RBNode) bool) { } func (tree *RBTree) InorderReverseOf(current *RBNode, cb func(n *RBNode) bool) { - if current != neel && current != nil { + if !current.isNil() && current != nil { tree.InorderReverseOf(current.right, cb) if !cb(current) { return @@ -426,7 +427,7 @@ func (tree *RBTree) Postorder(cb func(n *RBNode) bool) { } func (tree *RBTree) PostorderOf(current *RBNode, cb func(n *RBNode) bool) { - if current != neel && current != nil { + if !current.isNil() && current != nil { tree.PostorderOf(current.left, cb) tree.PostorderOf(current.right, cb) if !cb(current) { diff --git a/pkg/types/rbtree_node.go b/pkg/types/rbtree_node.go index b387afe56a..76560f6c18 100644 --- a/pkg/types/rbtree_node.go +++ b/pkg/types/rbtree_node.go @@ -20,3 +20,14 @@ type RBNode struct { key, value fixedpoint.Value color Color } + +func NewNil() *RBNode { + return &RBNode{color: Black} +} + +func (node *RBNode) isNil() bool { + if node == nil { + return true + } + return node.color == Black && node.left == nil && node.right == nil +} diff --git a/pkg/types/rbtree_test.go b/pkg/types/rbtree_test.go index 0daddf1997..f3bac6aa01 100644 --- a/pkg/types/rbtree_test.go +++ b/pkg/types/rbtree_test.go @@ -2,6 +2,7 @@ package types import ( "math/rand" + "sync" "testing" "github.com/stretchr/testify/assert" @@ -11,6 +12,33 @@ import ( var itov func(int64) fixedpoint.Value = fixedpoint.NewFromInt +func TestRBTree_ConcurrentIndependence(t *testing.T) { + // each RBTree instances must not affect each other in concurrent environment + var wg sync.WaitGroup + for w := 0; w < 10; w++ { + wg.Add(1) + go func() { + defer wg.Done() + tree := NewRBTree() + for stepCnt := 0; stepCnt < 10000; stepCnt++ { + switch opCode := rand.Intn(2); opCode { + case 0: + priceI := rand.Int63n(16) + price := fixedpoint.NewFromInt(priceI) + tree.Delete(price) + case 1: + priceI := rand.Int63n(16) + volumeI := rand.Int63n(8) + tree.Upsert(fixedpoint.NewFromInt(priceI), fixedpoint.NewFromInt(volumeI)) + default: + panic("impossible") + } + } + }() + } + wg.Wait() +} + func TestRBTree_InsertAndDelete(t *testing.T) { tree := NewRBTree() node := tree.Rightmost() @@ -154,12 +182,12 @@ func TestRBTree_bulkInsert(t *testing.T) { pvs[price] = volume } tree.Inorder(func(n *RBNode) bool { - if n.left != neel { + if !n.left.isNil() { if !assert.True(t, n.key.Compare(n.left.key) > 0) { return false } } - if n.right != neel { + if !n.right.isNil() { if !assert.True(t, n.key.Compare(n.right.key) < 0) { return false } @@ -206,12 +234,12 @@ func TestRBTree_bulkInsertAndDelete(t *testing.T) { // validate tree structure tree.Inorder(func(n *RBNode) bool { - if n.left != neel { + if !n.left.isNil() { if !assert.True(t, n.key.Compare(n.left.key) > 0) { return false } } - if n.right != neel { + if !n.right.isNil() { if !assert.True(t, n.key.Compare(n.right.key) < 0) { return false } diff --git a/pkg/types/reward.go b/pkg/types/reward.go index 97a9680035..307486d152 100644 --- a/pkg/types/reward.go +++ b/pkg/types/reward.go @@ -1,6 +1,7 @@ package types import ( + "fmt" "time" "github.com/c9s/bbgo/pkg/fixedpoint" @@ -9,27 +10,36 @@ import ( type RewardType string const ( - RewardAirdrop = RewardType("airdrop") - RewardCommission = RewardType("commission") - RewardHolding = RewardType("holding") - RewardMining = RewardType("mining") - RewardTrading = RewardType("trading") - RewardVipRebate = RewardType("vip_rebate") + RewardAirdrop = RewardType("airdrop") + RewardCommission = RewardType("commission") + RewardReferralKickback = RewardType("referral_kickback") + RewardHolding = RewardType("holding") + RewardMining = RewardType("mining") + RewardTrading = RewardType("trading") + RewardVipRebate = RewardType("vip_rebate") ) type Reward struct { - GID int64 `json:"gid" db:"gid"` - UUID string `json:"uuid" db:"uuid"` - Exchange ExchangeName `json:"exchange" db:"exchange"` - Type RewardType `json:"reward_type" db:"reward_type"` - Currency string `json:"currency" db:"currency"` - Quantity fixedpoint.Value `json:"quantity" db:"quantity"` - State string `json:"state" db:"state"` - Note string `json:"note" db:"note"` - Spent bool `json:"spent" db:"spent"` - - // Unix timestamp in seconds - CreatedAt Time `json:"created_at" db:"created_at"` + GID int64 `json:"gid" db:"gid"` + UUID string `json:"uuid" db:"uuid"` + Exchange ExchangeName `json:"exchange" db:"exchange"` + Type RewardType `json:"reward_type" db:"reward_type"` + Currency string `json:"currency" db:"currency"` + Quantity fixedpoint.Value `json:"quantity" db:"quantity"` + State string `json:"state" db:"state"` + Note string `json:"note" db:"note"` + Spent bool `json:"spent" db:"spent"` + CreatedAt Time `json:"created_at" db:"created_at"` +} + +func (r Reward) String() (s string) { + s = fmt.Sprintf("reward %s %s %20s %20f %5s @ %s", r.Exchange, r.UUID, r.Type, r.Quantity.Float64(), r.Currency, r.CreatedAt.String()) + + if r.Note != "" { + s += ": " + r.Note + } + + return s } type RewardSlice []Reward diff --git a/pkg/types/seriesbase_imp.go b/pkg/types/seriesbase_imp.go new file mode 100644 index 0000000000..98329ad296 --- /dev/null +++ b/pkg/types/seriesbase_imp.go @@ -0,0 +1,152 @@ +package types + +import "github.com/c9s/bbgo/pkg/datatype/floats" + +func (s *SeriesBase) Index(i int) float64 { + if s.Series == nil { + return 0 + } + return s.Series.Index(i) +} + +func (s *SeriesBase) Last() float64 { + if s.Series == nil { + return 0 + } + return s.Series.Last() +} + +func (s *SeriesBase) Length() int { + if s.Series == nil { + return 0 + } + return s.Series.Length() +} + +func (s *SeriesBase) Sum(limit ...int) float64 { + return Sum(s, limit...) +} + +func (s *SeriesBase) Mean(limit ...int) float64 { + return Mean(s, limit...) +} + +func (s *SeriesBase) Abs() SeriesExtend { + return Abs(s) +} + +func (s *SeriesBase) Predict(lookback int, offset ...int) float64 { + return Predict(s, lookback, offset...) +} + +func (s *SeriesBase) NextCross(b Series, lookback int) (int, float64, bool) { + return NextCross(s, b, lookback) +} + +func (s *SeriesBase) CrossOver(b Series) BoolSeries { + return CrossOver(s, b) +} + +func (s *SeriesBase) CrossUnder(b Series) BoolSeries { + return CrossUnder(s, b) +} + +func (s *SeriesBase) Highest(lookback int) float64 { + return Highest(s, lookback) +} + +func (s *SeriesBase) Lowest(lookback int) float64 { + return Lowest(s, lookback) +} + +func (s *SeriesBase) Add(b interface{}) SeriesExtend { + return Add(s, b) +} + +func (s *SeriesBase) Minus(b interface{}) SeriesExtend { + return Minus(s, b) +} + +func (s *SeriesBase) Div(b interface{}) SeriesExtend { + return Div(s, b) +} + +func (s *SeriesBase) Mul(b interface{}) SeriesExtend { + return Mul(s, b) +} + +func (s *SeriesBase) Dot(b interface{}, limit ...int) float64 { + return Dot(s, b, limit...) +} + +func (s *SeriesBase) Array(limit ...int) (result []float64) { + return Array(s, limit...) +} + +func (s *SeriesBase) Reverse(limit ...int) (result floats.Slice) { + return Reverse(s, limit...) +} + +func (s *SeriesBase) Change(offset ...int) SeriesExtend { + return Change(s, offset...) +} + +func (s *SeriesBase) PercentageChange(offset ...int) SeriesExtend { + return PercentageChange(s, offset...) +} + +func (s *SeriesBase) Stdev(params ...int) float64 { + return Stdev(s, params...) +} + +func (s *SeriesBase) Rolling(window int) *RollingResult { + return Rolling(s, window) +} + +func (s *SeriesBase) Shift(offset int) SeriesExtend { + return Shift(s, offset) +} + +func (s *SeriesBase) Skew(length int) float64 { + return Skew(s, length) +} + +func (s *SeriesBase) Variance(length int) float64 { + return Variance(s, length) +} + +func (s *SeriesBase) Covariance(b Series, length int) float64 { + return Covariance(s, b, length) +} + +func (s *SeriesBase) Correlation(b Series, length int, method ...CorrFunc) float64 { + return Correlation(s, b, length, method...) +} + +func (s *SeriesBase) AutoCorrelation(length int, lag ...int) float64 { + return AutoCorrelation(s, length, lag...) +} + +func (s *SeriesBase) Rank(length int) SeriesExtend { + return Rank(s, length) +} + +func (s *SeriesBase) Sigmoid() SeriesExtend { + return Sigmoid(s) +} + +func (s *SeriesBase) Softmax(window int) SeriesExtend { + return Softmax(s, window) +} + +func (s *SeriesBase) Entropy(window int) float64 { + return Entropy(s, window) +} + +func (s *SeriesBase) CrossEntropy(b Series, window int) float64 { + return CrossEntropy(s, b, window) +} + +func (s *SeriesBase) Filter(b func(int, float64) bool, length int) SeriesExtend { + return Filter(s, b, length) +} diff --git a/pkg/types/sharpe.go b/pkg/types/sharpe.go new file mode 100644 index 0000000000..a37c635bf9 --- /dev/null +++ b/pkg/types/sharpe.go @@ -0,0 +1,48 @@ +package types + +import ( + "math" +) + +// Sharpe: Calcluates the sharpe ratio of access returns +// +// @param returns (Series): Series of profit/loss percentage every specific interval +// @param periods (int): Freq. of returns (252/365 for daily, 12 for monthy, 1 for annually) +// @param annualize (bool): return annualize sharpe? +// @param smart (bool): return smart sharpe ratio +func Sharpe(returns Series, periods int, annualize bool, smart bool) float64 { + data := returns + var divisor = Stdev(data, data.Length(), 1) + if smart { + divisor *= autocorrPenalty(returns) + } + if divisor == 0 { + mean := Mean(data) + if mean > 0 { + return math.Inf(1) + } else if mean < 0 { + return math.Inf(-1) + } else { + return 0 + } + } + result := Mean(data) / divisor + if annualize { + return result * math.Sqrt(float64(periods)) + } + return result +} + +func avgReturnRate(returnRate float64, periods int) float64 { + return math.Pow(1.+returnRate, 1./float64(periods)) - 1. +} + +func autocorrPenalty(data Series) float64 { + num := data.Length() + coef := math.Abs(Correlation(data, Shift(data, 1), num-1)) + var sum = 0. + for i := 1; i < num; i++ { + sum += float64(num-i) / float64(num) * math.Pow(coef, float64(i)) + } + return math.Sqrt(1. + 2.*sum) +} diff --git a/pkg/types/sharpe_test.go b/pkg/types/sharpe_test.go new file mode 100644 index 0000000000..6d41a3c724 --- /dev/null +++ b/pkg/types/sharpe_test.go @@ -0,0 +1,29 @@ +package types + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/datatype/floats" +) + +/* +python + +import quantstats as qx +import pandas as pd + +print(qx.stats.sharpe(pd.Series([0.01, 0.1, 0.001]), 0, 0, False, False)) +print(qx.stats.sharpe(pd.Series([0.01, 0.1, 0.001]), 0, 252, False, False)) +print(qx.stats.sharpe(pd.Series([0.01, 0.1, 0.001]), 0, 252, True, False)) +*/ +func TestSharpe(t *testing.T) { + var a Series = &floats.Slice{0.01, 0.1, 0.001} + output := Sharpe(a, 0, false, false) + assert.InDelta(t, output, 0.67586, 0.0001) + output = Sharpe(a, 252, false, false) + assert.InDelta(t, output, 0.67586, 0.0001) + output = Sharpe(a, 252, true, false) + assert.InDelta(t, output, 10.7289, 0.0001) +} diff --git a/pkg/types/side.go b/pkg/types/side.go index 46c916aed2..75e7a9ae1e 100644 --- a/pkg/types/side.go +++ b/pkg/types/side.go @@ -5,6 +5,8 @@ import ( "strings" "github.com/pkg/errors" + + "github.com/c9s/bbgo/pkg/style" ) // SideType define side type of order @@ -74,14 +76,14 @@ func (side SideType) String() string { func (side SideType) Color() string { if side == SideTypeBuy { - return GreenColor + return style.GreenColor } if side == SideTypeSell { - return RedColor + return style.RedColor } - return GrayColor + return style.GrayColor } func SideToColorName(side SideType) string { diff --git a/pkg/types/sliceorderbook.go b/pkg/types/sliceorderbook.go index c542e7284e..777e30333b 100644 --- a/pkg/types/sliceorderbook.go +++ b/pkg/types/sliceorderbook.go @@ -153,7 +153,7 @@ func (b *SliceOrderBook) Update(book SliceOrderBook) { } func (b *SliceOrderBook) Print() { - fmt.Printf(b.String()) + fmt.Print(b.String()) } func (b *SliceOrderBook) String() string { diff --git a/pkg/types/sort.go b/pkg/types/sort.go index 3da34dc7c8..6893d52e39 100644 --- a/pkg/types/sort.go +++ b/pkg/types/sort.go @@ -11,3 +11,18 @@ func SortTradesAscending(trades []Trade) []Trade { }) return trades } + +func SortOrdersAscending(orders []Order) []Order { + sort.Slice(orders, func(i, j int) bool { + return orders[i].CreationTime.Time().Before(orders[j].CreationTime.Time()) + }) + return orders +} + +func SortKLinesAscending(klines []KLine) []KLine { + sort.Slice(klines, func(i, j int) bool { + return klines[i].StartTime.Unix() < klines[j].StartTime.Unix() + }) + + return klines +} diff --git a/pkg/types/sort_test.go b/pkg/types/sort_test.go index 02eadbcb36..4e5171acea 100644 --- a/pkg/types/sort_test.go +++ b/pkg/types/sort_test.go @@ -8,24 +8,24 @@ import ( ) func TestSortTradesAscending(t *testing.T) { - var trades = []Trade { + var trades = []Trade{ { - ID: 1, - Symbol: "BTCUSDT", - Side: SideTypeBuy, - IsBuyer: false, - IsMaker: false, - Time: Time(time.Unix(2000, 0 )), + ID: 1, + Symbol: "BTCUSDT", + Side: SideTypeBuy, + IsBuyer: false, + IsMaker: false, + Time: Time(time.Unix(2000, 0)), }, { - ID: 2, - Symbol: "BTCUSDT", - Side: SideTypeBuy, - IsBuyer: false, - IsMaker: false, - Time: Time(time.Unix(1000, 0 )), + ID: 2, + Symbol: "BTCUSDT", + Side: SideTypeBuy, + IsBuyer: false, + IsMaker: false, + Time: Time(time.Unix(1000, 0)), }, } trades = SortTradesAscending(trades) - assert.True(t ,trades[0].Time.Before(trades[1].Time.Time())) + assert.True(t, trades[0].Time.Before(trades[1].Time.Time())) } diff --git a/pkg/types/sortino.go b/pkg/types/sortino.go new file mode 100644 index 0000000000..32acdb3f47 --- /dev/null +++ b/pkg/types/sortino.go @@ -0,0 +1,55 @@ +package types + +import ( + "math" +) + +// Sortino: Calcluates the sotino ratio of access returns +// +// ROI_excess E[ROI] - ROI_risk_free +// sortino = ---------- = ----------------------- +// risk sqrt(E[ROI_drawdown^2]) +// +// @param returns (Series): Series of profit/loss percentage every specific interval +// @param riskFreeReturns (float): risk-free return rate of year +// @param periods (int): Freq. of returns (252/365 for daily, 12 for monthy, 1 for annually) +// @param annualize (bool): return annualize sortino? +// @param smart (bool): return smart sharpe ratio +func Sortino(returns Series, riskFreeReturns float64, periods int, annualize bool, smart bool) float64 { + avgRiskFreeReturns := 0. + excessReturn := Mean(returns) + if riskFreeReturns > 0. && periods > 0 { + avgRiskFreeReturns = avgReturnRate(riskFreeReturns, periods) + excessReturn -= avgRiskFreeReturns + } + + num := returns.Length() + if num == 0 { + return 0 + } + var sum = 0. + for i := 0; i < num; i++ { + exRet := returns.Index(i) - avgRiskFreeReturns + if exRet < 0 { + sum += exRet * exRet + } + } + var risk = math.Sqrt(sum / float64(num)) + if smart { + risk *= autocorrPenalty(returns) + } + if risk == 0 { + if excessReturn > 0 { + return math.Inf(1) + } else if excessReturn < 0 { + return math.Inf(-1) + } else { + return 0 + } + } + result := excessReturn / risk + if annualize { + return result * math.Sqrt(float64(periods)) + } + return result +} diff --git a/pkg/types/sortino_test.go b/pkg/types/sortino_test.go new file mode 100644 index 0000000000..bb8052757e --- /dev/null +++ b/pkg/types/sortino_test.go @@ -0,0 +1,29 @@ +package types + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/datatype/floats" +) + +/* +python + +import quantstats as qx +import pandas as pd + +print(qx.stats.sortino(pd.Series([0.01, -0.03, 0.1, -0.02, 0.001]), 0.00, 0, False, False)) +print(qx.stats.sortino(pd.Series([0.01, -0.03, 0.1, -0.02, 0.001]), 0.03, 252, False, False)) +print(qx.stats.sortino(pd.Series([0.01, -0.03, 0.1, -0.02, 0.001]), 0.03, 252, True, False)) +*/ +func TestSortino(t *testing.T) { + var a Series = &floats.Slice{0.01, -0.03, 0.1, -0.02, 0.001} + output := Sortino(a, 0.03, 0, false, false) + assert.InDelta(t, output, 0.75661, 0.0001) + output = Sortino(a, 0.03, 252, false, false) + assert.InDelta(t, output, 0.74597, 0.0001) + output = Sortino(a, 0.03, 252, true, false) + assert.InDelta(t, output, 11.84192, 0.0001) +} diff --git a/pkg/types/standardstream_callbacks.go b/pkg/types/standardstream_callbacks.go index 19fd476907..e0aa0d0fc8 100644 --- a/pkg/types/standardstream_callbacks.go +++ b/pkg/types/standardstream_callbacks.go @@ -134,6 +134,16 @@ func (s *StandardStream) EmitMarketTrade(trade Trade) { } } +func (s *StandardStream) OnAggTrade(cb func(trade Trade)) { + s.aggTradeCallbacks = append(s.aggTradeCallbacks, cb) +} + +func (s *StandardStream) EmitAggTrade(trade Trade) { + for _, cb := range s.aggTradeCallbacks { + cb(trade) + } +} + func (s *StandardStream) OnFuturesPositionUpdate(cb func(futuresPositions FuturesPositionMap)) { s.FuturesPositionUpdateCallbacks = append(s.FuturesPositionUpdateCallbacks, cb) } @@ -181,6 +191,8 @@ type StandardStreamEventHub interface { OnMarketTrade(cb func(trade Trade)) + OnAggTrade(cb func(trade Trade)) + OnFuturesPositionUpdate(cb func(futuresPositions FuturesPositionMap)) OnFuturesPositionSnapshot(cb func(futuresPositions FuturesPositionMap)) diff --git a/pkg/types/stream.go b/pkg/types/stream.go index a45a2d3fc6..ffef7ffeb4 100644 --- a/pkg/types/stream.go +++ b/pkg/types/stream.go @@ -28,7 +28,9 @@ type Stream interface { StandardStreamEventHub Subscribe(channel Channel, symbol string, options SubscribeOptions) + GetSubscriptions() []Subscription SetPublicOnly() + GetPublicOnly() bool Connect(ctx context.Context) error Close() error } @@ -98,12 +100,34 @@ type StandardStream struct { marketTradeCallbacks []func(trade Trade) + aggTradeCallbacks []func(trade Trade) + // Futures FuturesPositionUpdateCallbacks []func(futuresPositions FuturesPositionMap) FuturesPositionSnapshotCallbacks []func(futuresPositions FuturesPositionMap) } +type StandardStreamEmitter interface { + Stream + EmitStart() + EmitConnect() + EmitDisconnect() + EmitTradeUpdate(Trade) + EmitOrderUpdate(Order) + EmitBalanceSnapshot(BalanceMap) + EmitBalanceUpdate(BalanceMap) + EmitKLineClosed(KLine) + EmitKLine(KLine) + EmitBookUpdate(SliceOrderBook) + EmitBookTickerUpdate(BookTicker) + EmitBookSnapshot(SliceOrderBook) + EmitMarketTrade(Trade) + EmitAggTrade(Trade) + EmitFuturesPositionUpdate(FuturesPositionMap) + EmitFuturesPositionSnapshot(FuturesPositionMap) +} + func NewStandardStream() StandardStream { return StandardStream{ ReconnectC: make(chan struct{}, 1), @@ -115,6 +139,10 @@ func (s *StandardStream) SetPublicOnly() { s.PublicOnly = true } +func (s *StandardStream) GetPublicOnly() bool { + return s.PublicOnly +} + func (s *StandardStream) SetEndpointCreator(creator EndpointCreator) { s.endpointCreator = creator } @@ -229,7 +257,7 @@ func (s *StandardStream) Read(ctx context.Context, conn *websocket.Conn, cancel func (s *StandardStream) ping(ctx context.Context, conn *websocket.Conn, cancel context.CancelFunc, interval time.Duration) { defer func() { cancel() - log.Debug("ping worker stopped") + log.Debug("[websocket] ping worker stopped") }() var pingTicker = time.NewTicker(interval) @@ -245,7 +273,6 @@ func (s *StandardStream) ping(ctx context.Context, conn *websocket.Conn, cancel return case <-pingTicker.C: - log.Debugf("websocket -> ping") if err := conn.WriteControl(websocket.PingMessage, nil, time.Now().Add(writeTimeout)); err != nil { log.WithError(err).Error("ping error", err) s.Reconnect() @@ -254,6 +281,10 @@ func (s *StandardStream) ping(ctx context.Context, conn *websocket.Conn, cancel } } +func (s *StandardStream) GetSubscriptions() []Subscription { + return s.Subscriptions +} + func (s *StandardStream) Subscribe(channel Channel, symbol string, options SubscribeOptions) { s.Subscriptions = append(s.Subscriptions, Subscription{ Channel: channel, @@ -348,19 +379,18 @@ func (s *StandardStream) Dial(ctx context.Context, args ...string) (*websocket.C // Unsolicited pong frames are allowed. conn.SetPingHandler(nil) conn.SetPongHandler(func(string) error { - log.Debugf("websocket <- received pong") if err := conn.SetReadDeadline(time.Now().Add(readTimeout * 2)); err != nil { log.WithError(err).Error("pong handler can not set read deadline") } return nil }) - log.Infof("websocket connected, public = %v, read timeout = %v", s.PublicOnly, readTimeout) + log.Infof("[websocket] connected, public = %v, read timeout = %v", s.PublicOnly, readTimeout) return conn, nil } func (s *StandardStream) Close() error { - log.Debugf("closing stream...") + log.Debugf("[websocket] closing stream...") // close the close signal channel, so that reader and ping worker will stop close(s.CloseC) @@ -382,7 +412,7 @@ func (s *StandardStream) Close() error { return errors.Wrap(err, "websocket write close message error") } - log.Debugf("stream closed") + log.Debugf("[websocket] stream closed") // let the reader close the connection <-time.After(time.Second) @@ -410,14 +440,14 @@ const ( // SubscribeOptions provides the standard stream options type SubscribeOptions struct { // TODO: change to Interval type later - Interval string `json:"interval,omitempty"` - Depth Depth `json:"depth,omitempty"` - Speed Speed `json:"speed,omitempty"` + Interval Interval `json:"interval,omitempty"` + Depth Depth `json:"depth,omitempty"` + Speed Speed `json:"speed,omitempty"` } func (o SubscribeOptions) String() string { if len(o.Interval) > 0 { - return o.Interval + return string(o.Interval) } return string(o.Depth) diff --git a/pkg/types/streamorderbook_callbacks.go b/pkg/types/streamorderbook_callbacks.go new file mode 100644 index 0000000000..ac7e3ed16d --- /dev/null +++ b/pkg/types/streamorderbook_callbacks.go @@ -0,0 +1,25 @@ +// Code generated by "callbackgen -type StreamOrderBook"; DO NOT EDIT. + +package types + +import () + +func (sb *StreamOrderBook) OnUpdate(cb func(update SliceOrderBook)) { + sb.updateCallbacks = append(sb.updateCallbacks, cb) +} + +func (sb *StreamOrderBook) EmitUpdate(update SliceOrderBook) { + for _, cb := range sb.updateCallbacks { + cb(update) + } +} + +func (sb *StreamOrderBook) OnSnapshot(cb func(snapshot SliceOrderBook)) { + sb.snapshotCallbacks = append(sb.snapshotCallbacks, cb) +} + +func (sb *StreamOrderBook) EmitSnapshot(snapshot SliceOrderBook) { + for _, cb := range sb.snapshotCallbacks { + cb(snapshot) + } +} diff --git a/pkg/types/time.go b/pkg/types/time.go index 8502b605c2..5bc811c85b 100644 --- a/pkg/types/time.go +++ b/pkg/types/time.go @@ -7,8 +7,6 @@ import ( "strconv" "strings" "time" - - "github.com/c9s/bbgo/pkg/util" ) var numOfDigitsOfUnixTimestamp = len(strconv.FormatInt(time.Now().Unix(), 10)) @@ -115,8 +113,7 @@ func (t *MillisecondTimestamp) UnmarshalJSON(data []byte) error { } - // fallback to RFC3339 - return (*time.Time)(t).UnmarshalJSON(data) + // Unreachable } func convertFloat64ToTime(vt string, f float64) (time.Time, error) { @@ -166,6 +163,10 @@ func (t Time) UnixMilli() int64 { return time.Time(t).UnixMilli() } +func (t Time) Equal(time2 time.Time) bool { + return time.Time(t).Equal(time2) +} + func (t Time) After(time2 time.Time) bool { return time.Time(t).After(time2) } @@ -188,6 +189,11 @@ func (t Time) Value() (driver.Value, error) { } func (t *Time) Scan(src interface{}) error { + // skip nil time + if src == nil { + return nil + } + switch d := src.(type) { case *time.Time: @@ -235,18 +241,47 @@ var looseTimeFormats = []string{ // LooseFormatTime parses date time string with a wide range of formats. type LooseFormatTime time.Time +func ParseLooseFormatTime(s string) (LooseFormatTime, error) { + var t time.Time + switch s { + case "now": + t = time.Now() + return LooseFormatTime(t), nil + + case "yesterday": + t = time.Now().AddDate(0, 0, -1) + return LooseFormatTime(t), nil + + case "last month": + t = time.Now().AddDate(0, -1, 0) + return LooseFormatTime(t), nil + + case "last year": + t = time.Now().AddDate(-1, 0, 0) + return LooseFormatTime(t), nil + + } + + tv, err := ParseTimeWithFormats(s, looseTimeFormats) + if err != nil { + return LooseFormatTime{}, err + } + + return LooseFormatTime(tv), nil +} + func (t *LooseFormatTime) UnmarshalYAML(unmarshal func(interface{}) error) error { var str string if err := unmarshal(&str); err != nil { return err } - tv, err := util.ParseTimeWithFormats(str, looseTimeFormats) + lt, err := ParseLooseFormatTime(str) if err != nil { return err } - *t = LooseFormatTime(tv) + *t = lt return nil } @@ -257,7 +292,7 @@ func (t *LooseFormatTime) UnmarshalJSON(data []byte) error { return err } - tv, err := util.ParseTimeWithFormats(v, looseTimeFormats) + tv, err := ParseTimeWithFormats(v, looseTimeFormats) if err != nil { return err } @@ -266,6 +301,10 @@ func (t *LooseFormatTime) UnmarshalJSON(data []byte) error { return nil } +func (t LooseFormatTime) MarshalJSON() ([]byte, error) { + return []byte(strconv.Quote(time.Time(t).Format(time.RFC3339))), nil +} + func (t LooseFormatTime) Time() time.Time { return time.Time(t) } @@ -299,3 +338,23 @@ func (t *Timestamp) UnmarshalJSON(o []byte) error { *t = Timestamp(time.Unix(timestamp, 0)) return nil } + +func ParseTimeWithFormats(strTime string, formats []string) (time.Time, error) { + for _, format := range formats { + tt, err := time.Parse(format, strTime) + if err == nil { + return tt, nil + } + } + return time.Time{}, fmt.Errorf("failed to parse time %s, valid formats are %+v", strTime, formats) +} + +func BeginningOfTheDay(t time.Time) time.Time { + year, month, day := t.Date() + return time.Date(year, month, day, 0, 0, 0, 0, t.Location()) +} + +func Over24Hours(since time.Time) bool { + return time.Since(since) >= 24*time.Hour +} + diff --git a/pkg/types/time_test.go b/pkg/types/time_test.go index ae21984cf3..9fcb613c32 100644 --- a/pkg/types/time_test.go +++ b/pkg/types/time_test.go @@ -7,6 +7,22 @@ import ( "github.com/stretchr/testify/assert" ) +func TestParseLooseFormatTime_alias_now(t *testing.T) { + lt, err := ParseLooseFormatTime("now") + assert.NoError(t, err) + + now := time.Now() + assert.True(t, now.Sub(lt.Time()) < 10*time.Millisecond) +} + +func TestParseLooseFormatTime_alias_yesterday(t *testing.T) { + lt, err := ParseLooseFormatTime("yesterday") + assert.NoError(t, err) + + tt := time.Now().AddDate(0, 0, -1) + assert.True(t, tt.Sub(lt.Time()) < 10*time.Millisecond) +} + func TestLooseFormatTime_UnmarshalJSON(t *testing.T) { tests := []struct { name string diff --git a/pkg/types/trade.go b/pkg/types/trade.go index 02ddfaeaf2..0db78b6efe 100644 --- a/pkg/types/trade.go +++ b/pkg/types/trade.go @@ -11,7 +11,7 @@ import ( "github.com/slack-go/slack" "github.com/c9s/bbgo/pkg/fixedpoint" - "github.com/c9s/bbgo/pkg/util" + "github.com/c9s/bbgo/pkg/util/templateutil" ) func init() { @@ -27,7 +27,7 @@ type TradeSlice struct { func (s *TradeSlice) Copy() []Trade { s.mu.Lock() - slice := make([]Trade, len(s.Trades), len(s.Trades)) + slice := make([]Trade, len(s.Trades)) copy(slice, s.Trades) s.mu.Unlock() @@ -74,10 +74,34 @@ type Trade struct { // The following fields are null-able fields // StrategyID is the strategy that execute this trade - StrategyID sql.NullString `json:"strategyID" db:"strategy"` + StrategyID sql.NullString `json:"strategyID" db:"strategy"` // PnL is the profit and loss value of the executed trade - PnL sql.NullFloat64 `json:"pnl" db:"pnl"` + PnL sql.NullFloat64 `json:"pnl" db:"pnl"` +} + +func (trade Trade) CsvHeader() []string { + return []string{"id", "order_id", "exchange", "symbol", "price", "quantity", "quote_quantity", "side", "is_buyer", "is_maker", "fee", "fee_currency", "time"} +} + +func (trade Trade) CsvRecords() [][]string { + return [][]string{ + { + strconv.FormatUint(trade.ID, 10), + strconv.FormatUint(trade.OrderID, 10), + trade.Exchange.String(), + trade.Symbol, + trade.Price.String(), + trade.Quantity.String(), + trade.QuoteQuantity.String(), + trade.Side.String(), + strconv.FormatBool(trade.IsBuyer), + strconv.FormatBool(trade.IsMaker), + trade.Fee.String(), + trade.FeeCurrency, + trade.Time.Time().Format(time.RFC1123), + }, + } } func (trade Trade) PositionChange() fixedpoint.Value { @@ -122,7 +146,7 @@ func trimTrailingZero(a float64) string { // String is for console output func (trade Trade) String() string { - return fmt.Sprintf("TRADE %s %s %4s %s @ %s amount %s fee %s %s orderID %d %s", + return fmt.Sprintf("TRADE %s %s %4s %-4s @ %-6s | AMOUNT %s | FEE %s %s | OrderID %d | TID %d | %s", trade.Exchange.String(), trade.Symbol, trade.Side, @@ -132,6 +156,7 @@ func (trade Trade) String() string { trade.Fee.String(), trade.FeeCurrency, trade.OrderID, + trade.ID, trade.Time.Time().Format(time.StampMilli), ) } @@ -151,25 +176,6 @@ func (trade Trade) PlainText() string { var slackTradeTextTemplate = ":handshake: Trade {{ .Symbol }} {{ .Side }} {{ .Quantity }} @ {{ .Price }}" -func exchangeFooterIcon(exName ExchangeName) string { - footerIcon := "" - - switch exName { - case ExchangeBinance: - footerIcon = "https://bin.bnbstatic.com/static/images/common/favicon.ico" - case ExchangeMax: - footerIcon = "https://max.maicoin.com/favicon-16x16.png" - case ExchangeFTX: - footerIcon = "https://ftx.com/favicon.ico?v=2" - case ExchangeOKEx: - footerIcon = "https://static.okex.com/cdn/assets/imgs/MjAxODg/D91A7323087D31A588E0D2A379DD7747.png" - case ExchangeKucoin: - footerIcon = "https://assets.staticimg.com/cms/media/7AV75b9jzr9S8H3eNuOuoqj8PwdUjaDQGKGczGqTS.png" - } - - return footerIcon -} - func (trade Trade) SlackAttachment() slack.Attachment { var color = "#DC143C" @@ -178,8 +184,8 @@ func (trade Trade) SlackAttachment() slack.Attachment { } liquidity := trade.Liquidity() - text := util.Render(slackTradeTextTemplate, trade) - footerIcon := exchangeFooterIcon(trade.Exchange) + text := templateutil.Render(slackTradeTextTemplate, trade) + footerIcon := ExchangeFooterIcon(trade.Exchange) return slack.Attachment{ Text: text, @@ -197,7 +203,7 @@ func (trade Trade) SlackAttachment() slack.Attachment { {Title: "Order ID", Value: strconv.FormatUint(trade.OrderID, 10), Short: true}, }, FooterIcon: footerIcon, - Footer: strings.ToLower(trade.Exchange.String()) + util.Render(" creation time {{ . }}", trade.Time.Time().Format(time.StampMilli)), + Footer: strings.ToLower(trade.Exchange.String()) + templateutil.Render(" creation time {{ . }}", trade.Time.Time().Format(time.StampMilli)), } } @@ -224,3 +230,7 @@ type TradeKey struct { ID uint64 Side SideType } + +func (k TradeKey) String() string { + return k.Exchange.String() + strconv.FormatUint(k.ID, 10) + k.Side.String() +} diff --git a/pkg/types/trade_stats.go b/pkg/types/trade_stats.go new file mode 100644 index 0000000000..4bbb688fac --- /dev/null +++ b/pkg/types/trade_stats.go @@ -0,0 +1,427 @@ +package types + +import ( + "encoding/json" + "math" + "sort" + "strconv" + "time" + + log "github.com/sirupsen/logrus" + + "gopkg.in/yaml.v3" + + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/fixedpoint" +) + +type IntervalProfitCollector struct { + Interval Interval `json:"interval"` + Profits *floats.Slice `json:"profits"` + Timestamp *floats.Slice `json:"timestamp"` + tmpTime time.Time `json:"tmpTime"` +} + +func NewIntervalProfitCollector(i Interval, startTime time.Time) *IntervalProfitCollector { + return &IntervalProfitCollector{Interval: i, tmpTime: startTime, Profits: &floats.Slice{1.}, Timestamp: &floats.Slice{float64(startTime.Unix())}} +} + +// Update the collector by every traded profit +func (s *IntervalProfitCollector) Update(profit *Profit) { + if s.tmpTime.IsZero() { + panic("No valid start time. Did you create IntervalProfitCollector instance using NewIntervalProfitCollector?") + } else { + duration := s.Interval.Duration() + if profit.TradedAt.Before(s.tmpTime.Add(duration)) { + (*s.Profits)[len(*s.Profits)-1] *= 1. + profit.NetProfitMargin.Float64() + } else { + for { + s.Profits.Update(1.) + s.tmpTime = s.tmpTime.Add(duration) + s.Timestamp.Update(float64(s.tmpTime.Unix())) + if profit.TradedAt.Before(s.tmpTime.Add(duration)) { + (*s.Profits)[len(*s.Profits)-1] *= 1. + profit.NetProfitMargin.Float64() + break + } + } + } + } +} + +type ProfitReport struct { + StartTime time.Time `json:"startTime"` + Profit float64 `json:"profit"` + Interval Interval `json:"interval"` +} + +func (s ProfitReport) String() string { + b, err := json.MarshalIndent(s, "", "\t") + if err != nil { + log.Fatal(err) + } + return string(b) +} + +// Get all none-profitable intervals +func (s *IntervalProfitCollector) GetNonProfitableIntervals() (result []ProfitReport) { + if s.Profits == nil { + return result + } + l := s.Profits.Length() + for i := 0; i < l; i++ { + if s.Profits.Index(i) <= 1. { + result = append(result, ProfitReport{StartTime: time.Unix(int64(s.Timestamp.Index(i)), 0), Profit: s.Profits.Index(i), Interval: s.Interval}) + } + } + return result +} + +// Get all profitable intervals +func (s *IntervalProfitCollector) GetProfitableIntervals() (result []ProfitReport) { + if s.Profits == nil { + return result + } + l := s.Profits.Length() + for i := 0; i < l; i++ { + if s.Profits.Index(i) > 1. { + result = append(result, ProfitReport{StartTime: time.Unix(int64(s.Timestamp.Index(i)), 0), Profit: s.Profits.Index(i), Interval: s.Interval}) + } + } + return result +} + +// Get number of profitable traded intervals +func (s *IntervalProfitCollector) GetNumOfProfitableIntervals() (profit int) { + if s.Profits == nil { + panic("profits array empty. Did you create IntervalProfitCollector instance using NewIntervalProfitCollector?") + } + for _, v := range *s.Profits { + if v > 1. { + profit += 1 + } + } + return profit +} + +// Get number of non-profitable traded intervals +// (no trade within the interval or pnl = 0 will be also included here) +func (s *IntervalProfitCollector) GetNumOfNonProfitableIntervals() (nonprofit int) { + if s.Profits == nil { + panic("profits array empty. Did you create IntervalProfitCollector instance using NewIntervalProfitCollector?") + } + for _, v := range *s.Profits { + if v <= 1. { + nonprofit += 1 + } + } + return nonprofit +} + +// Get sharpe value with the interval of profit collected. +// no smart sharpe ON for the calculated result +func (s *IntervalProfitCollector) GetSharpe() float64 { + if s.tmpTime.IsZero() { + panic("No valid start time. Did you create IntervalProfitCollector instance using NewIntervalProfitCollector?") + } + if s.Profits == nil { + panic("profits array empty. Did you create IntervalProfitCollector instance using NewIntervalProfitCollector?") + } + return Sharpe(Minus(s.Profits, 1.), s.Profits.Length(), true, false) +} + +// Get sortino value with the interval of profit collected. +// No risk-free return rate and smart sortino OFF for the calculated result. +func (s *IntervalProfitCollector) GetSortino() float64 { + if s.tmpTime.IsZero() { + panic("No valid start time. Did you create IntervalProfitCollector instance using NewIntervalProfitCollector?") + } + if s.Profits == nil { + panic("profits array empty. Did you create IntervalProfitCollector instance using NewIntervalProfitCollector?") + } + return Sortino(Minus(s.Profits, 1.), 0., s.Profits.Length(), true, false) +} + +func (s *IntervalProfitCollector) GetOmega() float64 { + return Omega(Minus(s.Profits, 1.)) +} + +func (s IntervalProfitCollector) MarshalYAML() (interface{}, error) { + result := make(map[string]interface{}) + result["Sharpe Ratio"] = s.GetSharpe() + result["Sortino Ratio"] = s.GetSortino() + result["Omega Ratio"] = s.GetOmega() + result["Profitable Count"] = s.GetNumOfProfitableIntervals() + result["NonProfitable Count"] = s.GetNumOfNonProfitableIntervals() + return result, nil +} + +// TODO: Add more stats from the reference: +// See https://www.metatrader5.com/en/terminal/help/algotrading/testing_report +type TradeStats struct { + Symbol string `json:"symbol,omitempty"` + + WinningRatio fixedpoint.Value `json:"winningRatio" yaml:"winningRatio"` + NumOfLossTrade int `json:"numOfLossTrade" yaml:"numOfLossTrade"` + NumOfProfitTrade int `json:"numOfProfitTrade" yaml:"numOfProfitTrade"` + + GrossProfit fixedpoint.Value `json:"grossProfit" yaml:"grossProfit"` + GrossLoss fixedpoint.Value `json:"grossLoss" yaml:"grossLoss"` + + Profits []fixedpoint.Value `json:"profits,omitempty" yaml:"profits,omitempty"` + Losses []fixedpoint.Value `json:"losses,omitempty" yaml:"losses,omitempty"` + + orderProfits map[uint64][]*Profit + + LargestProfitTrade fixedpoint.Value `json:"largestProfitTrade,omitempty" yaml:"largestProfitTrade"` + LargestLossTrade fixedpoint.Value `json:"largestLossTrade,omitempty" yaml:"largestLossTrade"` + AverageProfitTrade fixedpoint.Value `json:"averageProfitTrade" yaml:"averageProfitTrade"` + AverageLossTrade fixedpoint.Value `json:"averageLossTrade" yaml:"averageLossTrade"` + + ProfitFactor fixedpoint.Value `json:"profitFactor" yaml:"profitFactor"` + TotalNetProfit fixedpoint.Value `json:"totalNetProfit" yaml:"totalNetProfit"` + IntervalProfits map[Interval]*IntervalProfitCollector `json:"intervalProfits,omitempty" yaml:"intervalProfits,omitempty"` + + // MaximumConsecutiveWins - (counter) the longest series of winning trades + MaximumConsecutiveWins int `json:"maximumConsecutiveWins" yaml:"maximumConsecutiveWins"` + + // MaximumConsecutiveLosses - (counter) the longest series of losing trades + MaximumConsecutiveLosses int `json:"maximumConsecutiveLosses" yaml:"maximumConsecutiveLosses"` + + // MaximumConsecutiveProfit - ($) the longest series of winning trades and their total profit; + MaximumConsecutiveProfit fixedpoint.Value `json:"maximumConsecutiveProfit" yaml:"maximumConsecutiveProfit"` + + // MaximumConsecutiveLoss - ($) the longest series of losing trades and their total loss; + MaximumConsecutiveLoss fixedpoint.Value `json:"maximumConsecutiveLoss" yaml:"maximumConsecutiveLoss"` + + lastOrderID uint64 + consecutiveSide int + consecutiveCounter int + consecutiveAmount fixedpoint.Value +} + +func NewTradeStats(symbol string) *TradeStats { + return &TradeStats{Symbol: symbol, IntervalProfits: make(map[Interval]*IntervalProfitCollector)} +} + +// Set IntervalProfitCollector explicitly to enable the sharpe ratio calculation +func (s *TradeStats) SetIntervalProfitCollector(c *IntervalProfitCollector) { + s.IntervalProfits[c.Interval] = c +} + +func (s *TradeStats) CsvHeader() []string { + return []string{ + "winningRatio", + "numOfProfitTrade", + "numOfLossTrade", + "grossProfit", + "grossLoss", + "profitFactor", + "largestProfitTrade", + "largestLossTrade", + "maximumConsecutiveWins", + "maximumConsecutiveLosses", + } +} + +func (s *TradeStats) CsvRecords() [][]string { + return [][]string{ + { + s.WinningRatio.String(), + strconv.Itoa(s.NumOfProfitTrade), + strconv.Itoa(s.NumOfLossTrade), + s.GrossProfit.String(), + s.GrossLoss.String(), + s.ProfitFactor.String(), + s.LargestProfitTrade.String(), + s.LargestLossTrade.String(), + strconv.Itoa(s.MaximumConsecutiveWins), + strconv.Itoa(s.MaximumConsecutiveLosses), + }, + } +} + +func (s *TradeStats) Add(profit *Profit) { + if s.Symbol != "" && profit.Symbol != s.Symbol { + return + } + + if s.orderProfits == nil { + s.orderProfits = make(map[uint64][]*Profit) + } + + if profit.OrderID > 0 { + s.orderProfits[profit.OrderID] = append(s.orderProfits[profit.OrderID], profit) + } + + s.add(profit) + + for _, v := range s.IntervalProfits { + v.Update(profit) + } +} + +func grossLossReducer(prev, curr fixedpoint.Value) fixedpoint.Value { + if curr.Sign() < 0 { + return prev.Add(curr) + } + + return prev +} + +func grossProfitReducer(prev, curr fixedpoint.Value) fixedpoint.Value { + if curr.Sign() > 0 { + return prev.Add(curr) + } + + return prev +} + +// Recalculate the trade stats fields from the orderProfits +// this is for live-trading, one order may have many trades, and we need to merge them. +func (s *TradeStats) Recalculate() { + if len(s.orderProfits) == 0 { + return + } + + var profitsByOrder []fixedpoint.Value + var netProfitsByOrder []fixedpoint.Value + for _, profits := range s.orderProfits { + var sumProfit = fixedpoint.Zero + var sumNetProfit = fixedpoint.Zero + for _, p := range profits { + sumProfit = sumProfit.Add(p.Profit) + sumNetProfit = sumNetProfit.Add(p.NetProfit) + } + + profitsByOrder = append(profitsByOrder, sumProfit) + netProfitsByOrder = append(netProfitsByOrder, sumNetProfit) + } + + s.NumOfProfitTrade = fixedpoint.Count(profitsByOrder, fixedpoint.PositiveTester) + s.NumOfLossTrade = fixedpoint.Count(profitsByOrder, fixedpoint.NegativeTester) + s.TotalNetProfit = fixedpoint.Reduce(profitsByOrder, fixedpoint.SumReducer) + s.GrossProfit = fixedpoint.Reduce(profitsByOrder, grossProfitReducer) + s.GrossLoss = fixedpoint.Reduce(profitsByOrder, grossLossReducer) + + sort.Sort(fixedpoint.Descending(profitsByOrder)) + sort.Sort(fixedpoint.Descending(netProfitsByOrder)) + + s.Profits = fixedpoint.Filter(profitsByOrder, fixedpoint.PositiveTester) + s.Losses = fixedpoint.Filter(profitsByOrder, fixedpoint.NegativeTester) + s.LargestProfitTrade = profitsByOrder[0] + s.LargestLossTrade = profitsByOrder[len(profitsByOrder)-1] + if s.LargestLossTrade.Sign() > 0 { + s.LargestLossTrade = fixedpoint.Zero + } + + s.ProfitFactor = s.GrossProfit.Div(s.GrossLoss.Abs()) + if len(s.Profits) > 0 { + s.AverageProfitTrade = fixedpoint.Avg(s.Profits) + } + if len(s.Losses) > 0 { + s.AverageLossTrade = fixedpoint.Avg(s.Losses) + } + + s.updateWinningRatio() +} + +func (s *TradeStats) add(profit *Profit) { + pnl := profit.Profit + + // order id changed + if s.lastOrderID != profit.OrderID { + if pnl.Sign() > 0 { + s.NumOfProfitTrade++ + s.GrossProfit = s.GrossProfit.Add(pnl) + + if s.consecutiveSide == 0 { + s.consecutiveSide = 1 + s.consecutiveCounter = 1 + s.consecutiveAmount = pnl + } else if s.consecutiveSide == 1 { + s.consecutiveCounter++ + s.consecutiveAmount = s.consecutiveAmount.Add(pnl) + s.MaximumConsecutiveWins = int(math.Max(float64(s.MaximumConsecutiveWins), float64(s.consecutiveCounter))) + s.MaximumConsecutiveProfit = fixedpoint.Max(s.MaximumConsecutiveProfit, s.consecutiveAmount) + } else { + s.MaximumConsecutiveLosses = int(math.Max(float64(s.MaximumConsecutiveLosses), float64(s.consecutiveCounter))) + s.MaximumConsecutiveLoss = fixedpoint.Min(s.MaximumConsecutiveLoss, s.consecutiveAmount) + s.consecutiveSide = 1 + s.consecutiveCounter = 1 + s.consecutiveAmount = pnl + } + } else { + s.NumOfLossTrade++ + s.GrossLoss = s.GrossLoss.Add(pnl) + + if s.consecutiveSide == 0 { + s.consecutiveSide = -1 + s.consecutiveCounter = 1 + s.consecutiveAmount = pnl + } else if s.consecutiveSide == -1 { + s.consecutiveCounter++ + s.consecutiveAmount = s.consecutiveAmount.Add(pnl) + s.MaximumConsecutiveLosses = int(math.Max(float64(s.MaximumConsecutiveLosses), float64(s.consecutiveCounter))) + s.MaximumConsecutiveLoss = fixedpoint.Min(s.MaximumConsecutiveLoss, s.consecutiveAmount) + } else { // was profit, now loss, store the last win and profit + s.MaximumConsecutiveWins = int(math.Max(float64(s.MaximumConsecutiveWins), float64(s.consecutiveCounter))) + s.MaximumConsecutiveProfit = fixedpoint.Max(s.MaximumConsecutiveProfit, s.consecutiveAmount) + s.consecutiveSide = -1 + s.consecutiveCounter = 1 + s.consecutiveAmount = pnl + } + } + } else { + s.consecutiveAmount = s.consecutiveAmount.Add(pnl) + } + + s.lastOrderID = profit.OrderID + s.TotalNetProfit = s.TotalNetProfit.Add(pnl) + s.ProfitFactor = s.GrossProfit.Div(s.GrossLoss.Abs()) + + s.updateWinningRatio() +} + +func (s *TradeStats) updateWinningRatio() { + // The win/loss ratio is your wins divided by your losses. + // In the example, suppose for the sake of simplicity that 60 trades were winners, and 40 were losers. + // Your win/loss ratio would be 60/40 = 1.5. That would mean that you are winning 50% more often than you are losing. + if s.NumOfLossTrade == 0 && s.NumOfProfitTrade == 0 { + s.WinningRatio = fixedpoint.Zero + } else if s.NumOfLossTrade == 0 && s.NumOfProfitTrade > 0 { + s.WinningRatio = fixedpoint.One + } else { + s.WinningRatio = fixedpoint.NewFromFloat(float64(s.NumOfProfitTrade) / float64(s.NumOfLossTrade)) + } +} + +// Output TradeStats without Profits and Losses +func (s *TradeStats) BriefString() string { + s.Recalculate() + out, _ := yaml.Marshal(&TradeStats{ + Symbol: s.Symbol, + WinningRatio: s.WinningRatio, + NumOfLossTrade: s.NumOfLossTrade, + NumOfProfitTrade: s.NumOfProfitTrade, + GrossProfit: s.GrossProfit, + GrossLoss: s.GrossLoss, + LargestProfitTrade: s.LargestProfitTrade, + LargestLossTrade: s.LargestLossTrade, + AverageProfitTrade: s.AverageProfitTrade, + AverageLossTrade: s.AverageLossTrade, + ProfitFactor: s.ProfitFactor, + TotalNetProfit: s.TotalNetProfit, + IntervalProfits: s.IntervalProfits, + MaximumConsecutiveWins: s.MaximumConsecutiveWins, + MaximumConsecutiveLosses: s.MaximumConsecutiveLosses, + MaximumConsecutiveProfit: s.MaximumConsecutiveProfit, + MaximumConsecutiveLoss: s.MaximumConsecutiveLoss, + }) + return string(out) +} + +func (s *TradeStats) String() string { + s.Recalculate() + out, _ := yaml.Marshal(s) + return string(out) +} diff --git a/pkg/types/trade_stats_test.go b/pkg/types/trade_stats_test.go new file mode 100644 index 0000000000..a543476d44 --- /dev/null +++ b/pkg/types/trade_stats_test.go @@ -0,0 +1,45 @@ +package types + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/fixedpoint" +) + +func number(v float64) fixedpoint.Value { + return fixedpoint.NewFromFloat(v) +} + +func TestTradeStats_consecutiveCounterAndAmount(t *testing.T) { + stats := NewTradeStats("BTCUSDT") + stats.add(&Profit{OrderID: 1, Profit: number(20.0)}) + stats.add(&Profit{OrderID: 1, Profit: number(30.0)}) + + assert.Equal(t, 1, stats.consecutiveSide) + assert.Equal(t, 1, stats.consecutiveCounter) + assert.Equal(t, "50", stats.consecutiveAmount.String()) + + stats.add(&Profit{OrderID: 2, Profit: number(50.0)}) + stats.add(&Profit{OrderID: 2, Profit: number(50.0)}) + assert.Equal(t, 1, stats.consecutiveSide) + assert.Equal(t, 2, stats.consecutiveCounter) + assert.Equal(t, "150", stats.consecutiveAmount.String()) + assert.Equal(t, 2, stats.MaximumConsecutiveWins) + + stats.add(&Profit{OrderID: 3, Profit: number(-50.0)}) + stats.add(&Profit{OrderID: 3, Profit: number(-50.0)}) + assert.Equal(t, -1, stats.consecutiveSide) + assert.Equal(t, 1, stats.consecutiveCounter) + assert.Equal(t, "-100", stats.consecutiveAmount.String()) + + assert.Equal(t, "150", stats.MaximumConsecutiveProfit.String()) + assert.Equal(t, "0", stats.MaximumConsecutiveLoss.String()) + + stats.add(&Profit{OrderID: 4, Profit: number(-100.0)}) + assert.Equal(t, -1, stats.consecutiveSide) + assert.Equal(t, 2, stats.consecutiveCounter) + assert.Equal(t, "-200", stats.MaximumConsecutiveLoss.String()) + assert.Equal(t, 2, stats.MaximumConsecutiveLosses) +} diff --git a/pkg/types/value_map.go b/pkg/types/value_map.go new file mode 100644 index 0000000000..9d67a68e11 --- /dev/null +++ b/pkg/types/value_map.go @@ -0,0 +1,157 @@ +package types + +import "github.com/c9s/bbgo/pkg/fixedpoint" + +type ValueMap map[string]fixedpoint.Value + +func (m ValueMap) Eq(n ValueMap) bool { + if len(m) != len(n) { + return false + } + + for m_k, m_v := range m { + n_v, ok := n[m_k] + if !ok { + return false + } + + if !m_v.Eq(n_v) { + return false + } + } + + return true +} + +func (m ValueMap) Add(n ValueMap) ValueMap { + if len(m) != len(n) { + panic("unequal length") + } + + o := ValueMap{} + + for m_k, m_v := range m { + n_v, ok := n[m_k] + if !ok { + panic("key not found") + } + + o[m_k] = m_v.Add(n_v) + } + + return o +} + +func (m ValueMap) Sub(n ValueMap) ValueMap { + if len(m) != len(n) { + panic("unequal length") + } + + o := ValueMap{} + + for m_k, m_v := range m { + n_v, ok := n[m_k] + if !ok { + panic("key not found") + } + + o[m_k] = m_v.Sub(n_v) + } + + return o +} + +func (m ValueMap) Mul(n ValueMap) ValueMap { + if len(m) != len(n) { + panic("unequal length") + } + + o := ValueMap{} + + for m_k, m_v := range m { + n_v, ok := n[m_k] + if !ok { + panic("key not found") + } + + o[m_k] = m_v.Mul(n_v) + } + + return o +} + +func (m ValueMap) Div(n ValueMap) ValueMap { + if len(m) != len(n) { + panic("unequal length") + } + + o := ValueMap{} + + for m_k, m_v := range m { + n_v, ok := n[m_k] + if !ok { + panic("key not found") + } + + o[m_k] = m_v.Div(n_v) + } + + return o +} + +func (m ValueMap) AddScalar(x fixedpoint.Value) ValueMap { + o := ValueMap{} + + for k, v := range m { + o[k] = v.Add(x) + } + + return o +} + +func (m ValueMap) SubScalar(x fixedpoint.Value) ValueMap { + o := ValueMap{} + + for k, v := range m { + o[k] = v.Sub(x) + } + + return o +} + +func (m ValueMap) MulScalar(x fixedpoint.Value) ValueMap { + o := ValueMap{} + + for k, v := range m { + o[k] = v.Mul(x) + } + + return o +} + +func (m ValueMap) DivScalar(x fixedpoint.Value) ValueMap { + o := ValueMap{} + + for k, v := range m { + o[k] = v.Div(x) + } + + return o +} + +func (m ValueMap) Sum() fixedpoint.Value { + var sum fixedpoint.Value + for _, v := range m { + sum = sum.Add(v) + } + return sum +} + +func (m ValueMap) Normalize() ValueMap { + sum := m.Sum() + if sum.Eq(fixedpoint.Zero) { + panic("zero sum") + } + + return m.DivScalar(sum) +} diff --git a/pkg/types/value_map_test.go b/pkg/types/value_map_test.go new file mode 100644 index 0000000000..c6eae497a6 --- /dev/null +++ b/pkg/types/value_map_test.go @@ -0,0 +1,125 @@ +package types + +import ( + "testing" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/stretchr/testify/assert" +) + +func Test_ValueMap_Eq(t *testing.T) { + m1 := ValueMap{ + "A": fixedpoint.NewFromFloat(3.0), + "B": fixedpoint.NewFromFloat(4.0), + } + + m2 := ValueMap{} + + m3 := ValueMap{"A": fixedpoint.NewFromFloat(5.0)} + + m4 := ValueMap{ + "A": fixedpoint.NewFromFloat(6.0), + "B": fixedpoint.NewFromFloat(7.0), + } + + m5 := ValueMap{ + "A": fixedpoint.NewFromFloat(3.0), + "B": fixedpoint.NewFromFloat(4.0), + } + + assert.True(t, m1.Eq(m1)) + assert.False(t, m1.Eq(m2)) + assert.False(t, m1.Eq(m3)) + assert.False(t, m1.Eq(m4)) + assert.True(t, m1.Eq(m5)) +} + +func Test_ValueMap_Add(t *testing.T) { + m1 := ValueMap{ + "A": fixedpoint.NewFromFloat(3.0), + "B": fixedpoint.NewFromFloat(4.0), + } + + m2 := ValueMap{ + "A": fixedpoint.NewFromFloat(5.0), + "B": fixedpoint.NewFromFloat(6.0), + } + + m3 := ValueMap{ + "A": fixedpoint.NewFromFloat(8.0), + "B": fixedpoint.NewFromFloat(10.0), + } + + m4 := ValueMap{"A": fixedpoint.NewFromFloat(8.0)} + + assert.Equal(t, m3, m1.Add(m2)) + assert.Panics(t, func() { m1.Add(m4) }) +} + +func Test_ValueMap_AddScalar(t *testing.T) { + x := fixedpoint.NewFromFloat(5.0) + + m1 := ValueMap{ + "A": fixedpoint.NewFromFloat(3.0), + "B": fixedpoint.NewFromFloat(4.0), + } + + m2 := ValueMap{ + "A": fixedpoint.NewFromFloat(3.0).Add(x), + "B": fixedpoint.NewFromFloat(4.0).Add(x), + } + + assert.Equal(t, m2, m1.AddScalar(x)) +} + +func Test_ValueMap_DivScalar(t *testing.T) { + x := fixedpoint.NewFromFloat(5.0) + + m1 := ValueMap{ + "A": fixedpoint.NewFromFloat(3.0), + "B": fixedpoint.NewFromFloat(4.0), + } + + m2 := ValueMap{ + "A": fixedpoint.NewFromFloat(3.0).Div(x), + "B": fixedpoint.NewFromFloat(4.0).Div(x), + } + + assert.Equal(t, m2, m1.DivScalar(x)) +} + +func Test_ValueMap_Sum(t *testing.T) { + m := ValueMap{ + "A": fixedpoint.NewFromFloat(3.0), + "B": fixedpoint.NewFromFloat(4.0), + } + + assert.Equal(t, fixedpoint.NewFromFloat(7.0), m.Sum()) +} + +func Test_ValueMap_Normalize(t *testing.T) { + a := fixedpoint.NewFromFloat(3.0) + b := fixedpoint.NewFromFloat(4.0) + c := a.Add(b) + + m := ValueMap{ + "A": a, + "B": b, + } + + n := ValueMap{ + "A": a.Div(c), + "B": b.Div(c), + } + + assert.True(t, m.Normalize().Eq(n)) +} + +func Test_ValueMap_Normalize_zero_sum(t *testing.T) { + m := ValueMap{ + "A": fixedpoint.Zero, + "B": fixedpoint.Zero, + } + + assert.Panics(t, func() { m.Normalize() }) +} diff --git a/pkg/types/withdraw.go b/pkg/types/withdraw.go index 6a3d5dae57..18781341cd 100644 --- a/pkg/types/withdraw.go +++ b/pkg/types/withdraw.go @@ -2,8 +2,9 @@ package types import ( "fmt" - "github.com/c9s/bbgo/pkg/fixedpoint" "time" + + "github.com/c9s/bbgo/pkg/fixedpoint" ) type Withdraw struct { @@ -23,8 +24,37 @@ type Withdraw struct { Network string `json:"network" db:"network"` } -func (w Withdraw) String() string { - return fmt.Sprintf("withdraw %s %v to %s at %s", w.Asset, w.Amount, w.Address, w.ApplyTime.Time()) +func cutstr(s string, maxLen, head, tail int) string { + if len(s) > maxLen { + l := len(s) + return s[0:head] + "..." + s[l-tail:] + } + return s +} + +func (w Withdraw) String() (o string) { + o = fmt.Sprintf("%s WITHDRAW %8f %s -> ", w.Exchange, w.Amount.Float64(), w.Asset) + + if len(w.Network) > 0 && w.Network != w.Asset { + o += w.Network + ":" + } + + o += fmt.Sprintf("%s @ %s", w.Address, w.ApplyTime.Time()) + + if !w.TransactionFee.IsZero() { + feeCurrency := w.TransactionFeeCurrency + if feeCurrency == "" { + feeCurrency = w.Asset + } + + o += fmt.Sprintf(" FEE %4f %5s", w.TransactionFee.Float64(), feeCurrency) + } + + if len(w.TransactionID) > 0 { + o += fmt.Sprintf(" TxID: %s", cutstr(w.TransactionID, 12, 4, 4)) + } + + return o } func (w Withdraw) EffectiveTime() time.Time { diff --git a/pkg/util/dir.go b/pkg/util/dir.go new file mode 100644 index 0000000000..5e1914c0e2 --- /dev/null +++ b/pkg/util/dir.go @@ -0,0 +1,23 @@ +package util + +import ( + "fmt" + "os" +) + +func SafeMkdirAll(p string) error { + st, err := os.Stat(p) + if err == nil { + if !st.IsDir() { + return fmt.Errorf("path %s is not a directory", p) + } + + return nil + } + + if os.IsNotExist(err) { + return os.MkdirAll(p, 0755) + } + + return nil +} diff --git a/pkg/exchange/max/group_id.go b/pkg/util/fnv.go similarity index 60% rename from pkg/exchange/max/group_id.go rename to pkg/util/fnv.go index 840b5bc05c..9c84294001 100644 --- a/pkg/exchange/max/group_id.go +++ b/pkg/util/fnv.go @@ -1,8 +1,8 @@ -package max +package util import "hash/fnv" -func GenerateGroupID(s string) uint32 { +func FNV32(s string) uint32 { h := fnv.New32a() h.Write([]byte(s)) return h.Sum32() diff --git a/pkg/util/json.go b/pkg/util/json.go new file mode 100644 index 0000000000..1e7740967c --- /dev/null +++ b/pkg/util/json.go @@ -0,0 +1,15 @@ +package util + +import ( + "encoding/json" + "io/ioutil" +) + +func WriteJsonFile(p string, obj interface{}) error { + out, err := json.Marshal(obj) + if err != nil { + return err + } + + return ioutil.WriteFile(p, out, 0644) +} diff --git a/pkg/util/math.go b/pkg/util/math.go index 31e49b5411..bf73885e70 100644 --- a/pkg/util/math.go +++ b/pkg/util/math.go @@ -1,9 +1,10 @@ package util import ( - "github.com/c9s/bbgo/pkg/fixedpoint" "math" "strconv" + + "github.com/c9s/bbgo/pkg/fixedpoint" ) const MaxDigits = 18 // MAX_INT64 ~ 9 * 10^18 @@ -56,3 +57,4 @@ func Zero(v float64) bool { func NotZero(v float64) bool { return math.Abs(v) > epsilon } + diff --git a/pkg/util/paper_trade.go b/pkg/util/paper_trade.go new file mode 100644 index 0000000000..b3a09d68b5 --- /dev/null +++ b/pkg/util/paper_trade.go @@ -0,0 +1,6 @@ +package util + +func IsPaperTrade() bool { + v, ok := GetEnvVarBool("PAPER_TRADE") + return ok && v +} diff --git a/pkg/util/pointer.go b/pkg/util/pointer.go new file mode 100644 index 0000000000..35d469c6eb --- /dev/null +++ b/pkg/util/pointer.go @@ -0,0 +1,8 @@ +//go:build !go1.18 +// +build !go1.18 + +package util + +import "reflect" + +const Pointer = reflect.Ptr diff --git a/pkg/util/pointer_18.go b/pkg/util/pointer_18.go new file mode 100644 index 0000000000..d2f172734b --- /dev/null +++ b/pkg/util/pointer_18.go @@ -0,0 +1,8 @@ +//go:build go1.18 +// +build go1.18 + +package util + +import "reflect" + +const Pointer = reflect.Pointer diff --git a/pkg/util/profile.go b/pkg/util/profile.go index 830e585927..1d3753aa23 100644 --- a/pkg/util/profile.go +++ b/pkg/util/profile.go @@ -1,18 +1,25 @@ package util -import "time" +import ( + "time" +) type TimeProfile struct { + Name string StartTime, EndTime time.Time Duration time.Duration } -func StartTimeProfile() TimeProfile { - return TimeProfile{StartTime: time.Now()} +func StartTimeProfile(args ...string) TimeProfile { + name := "" + if len(args) > 0 { + name = args[0] + } + return TimeProfile{StartTime: time.Now(), Name: name} } func (p *TimeProfile) TilNow() time.Duration { - return time.Now().Sub(p.StartTime) + return time.Since(p.StartTime) } func (p *TimeProfile) Stop() time.Duration { @@ -20,3 +27,16 @@ func (p *TimeProfile) Stop() time.Duration { p.Duration = p.EndTime.Sub(p.StartTime) return p.Duration } + +type logFunction func(format string, args ...interface{}) + +func (p *TimeProfile) StopAndLog(f logFunction) { + duration := p.Stop() + s := "[profile] " + if len(p.Name) > 0 { + s += p.Name + } + + s += " " + duration.String() + f(s) +} diff --git a/pkg/util/render.go b/pkg/util/render.go index 69ccb78b26..a5e4c54faa 100644 --- a/pkg/util/render.go +++ b/pkg/util/render.go @@ -1,25 +1,2 @@ package util -import ( - "bytes" - "text/template" - - "github.com/sirupsen/logrus" -) - -func Render(tpl string, args interface{}) string { - var buf = bytes.NewBuffer(nil) - tmpl, err := template.New("tmp").Parse(tpl) - if err != nil { - logrus.WithError(err).Error("template parse error") - return "" - } - - err = tmpl.Execute(buf, args) - if err != nil { - logrus.WithError(err).Error("template execute error") - return "" - } - - return buf.String() -} diff --git a/pkg/util/reonce_test.go b/pkg/util/reonce_test.go index 7a18fc4374..bd35284811 100644 --- a/pkg/util/reonce_test.go +++ b/pkg/util/reonce_test.go @@ -1,6 +1,7 @@ package util import ( + "sync" "testing" "time" @@ -10,14 +11,19 @@ import ( func TestReonce_DoAndReset(t *testing.T) { var cnt = 0 var reonce Reonce + var wgAll, wg sync.WaitGroup + wg.Add(1) + wgAll.Add(2) go reonce.Do(func() { t.Log("once #1") time.Sleep(10 * time.Millisecond) cnt++ + wg.Done() + wgAll.Done() }) // make sure it's locked - time.Sleep(10 * time.Millisecond) + wg.Wait() t.Logf("reset") reonce.Reset() @@ -25,8 +31,9 @@ func TestReonce_DoAndReset(t *testing.T) { t.Log("once #2") time.Sleep(10 * time.Millisecond) cnt++ + wgAll.Done() }) - time.Sleep(time.Second) + wgAll.Wait() assert.Equal(t, 2, cnt) } diff --git a/pkg/util/simple_args.go b/pkg/util/simple_args.go new file mode 100644 index 0000000000..e71b8ca9ce --- /dev/null +++ b/pkg/util/simple_args.go @@ -0,0 +1,31 @@ +package util + +import ( + "reflect" + "time" + + "github.com/c9s/bbgo/pkg/fixedpoint" +) + +// FilterSimpleArgs filters out the simple type arguments +// int, string, bool, and []byte +func FilterSimpleArgs(args []interface{}) (simpleArgs []interface{}) { + for _, arg := range args { + switch arg.(type) { + case int, int64, int32, uint64, uint32, string, []string, []byte, float64, []float64, float32, fixedpoint.Value, time.Time: + simpleArgs = append(simpleArgs, arg) + default: + rt := reflect.TypeOf(arg) + if rt.Kind() == reflect.Ptr { + rt = rt.Elem() + } + + switch rt.Kind() { + case reflect.Float64, reflect.Float32, reflect.String, reflect.Int, reflect.Int32, reflect.Uint32, reflect.Int64, reflect.Uint64, reflect.Bool: + simpleArgs = append(simpleArgs, arg) + } + } + } + + return simpleArgs +} diff --git a/pkg/util/string.go b/pkg/util/string.go index 268233acbc..af3b25bf44 100644 --- a/pkg/util/string.go +++ b/pkg/util/string.go @@ -1,6 +1,9 @@ package util -import "strings" +import ( + "strings" + "unicode/utf8" +) func StringSliceContains(slice []string, needle string) bool { for _, s := range slice { @@ -27,3 +30,17 @@ func MaskKey(key string) string { maskKey += key[len(key)-h:] return maskKey } + +func StringSplitByLength(s string, length int) (result []string) { + var left, right int + for left, right = 0, length; right < len(s); left, right = right, right+length { + for !utf8.RuneStart(s[right]) { + right-- + } + result = append(result, s[left:right]) + } + if len(s)-left > 0 { + result = append(result, s[left:]) + } + return result +} diff --git a/pkg/util/string_test.go b/pkg/util/string_test.go index 7d55f425a7..4f09a3a268 100644 --- a/pkg/util/string_test.go +++ b/pkg/util/string_test.go @@ -1,6 +1,10 @@ package util -import "testing" +import ( + "testing" + + "github.com/stretchr/testify/assert" +) func TestMaskKey(t *testing.T) { type args struct { @@ -40,3 +44,10 @@ func TestMaskKey(t *testing.T) { }) } } + +func TestStringSplitByLength(t *testing.T) { + result := StringSplitByLength("1234567890", 3) + assert.Equal(t, result, []string{"123", "456", "789", "0"}) + result = StringSplitByLength("123èš±456", 4) + assert.Equal(t, result, []string{"123", "èš±4", "56"}) +} diff --git a/pkg/util/templateutil/render.go b/pkg/util/templateutil/render.go new file mode 100644 index 0000000000..8e19fc1b29 --- /dev/null +++ b/pkg/util/templateutil/render.go @@ -0,0 +1,25 @@ +package templateutil + +import ( + "bytes" + "text/template" + + "github.com/sirupsen/logrus" +) + +func Render(tpl string, args interface{}) string { + var buf = bytes.NewBuffer(nil) + tmpl, err := template.New("tmp").Parse(tpl) + if err != nil { + logrus.WithError(err).Error("template parse error") + return "" + } + + err = tmpl.Execute(buf, args) + if err != nil { + logrus.WithError(err).Error("template execute error") + return "" + } + + return buf.String() +} diff --git a/pkg/util/time.go b/pkg/util/time.go index d95d1a9a14..df1ac8d888 100644 --- a/pkg/util/time.go +++ b/pkg/util/time.go @@ -1,7 +1,6 @@ package util import ( - "fmt" "math/rand" "time" ) @@ -11,25 +10,7 @@ func MillisecondsJitter(d time.Duration, jitterInMilliseconds int) time.Duration return d + time.Duration(n)*time.Millisecond } -func BeginningOfTheDay(t time.Time) time.Time { - year, month, day := t.Date() - return time.Date(year, month, day, 0, 0, 0, 0, t.Location()) -} - -func Over24Hours(since time.Time) bool { - return time.Since(since) >= 24*time.Hour -} - func UnixMilli() int64 { return time.Now().UnixNano() / int64(time.Millisecond) } -func ParseTimeWithFormats(strTime string, formats []string) (time.Time, error) { - for _, format := range formats { - tt, err := time.Parse(format, strTime) - if err == nil { - return tt, nil - } - } - return time.Time{}, fmt.Errorf("failed to parse time %s, valid formats are %+v", strTime, formats) -} diff --git a/pkg/util/trylock.go b/pkg/util/trylock.go new file mode 100644 index 0000000000..5913b76bd9 --- /dev/null +++ b/pkg/util/trylock.go @@ -0,0 +1,16 @@ +//go:build !go1.18 +// +build !go1.18 + +package util + +import "sync" + +func TryLock(lock *sync.RWMutex) bool { + lock.Lock() + return true +} + +func TryRLock(lock *sync.RWMutex) bool { + lock.RLock() + return true +} diff --git a/pkg/util/trylock_18.go b/pkg/util/trylock_18.go new file mode 100644 index 0000000000..9e9323789a --- /dev/null +++ b/pkg/util/trylock_18.go @@ -0,0 +1,14 @@ +//go:build go1.18 +// +build go1.18 + +package util + +import "sync" + +func TryLock(lock *sync.RWMutex) bool { + return lock.TryLock() +} + +func TryRLock(lock *sync.RWMutex) bool { + return lock.TryRLock() +} diff --git a/pkg/version/dev.go b/pkg/version/dev.go index a60f673f50..549f3697d3 100644 --- a/pkg/version/dev.go +++ b/pkg/version/dev.go @@ -1,8 +1,8 @@ +//go:build !release // +build !release package version -const Version = "v1.31.3-fa2eb872-dev" - -const VersionGitRef = "fa2eb872" +const Version = "v1.42.0-7204e255-dev" +const VersionGitRef = "7204e255" diff --git a/pkg/version/version.go b/pkg/version/version.go index 4bd0431748..76ec8cbfba 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -1,8 +1,8 @@ +//go:build release // +build release package version -const Version = "v1.31.3-fa2eb872" - -const VersionGitRef = "fa2eb872" +const Version = "v1.42.0-7204e255" +const VersionGitRef = "7204e255" diff --git a/python/bbgo/data/balance.py b/python/bbgo/data/balance.py index c3ab28f992..e81b58b4c0 100644 --- a/python/bbgo/data/balance.py +++ b/python/bbgo/data/balance.py @@ -1,27 +1,30 @@ from __future__ import annotations from dataclasses import dataclass +from decimal import Decimal import bbgo_pb2 +from ..utils import parse_number + @dataclass class Balance: exchange: str currency: str - available: float - locked: float - borrowed: str + available: Decimal + locked: Decimal + borrowed: Decimal @classmethod def from_pb(cls, obj: bbgo_pb2.Balance) -> Balance: return cls( exchange=obj.exchange, currency=obj.currency, - available=float(obj.available), - locked=float(obj.locked), - borrowed=obj.borrowed, + available=parse_number(obj.available), + locked=parse_number(obj.locked), + borrowed=parse_number(obj.borrowed), ) - def total(self) -> float: + def total(self) -> Decimal: return self.available + self.locked diff --git a/python/bbgo/data/depth.py b/python/bbgo/data/depth.py index 5718ea2a2c..55d9c26f85 100644 --- a/python/bbgo/data/depth.py +++ b/python/bbgo/data/depth.py @@ -1,9 +1,12 @@ from __future__ import annotations from dataclasses import dataclass +from decimal import Decimal +from typing import List + import bbgo_pb2 -from typing import List +from ..utils import parse_number @dataclass @@ -25,12 +28,12 @@ def from_pb(cls, obj: bbgo_pb2.Depth): @dataclass class PriceVolume: - price: float - volume: float + price: Decimal + volume: Decimal @classmethod def from_pb(cls, obj: bbgo_pb2.PriceVolume): return cls( - price=float(obj.price), - volume=float(obj.volume), + price=parse_number(obj.price), + volume=parse_number(obj.volume), ) diff --git a/python/bbgo/data/kline.py b/python/bbgo/data/kline.py index 3b3f89e5b4..42f3563d33 100644 --- a/python/bbgo/data/kline.py +++ b/python/bbgo/data/kline.py @@ -2,10 +2,11 @@ from dataclasses import dataclass from datetime import datetime +from decimal import Decimal import bbgo_pb2 -from ..utils import parse_float +from ..utils import parse_number from ..utils import parse_time @@ -13,15 +14,15 @@ class KLine: exchange: str symbol: str - open: float - high: float - low: float - close: float - volume: float + open: Decimal + high: Decimal + low: Decimal + close: Decimal + volume: Decimal session: str = None start_time: datetime = None end_time: datetime = None - quote_volume: float = None + quote_volume: Decimal = None closed: bool = None @classmethod @@ -29,12 +30,12 @@ def from_pb(cls, obj: bbgo_pb2.KLine) -> KLine: return cls( exchange=obj.exchange, symbol=obj.symbol, - open=parse_float(obj.open), - high=parse_float(obj.high), - low=parse_float(obj.low), - close=parse_float(obj.close), - volume=parse_float(obj.volume), - quote_volume=parse_float(obj.quote_volume), + open=parse_number(obj.open), + high=parse_number(obj.high), + low=parse_number(obj.low), + close=parse_number(obj.close), + volume=parse_number(obj.volume), + quote_volume=parse_number(obj.quote_volume), start_time=parse_time(obj.start_time), end_time=parse_time(obj.end_time), closed=obj.closed, diff --git a/python/bbgo/data/order.py b/python/bbgo/data/order.py index cc5802491d..c9c61d1a82 100644 --- a/python/bbgo/data/order.py +++ b/python/bbgo/data/order.py @@ -2,12 +2,13 @@ from dataclasses import dataclass from datetime import datetime +from decimal import Decimal import bbgo_pb2 from ..enums import OrderType from ..enums import SideType -from ..utils import parse_float +from ..utils import parse_number from ..utils import parse_time @@ -18,11 +19,11 @@ class Order: order_id: str side: SideType order_type: OrderType - price: float - stop_price: float + price: Decimal + stop_price: Decimal status: str - quantity: float - executed_quantity: float + quantity: Decimal + executed_quantity: Decimal client_order_id: str group_id: int created_at: datetime @@ -35,11 +36,11 @@ def from_pb(cls, obj: bbgo_pb2.Order) -> Order: order_id=obj.id, side=SideType(obj.side), order_type=OrderType(obj.order_type), - price=parse_float(obj.price), - stop_price=parse_float(obj.stop_price), + price=parse_number(obj.price), + stop_price=parse_number(obj.stop_price), status=obj.status, - quantity=parse_float(obj.quantity), - executed_quantity=parse_float(obj.executed_quantity), + quantity=parse_number(obj.quantity), + executed_quantity=parse_number(obj.executed_quantity), client_order_id=obj.client_order_id, group_id=obj.group_id, created_at=parse_time(obj.created_at), diff --git a/python/bbgo/data/submit_order.py b/python/bbgo/data/submit_order.py index 18f8e89d2f..bc3572bbbf 100644 --- a/python/bbgo/data/submit_order.py +++ b/python/bbgo/data/submit_order.py @@ -1,6 +1,7 @@ from __future__ import annotations from dataclasses import dataclass +from decimal import Decimal import bbgo_pb2 @@ -14,10 +15,10 @@ class SubmitOrder: exchange: str symbol: str side: SideType - quantity: float + quantity: Decimal order_type: OrderType - price: float = None - stop_price: float = None + price: Decimal = None + stop_price: Decimal = None client_order_id: str = None group_id: int = None diff --git a/python/bbgo/data/ticker.py b/python/bbgo/data/ticker.py index c43635b8b7..e809aa3c10 100644 --- a/python/bbgo/data/ticker.py +++ b/python/bbgo/data/ticker.py @@ -1,30 +1,31 @@ from __future__ import annotations from dataclasses import dataclass +from decimal import Decimal import bbgo_pb2 -from ..utils import parse_float +from ..utils import parse_number @dataclass class Ticker: exchange: str symbol: str - open: float - high: float - low: float - close: float - volume: float + open: Decimal + high: Decimal + low: Decimal + close: Decimal + volume: Decimal @classmethod def from_pb(cls, obj: bbgo_pb2.KLine) -> Ticker: return cls( exchange=obj.exchange, symbol=obj.symbol, - open=parse_float(obj.open), - high=parse_float(obj.high), - low=parse_float(obj.low), - close=parse_float(obj.close), - volume=parse_float(obj.volume), + open=parse_number(obj.open), + high=parse_number(obj.high), + low=parse_number(obj.low), + close=parse_number(obj.close), + volume=parse_number(obj.volume), ) diff --git a/python/bbgo/data/trade.py b/python/bbgo/data/trade.py index da6a335f75..9602225f30 100644 --- a/python/bbgo/data/trade.py +++ b/python/bbgo/data/trade.py @@ -2,11 +2,12 @@ from dataclasses import dataclass from datetime import datetime +from decimal import Decimal import bbgo_pb2 from ..enums import SideType -from ..utils import parse_float +from ..utils import parse_number from ..utils import parse_time @@ -16,12 +17,12 @@ class Trade: exchange: str symbol: str trade_id: str - price: float - quantity: float + price: Decimal + quantity: Decimal created_at: datetime side: SideType fee_currency: str - fee: float + fee: Decimal maker: bool @classmethod @@ -31,11 +32,11 @@ def from_pb(cls, obj: bbgo_pb2.Trade) -> Trade: exchange=obj.exchange, symbol=obj.symbol, trade_id=obj.id, - price=parse_float(obj.price), - quantity=parse_float(obj.quantity), + price=parse_number(obj.price), + quantity=parse_number(obj.quantity), created_at=parse_time(obj.created_at), side=SideType(obj.side), fee_currency=obj.fee_currency, - fee=parse_float(obj.fee), + fee=parse_number(obj.fee), maker=obj.maker, ) diff --git a/python/bbgo/utils/__init__.py b/python/bbgo/utils/__init__.py index 6e82f812b2..ff84a50651 100644 --- a/python/bbgo/utils/__init__.py +++ b/python/bbgo/utils/__init__.py @@ -1,4 +1,4 @@ -from .convert import parse_float +from .convert import parse_number from .convert import parse_time from .grpc_utils import get_credentials_from_env from .grpc_utils import get_grpc_cert_file_from_env diff --git a/python/bbgo/utils/convert.py b/python/bbgo/utils/convert.py index 8159f51539..60e35a9c7e 100644 --- a/python/bbgo/utils/convert.py +++ b/python/bbgo/utils/convert.py @@ -1,15 +1,16 @@ from datetime import datetime +from decimal import Decimal from typing import Union -def parse_float(s: Union[str, float]) -> float: +def parse_number(s: Union[str, float]) -> Decimal: if s is None: return 0 if s == "": return 0 - return float(s) + return Decimal(s) def parse_time(t: Union[str, int]) -> datetime: diff --git a/python/pyproject.toml b/python/pyproject.toml index c027736be9..4ffbcbea3e 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "bbgo" -version = "0.1.8" +version = "0.1.9" description = "" authors = ["ăȘるみ "] packages = [ diff --git a/python/tests/test_data.py b/python/tests/test_data.py index 70703c88f1..e5aee9e43e 100644 --- a/python/tests/test_data.py +++ b/python/tests/test_data.py @@ -1,3 +1,5 @@ +from decimal import Decimal + import bbgo_pb2 from bbgo.data import Balance from bbgo.data import ErrorMessage @@ -10,7 +12,7 @@ def test_balance_from_pb(): currency = 'BTCUSDT' available = '3.1415926' locked = '2.7182818' - borrowed = 'borrowed' + borrowed = '0.1234567' balance_pb = bbgo_pb2.Balance( exchange=exchange, @@ -24,9 +26,9 @@ def test_balance_from_pb(): assert balance.exchange == exchange assert balance.currency == currency - assert balance.available == float(available) - assert balance.locked == float(locked) - assert balance.borrowed == borrowed + assert balance.available == Decimal(available) + assert balance.locked == Decimal(locked) + assert balance.borrowed == Decimal(borrowed) def test_kline_from_pb(): @@ -58,12 +60,12 @@ def test_kline_from_pb(): assert kline.exchange == exchange assert kline.symbol == symbol - assert kline.open == float(open) - assert kline.high == float(high) - assert kline.low == float(low) - assert kline.close == float(close) - assert kline.volume == float(volume) - assert kline.quote_volume == float(quote_volume) + assert kline.open == Decimal(open) + assert kline.high == Decimal(high) + assert kline.low == Decimal(low) + assert kline.close == Decimal(close) + assert kline.volume == Decimal(volume) + assert kline.quote_volume == Decimal(quote_volume) assert kline.start_time == parse_time(start_time) assert kline.end_time == parse_time(end_time) assert closed == closed diff --git a/python/tests/test_utils.py b/python/tests/test_utils.py index 591bec147c..bf673c8d7a 100644 --- a/python/tests/test_utils.py +++ b/python/tests/test_utils.py @@ -1,4 +1,6 @@ -from bbgo.utils import parse_float +from decimal import Decimal + +from bbgo.utils import parse_number from bbgo.utils import parse_time @@ -10,9 +12,8 @@ def test_parse_time(): def test_parse_float(): - assert parse_float(None) == 0 - assert parse_float("") == 0 + assert parse_number(None) == 0 + assert parse_number("") == 0 s = "3.14159265358979" - f = 3.14159265358979 - assert parse_float(s) == f + assert parse_number(s) == Decimal(s) diff --git a/rockhopper_mysql.yaml b/rockhopper_mysql.yaml index d8b459dbbd..2519de0ae3 100644 --- a/rockhopper_mysql.yaml +++ b/rockhopper_mysql.yaml @@ -8,7 +8,7 @@ dialect: mysql # dsn: "root:123123@unix(/opt/local/var/run/mysql57/mysqld.sock)/bbgo_dev?parseTime=true" # tcp connection to mysql with password -# dsn: "root:123123@tcp(localhost:3306)/bbgo_dev?parseTime=true" +dsn: "root:root@tcp(localhost:3306)/bbgo?parseTime=true" # tcp connection to mysql without password # dsn: "root@tcp(localhost:3306)/bbgo_dev?parseTime=true" diff --git a/scripts/release-test.sh b/scripts/release-test.sh new file mode 100644 index 0000000000..c33afe69a3 --- /dev/null +++ b/scripts/release-test.sh @@ -0,0 +1,12 @@ +#!/bin/bash +set -e +echo "testing sync..." +dotenv -f .env.local.mysql -- go run ./cmd/bbgo sync --session binance --config config/sync.yaml +dotenv -f .env.local.sqlite -- go run ./cmd/bbgo sync --session binance --config config/sync.yaml + +echo "backtest sync..." +echo "backtest mysql sync..." +dotenv -f .env.local.mysql -- go run ./cmd/bbgo backtest --config config/dca.yaml --sync --sync-only --verify + +echo "backtest sqlite sync..." +dotenv -f .env.local.sqlite -- go run ./cmd/bbgo backtest --config config/dca.yaml --sync --sync-only --verify diff --git a/scripts/setup-bollgrid.sh b/scripts/setup-bollgrid.sh index baf0616662..fce4efef49 100755 --- a/scripts/setup-bollgrid.sh +++ b/scripts/setup-bollgrid.sh @@ -31,6 +31,13 @@ case $(uname -m) in exit 1;; esac dist_file=bbgo-$version-$osf-$arch.tar.gz +exchange=max + +if [[ -n $1 ]] ; then + exchange=$1 +fi + +exchange_upper=$(echo -n $exchange | tr 'a-z' 'A-Z') info "downloading..." curl -O -L https://github.com/c9s/bbgo/releases/download/$version/$dist_file @@ -41,14 +48,15 @@ info "downloaded successfully" function gen_dotenv() { - read -p "Enter your MAX API key: " api_key - read -p "Enter your MAX API secret: " api_secret - echo "Generating your .env.local file..." + read -p "Enter your $exchange_upper API key: " api_key + read -p "Enter your $exchange_upper API secret: " api_secret + info "Generating your .env.local file..." cat < .env.local -MAX_API_KEY=$api_key -MAX_API_SECRET=$api_secret +${exchange_upper}_API_KEY=$api_key +${exchange_upper}_API_SECRET=$api_secret END + info "dotenv is configured successfully" } if [[ -e ".env.local" ]] ; then @@ -72,7 +80,7 @@ fi cat < bbgo.yaml --- exchangeStrategies: -- on: max +- on: ${exchange} bollgrid: symbol: BTCUSDT interval: 1h diff --git a/scripts/setup-bollmaker.sh b/scripts/setup-bollmaker.sh new file mode 100755 index 0000000000..11a1d90eaa --- /dev/null +++ b/scripts/setup-bollmaker.sh @@ -0,0 +1,185 @@ +#!/bin/bash +set -e + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +NC='\033[0m' # No Color + +function warn() +{ + echo -e "${YELLOW}$@${NC}" +} + +function error() +{ + echo -e "${RED}$@${NC}" +} + +function info() +{ + echo -e "${GREEN}$@${NC}" +} +version=$(curl -fs https://api.github.com/repos/c9s/bbgo/releases/latest | awk -F '"' '/tag_name/{print $4}') +osf=$(uname | tr '[:upper:]' '[:lower:]') +arch="" +case $(uname -m) in + x86_64 | ia64) arch="amd64";; + arm64 | aarch64 | arm) arch="arm64";; + *) + echo "unsupported architecture: $(uname -m)" + exit 1;; +esac +dist_file=bbgo-$version-$osf-$arch.tar.gz + +info "downloading..." +curl -O -L https://github.com/c9s/bbgo/releases/download/$version/$dist_file +tar xzf $dist_file +mv bbgo-$osf-$arch bbgo +chmod +x bbgo +info "downloaded successfully" + +function gen_dotenv() +{ + read -p "Enter your Binance API key: " api_key + read -p "Enter your Binance API secret: " api_secret + echo "Generating your .env.local file..." +cat < .env.local +BINANCE_API_KEY=$api_key +BINANCE_API_SECRET=$api_secret +END + +} + +if [[ -e ".env.local" ]] ; then + echo "Found existing .env.local, you will overwrite the existing .env.local file!" + read -p "Are you sure? (Y/n) " a + if [[ $a != "n" ]] ; then + gen_dotenv + fi +else + gen_dotenv +fi + +if [[ -e "bbgo.yaml" ]] ; then + echo "Found existing bbgo.yaml, you will overwrite the existing bbgo.yaml file!" + read -p "Are you sure? (Y/n) " a + if [[ $a == "n" ]] ; then + exit + fi +fi + +cat < bbgo.yaml +--- +sessions: + binance: + exchange: binance + envVarPrefix: BINANCE + +persistence: + redis: + host: 127.0.0.1 + port: 6379 + db: 0 + +exchangeStrategies: +- on: binance + bollmaker: + symbol: ETHUSDT + + # interval is how long do you want to update your order price and quantity + interval: 1m + + # quantity is the base order quantity for your buy/sell order. + quantity: 0.05 + + # useTickerPrice use the ticker api to get the mid price instead of the closed kline price. + # The back-test engine is kline-based, so the ticker price api is not supported. + # Turn this on if you want to do real trading. + useTickerPrice: false + + # spread is the price spread from the middle price. + # For ask orders, the ask price is ((bestAsk + bestBid) / 2 * (1.0 + spread)) + # For bid orders, the bid price is ((bestAsk + bestBid) / 2 * (1.0 - spread)) + # Spread can be set by percentage or floating number. e.g., 0.1% or 0.001 + spread: 0.09% + + # minProfitSpread is the minimal order price spread from the current average cost. + # For long position, you will only place sell order above the price (= average cost * (1 + minProfitSpread)) + # For short position, you will only place buy order below the price (= average cost * (1 - minProfitSpread)) + minProfitSpread: 0.5% + + # dynamicExposurePositionScale overrides maxExposurePosition + # for domain, + # -1 means -100%, the price is on the lower band price. + # if the price breaks the lower band, a number less than -1 will be given. + # 1 means 100%, the price is on the upper band price. + # if the price breaks the upper band, a number greater than 1 will be given, for example, 1.2 for 120%, and 1.3 for 130%. + dynamicExposurePositionScale: + byPercentage: + # exp means we want to use exponential scale, you can replace "exp" with "linear" for linear scale + exp: + # from lower band -100% (-1) to upper band 100% (+1) + domain: [ -1, 1 ] + # when in down band, holds 1.0 by maximum + # when in up band, holds 0.05 by maximum + range: [ 10.0, 1.0 ] + + # DisableShort means you can don't want short position during the market making + # THe short here means you might sell some of your existing inventory. + disableShort: true + + # uptrendSkew, like the strongUptrendSkew, but the price is still in the default band. + uptrendSkew: 0.8 + + # downtrendSkew, like the strongDowntrendSkew, but the price is still in the default band. + downtrendSkew: 1.2 + + defaultBollinger: + interval: "1h" + window: 21 + bandWidth: 2.0 + + # neutralBollinger is the smaller range of the bollinger band + # If price is in this band, it usually means the price is oscillating. + neutralBollinger: + interval: "5m" + window: 21 + bandWidth: 2.0 + + # tradeInBand: when tradeInBand is set, you will only place orders in the bollinger band. + tradeInBand: false + + # buyBelowNeutralSMA: when this set, it will only place buy order when the current price is below the SMA line. + buyBelowNeutralSMA: false + + persistence: + type: redis + +END + +info "config file is generated successfully" +echo "================================================================" +echo "now you can edit your strategy config file bbgo.yaml to run bbgo" + +if [[ $osf == "darwin" ]] ; then + echo "we found you're using MacOS, you can type:" + echo "" + echo " open -a TextEdit bbgo.yaml" + echo "" +else + echo "you look like a pro user, you can edit the config by:" + echo "" + echo " vim bbgo.yaml" + echo "" +fi + +echo "To run bbgo just type: " +echo "" +echo " ./bbgo run" +echo "" +echo "To stop bbgo, just hit CTRL-C" + +if [[ $osf == "darwin" ]] ; then + open -a TextEdit bbgo.yaml +fi diff --git a/utils/changelog.sh b/utils/changelog.sh old mode 100644 new mode 100755 diff --git a/util/embed/main.go b/utils/embed/main.go similarity index 83% rename from util/embed/main.go rename to utils/embed/main.go index 789d456325..1d45db115b 100644 --- a/util/embed/main.go +++ b/utils/embed/main.go @@ -17,9 +17,9 @@ var funcs = map[string]interface{}{ } var ( - tmpl = template.Must(template.New("").Funcs(funcs).Parse(`// +build web + tmpl = template.Must(template.New("").Funcs(funcs).Parse(`{{- if .Tag -}} // +build {{ .Tag }} {{- end }} -// code is generated by embed. DO NOT EDIT. +// Code generated by "embed"; DO NOT EDIT. package {{ .Package }} import ( @@ -81,19 +81,22 @@ func (f *file) ModTime() time.Time{ return time.Time{} } func (f *file) IsDir() bool { return false } func (f *file) Sys() interface{} { return nil } -`))) +`)) +) // Embed is a helper function that embeds assets from the given directories // into a Go source file. It is designed to be called from some generator // script, see example project to find out how it can be used. -func Embed(packageName, file string, dirs ...string) error { +func Embed(file string, dirs ...string) error { var buf bytes.Buffer - // Execute template - if err := tmpl.Execute(&buf, struct{ + // execute template + if err := tmpl.Execute(&buf, struct { Package string + Tag string }{ Package: packageName, + Tag: tag, }); err != nil { return err } @@ -142,16 +145,17 @@ func formatBytes(s []byte) string { return builder.String() } +var packageName string +var outputFile string +var tag string func main() { - var packageName string - var outputFile string - - flag.StringVar(&packageName,"package", "", "package name") - flag.StringVar(&outputFile,"output", "assets.go", "output filename") + flag.StringVar(&packageName, "package", "", "package name") + flag.StringVar(&tag, "tag", "", "build tag in the generated file") + flag.StringVar(&outputFile, "output", "assets.go", "output filename") flag.Parse() args := flag.Args() - if err := Embed(packageName, outputFile, args...) ; err != nil { + if err := Embed(outputFile, args...); err != nil { log.Fatal(err) } } diff --git a/utils/generate-version-file.sh b/utils/generate-version-file.sh index f2a1a3ed90..6e66eec8ee 100755 --- a/utils/generate-version-file.sh +++ b/utils/generate-version-file.sh @@ -18,6 +18,7 @@ if [[ -n $VERSION_SUFFIX ]] ; then fi cat <