Compare commits

...

106 Commits

Author SHA1 Message Date
go-dockly
de5bdfeda9
Merge 5316bc5e3a into 37106c35b7 2024-09-19 09:41:10 +08:00
c9s
37106c35b7
Merge pull request #1748 from c9s/c9s/add-pos-index
Some checks failed
Go / build (1.21, 6.2) (push) Has been cancelled
golang-lint / lint (push) Has been cancelled
IMPROVE: [db] add index on positions table
2024-09-18 17:06:34 +08:00
c9s
8265ada5a0
compile and update migration package 2024-09-18 13:30:56 +08:00
c9s
744ca57c71
migrations: add index on positions table 2024-09-18 13:30:20 +08:00
c9s
17d3097e06
add v1.60.3 release note
Some checks failed
Go / build (1.21, 6.2) (push) Has been cancelled
golang-lint / lint (push) Has been cancelled
2024-09-16 00:31:00 +08:00
c9s
a0c41f89f2
bump version to v1.60.3 2024-09-16 00:31:00 +08:00
c9s
35a6639530
update command doc files 2024-09-16 00:30:59 +08:00
c9s
26b1fd2ae7
xmaker: fix price initialization 2024-09-16 00:29:37 +08:00
c9s
80430fec46
Merge pull request #1744 from lanphan/activeorder
Some checks are pending
Go / build (1.21, 6.2) (push) Waiting to run
golang-lint / lint (push) Waiting to run
2024-09-14 23:19:10 +08:00
Lan Phan
1f8b2b3710 call b.EmitNew() when new order is added into activeorderbook 2024-09-14 18:26:36 +07:00
c9s
25a2203000
add v1.60.2 release note
Some checks failed
Go / build (1.21, 6.2) (push) Has been cancelled
golang-lint / lint (push) Has been cancelled
2024-09-12 17:51:57 +08:00
c9s
aca2c32442
bump version to v1.60.2 2024-09-12 17:51:57 +08:00
c9s
df915d6ee8
update command doc files 2024-09-12 17:51:57 +08:00
c9s
0d6b7b29d5
Merge pull request #1742 from c9s/c9s/fix-ws-close-err
FIX: types/stream: change errorf to warnf
2024-09-12 17:46:24 +08:00
c9s
2784ef4687
Merge pull request #1741 from c9s/c9s/upgrade-requestgen
FIX: upgrade github.com/c9s/requestgen to 1.4.3
2024-09-12 17:37:55 +08:00
c9s
ea8f3a5485
types/stream: change errorf to warnf 2024-09-12 17:35:13 +08:00
c9s
52f32e0ad0
upgrade github.com/c9s/requestgen to 1.4.3 2024-09-12 17:27:30 +08:00
c9s
50cdf617f2
Merge pull request #1740 from c9s/c9s/upgrade-requestgen
FIX: upgrade requestgen and re-generate max cancel order request files
2024-09-12 12:31:08 +08:00
c9s
de0d11b511
max: regenerate order cancel requests 2024-09-11 16:47:20 +08:00
c9s
789bb1e53e
upgrade github.com/c9s/requestgen to 1.4.2 2024-09-11 16:38:35 +08:00
kbearXD
a9b71adce9
Merge pull request #1739 from c9s/kbearXD/dca2/set-exchange-fee-rate
Some checks failed
Go / build (1.21, 6.2) (push) Has been cancelled
golang-lint / lint (push) Has been cancelled
FEATURE: [dca2] set exchange fee rate for round position
2024-09-11 16:15:13 +08:00
kbearXD
f83491af26 FEATURE: [dca2] set exchange fee rate for round position 2024-09-11 15:40:59 +08:00
bailantaotao
82d07a0098
Merge pull request #1738 from c9s/edwin/okx/add-polusdt
Some checks are pending
Go / build (1.21, 6.2) (push) Waiting to run
golang-lint / lint (push) Waiting to run
FEATURE: [okx] update symbols to latest
2024-09-10 17:23:42 +08:00
edwin
619cce53f6 pkg/exchange: update to latest 2024-09-10 17:11:58 +08:00
c9s
643ecde2e9
Merge pull request #1726 from c9s/dependabot/github_actions/morphy2k/revive-action-2.5.10
Some checks are pending
Go / build (1.21, 6.2) (push) Waiting to run
golang-lint / lint (push) Waiting to run
dep: bump morphy2k/revive-action from 2.5.9 to 2.5.10
2024-09-10 10:28:58 +08:00
c9s
d7ddc9c462
Merge pull request #1737 from c9s/c9s/xmaker/ioc-arb
Some checks are pending
Go / build (1.21, 6.2) (push) Waiting to run
golang-lint / lint (push) Waiting to run
FEATURE: [xmaker] implement tryArbitrage
2024-09-09 22:57:20 +08:00
c9s
bd19b63c7b
go: update sum file 2024-09-09 22:23:25 +08:00
c9s
83ed9b0811
go: upgrade go sqlite 2024-09-09 22:23:02 +08:00
c9s
34ef50d889
xmaker: refactor and clean up tryArbitrage 2024-09-09 22:03:06 +08:00
c9s
52925c5643
xmaker: calculate balance for arbitrage 2024-09-09 18:12:46 +08:00
c9s
b4f2748892
xmaker: fix sides 2024-09-09 18:03:03 +08:00
c9s
ceda1e06b9
xmaker: implement tryArbitrage 2024-09-09 17:49:53 +08:00
c9s
bc1715f8ad
Merge pull request #1736 from c9s/kbearXD/session/remove-log
Some checks are pending
Go / build (1.21, 6.2) (push) Waiting to run
golang-lint / lint (push) Waiting to run
MINOR: [session] remove environment nil validation log
2024-09-09 16:17:17 +08:00
c9s
f361b19564
Merge pull request #1734 from c9s/c9s/xmaker/ioc-arb
REFACTOR: [xmaker] refactor for supporting ioc arb [part1]
2024-09-09 16:05:11 +08:00
kbearXD
f44486447e MINOR: [session] remove environment nil validation log 2024-09-09 16:04:04 +08:00
kbearXD
a2eca66af5
Merge pull request #1735 from c9s/kbearXD/dca2/fix-memory-leak
FIX: configure environment
2024-09-09 15:50:24 +08:00
kbearXD
129e2c438e FIX: add debug log 2024-09-09 15:13:02 +08:00
c9s
90749f4873
xmaker: pull out s.UseDepthPrice dependency 2024-09-09 15:04:56 +08:00
c9s
77dfe213e5
xmaker: pull out getLayerPrice and add test against the method 2024-09-09 14:41:41 +08:00
c9s
960ea89d8c
testhelper: add more test helpers 2024-09-09 14:41:27 +08:00
c9s
f24a96c8c3
xmaker: refactor getInitialLayerQuantity for quantity multiplier 2024-09-07 14:19:07 +08:00
c9s
6ad16b7488
xmaker: add EnableArbitrage option and makerBook 2024-09-07 13:47:34 +08:00
c9s
e14f09a914
xmaker: add sourceDepthLevel option 2024-09-06 21:47:43 +08:00
c9s
3cc96ff6ad
Merge pull request #1724 from dropbigfish/main
Some checks failed
Go / build (1.21, 6.2) (push) Has been cancelled
golang-lint / lint (push) Has been cancelled
fix: fix slice init length
2024-09-06 18:06:21 +08:00
c9s
6ea996bec4
Merge pull request #1733 from c9s/c9s/fix-initialize-defaults-steps
FIX: [bbgo] fix the defaults / initialize steps
2024-09-06 17:54:13 +08:00
c9s
ef935f8ca0
Merge pull request #1732 from lanphan/telegram_doc
fix env name
2024-09-06 17:40:26 +08:00
c9s
a282654c02
bbgo: fix the defaults / initialize steps 2024-09-06 17:33:31 +08:00
Lan Phan
336dd7a108 fix env name 2024-09-05 22:51:43 +07:00
kbearXD
f2a443a499
Merge pull request #1728 from c9s/kbearXD/dca2/fix-memory-leak
Some checks are pending
Go / build (1.21, 6.2) (push) Waiting to run
golang-lint / lint (push) Waiting to run
FIX: [core] fix memory leak
2024-09-05 17:52:41 +08:00
kbearXD
63a58e1b12 FIX: fix memory leak 2024-09-05 17:05:58 +08:00
c9s
1b40118bba
Merge pull request #1731 from longhutianjie/main
Some checks are pending
Go / build (1.21, 6.2) (push) Waiting to run
golang-lint / lint (push) Waiting to run
bug: fix json tag
2024-09-05 13:57:31 +08:00
longhutianjie
c75a685cc0
bug: fix json tag 2024-09-04 17:58:27 +08:00
c9s
50262f2a84
Merge pull request #1730 from c9s/c9s/xmaker/market-trade-signal
Some checks are pending
Go / build (1.21, 6.2) (push) Waiting to run
golang-lint / lint (push) Waiting to run
FEATURE: [xmaker] add market trade signal
2024-09-04 16:27:42 +08:00
c9s
9fc3a1b44a
xmaker: rename to aggTradeVolume 2024-09-04 16:09:58 +08:00
c9s
656112de45
xmaker: call signalConfig.TradeVolumeWindowSignal.Bind 2024-09-04 16:07:28 +08:00
c9s
ba73eeaad1
xmaker: add TradeVolumeWindowSignal 2024-09-04 15:59:21 +08:00
c9s
2527c0c7b7
max: convert v3 DepositStateFailed into rejected 2024-09-04 15:00:37 +08:00
c9s
a2f8fe5f72
max: add v3 DepositStateFailed state 2024-09-04 14:59:58 +08:00
c9s
ed51eff242
max: drop unused function 2024-09-04 14:59:10 +08:00
c9s
f6865f664c
add v1.60.1 release note
Some checks are pending
Go / build (1.21, 6.2) (push) Waiting to run
golang-lint / lint (push) Waiting to run
2024-09-04 14:58:07 +08:00
c9s
24de49860f
bump version to v1.60.1 2024-09-04 14:58:07 +08:00
c9s
83dc981c92
update command doc files 2024-09-04 14:58:07 +08:00
c9s
ec68e3c5f6
Merge pull request #1727 from lanphan/ioc
FIX: update timeInForce for binance margin order
2024-09-04 14:38:40 +08:00
c9s
699164484b
Merge pull request #1729 from c9s/c9s/max/fix-v3-deposit-state-conversion
FIX: [max] fix v3 deposit state conversion
2024-09-04 11:41:22 +08:00
c9s
f27afac77b
max: use error log instead of warning log for convertion 2024-09-04 11:20:30 +08:00
c9s
d404b20bd1
deposit2transfer: fix comments 2024-09-04 11:19:43 +08:00
c9s
1b8d7bd805
max: fix v3 deposit state conversion 2024-09-04 11:17:56 +08:00
c9s
7d034d1ba8
bbgo: add stringer method to the quota struct
Some checks failed
Go / build (1.21, 6.2) (push) Has been cancelled
golang-lint / lint (push) Has been cancelled
2024-09-03 03:26:47 +08:00
c9s
7135895006
xmaker: fix MaxExposurePosition check condition 2024-09-03 03:25:37 +08:00
Lan Phan
ba913ce4de update timeInForce for binance margin order 2024-09-03 00:38:17 +07:00
c9s
f12ba1adb9
bbgo: add comments to the quota methods
Some checks are pending
Go / build (1.21, 6.2) (push) Waiting to run
golang-lint / lint (push) Waiting to run
2024-09-02 22:18:13 +08:00
c9s
294e529a98
xmaker: add more logs 2024-09-02 16:08:51 +08:00
c9s
f30aca1b5a
xmaker: update position metrics when restored 2024-09-02 15:51:31 +08:00
c9s
f9b9832fff
add more logs 2024-09-02 15:51:31 +08:00
c9s
2bf1072977
Merge pull request #1725 from c9s/c9s/xmaker/stb-improvements
IMPROVE: [xmaker] improve hedge margin account leverage calculation
2024-09-02 15:29:53 +08:00
dependabot[bot]
01f8b78008
dep: bump morphy2k/revive-action from 2.5.9 to 2.5.10
Bumps [morphy2k/revive-action](https://github.com/morphy2k/revive-action) from 2.5.9 to 2.5.10.
- [Release notes](https://github.com/morphy2k/revive-action/releases)
- [Commits](https://github.com/morphy2k/revive-action/compare/v2.5.9...v2.5.10)

---
updated-dependencies:
- dependency-name: morphy2k/revive-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-02 06:39:35 +00:00
c9s
4d1c357c3d
xmaker: reuse makerMarket field 2024-09-01 17:55:00 +08:00
c9s
a4833524cf
xmaker: add more logs 2024-09-01 16:41:16 +08:00
c9s
ed073264f1
xmaker: add MaxHedgeAccountLeverage option 2024-09-01 15:42:36 +08:00
c9s
ad6056834e
xmaker: separate maximumValueInUsd in a new var 2024-09-01 01:34:25 +08:00
c9s
8b1306a6a6
xmaker: calculate maximum leveraged account value 2024-09-01 01:31:50 +08:00
c9s
d85da78e17
xmaker: improve hedge account credit calculation 2024-09-01 00:58:50 +08:00
dropbigfish
9d581adc04 fix: fix slice init length
Signed-off-by: dropbigfish <fillfish@foxmail.com>
2024-09-01 00:36:43 +08:00
c9s
cff7103ece
fix math import
Some checks failed
Go / build (1.21, 6.2) (push) Has been cancelled
golang-lint / lint (push) Has been cancelled
2024-08-30 22:41:13 +08:00
c9s
d501e8ff4d
xmaker: apply math.Abs on signal for margin scale 2024-08-30 22:38:59 +08:00
c9s
b87213827e
Merge pull request #1723 from c9s/c9s/xmaker/add-signals
FIX: [xmaker] avoid calculate margin from 0.0 signal
2024-08-30 18:01:57 +08:00
c9s
ec80cbfd9f
xmaker: check 0.0 2024-08-30 17:52:28 +08:00
c9s
04bed165d0
Merge pull request #1722 from c9s/c9s/xmaker/add-signals
FEATURE: [xmaker] add signals
2024-08-30 17:50:52 +08:00
c9s
7c4b3e81df
xmaker: add more logs 2024-08-30 17:42:20 +08:00
c9s
cc820d3df0
xmaker: apply margin from signal 2024-08-30 17:39:25 +08:00
c9s
371db8e7d1
xmaker: update signal conditions to metrics 2024-08-30 17:18:29 +08:00
c9s
b8abc065de
xmaker: initialize bollinger band signal 2024-08-30 17:15:12 +08:00
c9s
9ebab4f4f7
xmaker: add signal providers 2024-08-30 15:44:55 +08:00
c9s
d9fb9ff3e0
xmaker: remove unused var 2024-08-29 13:18:50 +08:00
c9s
88d7783843
bbgo/activeOrderBook: filter market order when filtering existing orders 2024-08-29 00:38:44 +08:00
c9s
86e464b1bc
xmaker: when submitting hedge orders, do not add it to the active orderbook 2024-08-29 00:33:04 +08:00
Sven Woldt
5316bc5e3a
new trade stats 2023-11-10 23:07:40 +01:00
Sven Woldt
3c861e3782
new trade stats 2023-11-10 23:06:49 +01:00
go-dockly
cb8ca56afc
Merge branch 'c9s:main' into feat/tradestats 2023-11-10 22:58:59 +01:00
Sven Woldt
5b64195797
update createSymbolReport 2023-11-07 00:41:59 +01:00
Sven Woldt
0d7990fc18
reset main 2023-11-02 18:03:16 +01:00
Sven Woldt
f7d54291f2
Merge remote-tracking branch 'origin' 2023-11-02 07:02:15 +01:00
Sven Woldt
539e35d290
fix drawdown 2023-11-02 06:59:40 +01:00
go-dockly
93c619c32a
Merge branch 'main' into main 2023-11-02 02:34:25 +01:00
Sven Woldt
0c2f4aef9d
clean up 2023-11-02 02:28:00 +01:00
Sven Woldt
f19bbbc5aa
add more trade stats 2023-11-02 02:16:23 +01:00
81 changed files with 2596 additions and 988 deletions

View File

@ -90,7 +90,7 @@ jobs:
sed -i -e '/_requestgen.go/d' coverage_dnum.txt sed -i -e '/_requestgen.go/d' coverage_dnum.txt
- name: Revive Check - name: Revive Check
uses: morphy2k/revive-action@v2.5.9 # https://github.com/mgechev/revive/issues/956 uses: morphy2k/revive-action@v2.5.10 # https://github.com/mgechev/revive/issues/956
with: with:
reporter: github-pr-review reporter: github-pr-review
fail_on_error: true fail_on_error: true

View File

@ -58,4 +58,4 @@ bbgo [flags]
* [bbgo userdatastream](bbgo_userdatastream.md) - Listen to session events (orderUpdate, tradeUpdate, balanceUpdate, balanceSnapshot) * [bbgo userdatastream](bbgo_userdatastream.md) - Listen to session events (orderUpdate, tradeUpdate, balanceUpdate, balanceSnapshot)
* [bbgo version](bbgo_version.md) - show version name * [bbgo version](bbgo_version.md) - show version name
###### Auto generated by spf13/cobra on 21-Aug-2024 ###### Auto generated by spf13/cobra on 16-Sep-2024

View File

@ -41,4 +41,4 @@ bbgo account [--session SESSION] [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot * [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 21-Aug-2024 ###### Auto generated by spf13/cobra on 16-Sep-2024

View File

@ -50,4 +50,4 @@ bbgo backtest [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot * [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 21-Aug-2024 ###### Auto generated by spf13/cobra on 16-Sep-2024

View File

@ -40,4 +40,4 @@ bbgo balances [--session SESSION] [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot * [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 21-Aug-2024 ###### Auto generated by spf13/cobra on 16-Sep-2024

View File

@ -39,4 +39,4 @@ bbgo build [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot * [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 21-Aug-2024 ###### Auto generated by spf13/cobra on 16-Sep-2024

View File

@ -49,4 +49,4 @@ bbgo cancel-order [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot * [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 21-Aug-2024 ###### Auto generated by spf13/cobra on 16-Sep-2024

View File

@ -41,4 +41,4 @@ bbgo deposits [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot * [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 21-Aug-2024 ###### Auto generated by spf13/cobra on 16-Sep-2024

View File

@ -9,16 +9,18 @@ bbgo execute-order --session SESSION --symbol SYMBOL --side SIDE --target-quanti
### Options ### Options
``` ```
--deadline duration deadline of the order execution --deadline duration deadline duration of the order execution, e.g. 1h
-h, --help help for execute-order --delay-interval duration order delay time after filled (default 3s)
--price-ticks int the number of price tick for the jump spread, default to 0 -h, --help help for execute-order
--session string the exchange session name for sync --order-update-rate-limit string order update rate limit, syntax: 1+1/1m (default "1s")
--side string the trading side: buy or sell --price-ticks int the number of price tick for the jump spread, default to 0
--slice-quantity string slice quantity --session string the exchange session name for sync
--stop-price string stop price (default "0") --side string the trading side: buy or sell
--symbol string the trading pair, like btcusdt --slice-quantity string slice quantity
--target-quantity string target quantity --stop-price string stop price (default "0")
--update-interval duration order update time (default 10s) --symbol string the trading pair, like btcusdt
--target-quantity string target quantity
--update-interval duration order update time (default 10s)
``` ```
### Options inherited from parent commands ### Options inherited from parent commands
@ -48,4 +50,4 @@ bbgo execute-order --session SESSION --symbol SYMBOL --side SIDE --target-quanti
* [bbgo](bbgo.md) - bbgo is a crypto trading bot * [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 21-Aug-2024 ###### Auto generated by spf13/cobra on 16-Sep-2024

View File

@ -42,4 +42,4 @@ bbgo get-order --session SESSION --order-id ORDER_ID [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot * [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 21-Aug-2024 ###### Auto generated by spf13/cobra on 16-Sep-2024

View File

@ -45,4 +45,4 @@ bbgo hoptimize [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot * [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 21-Aug-2024 ###### Auto generated by spf13/cobra on 16-Sep-2024

View File

@ -42,4 +42,4 @@ bbgo kline [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot * [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 21-Aug-2024 ###### Auto generated by spf13/cobra on 16-Sep-2024

View File

@ -41,4 +41,4 @@ bbgo list-orders open|closed --session SESSION --symbol SYMBOL [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot * [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 21-Aug-2024 ###### Auto generated by spf13/cobra on 16-Sep-2024

View File

@ -38,4 +38,4 @@ margin related history
* [bbgo margin loans](bbgo_margin_loans.md) - query loans history * [bbgo margin loans](bbgo_margin_loans.md) - query loans history
* [bbgo margin repays](bbgo_margin_repays.md) - query repay history * [bbgo margin repays](bbgo_margin_repays.md) - query repay history
###### Auto generated by spf13/cobra on 21-Aug-2024 ###### Auto generated by spf13/cobra on 16-Sep-2024

View File

@ -41,4 +41,4 @@ bbgo margin interests --session=SESSION_NAME --asset=ASSET [flags]
* [bbgo margin](bbgo_margin.md) - margin related history * [bbgo margin](bbgo_margin.md) - margin related history
###### Auto generated by spf13/cobra on 21-Aug-2024 ###### Auto generated by spf13/cobra on 16-Sep-2024

View File

@ -41,4 +41,4 @@ bbgo margin loans --session=SESSION_NAME --asset=ASSET [flags]
* [bbgo margin](bbgo_margin.md) - margin related history * [bbgo margin](bbgo_margin.md) - margin related history
###### Auto generated by spf13/cobra on 21-Aug-2024 ###### Auto generated by spf13/cobra on 16-Sep-2024

View File

@ -41,4 +41,4 @@ bbgo margin repays --session=SESSION_NAME --asset=ASSET [flags]
* [bbgo margin](bbgo_margin.md) - margin related history * [bbgo margin](bbgo_margin.md) - margin related history
###### Auto generated by spf13/cobra on 21-Aug-2024 ###### Auto generated by spf13/cobra on 16-Sep-2024

View File

@ -40,4 +40,4 @@ bbgo market [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot * [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 21-Aug-2024 ###### Auto generated by spf13/cobra on 16-Sep-2024

View File

@ -44,4 +44,4 @@ bbgo optimize [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot * [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 21-Aug-2024 ###### Auto generated by spf13/cobra on 16-Sep-2024

View File

@ -42,4 +42,4 @@ bbgo orderbook --session=[exchange_name] --symbol=[pair_name] [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot * [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 21-Aug-2024 ###### Auto generated by spf13/cobra on 16-Sep-2024

View File

@ -40,4 +40,4 @@ bbgo orderupdate [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot * [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 21-Aug-2024 ###### Auto generated by spf13/cobra on 16-Sep-2024

View File

@ -49,4 +49,4 @@ bbgo pnl [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot * [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 21-Aug-2024 ###### Auto generated by spf13/cobra on 16-Sep-2024

View File

@ -51,4 +51,4 @@ bbgo run [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot * [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 21-Aug-2024 ###### Auto generated by spf13/cobra on 16-Sep-2024

View File

@ -46,4 +46,4 @@ bbgo submit-order --session SESSION --symbol SYMBOL --side SIDE --quantity QUANT
* [bbgo](bbgo.md) - bbgo is a crypto trading bot * [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 21-Aug-2024 ###### Auto generated by spf13/cobra on 16-Sep-2024

View File

@ -42,4 +42,4 @@ bbgo sync [--session=[exchange_name]] [--symbol=[pair_name]] [[--since=yyyy/mm/d
* [bbgo](bbgo.md) - bbgo is a crypto trading bot * [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 21-Aug-2024 ###### Auto generated by spf13/cobra on 16-Sep-2024

View File

@ -42,4 +42,4 @@ bbgo trades --session=[exchange_name] --symbol=[pair_name] [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot * [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 21-Aug-2024 ###### Auto generated by spf13/cobra on 16-Sep-2024

View File

@ -40,4 +40,4 @@ bbgo tradeupdate --session=[exchange_name] [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot * [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 21-Aug-2024 ###### Auto generated by spf13/cobra on 16-Sep-2024

View File

@ -42,4 +42,4 @@ bbgo transfer-history [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot * [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 21-Aug-2024 ###### Auto generated by spf13/cobra on 16-Sep-2024

View File

@ -40,4 +40,4 @@ bbgo userdatastream [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot * [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 21-Aug-2024 ###### Auto generated by spf13/cobra on 16-Sep-2024

View File

@ -39,4 +39,4 @@ bbgo version [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot * [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 21-Aug-2024 ###### Auto generated by spf13/cobra on 16-Sep-2024

View File

@ -17,7 +17,7 @@ TELEGRAM_BOT_TOKEN=347374838:ABFTjfiweajfiawoejfiaojfeijoaef
``` ```
For the telegram chat authentication (your bot needs to verify it's you), if you only need a fixed authentication token, For the telegram chat authentication (your bot needs to verify it's you), if you only need a fixed authentication token,
you can set `TELEGRAM_AUTH_TOKEN` in the `.env.local` file, e.g., you can set `TELEGRAM_BOT_AUTH_TOKEN` in the `.env.local` file, e.g.,
```sh ```sh
TELEGRAM_BOT_AUTH_TOKEN=itsme55667788 TELEGRAM_BOT_AUTH_TOKEN=itsme55667788

35
doc/release/v1.60.1.md Normal file
View File

@ -0,0 +1,35 @@
## Fixes
- fixed xmaker bugs
- updated helm chart for sync cronjob
- fixed max deposits api
[Full Changelog](https://github.com/c9s/bbgo/compare/v1.60.0...main)
- [#1727](https://github.com/c9s/bbgo/pull/1727): FIX: update timeInForce for binance margin order
- [#1729](https://github.com/c9s/bbgo/pull/1729): FIX: [max] fix v3 deposit state conversion
- [#1723](https://github.com/c9s/bbgo/pull/1723): FIX: [xmaker] avoid calculate margin from 0.0 signal
- [#1721](https://github.com/c9s/bbgo/pull/1721): FIX: [xmaker] fix aggregatePrice method
- [#1725](https://github.com/c9s/bbgo/pull/1725): IMPROVE: [xmaker] improve hedge margin account leverage calculation
- [#1722](https://github.com/c9s/bbgo/pull/1722): FEATURE: [xmaker] add signals
- [#1720](https://github.com/c9s/bbgo/pull/1720): FEATURE: [xmaker] margin credit improvement
- [#1718](https://github.com/c9s/bbgo/pull/1718): FEATURE: [xmaker] add more config metrics
- [#1719](https://github.com/c9s/bbgo/pull/1719): IMPROVE: [xmaker] fix bollinger band price calculation
- [#1709](https://github.com/c9s/bbgo/pull/1709): IMPROVE: [xmaker] improve profit stats ticker and integrate rate limiter
- [#1708](https://github.com/c9s/bbgo/pull/1708): FEATURE: [xmaker] integrate circuit breaker
- [#1712](https://github.com/c9s/bbgo/pull/1712): FEATURE: [xmaker] add profit fixer
- [#1710](https://github.com/c9s/bbgo/pull/1710): IMPROVE: [xmaker] improve stability
- [#1717](https://github.com/c9s/bbgo/pull/1717): REFACTOR: [xmaker] refactor hedge worker and quote worker
- [#1716](https://github.com/c9s/bbgo/pull/1716): FIX: [xmaker] profit object can be nil
- [#1707](https://github.com/c9s/bbgo/pull/1707): FIX: [xmaker] position metrics missing label
- [#1715](https://github.com/c9s/bbgo/pull/1715): UPGRADE: [go] upgrade packages that are too old
- [#1713](https://github.com/c9s/bbgo/pull/1713): FEATURE: [chart] add env vars section
- [#1711](https://github.com/c9s/bbgo/pull/1711): FEATURE: [binance] add new margin order side effect AUTO_BORROW_REPAY
- [#1705](https://github.com/c9s/bbgo/pull/1705): FIX: [k8s] fix sync.enabled option
- [#1704](https://github.com/c9s/bbgo/pull/1704): FEATURE: [k8s] add cronjob for sync
- [#1700](https://github.com/c9s/bbgo/pull/1700): Fix: [autobuy] fix error when bollinger settings is not set
- [#1703](https://github.com/c9s/bbgo/pull/1703): FEATURE: [core] add position metrics
- [#1702](https://github.com/c9s/bbgo/pull/1702): IMPROVE: improve balance related metrics
- [#1699](https://github.com/c9s/bbgo/pull/1699): REFACTOR: [twap] upgrade twap command and add optional order update rate limiter
- [#1701](https://github.com/c9s/bbgo/pull/1701): RELEASE: v1.60.0
- [#1714](https://github.com/c9s/bbgo/pull/1714): dep: bump actions/setup-node from 2 to 4

18
doc/release/v1.60.2.md Normal file
View File

@ -0,0 +1,18 @@
[Full Changelog](https://github.com/c9s/bbgo/compare/v1.60.1...main)
- [#1739](https://github.com/c9s/bbgo/pull/1739): FEATURE: [dca2] set exchange fee rate for round position
- [#1738](https://github.com/c9s/bbgo/pull/1738): FEATURE: [okx] update symbols to latest
- [#1737](https://github.com/c9s/bbgo/pull/1737): FEATURE: [xmaker] implement tryArbitrage
- [#1730](https://github.com/c9s/bbgo/pull/1730): FEATURE: [xmaker] add market trade signal
- [#1734](https://github.com/c9s/bbgo/pull/1734): REFACTOR: [xmaker] refactor for supporting ioc arb [part1]
- [#1736](https://github.com/c9s/bbgo/pull/1736): MINOR: [session] remove environment nil validation log
- [#1742](https://github.com/c9s/bbgo/pull/1742): FIX: types/stream: change errorf to warnf
- [#1741](https://github.com/c9s/bbgo/pull/1741): FIX: upgrade github.com/c9s/requestgen to 1.4.3
- [#1740](https://github.com/c9s/bbgo/pull/1740): FIX: upgrade requestgen and re-generate max cancel order request files
- [#1726](https://github.com/c9s/bbgo/pull/1726): dep: bump morphy2k/revive-action from 2.5.9 to 2.5.10
- [#1735](https://github.com/c9s/bbgo/pull/1735): FIX: configure environment
- [#1724](https://github.com/c9s/bbgo/pull/1724): FIX: fix slice init length
- [#1733](https://github.com/c9s/bbgo/pull/1733): FIX: [bbgo] fix the defaults / initialize steps
- [#1732](https://github.com/c9s/bbgo/pull/1732): FIX: fix env name
- [#1728](https://github.com/c9s/bbgo/pull/1728): FIX: [core] fix memory leak
- [#1731](https://github.com/c9s/bbgo/pull/1731): FIX: fix json tag

4
doc/release/v1.60.3.md Normal file
View File

@ -0,0 +1,4 @@
[Full Changelog](https://github.com/c9s/bbgo/compare/v1.60.2...main)
- FIX: fix xmaker default price
- [#1744](https://github.com/c9s/bbgo/pull/1744): call b.EmitNew() when new order is added into activeorderbook

8
go.mod
View File

@ -11,7 +11,7 @@ require (
github.com/Masterminds/squirrel v1.5.3 github.com/Masterminds/squirrel v1.5.3
github.com/adshao/go-binance/v2 v2.6.0 github.com/adshao/go-binance/v2 v2.6.0
github.com/c-bata/goptuna v0.8.1 github.com/c-bata/goptuna v0.8.1
github.com/c9s/requestgen v1.3.6 github.com/c9s/requestgen v1.4.3
github.com/c9s/rockhopper/v2 v2.0.4 github.com/c9s/rockhopper/v2 v2.0.4
github.com/cenkalti/backoff/v4 v4.2.0 github.com/cenkalti/backoff/v4 v4.2.0
github.com/cheggaaa/pb/v3 v3.0.8 github.com/cheggaaa/pb/v3 v3.0.8
@ -119,7 +119,7 @@ require (
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect github.com/mattn/go-sqlite3 v1.14.23 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
@ -153,12 +153,12 @@ require (
golang.org/x/crypto v0.26.0 // indirect golang.org/x/crypto v0.26.0 // indirect
golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect
golang.org/x/image v0.5.0 // indirect golang.org/x/image v0.5.0 // indirect
golang.org/x/mod v0.17.0 // indirect golang.org/x/mod v0.20.0 // indirect
golang.org/x/net v0.28.0 // indirect golang.org/x/net v0.28.0 // indirect
golang.org/x/sys v0.24.0 // indirect golang.org/x/sys v0.24.0 // indirect
golang.org/x/term v0.23.0 // indirect golang.org/x/term v0.23.0 // indirect
golang.org/x/text v0.17.0 // indirect golang.org/x/text v0.17.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect golang.org/x/tools v0.24.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect

16
go.sum
View File

@ -86,8 +86,8 @@ github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP
github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/c-bata/goptuna v0.8.1 h1:25+n1MLv0yvCsD56xv4nqIus3oLHL9GuPAZDLIqmX1U= 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/c-bata/goptuna v0.8.1/go.mod h1:knmS8+Iyq5PPy1YUeIEq0pMFR4Y6x7z/CySc9HlZTCY=
github.com/c9s/requestgen v1.3.6 h1:ul7dZ2uwGYjNBjreooRfSY10WTXvQmQSjZsHebz6QfE= github.com/c9s/requestgen v1.4.3 h1:0QZ27RVBLb9QuBKfiSBTOB5zSUuasrJm2p6/GZZHZZw=
github.com/c9s/requestgen v1.3.6/go.mod h1:QwkZudcv84kJ8g9+E0RDTj+13btFXbTvv2aI+zbuLbc= github.com/c9s/requestgen v1.4.3/go.mod h1:3gk1M2ihvNU2wWl7WLUc09myp7XpHMP33Dx96+Vr8A0=
github.com/c9s/rockhopper/v2 v2.0.4 h1:1cQEzU7rzCSz09B2RYdyPWwBW9gZ/DoFqD1b2xLLmAk= github.com/c9s/rockhopper/v2 v2.0.4 h1:1cQEzU7rzCSz09B2RYdyPWwBW9gZ/DoFqD1b2xLLmAk=
github.com/c9s/rockhopper/v2 v2.0.4/go.mod h1:x0XuYI2Su3kS/74UYu/3Cqc9m5Dtzqh7j7JZarczfss= github.com/c9s/rockhopper/v2 v2.0.4/go.mod h1:x0XuYI2Su3kS/74UYu/3Cqc9m5Dtzqh7j7JZarczfss=
github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4= github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4=
@ -472,8 +472,8 @@ github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lL
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= 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.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.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= 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/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
@ -759,8 +759,8 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 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.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 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-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-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -951,8 +951,8 @@ 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-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-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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-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-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@ -0,0 +1,10 @@
-- +up
-- +begin
CREATE INDEX positions_traded_at ON positions (traded_at, profit);
-- +end
-- +down
-- +begin
DROP INDEX positions_traded_at ON positions;
-- +end

View File

@ -0,0 +1,10 @@
-- +up
-- +begin
CREATE INDEX positions_traded_at ON positions (traded_at, profit);
-- +end
-- +down
-- +begin
DROP INDEX positions_traded_at;
-- +end

View File

@ -68,21 +68,49 @@ func ReadSummaryReport(filename string) (*SummaryReport, error) {
// SessionSymbolReport is the report per exchange session // SessionSymbolReport is the report per exchange session
// trades are merged, collected and re-calculated // trades are merged, collected and re-calculated
type SessionSymbolReport struct { type SessionSymbolReport struct {
Exchange types.ExchangeName `json:"exchange"` Exchange types.ExchangeName `json:"exchange"`
Symbol string `json:"symbol,omitempty"` Symbol string `json:"symbol,omitempty"`
Intervals []types.Interval `json:"intervals,omitempty"` Intervals []types.Interval `json:"intervals,omitempty"`
Subscriptions []types.Subscription `json:"subscriptions"` Subscriptions []types.Subscription `json:"subscriptions"`
Market types.Market `json:"market"` Market types.Market `json:"market"`
LastPrice fixedpoint.Value `json:"lastPrice,omitempty"` LastPrice fixedpoint.Value `json:"lastPrice,omitempty"`
StartPrice fixedpoint.Value `json:"startPrice,omitempty"` StartPrice fixedpoint.Value `json:"startPrice,omitempty"`
PnL *pnl.AverageCostPnLReport `json:"pnl,omitempty"` PnL *pnl.AverageCostPnLReport `json:"pnl,omitempty"`
InitialBalances types.BalanceMap `json:"initialBalances,omitempty"` InitialBalances types.BalanceMap `json:"initialBalances,omitempty"`
FinalBalances types.BalanceMap `json:"finalBalances,omitempty"` FinalBalances types.BalanceMap `json:"finalBalances,omitempty"`
Manifests Manifests `json:"manifests,omitempty"` Manifests Manifests `json:"manifests,omitempty"`
Sharpe fixedpoint.Value `json:"sharpeRatio"` TradeCount fixedpoint.Value `json:"tradeCount,omitempty"`
Sortino fixedpoint.Value `json:"sortinoRatio"` RoundTurnCount fixedpoint.Value `json:"roundTurnCount,omitempty"`
ProfitFactor fixedpoint.Value `json:"profitFactor"` TotalNetProfit fixedpoint.Value `json:"totalNetProfit,omitempty"`
WinningRatio fixedpoint.Value `json:"winningRatio"` AvgNetProfit fixedpoint.Value `json:"avgNetProfit,omitempty"`
GrossProfit fixedpoint.Value `json:"grossProfit,omitempty"`
GrossLoss fixedpoint.Value `json:"grossLoss,omitempty"`
PRR fixedpoint.Value `json:"prr,omitempty"`
PercentProfitable fixedpoint.Value `json:"percentProfitable,omitempty"`
MaxDrawdown fixedpoint.Value `json:"maxDrawdown,omitempty"`
AverageDrawdown fixedpoint.Value `json:"avgDrawdown,omitempty"`
MaxProfit fixedpoint.Value `json:"maxProfit,omitempty"`
MaxLoss fixedpoint.Value `json:"maxLoss,omitempty"`
AvgProfit fixedpoint.Value `json:"avgProfit,omitempty"`
AvgLoss fixedpoint.Value `json:"avgLoss,omitempty"`
TotalTimeInMarketSec int64 `json:"totalTimeInMarketSec,omitempty"`
AvgHoldSec int64 `json:"avgHoldSec,omitempty"`
WinningCount int `json:"winningCount,omitempty"`
LosingCount int `json:"losingCount,omitempty"`
MaxLossStreak int `json:"maxLossStreak,omitempty"`
Sharpe fixedpoint.Value `json:"sharpeRatio"`
AnnualHistoricVolatility fixedpoint.Value `json:"annualHistoricVolatility,omitempty"`
CAGR fixedpoint.Value `json:"cagr,omitempty"`
Calmar fixedpoint.Value `json:"calmar,omitempty"`
Sterling fixedpoint.Value `json:"sterling,omitempty"`
Burke fixedpoint.Value `json:"burke,omitempty"`
Kelly fixedpoint.Value `json:"kelly,omitempty"`
OptimalF fixedpoint.Value `json:"optimalF,omitempty"`
StatN fixedpoint.Value `json:"statN,omitempty"`
StdErr fixedpoint.Value `json:"statNStdErr,omitempty"`
Sortino fixedpoint.Value `json:"sortinoRatio"`
ProfitFactor fixedpoint.Value `json:"profitFactor"`
WinningRatio fixedpoint.Value `json:"winningRatio"`
} }
func (r *SessionSymbolReport) InitialEquityValue() fixedpoint.Value { func (r *SessionSymbolReport) InitialEquityValue() fixedpoint.Value {

View File

@ -359,6 +359,7 @@ func (b *ActiveOrderBook) Add(orders ...types.Order) {
} }
b.add(order) b.add(order)
b.EmitNew(order)
} }
} }
@ -466,6 +467,12 @@ func (b *ActiveOrderBook) Lookup(f func(o types.Order) bool) *types.Order {
func (b *ActiveOrderBook) filterExistingOrders(orders []types.Order) (existingOrders types.OrderSlice) { func (b *ActiveOrderBook) filterExistingOrders(orders []types.Order) (existingOrders types.OrderSlice) {
for _, o := range orders { for _, o := range orders {
// skip market order
// this prevents if someone added a market order to the active order book
if o.Type == types.OrderTypeMarket {
continue
}
if b.Exists(o) { if b.Exists(o) {
existingOrders.Add(o) existingOrders.Add(o)
} }

View File

@ -5,6 +5,8 @@ import "github.com/c9s/bbgo/pkg/types"
const MaxNumOfKLines = 5_000 const MaxNumOfKLines = 5_000
const MaxNumOfKLinesTruncate = 100 const MaxNumOfKLinesTruncate = 100
const CapacityOfKLineWindowLimit = 5_000
// MarketDataStore receives and maintain the public market data of a single symbol // MarketDataStore receives and maintain the public market data of a single symbol
//go:generate callbackgen -type MarketDataStore //go:generate callbackgen -type MarketDataStore
type MarketDataStore struct { type MarketDataStore struct {
@ -57,10 +59,20 @@ func (store *MarketDataStore) AddKLine(k types.KLine) {
} }
window.Add(k) window.Add(k)
if len(*window) > MaxNumOfKLines { truncateKLineWindowIfNeeded(window)
*window = (*window)[MaxNumOfKLinesTruncate-1:]
}
store.EmitKLineClosed(k) store.EmitKLineClosed(k)
store.EmitKLineWindowUpdate(k.Interval, *window) store.EmitKLineWindowUpdate(k.Interval, *window)
} }
func truncateKLineWindowIfNeeded(window *types.KLineWindow) {
lenOfWindow := len(*window)
capOfWindow := cap(*window)
if lenOfWindow == capOfWindow && capOfWindow > CapacityOfKLineWindowLimit {
size := CapacityOfKLineWindowLimit / 2
start := lenOfWindow - size
copy(*window, (*window)[start:])
*window = (*window)[:size]
}
}

View File

@ -0,0 +1,45 @@
package bbgo
import (
"testing"
"github.com/c9s/bbgo/pkg/types"
"github.com/stretchr/testify/assert"
)
func TestMarketDataStore_AddKLineAndTruncateWindow(t *testing.T) {
store := NewMarketDataStore("BTCUSD")
interval := types.Interval1s
var maxCap int = 0
capFixed := false
var gid uint64 = 0
// insert 1.5 * CapacityOfKLineWindowLimit KLine into window
for ; gid < CapacityOfKLineWindowLimit+(CapacityOfKLineWindowLimit/2); gid++ {
store.AddKLine(types.KLine{
Interval: interval,
GID: gid,
})
// if the capacity is > CapacityOfKLineWindowLimit, the capacity should be fixed. We use this if expression to verify it then.
if !capFixed && cap(*store.KLineWindows[interval]) > CapacityOfKLineWindowLimit {
maxCap = cap(*store.KLineWindows[interval])
capFixed = true
}
}
window := store.KLineWindows[interval]
// make sure the capacity is fixed
assert.Equal(t, maxCap, cap(*window))
// after truncate, it will remain (CapacityOfKLineWindowLimit / 2) KLine in the window
// so the first GIC will be the maxCap - (CapacityOfKLineWindowLimit / 2)
truncatedGID := uint64(maxCap - (CapacityOfKLineWindowLimit / 2))
for _, kline := range *window {
assert.Equal(t, truncatedGID, kline.GID)
truncatedGID++
}
}

View File

@ -12,12 +12,16 @@ type Quota struct {
Locked fixedpoint.Value Locked fixedpoint.Value
} }
// Add adds the fund to the available quota
func (q *Quota) Add(fund fixedpoint.Value) { func (q *Quota) Add(fund fixedpoint.Value) {
q.mu.Lock() q.mu.Lock()
q.Available = q.Available.Add(fund) q.Available = q.Available.Add(fund)
q.mu.Unlock() q.mu.Unlock()
} }
// Lock locks the fund from the available quota
// returns true if the fund is locked successfully
// returns false if the fund is not enough
func (q *Quota) Lock(fund fixedpoint.Value) bool { func (q *Quota) Lock(fund fixedpoint.Value) bool {
if fund.Compare(q.Available) > 0 { if fund.Compare(q.Available) > 0 {
return false return false
@ -31,12 +35,15 @@ func (q *Quota) Lock(fund fixedpoint.Value) bool {
return true return true
} }
// Commit commits the locked fund
func (q *Quota) Commit() { func (q *Quota) Commit() {
q.mu.Lock() q.mu.Lock()
q.Locked = fixedpoint.Zero q.Locked = fixedpoint.Zero
q.mu.Unlock() q.mu.Unlock()
} }
// Rollback rolls back the locked fund
// this will move the locked fund to the available quota
func (q *Quota) Rollback() { func (q *Quota) Rollback() {
q.mu.Lock() q.mu.Lock()
q.Available = q.Available.Add(q.Locked) q.Available = q.Available.Add(q.Locked)
@ -44,12 +51,21 @@ func (q *Quota) Rollback() {
q.mu.Unlock() q.mu.Unlock()
} }
func (q *Quota) String() string {
q.mu.Lock()
defer q.mu.Unlock()
return q.Locked.String() + "/" + q.Available.String()
}
// QuotaTransaction is a transactional quota manager
type QuotaTransaction struct { type QuotaTransaction struct {
mu sync.Mutex mu sync.Mutex
BaseAsset Quota BaseAsset Quota
QuoteAsset Quota QuoteAsset Quota
} }
// Commit commits the transaction
func (m *QuotaTransaction) Commit() bool { func (m *QuotaTransaction) Commit() bool {
m.mu.Lock() m.mu.Lock()
m.BaseAsset.Commit() m.BaseAsset.Commit()
@ -58,6 +74,7 @@ func (m *QuotaTransaction) Commit() bool {
return true return true
} }
// Rollback rolls back the transaction
func (m *QuotaTransaction) Rollback() bool { func (m *QuotaTransaction) Rollback() bool {
m.mu.Lock() m.mu.Lock()
m.BaseAsset.Rollback() m.BaseAsset.Rollback()

View File

@ -405,6 +405,8 @@ func (session *ExchangeSession) initSymbol(ctx context.Context, environ *Environ
return fmt.Errorf("market %s is not defined", symbol) return fmt.Errorf("market %s is not defined", symbol)
} }
session.logger.Infof("environment config: %+v", environ.environmentConfig)
disableMarketDataStore := environ.environmentConfig != nil && environ.environmentConfig.DisableMarketDataStore disableMarketDataStore := environ.environmentConfig != nil && environ.environmentConfig.DisableMarketDataStore
disableSessionTradeBuffer := environ.environmentConfig != nil && environ.environmentConfig.DisableSessionTradeBuffer disableSessionTradeBuffer := environ.environmentConfig != nil && environ.environmentConfig.DisableSessionTradeBuffer
maxSessionTradeBufferSize := 0 maxSessionTradeBufferSize := 0

View File

@ -16,8 +16,18 @@ import (
) )
// Strategy method calls: // Strategy method calls:
// -> Initialize() (optional method)
// -> Defaults() (optional method) // -> Defaults() (optional method)
//
// setup default static values from constants
//
// -> Initialize() (optional method)
//
// initialize dynamic runtime objects
//
// -> Subscribe()
//
// register the subscriptions
//
// -> Validate() (optional method) // -> Validate() (optional method)
// -> Run() (optional method) // -> Run() (optional method)
// -> Shutdown(shutdownCtx context.Context, wg *sync.WaitGroup) // -> Shutdown(shutdownCtx context.Context, wg *sync.WaitGroup)
@ -112,6 +122,12 @@ func (trader *Trader) DisableLogging() {
} }
func (trader *Trader) Configure(userConfig *Config) error { func (trader *Trader) Configure(userConfig *Config) error {
// config environment
if userConfig.Environment != nil && trader.environment != nil {
trader.environment.environmentConfig = userConfig.Environment
}
// config risk control
if userConfig.RiskControls != nil { if userConfig.RiskControls != nil {
trader.SetRiskControls(userConfig.RiskControls) trader.SetRiskControls(userConfig.RiskControls)
} }
@ -171,12 +187,6 @@ func (trader *Trader) SetRiskControls(riskControls *RiskControls) {
func (trader *Trader) RunSingleExchangeStrategy( func (trader *Trader) RunSingleExchangeStrategy(
ctx context.Context, strategy SingleExchangeStrategy, session *ExchangeSession, orderExecutor OrderExecutor, ctx context.Context, strategy SingleExchangeStrategy, session *ExchangeSession, orderExecutor OrderExecutor,
) error { ) error {
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 { if shutdown, ok := strategy.(StrategyShutdown); ok {
trader.gracefulShutdown.OnShutdown(shutdown.Shutdown) trader.gracefulShutdown.OnShutdown(shutdown.Shutdown)
} }
@ -238,12 +248,6 @@ func (trader *Trader) injectFieldsAndSubscribe(ctx context.Context) error {
return err return err
} }
if defaulter, ok := strategy.(StrategyDefaulter); ok {
if err := defaulter.Defaults(); err != nil {
panic(err)
}
}
if subscriber, ok := strategy.(ExchangeSessionSubscriber); ok { if subscriber, ok := strategy.(ExchangeSessionSubscriber); ok {
subscriber.Subscribe(session) subscriber.Subscribe(session)
} else { } else {
@ -304,12 +308,6 @@ func (trader *Trader) injectFieldsAndSubscribe(ctx context.Context) error {
} }
} }
if initializer, ok := strategy.(StrategyInitializer); ok {
if err := initializer.Initialize(); err != nil {
return err
}
}
if subscriber, ok := strategy.(CrossExchangeSessionSubscriber); ok { if subscriber, ok := strategy.(CrossExchangeSessionSubscriber); ok {
subscriber.CrossSubscribe(trader.environment.sessions) subscriber.CrossSubscribe(trader.environment.sessions)
} else { } else {
@ -356,8 +354,23 @@ func (trader *Trader) Run(ctx context.Context) error {
return trader.environment.Connect(ctx) return trader.environment.Connect(ctx)
} }
// Initialize initializes the strategies, this method is called before the Run method.
// It sets the default values and validates the strategy configurations.
// And calls the Initialize method if the strategy implements the Initialize method.
func (trader *Trader) Initialize(ctx context.Context) error { func (trader *Trader) Initialize(ctx context.Context) error {
return trader.IterateStrategies(func(strategy StrategyID) error { return trader.IterateStrategies(func(strategy StrategyID) error {
if defaulter, ok := strategy.(StrategyDefaulter); ok {
if err := defaulter.Defaults(); err != nil {
return err
}
}
if v, ok := strategy.(StrategyValidator); ok {
if err := v.Validate(); err != nil {
return fmt.Errorf("found invalid strategy config: %w", err)
}
}
if initializer, ok := strategy.(StrategyInitializer); ok { if initializer, ok := strategy.(StrategyInitializer); ok {
return initializer.Initialize() return initializer.Initialize()
} }

View File

@ -12,12 +12,6 @@ import (
"github.com/fatih/color" "github.com/fatih/color"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/c9s/bbgo/pkg/cmd/cmdutil"
"github.com/c9s/bbgo/pkg/core"
"github.com/c9s/bbgo/pkg/data/tsv"
"github.com/c9s/bbgo/pkg/util"
"github.com/pkg/errors" "github.com/pkg/errors"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -26,10 +20,14 @@ import (
"github.com/c9s/bbgo/pkg/accounting/pnl" "github.com/c9s/bbgo/pkg/accounting/pnl"
"github.com/c9s/bbgo/pkg/backtest" "github.com/c9s/bbgo/pkg/backtest"
"github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/cmd/cmdutil"
"github.com/c9s/bbgo/pkg/core"
"github.com/c9s/bbgo/pkg/data/tsv"
"github.com/c9s/bbgo/pkg/exchange" "github.com/c9s/bbgo/pkg/exchange"
"github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/service" "github.com/c9s/bbgo/pkg/service"
"github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/types"
"github.com/c9s/bbgo/pkg/util"
) )
func init() { func init() {
@ -547,12 +545,9 @@ var BacktestCmd = &cobra.Command{
continue continue
} }
tradeState := sessionTradeStats[session.Name][symbol] tradeStats := sessionTradeStats[session.Name][symbol]
profitFactor := tradeState.ProfitFactor
winningRatio := tradeState.WinningRatio
intervalProfits := tradeState.IntervalProfits[types.Interval1d]
symbolReport, err := createSymbolReport(userConfig, session, symbol, trades.Copy(), intervalProfits, profitFactor, winningRatio) symbolReport, err := createSymbolReport(userConfig, session, symbol, trades.Copy(), tradeStats)
if err != nil { if err != nil {
return err return err
} }
@ -563,8 +558,8 @@ var BacktestCmd = &cobra.Command{
summaryReport.TotalUnrealizedProfit = symbolReport.PnL.UnrealizedProfit summaryReport.TotalUnrealizedProfit = symbolReport.PnL.UnrealizedProfit
summaryReport.InitialEquityValue = summaryReport.InitialEquityValue.Add(symbolReport.InitialEquityValue()) summaryReport.InitialEquityValue = summaryReport.InitialEquityValue.Add(symbolReport.InitialEquityValue())
summaryReport.FinalEquityValue = summaryReport.FinalEquityValue.Add(symbolReport.FinalEquityValue()) summaryReport.FinalEquityValue = summaryReport.FinalEquityValue.Add(symbolReport.FinalEquityValue())
summaryReport.TotalGrossProfit.Add(symbolReport.PnL.GrossProfit) summaryReport.TotalGrossProfit = summaryReport.TotalGrossProfit.Add(symbolReport.PnL.GrossProfit)
summaryReport.TotalGrossLoss.Add(symbolReport.PnL.GrossLoss) summaryReport.TotalGrossLoss = summaryReport.TotalGrossLoss.Add(symbolReport.PnL.GrossLoss)
// write report to a file // write report to a file
if generatingReport { if generatingReport {
@ -617,14 +612,12 @@ var BacktestCmd = &cobra.Command{
}, },
} }
func createSymbolReport( func createSymbolReport(userConfig *bbgo.Config, session *bbgo.ExchangeSession, symbol string, trades []types.Trade, tradeStats *types.TradeStats) (
userConfig *bbgo.Config, session *bbgo.ExchangeSession, symbol string, trades []types.Trade,
intervalProfit *types.IntervalProfitCollector,
profitFactor, winningRatio fixedpoint.Value,
) (
*backtest.SessionSymbolReport, *backtest.SessionSymbolReport,
error, error,
) { ) {
intervalProfit := tradeStats.IntervalProfits[types.Interval1d]
backtestExchange, ok := session.Exchange.(*backtest.Exchange) backtestExchange, ok := session.Exchange.(*backtest.Exchange)
if !ok { if !ok {
return nil, fmt.Errorf("unexpected error, exchange instance is not a backtest exchange") return nil, fmt.Errorf("unexpected error, exchange instance is not a backtest exchange")
@ -634,6 +627,11 @@ func createSymbolReport(
if !ok { if !ok {
return nil, fmt.Errorf("market not found: %s, %s", symbol, session.Exchange.Name()) return nil, fmt.Errorf("market not found: %s, %s", symbol, session.Exchange.Name())
} }
tStart, tEnd := trades[0].Time, trades[len(trades)-1].Time
periodStart := tStart.Time()
periodEnd := tEnd.Time()
period := periodEnd.Sub(periodStart)
startPrice, ok := session.StartPrice(symbol) startPrice, ok := session.StartPrice(symbol)
if !ok { if !ok {
@ -650,29 +648,81 @@ func createSymbolReport(
Market: market, Market: market,
} }
sharpeRatio := fixedpoint.NewFromFloat(intervalProfit.GetSharpe())
sortinoRatio := fixedpoint.NewFromFloat(intervalProfit.GetSortino())
report := calculator.Calculate(symbol, trades, lastPrice) report := calculator.Calculate(symbol, trades, lastPrice)
accountConfig := userConfig.Backtest.GetAccount(session.Exchange.Name().String()) accountConfig := userConfig.Backtest.GetAccount(session.Exchange.Name().String())
initBalances := accountConfig.Balances.BalanceMap() initBalances := accountConfig.Balances.BalanceMap()
finalBalances := session.GetAccount().Balances() finalBalances := session.GetAccount().Balances()
maxProfit := n(intervalProfit.Profits.Max())
maxLoss := n(intervalProfit.Profits.Min())
drawdown := types.Drawdown(intervalProfit.Profits)
maxDrawdown := drawdown.Max()
avgDrawdown := drawdown.Average()
roundTurnCount := n(float64(tradeStats.NumOfProfitTrade + tradeStats.NumOfLossTrade))
roundTurnLength := n(float64(intervalProfit.Profits.Length()))
winningCount := n(float64(tradeStats.NumOfProfitTrade))
loosingCount := n(float64(tradeStats.NumOfLossTrade))
avgProfit := tradeStats.GrossProfit.Div(n(types.NNZ(float64(tradeStats.NumOfProfitTrade), 1)))
avgLoss := tradeStats.GrossLoss.Div(n(types.NNZ(float64(tradeStats.NumOfLossTrade), 1)))
winningPct := winningCount.Div(roundTurnCount)
// losingPct := fixedpoint.One.Sub(winningPct)
sharpeRatio := n(intervalProfit.GetSharpe())
sortinoRatio := n(intervalProfit.GetSortino())
annVolHis := n(types.AnnualHistoricVolatility(intervalProfit.Profits))
totalTimeInMarketSec, avgHoldSec := intervalProfit.GetTimeInMarket()
statn, stdErr := types.StatN(intervalProfit.Profits)
symbolReport := backtest.SessionSymbolReport{ symbolReport := backtest.SessionSymbolReport{
Exchange: session.Exchange.Name(), Exchange: session.Exchange.Name(),
Symbol: symbol, Symbol: symbol,
Market: market, Market: market,
LastPrice: lastPrice, LastPrice: lastPrice,
StartPrice: startPrice, StartPrice: startPrice,
PnL: report, InitialBalances: initBalances,
InitialBalances: initBalances, FinalBalances: finalBalances,
FinalBalances: finalBalances, TradeCount: fixedpoint.NewFromInt(int64(len(trades))),
// Manifests: manifests, GrossLoss: tradeStats.GrossLoss,
Sharpe: sharpeRatio, GrossProfit: tradeStats.GrossProfit,
Sortino: sortinoRatio, WinningCount: tradeStats.NumOfProfitTrade,
ProfitFactor: profitFactor, LosingCount: tradeStats.NumOfLossTrade,
WinningRatio: winningRatio, RoundTurnCount: roundTurnCount,
WinningRatio: tradeStats.WinningRatio,
PercentProfitable: winningPct,
ProfitFactor: tradeStats.ProfitFactor,
MaxDrawdown: n(maxDrawdown),
AverageDrawdown: n(avgDrawdown),
MaxProfit: maxProfit,
MaxLoss: maxLoss,
MaxLossStreak: tradeStats.MaximumConsecutiveLosses,
TotalTimeInMarketSec: totalTimeInMarketSec,
AvgHoldSec: avgHoldSec,
AvgProfit: avgProfit,
AvgLoss: avgLoss,
AvgNetProfit: tradeStats.TotalNetProfit.Div(roundTurnLength),
TotalNetProfit: tradeStats.TotalNetProfit,
AnnualHistoricVolatility: annVolHis,
PnL: report,
PRR: types.PRR(tradeStats.GrossProfit, tradeStats.GrossLoss, winningCount, loosingCount),
Kelly: types.KellyCriterion(tradeStats.ProfitFactor, winningPct),
OptimalF: types.OptimalF(intervalProfit.Profits),
StatN: statn,
StdErr: stdErr,
Sharpe: sharpeRatio,
Sortino: sortinoRatio,
} }
cagr := types.NN(
types.CAGR(
symbolReport.InitialEquityValue().Float64(),
symbolReport.FinalEquityValue().Float64(),
int(period.Hours())/24,
), 0)
symbolReport.CAGR = n(cagr)
symbolReport.Calmar = n(types.CalmarRatio(cagr, maxDrawdown))
symbolReport.Sterling = n(types.SterlingRatio(cagr, avgDrawdown))
symbolReport.Burke = n(types.BurkeRatio(cagr, drawdown.AverageSquared()))
for _, s := range session.Subscriptions { for _, s := range session.Subscriptions {
symbolReport.Subscriptions = append(symbolReport.Subscriptions, s) symbolReport.Subscriptions = append(symbolReport.Subscriptions, s)
} }
@ -691,6 +741,10 @@ func createSymbolReport(
return &symbolReport, nil return &symbolReport, nil
} }
func n(v float64) fixedpoint.Value {
return fixedpoint.NewFromFloat(v)
}
func verify( func verify(
userConfig *bbgo.Config, backtestService *service.BacktestService, userConfig *bbgo.Config, backtestService *service.BacktestService,
sourceExchanges map[types.ExchangeName]types.Exchange, startTime, endTime time.Time, sourceExchanges map[types.ExchangeName]types.Exchange, startTime, endTime time.Time,

View File

@ -112,6 +112,18 @@ func (s Slice) Average() float64 {
return total / float64(len(s)) return total / float64(len(s))
} }
func (s Slice) AverageSquared() float64 {
if len(s) == 0 {
return 0.0
}
total := 0.0
for _, value := range s {
total += math.Pow(value, 2)
}
return total / float64(len(s))
}
func (s Slice) Diff() (values Slice) { func (s Slice) Diff() (values Slice) {
for i, v := range s { for i, v := range s {
if i == 0 { if i == 0 {

View File

@ -1005,16 +1005,13 @@ func (e *Exchange) submitMarginOrder(ctx context.Context, order types.SubmitOrde
} }
} }
// could be IOC or FOK if len(order.TimeInForce) > 0 {
switch order.Type { // TODO: check the TimeInForce value
case types.OrderTypeLimit, types.OrderTypeStopLimit: req.TimeInForce(binance.TimeInForceType(order.TimeInForce))
req.TimeInForce(binance.TimeInForceTypeGTC) } else {
case types.OrderTypeLimitMaker: switch order.Type {
// do not set TimeInForce for LimitMaker case types.OrderTypeLimit, types.OrderTypeStopLimit:
default: req.TimeInForce(binance.TimeInForceTypeGTC)
if len(order.TimeInForce) > 0 {
// TODO: check the TimeInForce value
req.TimeInForce(binance.TimeInForceType(order.TimeInForce))
} }
} }

View File

@ -247,29 +247,6 @@ func toGlobalTradeV3(t v3.Trade) ([]types.Trade, error) {
return trades, nil return trades, nil
} }
func toGlobalTradeV2(t max.Trade) (*types.Trade, error) {
isMargin := t.WalletType == max.WalletTypeMargin
side := toGlobalSideType(t.Side)
return &types.Trade{
ID: t.ID,
OrderID: t.OrderID,
Price: t.Price,
Symbol: toGlobalSymbol(t.Market),
Exchange: types.ExchangeMax,
Quantity: t.Volume,
Side: side,
IsBuyer: t.IsBuyer(),
IsMaker: t.IsMaker(),
Fee: t.Fee,
FeeCurrency: toGlobalCurrency(t.FeeCurrency),
QuoteQuantity: t.Funds,
Time: types.Time(t.CreatedAt),
IsMargin: isMargin,
IsIsolated: false,
IsFutures: false,
}, nil
}
func toGlobalDepositStatus(a max.DepositState) types.DepositStatus { func toGlobalDepositStatus(a max.DepositState) types.DepositStatus {
switch a { switch a {
@ -284,11 +261,21 @@ func toGlobalDepositStatus(a max.DepositState) types.DepositStatus {
case max.DepositStateAccepted: case max.DepositStateAccepted:
return types.DepositSuccess return types.DepositSuccess
case max.DepositStateFailed: // v3 state
return types.DepositRejected
case max.DepositStateProcessing: // v3 states
return types.DepositPending
case max.DepositStateDone: // v3 states
return types.DepositSuccess
} }
// other states goes to this // other states goes to this
// max.DepositStateSuspect, max.DepositStateSuspended // max.DepositStateSuspect, max.DepositStateSuspended
log.Warnf("unsupported deposit state %q from max exchange", a) log.Errorf("unsupported deposit state %q from max exchange", a)
return types.DepositStatus(a) return types.DepositStatus(a)
} }

View File

@ -116,6 +116,11 @@ const (
DepositStateSuspended DepositState = "suspended" DepositStateSuspended DepositState = "suspended"
DepositStateAccepted DepositState = "accepted" DepositStateAccepted DepositState = "accepted"
DepositStateChecking DepositState = "checking" DepositStateChecking DepositState = "checking"
// v3 states
DepositStateProcessing DepositState = "processing"
DepositStateFailed DepositState = "failed"
DepositStateDone DepositState = "done"
) )
type Deposit struct { type Deposit struct {

View File

@ -136,6 +136,12 @@ func (c *CancelOrderRequest) GetSlugsMap() (map[string]string, error) {
return slugs, nil return slugs, nil
} }
// GetPath returns the request path of the API
func (c *CancelOrderRequest) GetPath() string {
return "/api/v3/order"
}
// Do generates the request object and send the request object to the API endpoint
func (c *CancelOrderRequest) Do(ctx context.Context) (*max.Order, error) { func (c *CancelOrderRequest) Do(ctx context.Context) (*max.Order, error) {
params, err := c.GetParameters() params, err := c.GetParameters()
@ -144,7 +150,9 @@ func (c *CancelOrderRequest) Do(ctx context.Context) (*max.Order, error) {
} }
query := url.Values{} query := url.Values{}
apiURL := "/api/v3/order" var apiURL string
apiURL = c.GetPath()
req, err := c.client.NewAuthenticatedRequest(ctx, "DELETE", apiURL, query, params) req, err := c.client.NewAuthenticatedRequest(ctx, "DELETE", apiURL, query, params)
if err != nil { if err != nil {
@ -157,8 +165,32 @@ func (c *CancelOrderRequest) Do(ctx context.Context) (*max.Order, error) {
} }
var apiResponse max.Order var apiResponse max.Order
if err := response.DecodeJSON(&apiResponse); err != nil {
return nil, err type responseUnmarshaler interface {
Unmarshal(data []byte) error
}
if unmarshaler, ok := interface{}(&apiResponse).(responseUnmarshaler); ok {
if err := unmarshaler.Unmarshal(response.Body); err != nil {
return nil, err
}
} else {
// The line below checks the content type, however, some API server might not send the correct content type header,
// Hence, this is commented for backward compatibility
// response.IsJSON()
if err := response.DecodeJSON(&apiResponse); err != nil {
return nil, err
}
}
type responseValidator interface {
Validate() error
}
if validator, ok := interface{}(&apiResponse).(responseValidator); ok {
if err := validator.Validate(); err != nil {
return nil, err
}
} }
return &apiResponse, nil return &apiResponse, nil
} }

View File

@ -6,11 +6,10 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/c9s/bbgo/pkg/exchange/max/maxapi"
"net/url" "net/url"
"reflect" "reflect"
"regexp" "regexp"
max "github.com/c9s/bbgo/pkg/exchange/max/maxapi"
) )
func (c *CancelWalletOrderAllRequest) Side(side string) *CancelWalletOrderAllRequest { func (c *CancelWalletOrderAllRequest) Side(side string) *CancelWalletOrderAllRequest {
@ -166,6 +165,12 @@ func (c *CancelWalletOrderAllRequest) GetSlugsMap() (map[string]string, error) {
return slugs, nil return slugs, nil
} }
// GetPath returns the request path of the API
func (c *CancelWalletOrderAllRequest) GetPath() string {
return "/api/v3/wallet/:walletType/orders"
}
// Do generates the request object and send the request object to the API endpoint
func (c *CancelWalletOrderAllRequest) Do(ctx context.Context) ([]OrderCancelResponse, error) { func (c *CancelWalletOrderAllRequest) Do(ctx context.Context) ([]OrderCancelResponse, error) {
params, err := c.GetParameters() params, err := c.GetParameters()
@ -174,7 +179,9 @@ func (c *CancelWalletOrderAllRequest) Do(ctx context.Context) ([]OrderCancelResp
} }
query := url.Values{} query := url.Values{}
apiURL := "/api/v3/wallet/:walletType/orders" var apiURL string
apiURL = c.GetPath()
slugs, err := c.GetSlugsMap() slugs, err := c.GetSlugsMap()
if err != nil { if err != nil {
return nil, err return nil, err
@ -193,8 +200,32 @@ func (c *CancelWalletOrderAllRequest) Do(ctx context.Context) ([]OrderCancelResp
} }
var apiResponse []OrderCancelResponse var apiResponse []OrderCancelResponse
if err := response.DecodeJSON(&apiResponse); err != nil {
return nil, err type responseUnmarshaler interface {
Unmarshal(data []byte) error
}
if unmarshaler, ok := interface{}(&apiResponse).(responseUnmarshaler); ok {
if err := unmarshaler.Unmarshal(response.Body); err != nil {
return nil, err
}
} else {
// The line below checks the content type, however, some API server might not send the correct content type header,
// Hence, this is commented for backward compatibility
// response.IsJSON()
if err := response.DecodeJSON(&apiResponse); err != nil {
return nil, err
}
}
type responseValidator interface {
Validate() error
}
if validator, ok := interface{}(&apiResponse).(responseValidator); ok {
if err := validator.Validate(); err != nil {
return nil, err
}
} }
return apiResponse, nil return apiResponse, nil
} }

View File

@ -236,6 +236,12 @@ func (c *CreateWalletOrderRequest) GetSlugsMap() (map[string]string, error) {
return slugs, nil return slugs, nil
} }
// GetPath returns the request path of the API
func (c *CreateWalletOrderRequest) GetPath() string {
return "/api/v3/wallet/:walletType/order"
}
// Do generates the request object and send the request object to the API endpoint
func (c *CreateWalletOrderRequest) Do(ctx context.Context) (*max.Order, error) { func (c *CreateWalletOrderRequest) Do(ctx context.Context) (*max.Order, error) {
params, err := c.GetParameters() params, err := c.GetParameters()
@ -244,7 +250,9 @@ func (c *CreateWalletOrderRequest) Do(ctx context.Context) (*max.Order, error) {
} }
query := url.Values{} query := url.Values{}
apiURL := "/api/v3/wallet/:walletType/order" var apiURL string
apiURL = c.GetPath()
slugs, err := c.GetSlugsMap() slugs, err := c.GetSlugsMap()
if err != nil { if err != nil {
return nil, err return nil, err
@ -263,8 +271,32 @@ func (c *CreateWalletOrderRequest) Do(ctx context.Context) (*max.Order, error) {
} }
var apiResponse max.Order var apiResponse max.Order
if err := response.DecodeJSON(&apiResponse); err != nil {
return nil, err type responseUnmarshaler interface {
Unmarshal(data []byte) error
}
if unmarshaler, ok := interface{}(&apiResponse).(responseUnmarshaler); ok {
if err := unmarshaler.Unmarshal(response.Body); err != nil {
return nil, err
}
} else {
// The line below checks the content type, however, some API server might not send the correct content type header,
// Hence, this is commented for backward compatibility
// response.IsJSON()
if err := response.DecodeJSON(&apiResponse); err != nil {
return nil, err
}
}
type responseValidator interface {
Validate() error
}
if validator, ok := interface{}(&apiResponse).(responseValidator); ok {
if err := validator.Validate(); err != nil {
return nil, err
}
} }
return &apiResponse, nil return &apiResponse, nil
} }

View File

@ -109,13 +109,21 @@ func (g *GetMarginADRatioRequest) GetSlugsMap() (map[string]string, error) {
return slugs, nil return slugs, nil
} }
// GetPath returns the request path of the API
func (g *GetMarginADRatioRequest) GetPath() string {
return "/api/v3/wallet/m/ad_ratio"
}
// Do generates the request object and send the request object to the API endpoint
func (g *GetMarginADRatioRequest) Do(ctx context.Context) (*ADRatio, error) { func (g *GetMarginADRatioRequest) Do(ctx context.Context) (*ADRatio, error) {
// no body params // no body params
var params interface{} var params interface{}
query := url.Values{} query := url.Values{}
apiURL := "/api/v3/wallet/m/ad_ratio" var apiURL string
apiURL = g.GetPath()
req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params)
if err != nil { if err != nil {
@ -128,8 +136,32 @@ func (g *GetMarginADRatioRequest) Do(ctx context.Context) (*ADRatio, error) {
} }
var apiResponse ADRatio var apiResponse ADRatio
if err := response.DecodeJSON(&apiResponse); err != nil {
return nil, err type responseUnmarshaler interface {
Unmarshal(data []byte) error
}
if unmarshaler, ok := interface{}(&apiResponse).(responseUnmarshaler); ok {
if err := unmarshaler.Unmarshal(response.Body); err != nil {
return nil, err
}
} else {
// The line below checks the content type, however, some API server might not send the correct content type header,
// Hence, this is commented for backward compatibility
// response.IsJSON()
if err := response.DecodeJSON(&apiResponse); err != nil {
return nil, err
}
}
type responseValidator interface {
Validate() error
}
if validator, ok := interface{}(&apiResponse).(responseValidator); ok {
if err := validator.Validate(); err != nil {
return nil, err
}
} }
return &apiResponse, nil return &apiResponse, nil
} }

View File

@ -136,6 +136,12 @@ func (g *GetOrderRequest) GetSlugsMap() (map[string]string, error) {
return slugs, nil return slugs, nil
} }
// GetPath returns the request path of the API
func (g *GetOrderRequest) GetPath() string {
return "/api/v3/order"
}
// Do generates the request object and send the request object to the API endpoint
func (g *GetOrderRequest) Do(ctx context.Context) (*max.Order, error) { func (g *GetOrderRequest) Do(ctx context.Context) (*max.Order, error) {
// empty params for GET operation // empty params for GET operation
@ -145,7 +151,9 @@ func (g *GetOrderRequest) Do(ctx context.Context) (*max.Order, error) {
return nil, err return nil, err
} }
apiURL := "/api/v3/order" var apiURL string
apiURL = g.GetPath()
req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params)
if err != nil { if err != nil {
@ -158,8 +166,32 @@ func (g *GetOrderRequest) Do(ctx context.Context) (*max.Order, error) {
} }
var apiResponse max.Order var apiResponse max.Order
if err := response.DecodeJSON(&apiResponse); err != nil {
return nil, err type responseUnmarshaler interface {
Unmarshal(data []byte) error
}
if unmarshaler, ok := interface{}(&apiResponse).(responseUnmarshaler); ok {
if err := unmarshaler.Unmarshal(response.Body); err != nil {
return nil, err
}
} else {
// The line below checks the content type, however, some API server might not send the correct content type header,
// Hence, this is commented for backward compatibility
// response.IsJSON()
if err := response.DecodeJSON(&apiResponse); err != nil {
return nil, err
}
}
type responseValidator interface {
Validate() error
}
if validator, ok := interface{}(&apiResponse).(responseValidator); ok {
if err := validator.Validate(); err != nil {
return nil, err
}
} }
return &apiResponse, nil return &apiResponse, nil
} }

View File

@ -135,6 +135,12 @@ func (g *GetOrderTradesRequest) GetSlugsMap() (map[string]string, error) {
return slugs, nil return slugs, nil
} }
// GetPath returns the request path of the API
func (g *GetOrderTradesRequest) GetPath() string {
return "/api/v3/order/trades"
}
// Do generates the request object and send the request object to the API endpoint
func (g *GetOrderTradesRequest) Do(ctx context.Context) ([]Trade, error) { func (g *GetOrderTradesRequest) Do(ctx context.Context) ([]Trade, error) {
// empty params for GET operation // empty params for GET operation
@ -144,7 +150,9 @@ func (g *GetOrderTradesRequest) Do(ctx context.Context) ([]Trade, error) {
return nil, err return nil, err
} }
apiURL := "/api/v3/order/trades" var apiURL string
apiURL = g.GetPath()
req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params)
if err != nil { if err != nil {
@ -157,8 +165,32 @@ func (g *GetOrderTradesRequest) Do(ctx context.Context) ([]Trade, error) {
} }
var apiResponse []Trade var apiResponse []Trade
if err := response.DecodeJSON(&apiResponse); err != nil {
return nil, err type responseUnmarshaler interface {
Unmarshal(data []byte) error
}
if unmarshaler, ok := interface{}(&apiResponse).(responseUnmarshaler); ok {
if err := unmarshaler.Unmarshal(response.Body); err != nil {
return nil, err
}
} else {
// The line below checks the content type, however, some API server might not send the correct content type header,
// Hence, this is commented for backward compatibility
// response.IsJSON()
if err := response.DecodeJSON(&apiResponse); err != nil {
return nil, err
}
}
type responseValidator interface {
Validate() error
}
if validator, ok := interface{}(&apiResponse).(responseValidator); ok {
if err := validator.Validate(); err != nil {
return nil, err
}
} }
return apiResponse, nil return apiResponse, nil
} }

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,29 @@
package mysql
import (
"context"
"github.com/c9s/rockhopper/v2"
)
func init() {
AddMigration("main", up_main_addPositionIndex, down_main_addPositionIndex)
}
func up_main_addPositionIndex(ctx context.Context, tx rockhopper.SQLExecutor) (err error) {
// This code is executed when the migration is applied.
_, err = tx.ExecContext(ctx, "CREATE INDEX positions_traded_at ON positions (traded_at, profit);")
if err != nil {
return err
}
return err
}
func down_main_addPositionIndex(ctx context.Context, tx rockhopper.SQLExecutor) (err error) {
// This code is executed when the migration is rolled back.
_, err = tx.ExecContext(ctx, "DROP INDEX positions_traded_at ON positions;")
if err != nil {
return err
}
return err
}

View File

@ -0,0 +1,29 @@
package sqlite3
import (
"context"
"github.com/c9s/rockhopper/v2"
)
func init() {
AddMigration("main", up_main_addPositionIndex, down_main_addPositionIndex)
}
func up_main_addPositionIndex(ctx context.Context, tx rockhopper.SQLExecutor) (err error) {
// This code is executed when the migration is applied.
_, err = tx.ExecContext(ctx, "CREATE INDEX positions_traded_at ON positions (traded_at, profit);")
if err != nil {
return err
}
return err
}
func down_main_addPositionIndex(ctx context.Context, tx rockhopper.SQLExecutor) (err error) {
// This code is executed when the migration is rolled back.
_, err = tx.ExecContext(ctx, "DROP INDEX positions_traded_at;")
if err != nil {
return err
}
return err
}

View File

@ -77,7 +77,7 @@ type Strategy struct {
// boll is the BOLLINGER indicator we used for predicting the price. // boll is the BOLLINGER indicator we used for predicting the price.
boll *indicator.BOLL boll *indicator.BOLL
CancelProfitOrdersOnShutdown bool `json: "shutdownCancelProfitOrders"` CancelProfitOrdersOnShutdown bool `json:"shutdownCancelProfitOrders"`
} }
func (s *Strategy) ID() string { func (s *Strategy) ID() string {

View File

@ -27,6 +27,10 @@ func (s *Strategy) placeTakeProfitOrders(ctx context.Context) error {
} }
roundPosition := types.NewPositionFromMarket(s.Market) roundPosition := types.NewPositionFromMarket(s.Market)
roundPosition.SetExchangeFeeRate(s.ExchangeSession.ExchangeName, types.ExchangeFee{
MakerFeeRate: s.ExchangeSession.MakerFeeRate,
TakerFeeRate: s.ExchangeSession.TakerFeeRate,
})
for _, trade := range trades { for _, trade := range trades {
s.logger.Infof("add trade into the position of this round %s", trade.String()) s.logger.Infof("add trade into the position of this round %s", trade.String())

View File

@ -236,7 +236,7 @@ func (s *Strategy) scanDepositHistory(ctx context.Context, asset string, duratio
s.watchingDeposits[deposit.TransactionID] = deposit s.watchingDeposits[deposit.TransactionID] = deposit
} }
} else { } else {
// ignore all initial deposit history that are already success // ignore all initial deposits that are already in success status
logger.Infof("ignored succeess deposit: %s %+v", deposit.TransactionID, deposit) logger.Infof("ignored succeess deposit: %s %+v", deposit.TransactionID, deposit)
} }

View File

@ -440,9 +440,9 @@ for t in 1 .. n:
return argmax(alpha[t,si] over si) return argmax(alpha[t,si] over si)
*/ */
func hmm(y_t []float64, x_t []float64, l int) float64 { func hmm(y_t []float64, x_t []float64, l int) float64 {
al := make([]float64, l) al := make([]float64, 0, l)
an := make([]float64, l) an := make([]float64, 0, l)
as := make([]float64, l) as := make([]float64, 0, l)
long := 0. long := 0.
neut := 0. neut := 0.
short := 0. short := 0.
@ -453,9 +453,9 @@ func hmm(y_t []float64, x_t []float64, l int) float64 {
sin := make([]float64, 3) sin := make([]float64, 3)
sis := make([]float64, 3) sis := make([]float64, 3)
for i := -1; i <= 1; i++ { for i := -1; i <= 1; i++ {
sil = append(sil, x_t[n-1-1]*transitProbability(i, j)) sil = append(sil, 0, x_t[n-1-1]*transitProbability(i, j))
sin = append(sin, x_t[n-1-1]*transitProbability(i, j)) sin = append(sin, 0, x_t[n-1-1]*transitProbability(i, j))
sis = append(sis, x_t[n-1-1]*transitProbability(i, j)) sis = append(sis, 0, x_t[n-1-1]*transitProbability(i, j))
} }
if j > 0 { if j > 0 {
_, longArr := floats.MinMax(sil, 3) _, longArr := floats.MinMax(sil, 3)

View File

@ -38,6 +38,12 @@ var askMarginMetrics = prometheus.NewGaugeVec(
Help: "the current ask margin (dynamic)", Help: "the current ask margin (dynamic)",
}, []string{"strategy_type", "strategy_id", "exchange", "symbol"}) }, []string{"strategy_type", "strategy_id", "exchange", "symbol"})
var aggregatedSignalMetrics = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "xmaker_aggregated_signal",
Help: "",
}, []string{"strategy_type", "strategy_id", "exchange", "symbol"})
var configNumOfLayersMetrics = prometheus.NewGaugeVec( var configNumOfLayersMetrics = prometheus.NewGaugeVec(
prometheus.GaugeOpts{ prometheus.GaugeOpts{
Name: "xmaker_config_num_of_layers", Name: "xmaker_config_num_of_layers",
@ -70,6 +76,7 @@ func init() {
makerBestAskPriceMetrics, makerBestAskPriceMetrics,
bidMarginMetrics, bidMarginMetrics,
askMarginMetrics, askMarginMetrics,
aggregatedSignalMetrics,
configNumOfLayersMetrics, configNumOfLayersMetrics,
configMaxExposureMetrics, configMaxExposureMetrics,
configBidMarginMetrics, configBidMarginMetrics,

View File

@ -0,0 +1,87 @@
package xmaker
import (
"context"
"github.com/prometheus/client_golang/prometheus"
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/indicator/v2"
"github.com/c9s/bbgo/pkg/types"
)
var bollingerBandSignalMetrics = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "xmaker_bollinger_band_signal",
Help: "",
}, []string{"symbol"})
func init() {
prometheus.MustRegister(bollingerBandSignalMetrics)
}
type BollingerBandTrendSignal struct {
types.IntervalWindow
MinBandWidth float64 `json:"minBandWidth"`
MaxBandWidth float64 `json:"maxBandWidth"`
indicator *indicatorv2.BOLLStream
symbol string
lastK *types.KLine
}
func (s *BollingerBandTrendSignal) Bind(ctx context.Context, session *bbgo.ExchangeSession, symbol string) error {
if s.MaxBandWidth == 0.0 {
s.MaxBandWidth = 2.0
}
if s.MinBandWidth == 0.0 {
s.MinBandWidth = 1.0
}
s.symbol = symbol
s.indicator = session.Indicators(symbol).BOLL(s.IntervalWindow, s.MinBandWidth)
session.MarketDataStream.OnKLineClosed(types.KLineWith(s.symbol, s.IntervalWindow.Interval, func(kline types.KLine) {
s.lastK = &kline
}))
bollingerBandSignalMetrics.WithLabelValues(s.symbol).Set(0.0)
return nil
}
func (s *BollingerBandTrendSignal) CalculateSignal(ctx context.Context) (float64, error) {
if s.lastK == nil {
return 0, nil
}
closePrice := s.lastK.Close
// 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
lastDownBand := fixedpoint.NewFromFloat(s.indicator.DownBand.Last(0))
lastUpBand := fixedpoint.NewFromFloat(s.indicator.UpBand.Last(0))
maxBandWidth := s.indicator.StdDev.Last(0) * s.MaxBandWidth
signal := 0.0
// if the price is inside the band, do not vote
if closePrice.Compare(lastDownBand) > 0 && closePrice.Compare(lastUpBand) < 0 {
signal = 0.0
} else if closePrice.Compare(lastDownBand) < 0 {
signal = lastDownBand.Sub(closePrice).Float64() / maxBandWidth * -2.0
} else if closePrice.Compare(lastUpBand) > 0 {
signal = closePrice.Sub(lastUpBand).Float64() / maxBandWidth * 2.0
}
log.Infof("[BollingerBandTrendSignal] %f up/down = %f/%f, close price = %f",
signal,
lastUpBand.Float64(),
lastDownBand.Float64(),
closePrice.Float64())
bollingerBandSignalMetrics.WithLabelValues(s.symbol).Set(signal)
return signal, nil
}

View File

@ -0,0 +1,68 @@
package xmaker
import (
"context"
"github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus"
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
)
var orderBookSignalMetrics = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "xmaker_order_book_signal",
Help: "",
}, []string{"symbol"})
func init() {
prometheus.MustRegister(orderBookSignalMetrics)
}
type OrderBookBestPriceVolumeSignal struct {
RatioThreshold fixedpoint.Value `json:"ratioThreshold"`
MinVolume fixedpoint.Value `json:"minVolume"`
symbol string
book *types.StreamOrderBook
}
func (s *OrderBookBestPriceVolumeSignal) Bind(ctx context.Context, session *bbgo.ExchangeSession, symbol string) error {
if s.book == nil {
return errors.New("s.book can not be nil")
}
s.symbol = symbol
orderBookSignalMetrics.WithLabelValues(s.symbol).Set(0.0)
return nil
}
func (s *OrderBookBestPriceVolumeSignal) CalculateSignal(ctx context.Context) (float64, error) {
bid, ask, ok := s.book.BestBidAndAsk()
if !ok {
return 0.0, nil
}
// TODO: may use scale to define this
sumVol := bid.Volume.Add(ask.Volume)
bidRatio := bid.Volume.Div(sumVol)
askRatio := ask.Volume.Div(sumVol)
denominator := fixedpoint.One.Sub(s.RatioThreshold)
signal := 0.0
if bid.Volume.Compare(s.MinVolume) < 0 && ask.Volume.Compare(s.MinVolume) < 0 {
signal = 0.0
} else if bidRatio.Compare(s.RatioThreshold) >= 0 {
numerator := bidRatio.Sub(s.RatioThreshold)
signal = numerator.Div(denominator).Float64()
} else if askRatio.Compare(s.RatioThreshold) >= 0 {
numerator := askRatio.Sub(s.RatioThreshold)
signal = -numerator.Div(denominator).Float64()
}
log.Infof("[OrderBookBestPriceVolumeSignal] %f bid/ask = %f/%f", signal, bid.Volume.Float64(), ask.Volume.Float64())
orderBookSignalMetrics.WithLabelValues(s.symbol).Set(signal)
return signal, nil
}

View File

@ -0,0 +1,111 @@
package xmaker
import (
"context"
"sync"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
)
var tradeVolumeWindowSignalMetrics = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "xmaker_trade_volume_window_signal",
Help: "",
}, []string{"symbol"})
func init() {
prometheus.MustRegister(tradeVolumeWindowSignalMetrics)
}
type TradeVolumeWindowSignal struct {
Threshold fixedpoint.Value `json:"threshold"`
Window types.Duration `json:"window"`
trades []types.Trade
symbol string
mu sync.Mutex
}
func (s *TradeVolumeWindowSignal) handleTrade(trade types.Trade) {
s.mu.Lock()
s.trades = append(s.trades, trade)
s.mu.Unlock()
}
func (s *TradeVolumeWindowSignal) Bind(ctx context.Context, session *bbgo.ExchangeSession, symbol string) error {
s.symbol = symbol
if s.Window == 0 {
s.Window = types.Duration(time.Minute)
}
if s.Threshold.IsZero() {
s.Threshold = fixedpoint.NewFromFloat(0.7)
}
session.MarketDataStream.OnMarketTrade(s.handleTrade)
return nil
}
func (s *TradeVolumeWindowSignal) filterTrades(now time.Time) []types.Trade {
startTime := now.Add(-time.Duration(s.Window))
startIdx := 0
s.mu.Lock()
defer s.mu.Unlock()
for idx, td := range s.trades {
// skip trades before the start time
if td.Time.Before(startTime) {
continue
}
startIdx = idx
break
}
trades := s.trades[startIdx:]
s.trades = trades
return trades
}
func (s *TradeVolumeWindowSignal) aggTradeVolume(trades []types.Trade) (buyVolume, sellVolume float64) {
for _, td := range trades {
if td.IsBuyer {
buyVolume += td.Quantity.Float64()
} else {
sellVolume += td.Quantity.Float64()
}
}
return buyVolume, sellVolume
}
func (s *TradeVolumeWindowSignal) CalculateSignal(_ context.Context) (float64, error) {
now := time.Now()
trades := s.filterTrades(now)
buyVolume, sellVolume := s.aggTradeVolume(trades)
totalVolume := buyVolume + sellVolume
threshold := s.Threshold.Float64()
buyRatio := buyVolume / totalVolume
sellRatio := sellVolume / totalVolume
sig := 0.0
if buyRatio > threshold {
sig = (buyRatio - threshold) / 2.0
} else if sellRatio > threshold {
sig = -(sellRatio - threshold) / 2.0
}
log.Infof("[TradeVolumeWindowSignal] %f buy/sell = %f/%f", sig, buyVolume, sellVolume)
tradeVolumeWindowSignalMetrics.WithLabelValues(s.symbol).Set(sig)
return sig, nil
}

View File

@ -0,0 +1,55 @@
package xmaker
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
. "github.com/c9s/bbgo/pkg/testing/testhelper"
)
var tradeId = 0
func Trade(symbol string, side types.SideType, price, quantity fixedpoint.Value, t time.Time) types.Trade {
tradeId++
return types.Trade{
ID: uint64(tradeId),
Symbol: symbol,
Side: side,
Price: price,
IsBuyer: side == types.SideTypeBuy,
Quantity: quantity,
Time: types.Time(t),
}
}
func TestMarketTradeWindowSignal(t *testing.T) {
now := time.Now()
symbol := "BTCUSDT"
sig := &TradeVolumeWindowSignal{
symbol: symbol,
Threshold: fixedpoint.NewFromFloat(0.65),
Window: types.Duration(time.Minute),
}
sig.trades = []types.Trade{
Trade(symbol, types.SideTypeBuy, Number(18000.0), Number(1.0), now.Add(-2*time.Minute)),
Trade(symbol, types.SideTypeSell, Number(18000.0), Number(0.5), now.Add(-2*time.Second)),
Trade(symbol, types.SideTypeBuy, Number(18000.0), Number(1.0), now.Add(-1*time.Second)),
}
ctx := context.Background()
sigNum, err := sig.CalculateSignal(ctx)
if assert.NoError(t, err) {
// buy ratio: 1/1.5 = 0.6666666666666666
// sell ratio: 0.5/1.5 = 0.3333333333333333
assert.InDelta(t, 0.0083333, sigNum, 0.0001)
}
assert.Len(t, sig.trades, 2)
}

File diff suppressed because it is too large Load Diff

View File

@ -2,28 +2,89 @@ package xmaker
import ( import (
"testing" "testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/types"
"github.com/stretchr/testify/assert"
. "github.com/c9s/bbgo/pkg/testing/testhelper"
) )
func Test_aggregatePrice(t *testing.T) { func TestStrategy_getLayerPrice(t *testing.T) {
bids := types.PriceVolumeSlice{ symbol := "BTCUSDT"
{ market := Market(symbol)
Price: fixedpoint.NewFromFloat(1000.0),
Volume: fixedpoint.NewFromFloat(1.0), s := &Strategy{
}, UseDepthPrice: true,
{ DepthQuantity: Number(3.0),
Price: fixedpoint.NewFromFloat(1200.0), makerMarket: market,
Volume: fixedpoint.NewFromFloat(1.0),
},
{
Price: fixedpoint.NewFromFloat(1400.0),
Volume: fixedpoint.NewFromFloat(1.0),
},
} }
sourceBook := types.NewStreamBook(symbol, types.ExchangeBinance)
sourceBook.Load(types.SliceOrderBook{
Symbol: symbol,
Bids: PriceVolumeSlice(
Number(1300.0), Number(1.0),
Number(1200.0), Number(2.0),
Number(1100.0), Number(3.0),
),
Asks: PriceVolumeSlice(
Number(1301.0), Number(1.0),
Number(1400.0), Number(2.0),
Number(1500.0), Number(3.0),
),
Time: time.Time{},
LastUpdateId: 1,
})
quote := &Quote{
BestBidPrice: Number(1300.0),
BestAskPrice: Number(1301.0),
BidMargin: Number(0.001),
AskMargin: Number(0.001),
BidLayerPips: Number(100.0),
AskLayerPips: Number(100.0),
}
t.Run("depthPrice bid price at 0", func(t *testing.T) {
price := s.getLayerPrice(0, types.SideTypeBuy, sourceBook, quote, s.DepthQuantity)
// (1300 + 1200*2)/3 * (1 - 0.001)
assert.InDelta(t, 1232.10, price.Float64(), 0.01)
})
t.Run("depthPrice bid price at 1", func(t *testing.T) {
price := s.getLayerPrice(1, types.SideTypeBuy, sourceBook, quote, s.DepthQuantity)
// (1300 + 1200*2)/3 * (1 - 0.001) - 100 * 0.01
assert.InDelta(t, 1231.10, price.Float64(), 0.01)
})
t.Run("depthPrice ask price at 0", func(t *testing.T) {
price := s.getLayerPrice(0, types.SideTypeSell, sourceBook, quote, s.DepthQuantity)
// (1301 + 1400*2)/3 * (1 + 0.001)
assert.InDelta(t, 1368.367, price.Float64(), 0.01)
})
t.Run("depthPrice ask price at 1", func(t *testing.T) {
price := s.getLayerPrice(1, types.SideTypeSell, sourceBook, quote, s.DepthQuantity)
// (1301 + 1400*2)/3 * (1 + 0.001) + 100 * 0.01
assert.InDelta(t, 1369.367, price.Float64(), 0.01)
})
}
func Test_aggregatePrice(t *testing.T) {
bids := PriceVolumeSliceFromText(`
1000.0, 1.0
1200.0, 1.0
1400.0, 1.0
`)
aggregatedPrice1 := aggregatePrice(bids, fixedpoint.NewFromFloat(0.5)) aggregatedPrice1 := aggregatePrice(bids, fixedpoint.NewFromFloat(0.5))
assert.Equal(t, fixedpoint.NewFromFloat(1000.0), aggregatedPrice1) assert.Equal(t, fixedpoint.NewFromFloat(1000.0), aggregatedPrice1)

View File

@ -0,0 +1,43 @@
package testhelper
import (
"fmt"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
)
var markets = map[string]types.Market{
"BTCUSDT": {
Symbol: "BTCUSDT",
PricePrecision: 2,
VolumePrecision: 8,
QuoteCurrency: "USDT",
BaseCurrency: "BTC",
MinNotional: fixedpoint.MustNewFromString("0.001"),
MinAmount: fixedpoint.MustNewFromString("10.0"),
MinQuantity: fixedpoint.MustNewFromString("0.001"),
TickSize: fixedpoint.MustNewFromString("0.01"),
},
"ETHUSDT": {
Symbol: "ETH",
PricePrecision: 2,
VolumePrecision: 8,
QuoteCurrency: "USDT",
BaseCurrency: "ETH",
MinNotional: fixedpoint.MustNewFromString("0.005"),
MinAmount: fixedpoint.MustNewFromString("10.0"),
MinQuantity: fixedpoint.MustNewFromString("0.001"),
TickSize: fixedpoint.MustNewFromString("0.01"),
},
}
func Market(symbol string) types.Market {
market, ok := markets[symbol]
if !ok {
panic(fmt.Errorf("%s market not found, valid markets: %+v", symbol, markets))
}
return market
}

View File

@ -0,0 +1,49 @@
package testhelper
import (
"fmt"
"strings"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
)
func PriceVolumeSliceFromText(str string) (slice types.PriceVolumeSlice) {
lines := strings.Split(str, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if len(line) == 0 {
continue
}
cols := strings.SplitN(line, ",", 2)
if len(cols) < 2 {
panic(fmt.Errorf("column length should be 2, got %d", len(cols)))
}
price := fixedpoint.MustNewFromString(strings.TrimSpace(cols[0]))
volume := fixedpoint.MustNewFromString(strings.TrimSpace(cols[1]))
slice = append(slice, types.PriceVolume{
Price: price,
Volume: volume,
})
}
return slice
}
func PriceVolumeSlice(values ...fixedpoint.Value) (slice types.PriceVolumeSlice) {
if len(values)%2 != 0 {
panic("values should be paired")
}
for i := 0; i < len(values); i += 2 {
slice = append(slice, types.PriceVolume{
Price: values[i],
Volume: values[i+1],
})
}
return slice
}

View File

@ -2,8 +2,9 @@ package types
import ( import (
"encoding/json" "encoding/json"
"github.com/stretchr/testify/assert"
"testing" "testing"
"github.com/stretchr/testify/assert"
) )
func TestKLineWindow_Tail(t *testing.T) { func TestKLineWindow_Tail(t *testing.T) {

View File

@ -656,6 +656,12 @@ func (p *Position) AddTrade(td Trade) (profit fixedpoint.Value, netProfit fixedp
return fixedpoint.Zero, fixedpoint.Zero, false return fixedpoint.Zero, fixedpoint.Zero, false
} }
func (p *Position) UpdateMetrics() {
p.Lock()
p.updateMetrics()
p.Unlock()
}
func (p *Position) updateMetrics() { func (p *Position) updateMetrics() {
// update the position metrics only if the position defines the strategy ID // update the position metrics only if the position defines the strategy ID
if p.StrategyInstanceID == "" || p.Strategy == "" { if p.StrategyInstanceID == "" || p.Strategy == "" {

View File

@ -13,6 +13,13 @@ type PriceVolume struct {
Price, Volume fixedpoint.Value Price, Volume fixedpoint.Value
} }
func NewPriceVolume(p, v fixedpoint.Value) PriceVolume {
return PriceVolume{
Price: p,
Volume: v,
}
}
func (p PriceVolume) InQuote() fixedpoint.Value { func (p PriceVolume) InQuote() fixedpoint.Value {
return p.Price.Mul(p.Volume) return p.Price.Mul(p.Volume)
} }

View File

@ -249,7 +249,7 @@ func (s *StandardStream) Read(ctx context.Context, conn *websocket.Conn, cancel
default: default:
if err := conn.SetReadDeadline(time.Now().Add(readTimeout)); err != nil { if err := conn.SetReadDeadline(time.Now().Add(readTimeout)); err != nil {
log.WithError(err).Errorf("set read deadline error: %s", err.Error()) log.WithError(err).Errorf("unable to set read deadline: %v", err)
} }
mt, message, err := conn.ReadMessage() mt, message, err := conn.ReadMessage()
@ -300,7 +300,7 @@ func (s *StandardStream) Read(ctx context.Context, conn *websocket.Conn, cancel
var e interface{} var e interface{}
e, err = s.parser(message) e, err = s.parser(message)
if err != nil { if err != nil {
log.WithError(err).Errorf("websocket event parse error, message: %s", message) log.WithError(err).Errorf("unable to parse the websocket message. err: %v, message: %s", err, message)
// emit raw message even if occurs error, because we want anything can be detected // emit raw message even if occurs error, because we want anything can be detected
s.EmitRawMessage(message) s.EmitRawMessage(message)
continue continue
@ -352,7 +352,7 @@ func (s *StandardStream) ping(
} }
if err := conn.WriteControl(websocket.PingMessage, nil, time.Now().Add(writeTimeout)); err != nil { if err := conn.WriteControl(websocket.PingMessage, nil, time.Now().Add(writeTimeout)); err != nil {
log.WithError(err).Error("ping error", err) log.WithError(err).Warnf("unable to write ws control message, ping error: %v", err)
s.Reconnect() s.Reconnect()
return return
} }
@ -439,7 +439,7 @@ func (s *StandardStream) reconnector(ctx context.Context) {
log.Warnf("re-connecting...") log.Warnf("re-connecting...")
if err := s.DialAndConnect(ctx); err != nil { if err := s.DialAndConnect(ctx); err != nil {
log.WithError(err).Errorf("re-connect error, try to reconnect later") log.WithError(err).Warnf("re-connect error: %v, will reconnect again later...", err)
// re-emit the re-connect signal if error // re-emit the re-connect signal if error
s.Reconnect() s.Reconnect()

151
pkg/types/trade_stat.go Normal file
View File

@ -0,0 +1,151 @@
package types
import (
"math"
"gonum.org/v1/gonum/stat"
"github.com/c9s/bbgo/pkg/datatype/floats"
"github.com/c9s/bbgo/pkg/fixedpoint"
)
const (
// DailyToAnnualFactor is the factor to scale daily observations to annual.
// Commonly defined as the number of public market trading days in a year.
DailyToAnnualFactor = 252 // todo does this apply to crypto at all?
)
// AnnualHistoricVolatility is the historic volatility of the equity curve as annualized std dev.
func AnnualHistoricVolatility(data Series) float64 {
var sd = Stdev(data, data.Length(), 1)
return sd * math.Sqrt(DailyToAnnualFactor)
}
// CAGR is the Compound Annual Growth Rate of the equity curve.
func CAGR(initial, final float64, days int) float64 {
var (
growthRate = (final - initial) / initial
x = 1 + growthRate
y = 365.0 / float64(days)
)
return math.Pow(x, y) - 1
}
// measures of risk-adjusted return based on drawdown risk
// calmar ratio - discounts expected excess return of a portfolio by the
// worst expected maximum draw down for that portfolio
// CR = E(re)/MD1 = (E(r) - rf) / MD1
func CalmarRatio(cagr, maxDrawdown float64) float64 {
return cagr / maxDrawdown
}
// Sterling ratio
// discounts the expected excess return of a portfolio by the average of the N worst
// expected maximum drawdowns for that portfolio
// CR = E(re) / (1/N)(sum MDi)
func SterlingRatio(cagr, avgDrawdown float64) float64 {
return cagr / avgDrawdown
}
// Burke Ratio
// similar to sterling, but less sensitive to outliers
// discounts the expected excess return of a portfolio by the square root of the average
// of the N worst expected maximum drawdowns for that portfolio
// BR = E(re) / ((1/N)(sum MD^2))^0.5 ---> smoothing, can take roots, logs etc
func BurkeRatio(cagr, avgDrawdownSquared float64) float64 {
return cagr / math.Sqrt(avgDrawdownSquared)
}
// KellyCriterion the famous method for trade sizing.
func KellyCriterion(profitFactor, winP fixedpoint.Value) fixedpoint.Value {
return profitFactor.Mul(winP).Sub(fixedpoint.One.Sub(winP)).Div(profitFactor)
}
// PRR (Pessimistic Return Ratio) is the profit factor with a penalty for a lower number of roundturns.
func PRR(profit, loss, winningN, losingN fixedpoint.Value) fixedpoint.Value {
var (
winF = 1 / math.Sqrt(1+winningN.Float64())
loseF = 1 / math.Sqrt(1+losingN.Float64())
)
return fixedpoint.NewFromFloat((1 - winF) / (1 + loseF) * (1 + profit.Float64()) / (1 + loss.Float64()))
}
// StatN returns the statistically significant number of samples required based on the distribution of a series.
// From: https://www.elitetrader.com/et/threads/minimum-number-of-roundturns-required-for-backtesting-results-to-be-trusted.356588/page-2
func StatN(xs floats.Slice) (sn, se fixedpoint.Value) {
var (
sd = Stdev(xs, xs.Length(), 1)
m = Mean(xs)
statn = math.Pow(4*(sd/m), 2)
stdErr = stat.StdErr(sd, float64(xs.Length()))
)
return fixedpoint.NewFromFloat(statn), fixedpoint.NewFromFloat(stdErr)
}
// OptimalF is a function that returns the 'OptimalF' for a series of trade returns as defined by Ralph Vince.
// It is a method for sizing positions to maximize geometric return whilst accounting for biggest trading loss.
// See: https://www.investopedia.com/terms/o/optimalf.asp
// Param roundturns is the series of profits (-ve amount for losses) for each trade
func OptimalF(roundturns floats.Slice) fixedpoint.Value {
var (
maxTWR, optimalF float64
maxLoss = roundturns.Min()
)
for i := 1.0; i <= 100.0; i++ {
twr := 1.0
f := i / 100
for j := range roundturns {
if roundturns[j] == 0 {
continue
}
hpr := 1 + f*(-roundturns[j]/maxLoss)
twr *= hpr
}
if twr > maxTWR {
maxTWR = twr
optimalF = f
}
}
return fixedpoint.NewFromFloat(optimalF)
}
// NN (Not Number) returns y if x is NaN or Inf.
func NN(x, y float64) float64 {
if math.IsNaN(x) || math.IsInf(x, 0) {
return y
}
return x
}
// NNZ (Not Number or Zero) returns y if x is NaN or Inf or Zero.
func NNZ(x, y float64) float64 {
if NN(x, y) == y || x == 0 {
return y
}
return x
}
// Compute the drawdown function associated to a portfolio equity curve,
// also called the portfolio underwater equity curve.
// Portfolio Optimization with Drawdown Constraints, Chekhlov et al., 2000
// http://papers.ssrn.com/sol3/papers.cfm?abstract_id=223323
func Drawdown(equityCurve floats.Slice) floats.Slice {
// Initialize highWaterMark
highWaterMark := math.Inf(-1)
// Create ddVector with the same length as equityCurve
ddVector := make([]float64, len(equityCurve))
// Loop over all the values to compute the drawdown vector
for i := 0; i < len(equityCurve); i++ {
if equityCurve[i] > highWaterMark {
highWaterMark = equityCurve[i]
}
ddVector[i] = (highWaterMark - equityCurve[i]) / highWaterMark
}
return ddVector
}

View File

@ -0,0 +1,56 @@
package types
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/c9s/bbgo/pkg/datatype/floats"
"github.com/c9s/bbgo/pkg/fixedpoint"
)
func TestCAGR(t *testing.T) {
giveInitial := 1000.0
giveFinal := 2500.0
giveDays := 190
want := 4.81
act := CAGR(giveInitial, giveFinal, giveDays)
assert.InDelta(t, want, act, 0.01)
}
func TestKellyCriterion(t *testing.T) {
var (
giveProfitFactor = fixedpoint.NewFromFloat(1.6)
giveWinP = fixedpoint.NewFromFloat(0.7)
want = 0.51
act = KellyCriterion(giveProfitFactor, giveWinP)
)
assert.InDelta(t, want, act.Float64(), 0.01)
}
func TestAnnualHistoricVolatility(t *testing.T) {
var (
give = floats.Slice{0.1, 0.2, -0.15, 0.1, 0.8, -0.3, 0.2}
want = 5.51
act = AnnualHistoricVolatility(give)
)
assert.InDelta(t, want, act, 0.01)
}
func TestOptimalF(t *testing.T) {
roundturns := floats.Slice{10, 20, 50, -10, 40, -40}
f := OptimalF(roundturns)
assert.EqualValues(t, 0.45, f.Float64())
}
func TestDrawdown(t *testing.T) {
roundturns := floats.Slice{100, 50, 100}
expected := []float64{.0, .5, .0}
drawdown := Drawdown(roundturns)
assert.EqualValues(t, 0.5, drawdown.Max())
assert.EqualValues(t, 0.16666666666666666, drawdown.Average())
assert.EqualValues(t, 0.08333333333333333, drawdown.AverageSquared())
for i, v := range expected {
assert.EqualValues(t, v, drawdown[i])
}
}

View File

@ -8,45 +8,16 @@ import (
"time" "time"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"github.com/c9s/bbgo/pkg/datatype/floats" "github.com/c9s/bbgo/pkg/datatype/floats"
"github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/fixedpoint"
) )
type IntervalProfitCollector struct { const (
Interval Interval `json:"interval"` ErrStartTimeNotValid = "No valid start time. Did you create IntervalProfitCollector instance using NewIntervalProfitCollector?"
Profits *floats.Slice `json:"profits"` ErrProfitArrEmpty = "profits array empty. Did you create IntervalProfitCollector instance using NewIntervalProfitCollector?"
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 { type ProfitReport struct {
StartTime time.Time `json:"startTime"` StartTime time.Time `json:"startTime"`
@ -62,6 +33,55 @@ func (s ProfitReport) String() string {
return string(b) return string(b)
} }
type IntervalProfitCollector struct {
Interval Interval `json:"interval"`
Profits floats.Slice `json:"profits"`
TimeInMarket []time.Duration `json:"timeInMarket"`
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(ErrStartTimeNotValid)
} else {
s.TimeInMarket = append(s.TimeInMarket, profit.TradedAt.Sub(profit.PositionOpenedAt))
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
}
}
}
}
}
// Determine average and total time spend in market
func (s *IntervalProfitCollector) GetTimeInMarket() (avgHoldSec, totalTimeInMarketSec int64) {
if s.Profits == nil {
return 0, 0
}
l := len(s.TimeInMarket)
for i := 0; i < l; i++ {
d := s.TimeInMarket[i]
totalTimeInMarketSec += int64(d / time.Millisecond)
}
avgHoldSec = totalTimeInMarketSec / int64(l)
return
}
// Get all none-profitable intervals // Get all none-profitable intervals
func (s *IntervalProfitCollector) GetNonProfitableIntervals() (result []ProfitReport) { func (s *IntervalProfitCollector) GetNonProfitableIntervals() (result []ProfitReport) {
if s.Profits == nil { if s.Profits == nil {
@ -93,9 +113,9 @@ func (s *IntervalProfitCollector) GetProfitableIntervals() (result []ProfitRepor
// Get number of profitable traded intervals // Get number of profitable traded intervals
func (s *IntervalProfitCollector) GetNumOfProfitableIntervals() (profit int) { func (s *IntervalProfitCollector) GetNumOfProfitableIntervals() (profit int) {
if s.Profits == nil { if s.Profits == nil {
panic("profits array empty. Did you create IntervalProfitCollector instance using NewIntervalProfitCollector?") panic(ErrProfitArrEmpty)
} }
for _, v := range *s.Profits { for _, v := range s.Profits {
if v > 1. { if v > 1. {
profit += 1 profit += 1
} }
@ -107,9 +127,9 @@ func (s *IntervalProfitCollector) GetNumOfProfitableIntervals() (profit int) {
// (no trade within the interval or pnl = 0 will be also included here) // (no trade within the interval or pnl = 0 will be also included here)
func (s *IntervalProfitCollector) GetNumOfNonProfitableIntervals() (nonprofit int) { func (s *IntervalProfitCollector) GetNumOfNonProfitableIntervals() (nonprofit int) {
if s.Profits == nil { if s.Profits == nil {
panic("profits array empty. Did you create IntervalProfitCollector instance using NewIntervalProfitCollector?") panic(ErrProfitArrEmpty)
} }
for _, v := range *s.Profits { for _, v := range s.Profits {
if v <= 1. { if v <= 1. {
nonprofit += 1 nonprofit += 1
} }
@ -121,10 +141,11 @@ func (s *IntervalProfitCollector) GetNumOfNonProfitableIntervals() (nonprofit in
// no smart sharpe ON for the calculated result // no smart sharpe ON for the calculated result
func (s *IntervalProfitCollector) GetSharpe() float64 { func (s *IntervalProfitCollector) GetSharpe() float64 {
if s.tmpTime.IsZero() { if s.tmpTime.IsZero() {
panic("No valid start time. Did you create IntervalProfitCollector instance using NewIntervalProfitCollector?") panic(ErrStartTimeNotValid)
} }
if s.Profits == nil { if s.Profits == nil {
panic("profits array empty. Did you create IntervalProfitCollector instance using NewIntervalProfitCollector?") panic(ErrStartTimeNotValid)
} }
return Sharpe(Sub(s.Profits, 1.), s.Profits.Length(), true, false) return Sharpe(Sub(s.Profits, 1.), s.Profits.Length(), true, false)
} }
@ -133,10 +154,10 @@ func (s *IntervalProfitCollector) GetSharpe() float64 {
// No risk-free return rate and smart sortino OFF for the calculated result. // No risk-free return rate and smart sortino OFF for the calculated result.
func (s *IntervalProfitCollector) GetSortino() float64 { func (s *IntervalProfitCollector) GetSortino() float64 {
if s.tmpTime.IsZero() { if s.tmpTime.IsZero() {
panic("No valid start time. Did you create IntervalProfitCollector instance using NewIntervalProfitCollector?") panic(ErrStartTimeNotValid)
} }
if s.Profits == nil { if s.Profits == nil {
panic("profits array empty. Did you create IntervalProfitCollector instance using NewIntervalProfitCollector?") panic(ErrProfitArrEmpty)
} }
return Sortino(Sub(s.Profits, 1.), 0., s.Profits.Length(), true, false) return Sortino(Sub(s.Profits, 1.), 0., s.Profits.Length(), true, false)
} }

View File

@ -3,6 +3,6 @@
package version package version
const Version = "v1.60.0-3a2e4dfd2-dev" const Version = "v1.60.3-26b1fd2ae-dev"
const VersionGitRef = "3a2e4dfd2" const VersionGitRef = "26b1fd2ae"

View File

@ -3,6 +3,6 @@
package version package version
const Version = "v1.60.0-3a2e4dfd2" const Version = "v1.60.3-26b1fd2ae"
const VersionGitRef = "3a2e4dfd2" const VersionGitRef = "26b1fd2ae"