diff --git a/config/rsmaker.yaml b/config/rsmaker.yaml new file mode 100644 index 000000000..a19941d18 --- /dev/null +++ b/config/rsmaker.yaml @@ -0,0 +1,101 @@ +--- +persistence: + redis: + host: 127.0.0.1 + port: 6379 + db: 0 + +sync: + # userDataStream is used to sync the trading data in real-time + # it uses the websocket connection to insert the trades + userDataStream: + trades: true + filledOrders: true + + # since is the start date of your trading data + since: 2021-08-01 + + # sessions is the list of session names you want to sync + # by default, BBGO sync all your available sessions. + sessions: + - binance + + # symbols is the list of symbols you want to sync + # by default, BBGO try to guess your symbols by your existing account balances. + symbols: + - NEARBUSD + - BTCUSDT + - ETHUSDT + - LINKUSDT + - BNBUSDT + - DOTUSDT + - DOTBUSD + + +sessions: + binance: + exchange: binance + envVarPrefix: binance +# futures: true + + +exchangeStrategies: +- on: binance + rsmaker: + symbol: BTCBUSD + interval: 1m +# quantity: 40 + amount: 20 + minProfitSpread: 0.1% + +# uptrendSkew: 0.7 + + # downtrendSkew, like the strongDowntrendSkew, but the price is still in the default band. +# downtrendSkew: 1.3 + + # tradeInBand: when tradeInBand is set, you will only place orders in the bollinger band. +# tradeInBand: true + + # buyBelowNeutralSMA: when this set, it will only place buy order when the current price is below the SMA line. +# buyBelowNeutralSMA: true + + defaultBollinger: + interval: "1h" + window: 21 + bandWidth: 2.0 + + # neutralBollinger is the smaller range of the bollinger band + # If price is in this band, it usually means the price is oscillating. + neutralBollinger: + interval: "5m" + window: 21 + bandWidth: 2.0 + + dynamicExposurePositionScale: + byPercentage: + # exp means we want to use exponential scale, you can replace "exp" with "linear" for linear scale + exp: + # from lower band -100% (-1) to upper band 100% (+1) + domain: [ -2, 2 ] + # when in down band, holds 1.0 by maximum + # when in up band, holds 0.05 by maximum + range: [ 1, 0.01 ] + + + +backtest: + sessions: + - binance + # for testing max draw down (MDD) at 03-12 + # see here for more details + # https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp + startTime: "2022-03-26" + endTime: "2022-04-12" + symbols: + - BTCBUSD + account: + binance: + makerFeeRate: 0.0 + balances: + BTC: 1 + BUSD: 45_000.0 diff --git a/go.mod b/go.mod index cf3c75980..d7d2f80d6 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ go 1.17 require ( github.com/DATA-DOG/go-sqlmock v1.5.0 + github.com/Masterminds/squirrel v1.5.3 github.com/adshao/go-binance/v2 v2.3.5 github.com/c9s/requestgen v1.3.0 github.com/c9s/rockhopper v1.2.2-0.20220617053729-ffdc87df194b @@ -26,6 +27,8 @@ require ( github.com/leekchan/accounting v0.0.0-20191218023648-17a4ce5f94d4 github.com/lestrrat-go/file-rotatelogs v2.2.0+incompatible github.com/mattn/go-shellwords v1.0.12 + github.com/muesli/clusters v0.0.0-20180605185049-a07a36e67d36 + github.com/muesli/kmeans v0.3.0 github.com/pkg/errors v0.9.1 github.com/pquerna/otp v1.3.0 github.com/prometheus/client_golang v1.11.0 @@ -52,7 +55,6 @@ require ( ) require ( - github.com/Masterminds/squirrel v1.5.3 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bitly/go-simplejson v0.5.0 // indirect github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect @@ -90,6 +92,8 @@ require ( github.com/mitchellh/mapstructure v1.4.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.1 // indirect + github.com/muesli/clusters v0.0.0-20180605185049-a07a36e67d36 // indirect + github.com/muesli/kmeans v0.3.0 // indirect github.com/pelletier/go-toml v1.8.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.2.0 // indirect diff --git a/go.sum b/go.sum index c8f6cae48..e1615e021 100644 --- a/go.sum +++ b/go.sum @@ -37,6 +37,7 @@ github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJc github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/Masterminds/squirrel v1.5.3 h1:YPpoceAcxuzIljlr5iWpNKaql7hLeG1KLSrhvdHpkZc= @@ -68,8 +69,6 @@ github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8 github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/c9s/requestgen v1.3.0 h1:3cTHvWIlrc37nGEdJLIO07XaVidDeOwcew06csBz++U= github.com/c9s/requestgen v1.3.0/go.mod h1:5n9FU3hr5307IiXAmbMiZbHYaPiys1u9jCWYexZr9qA= -github.com/c9s/rockhopper v1.2.1-0.20220426104534-f27cbb09846c h1:I3AHs+/fxnWX6eSRxzqQ/vp4jXW+ecVMGy1oy5d6fJ8= -github.com/c9s/rockhopper v1.2.1-0.20220426104534-f27cbb09846c/go.mod h1:EKObf66Cp7erWxym2de+07qNN5T1N9PXxHdh97N44EQ= github.com/c9s/rockhopper v1.2.2-0.20220617053729-ffdc87df194b h1:wT8c03PHLv7+nZUIGqxAzRvIfYHNxMCNVWwvdGkOXTs= github.com/c9s/rockhopper v1.2.2-0.20220617053729-ffdc87df194b/go.mod h1:EKObf66Cp7erWxym2de+07qNN5T1N9PXxHdh97N44EQ= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -103,8 +102,6 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= -github.com/denisenkom/go-mssqldb v0.12.0 h1:VtrkII767ttSPNRfFekePK3sctr+joXgO58stqQbtUA= -github.com/denisenkom/go-mssqldb v0.12.0/go.mod h1:iiK0YP1ZeepvmBQk/QpLEhhTNJgfzrpArPY/aFvc9yU= github.com/denisenkom/go-mssqldb v0.12.2 h1:1OcPn5GBIobjWNd+8yjfHNIaFX14B1pWI3F9HZy5KXw= github.com/denisenkom/go-mssqldb v0.12.2/go.mod h1:lnIw1mZukFRZDJYQ0Pb833QS2IaC3l5HkEfra2LJ+sk= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= @@ -178,8 +175,6 @@ github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zV github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= -github.com/golang-sql/sqlexp v0.0.0-20170517235910-f1bb20e5a188 h1:+eHOFJl1BaXrQxKX+T06f78590z4qA2ZzBTqahsKSE4= -github.com/golang-sql/sqlexp v0.0.0-20170517235910-f1bb20e5a188/go.mod h1:vXjM/+wXQnTPR4KqTKDgJukSZ6amVRtWMPEjE6sQoK8= github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= @@ -334,6 +329,7 @@ github.com/lib/pq v1.10.5 h1:J+gdV2cUmX7ZqL2B0lFcW0m+egaHC2V3lpO8nWxyYiQ= github.com/lib/pq v1.10.5/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs= github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.4 h1:8KGKTcQQGm0Kv7vEbKFErAoAOFyyacLStRtQSeYtvkY= github.com/magiconair/properties v1.8.4/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= @@ -348,8 +344,6 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= -github.com/mattn/go-sqlite3 v1.14.12 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ1Z0= -github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.13 h1:1tj15ngiFfcZzii7yd82foL+ks+ouQcj8j/TPq3fk1I= github.com/mattn/go-sqlite3 v1.14.13/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= @@ -374,6 +368,10 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= +github.com/muesli/clusters v0.0.0-20180605185049-a07a36e67d36 h1:KMCH+/bbZsAbFgzCXD3aB0DRZXnwAO8NYDmfIfslo+M= +github.com/muesli/clusters v0.0.0-20180605185049-a07a36e67d36/go.mod h1:mw5KDqUj0eLj/6DUNINLVJNoPTFkEuGMHtJsXLviLkY= +github.com/muesli/kmeans v0.3.0 h1:cI2cpeS8m3pm+gTOdzl+7SlzZYSe+x0XoqXUyUvb1ro= +github.com/muesli/kmeans v0.3.0/go.mod h1:eNyybq0tX9/iBEP6EMU4Y7dpmGK0uEhODdZpnG1a/iQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= @@ -506,6 +504,7 @@ github.com/ugorji/go/codec v1.2.3 h1:/mVYEV+Jo3IZKeA5gBngN0AvNnQltEDkR+eQikkWQu0 github.com/ugorji/go/codec v1.2.3/go.mod h1:5FxzDJIgeiWJZslYHPj+LS1dq1ZBQVelZFnjsFGI/Uc= github.com/valyala/fastjson v1.5.1 h1:SXaQZVSwLjZOVhDEhjiCcDtnX0Feu7Z7A1+C5atpoHM= github.com/valyala/fastjson v1.5.1/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= +github.com/wcharczuk/go-chart/v2 v2.1.0/go.mod h1:yx7MvAVNcP/kN9lKXM/NTce4au4DFN99j6i1OwDclNA= github.com/webview/webview v0.0.0-20210216142346-e0bfdf0e5d90 h1:G/O1RFjhc9hgVYjaPQ0Oceqxf3GwRQl/5XEAWYetjmg= github.com/webview/webview v0.0.0-20210216142346-e0bfdf0e5d90/go.mod h1:rpXAuuHgyEJb6kXcXldlkOjU6y4x+YcASKKXJNUhh0Y= github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg= @@ -553,8 +552,6 @@ golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA= -golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM= golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -574,6 +571,7 @@ golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMk golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -707,8 +705,6 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a h1:N2T1jUrTQE9Re6TFF5PhvEHXHCguynGhKjWVsIUt5cY= -golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c h1:aFV+BgZ4svzjfabn8ERpuB4JI4N6/rdy1iusx77G3oU= golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -900,7 +896,6 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/cmd/builtin.go b/pkg/cmd/builtin.go index d7aad0d14..84bfc5647 100644 --- a/pkg/cmd/builtin.go +++ b/pkg/cmd/builtin.go @@ -20,6 +20,7 @@ import ( _ "github.com/c9s/bbgo/pkg/strategy/pricealert" _ "github.com/c9s/bbgo/pkg/strategy/pricedrop" _ "github.com/c9s/bbgo/pkg/strategy/rebalance" + _ "github.com/c9s/bbgo/pkg/strategy/rsmaker" _ "github.com/c9s/bbgo/pkg/strategy/schedule" _ "github.com/c9s/bbgo/pkg/strategy/skeleton" _ "github.com/c9s/bbgo/pkg/strategy/supertrend" diff --git a/pkg/strategy/rsmaker/strategy.go b/pkg/strategy/rsmaker/strategy.go new file mode 100644 index 000000000..fa7461df4 --- /dev/null +++ b/pkg/strategy/rsmaker/strategy.go @@ -0,0 +1,883 @@ +package rsmaker + +import ( + "context" + "fmt" + "math" + "time" + + "github.com/c9s/bbgo/pkg/indicator" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/service" + "github.com/c9s/bbgo/pkg/types" + "github.com/muesli/clusters" + "github.com/muesli/kmeans" +) + +// TODO: +// 1) add option for placing orders only when in neutral band +// 2) add option for only placing buy orders when price is below the SMA line + +const ID = "rsmaker" + +const stateKey = "state-v1" + +var defaultFeeRate = fixedpoint.NewFromFloat(0.001) +var notionModifier = fixedpoint.NewFromFloat(1.1) +var two = fixedpoint.NewFromInt(2) + +var log = logrus.WithField("strategy", ID) + +func init() { + bbgo.RegisterStrategy(ID, &Strategy{}) +} + +type State struct { + Position *types.Position `json:"position,omitempty"` + ProfitStats types.ProfitStats `json:"profitStats,omitempty"` +} + +type BollingerSetting struct { + types.IntervalWindow + BandWidth float64 `json:"bandWidth"` +} + +type Strategy struct { + *bbgo.Graceful + *bbgo.Notifiability + *bbgo.Persistence + + Environment *bbgo.Environment + StandardIndicatorSet *bbgo.StandardIndicatorSet + Market types.Market + + // Symbol is the market symbol you want to trade + Symbol string `json:"symbol"` + + // Interval is how long do you want to update your order price and quantity + Interval types.Interval `json:"interval"` + + bbgo.QuantityOrAmount + + // Spread is the price spread from the middle price. + // For ask orders, the ask price is ((bestAsk + bestBid) / 2 * (1.0 + spread)) + // For bid orders, the bid price is ((bestAsk + bestBid) / 2 * (1.0 - spread)) + // Spread can be set by percentage or floating number. e.g., 0.1% or 0.001 + Spread fixedpoint.Value `json:"spread"` + + // BidSpread overrides the spread setting, this spread will be used for the buy order + BidSpread fixedpoint.Value `json:"bidSpread,omitempty"` + + // AskSpread overrides the spread setting, this spread will be used for the sell order + AskSpread fixedpoint.Value `json:"askSpread,omitempty"` + + // MinProfitSpread is the minimal order price spread from the current average cost. + // For long position, you will only place sell order above the price (= average cost * (1 + minProfitSpread)) + // For short position, you will only place buy order below the price (= average cost * (1 - minProfitSpread)) + MinProfitSpread fixedpoint.Value `json:"minProfitSpread"` + + // UseTickerPrice use the ticker api to get the mid price instead of the closed kline price. + // The back-test engine is kline-based, so the ticker price api is not supported. + // Turn this on if you want to do real trading. + UseTickerPrice bool `json:"useTickerPrice"` + + // MaxExposurePosition is the maximum position you can hold + // +10 means you can hold 10 ETH long position by maximum + // -10 means you can hold -10 ETH short position by maximum + MaxExposurePosition fixedpoint.Value `json:"maxExposurePosition"` + + // DynamicExposurePositionScale is used to define the exposure position range with the given percentage + // when DynamicExposurePositionScale is set, + // your MaxExposurePosition will be calculated dynamically according to the bollinger band you set. + DynamicExposurePositionScale *bbgo.PercentageScale `json:"dynamicExposurePositionScale"` + + // Long means your position will be long position + // Currently not used yet + Long *bool `json:"long,omitempty"` + + // Short means your position will be long position + // Currently not used yet + Short *bool `json:"short,omitempty"` + + // DisableShort means you can don't want short position during the market making + // Set to true if you want to hold more spot during market making. + DisableShort bool `json:"disableShort"` + + // BuyBelowNeutralSMA if true, the market maker will only place buy order when the current price is below the neutral band SMA. + BuyBelowNeutralSMA bool `json:"buyBelowNeutralSMA"` + + // NeutralBollinger is the smaller range of the bollinger band + // If price is in this band, it usually means the price is oscillating. + // If price goes out of this band, we tend to not place sell orders or buy orders + NeutralBollinger *BollingerSetting `json:"neutralBollinger"` + + // DefaultBollinger is the wide range of the bollinger band + // for controlling your exposure position + DefaultBollinger *BollingerSetting `json:"defaultBollinger"` + + // DowntrendSkew is the order quantity skew for normal downtrend band. + // The price is still in the default bollinger band. + // greater than 1.0 means when placing buy order, place sell order with less quantity + // less than 1.0 means when placing sell order, place buy order with less quantity + DowntrendSkew fixedpoint.Value `json:"downtrendSkew"` + + // UptrendSkew is the order quantity skew for normal uptrend band. + // The price is still in the default bollinger band. + // greater than 1.0 means when placing buy order, place sell order with less quantity + // less than 1.0 means when placing sell order, place buy order with less quantity + UptrendSkew fixedpoint.Value `json:"uptrendSkew"` + + // TradeInBand + // When this is on, places orders only when the current price is in the bollinger band. + TradeInBand bool `json:"tradeInBand"` + + // ShadowProtection is used to avoid placing bid order when price goes down strongly (without shadow) + ShadowProtection bool `json:"shadowProtection"` + ShadowProtectionRatio fixedpoint.Value `json:"shadowProtectionRatio"` + + bbgo.SmartStops + + session *bbgo.ExchangeSession + book *types.StreamOrderBook + + state *State + + activeMakerOrders *bbgo.ActiveOrderBook + orderStore *bbgo.OrderStore + tradeCollector *bbgo.TradeCollector + + groupID uint32 + + stopC chan struct{} + + // defaultBoll is the BOLLINGER indicator we used for predicting the price. + defaultBoll *indicator.BOLL + + // neutralBoll is the neutral price section + neutralBoll *indicator.BOLL + + // StrategyController + status types.StrategyStatus +} + +func (s *Strategy) ID() string { + return ID +} + +func (s *Strategy) Initialize() error { + return s.SmartStops.InitializeStopControllers(s.Symbol) +} + +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ + Interval: s.Interval, + }) + + //session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ + // Interval: types.Interval12h.String(), + //}) + + //if s.DefaultBollinger != nil && s.DefaultBollinger.Interval != "" { + // session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ + // Interval: string(s.DefaultBollinger.Interval), + // }) + //} + // + //if s.NeutralBollinger != nil && s.NeutralBollinger.Interval != "" { + // session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ + // Interval: string(s.NeutralBollinger.Interval), + // }) + //} + + //s.SmartStops.Subscribe(session) +} + +func (s *Strategy) Validate() error { + if len(s.Symbol) == 0 { + return errors.New("symbol is required") + } + + return nil +} + +func (s *Strategy) CurrentPosition() *types.Position { + return s.state.Position +} + +func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Value) error { + base := s.state.Position.GetBase() + if base.IsZero() { + return fmt.Errorf("no opened %s position", s.state.Position.Symbol) + } + + // make it negative + quantity := base.Mul(percentage).Abs() + side := types.SideTypeBuy + if base.Sign() > 0 { + side = types.SideTypeSell + } + + if quantity.Compare(s.Market.MinQuantity) < 0 { + return fmt.Errorf("order quantity %v is too small, less than %v", quantity, s.Market.MinQuantity) + } + + submitOrder := types.SubmitOrder{ + Symbol: s.Symbol, + Side: side, + Type: types.OrderTypeMarket, + Quantity: quantity, + Market: s.Market, + } + + s.Notify("Submitting %s %s order to close position by %v", s.Symbol, side.String(), percentage, submitOrder) + + createdOrders, err := s.session.Exchange.SubmitOrders(ctx, submitOrder) + if err != nil { + log.WithError(err).Errorf("can not place position close order") + } + + s.orderStore.Add(createdOrders...) + s.activeMakerOrders.Add(createdOrders...) + s.tradeCollector.Process() + + return err +} + +// StrategyController + +func (s *Strategy) GetStatus() types.StrategyStatus { + return s.status +} + +func (s *Strategy) Suspend(ctx context.Context) error { + s.status = types.StrategyStatusStopped + + // Cancel all order + if err := s.activeMakerOrders.GracefulCancel(ctx, s.session.Exchange); err != nil { + log.WithError(err).Errorf("graceful cancel order error") + s.Notify("graceful cancel order error") + } else { + s.Notify("All orders cancelled.") + } + + s.tradeCollector.Process() + + // Save state + if err := s.SaveState(); err != nil { + log.WithError(err).Errorf("can not save state: %+v", s.state) + } else { + log.Infof("%s position is saved.", s.Symbol) + } + + return nil +} + +func (s *Strategy) Resume(ctx context.Context) error { + s.status = types.StrategyStatusRunning + + return nil +} + +//func (s *Strategy) EmergencyStop(ctx context.Context) error { +// // Close 100% position +// percentage, _ := fixedpoint.NewFromString("100%") +// err := s.ClosePosition(ctx, percentage) +// +// // Suspend strategy +// _ = s.Suspend(ctx) +// +// return err +//} + +func (s *Strategy) SaveState() error { + if err := s.Persistence.Save(s.state, ID, s.Symbol, stateKey); err != nil { + return err + } + + log.Infof("state is saved => %+v", s.state) + return nil +} + +func (s *Strategy) LoadState() error { + var state State + + // load position + if err := s.Persistence.Load(&state, ID, s.Symbol, stateKey); err != nil { + if err != service.ErrPersistenceNotExists { + return err + } + + s.state = &State{} + } else { + s.state = &state + log.Infof("state is restored: %+v", s.state) + } + + // if position is nil, we need to allocate a new position for calculation + if s.state.Position == nil { + s.state.Position = types.NewPositionFromMarket(s.Market) + } + + // init profit states + s.state.ProfitStats.Symbol = s.Market.Symbol + s.state.ProfitStats.BaseCurrency = s.Market.BaseCurrency + s.state.ProfitStats.QuoteCurrency = s.Market.QuoteCurrency + if s.state.ProfitStats.AccumulatedSince == 0 { + s.state.ProfitStats.AccumulatedSince = time.Now().Unix() + } + + return nil +} + +func (s *Strategy) getCurrentAllowedExposurePosition(bandPercentage float64) (fixedpoint.Value, error) { + if s.DynamicExposurePositionScale != nil { + v, err := s.DynamicExposurePositionScale.Scale(bandPercentage) + if err != nil { + return fixedpoint.Zero, err + } + return fixedpoint.NewFromFloat(v), nil + } + + return s.MaxExposurePosition, nil +} + +func (s *Strategy) placeOrders(ctx context.Context, orderExecutor bbgo.OrderExecutor, midPrice fixedpoint.Value, klines []*types.KLine) { + //bidSpread := s.Spread + //if s.BidSpread.Sign() > 0 { + // bidSpread = s.BidSpread + //} + // + //askSpread := s.Spread + //if s.AskSpread.Sign() > 0 { + // askSpread = s.AskSpread + //} + // preprocessing + max := 0. + min := 100000. + + mv := 0. + for x := 0; x < 50; x++ { + if klines[x].High.Float64() > max { + max = klines[x].High.Float64() + } + if klines[x].Low.Float64() < min { + min = klines[x].High.Float64() + } + + mv += klines[x].Volume.Float64() + } + mv = mv / 50 + + //logrus.Info(max, min) + // set up a random two-dimensional data set (float64 values between 0.0 and 1.0) + var d clusters.Observations + for x := 0; x < 50; x++ { + //if klines[x].High.Float64() < max || klines[x].Low.Float64() > min { + if klines[x].Volume.Float64() > mv*0.3 { + d = append(d, clusters.Coordinates{ + klines[x].High.Float64(), + klines[x].Low.Float64(), + //klines[x].Open.Float64(), + //klines[x].Close.Float64(), + //klines[x].Volume.Float64(), + }) + } + //} + + } + log.Info(len(d)) + + // Partition the data points into 2 clusters + km := kmeans.New() + clusters, err := km.Partition(d, 3) + + //for _, c := range clusters { + //fmt.Printf("Centered at x: %.2f y: %.2f\n", c.Center[0], c.Center[1]) + //fmt.Printf("Matching data points: %+v\n\n", c.Observations) + //} + // clustered virtual kline_1's mid price + //vk1mp := fixedpoint.NewFromFloat((clusters[0].Center[0] + clusters[0].Center[1]) / 2.) + // clustered virtual kline_2's mid price + //vk2mp := fixedpoint.NewFromFloat((clusters[1].Center[0] + clusters[1].Center[1]) / 2.) + // clustered virtual kline_3's mid price + //vk3mp := fixedpoint.NewFromFloat((clusters[2].Center[0] + clusters[2].Center[1]) / 2.) + + // clustered virtual kline_1's high price + vk1hp := fixedpoint.NewFromFloat(clusters[0].Center[0]) + // clustered virtual kline_2's high price + vk2hp := fixedpoint.NewFromFloat(clusters[1].Center[0]) + // clustered virtual kline_3's high price + vk3hp := fixedpoint.NewFromFloat(clusters[2].Center[0]) + + // clustered virtual kline_1's low price + vk1lp := fixedpoint.NewFromFloat(clusters[0].Center[1]) + // clustered virtual kline_2's low price + vk2lp := fixedpoint.NewFromFloat(clusters[1].Center[1]) + // clustered virtual kline_3's low price + vk3lp := fixedpoint.NewFromFloat(clusters[2].Center[1]) + + askPrice := fixedpoint.NewFromFloat(math.Max(math.Max(vk1hp.Float64(), vk2hp.Float64()), vk3hp.Float64())) //fixedpoint.NewFromFloat(math.Max(math.Max(vk1mp.Float64(), vk2mp.Float64()), vk3mp.Float64())) + bidPrice := fixedpoint.NewFromFloat(math.Min(math.Min(vk1lp.Float64(), vk2lp.Float64()), vk3lp.Float64())) //fixedpoint.NewFromFloat(math.Min(math.Min(vk1mp.Float64(), vk2mp.Float64()), vk3mp.Float64())) + + //if vk1mp.Compare(vk2mp) > 0 { + // askPrice = vk1mp //.Mul(fixedpoint.NewFromFloat(1.001)) + // bidPrice = vk2mp //.Mul(fixedpoint.NewFromFloat(0.999)) + //} else if vk1mp.Compare(vk2mp) < 0 { + // askPrice = vk2mp //.Mul(fixedpoint.NewFromFloat(1.001)) + // bidPrice = vk1mp //.Mul(fixedpoint.NewFromFloat(0.999)) + //} + //midPrice.Mul(fixedpoint.One.Add(askSpread)) + //midPrice.Mul(fixedpoint.One.Sub(bidSpread)) + base := s.state.Position.GetBase() + //balances := s.session.GetAccount().Balances() + + //log.Infof("mid price:%v spread: %s ask:%v bid: %v position: %s", + // midPrice, + // s.Spread.Percentage(), + // askPrice, + // bidPrice, + // s.state.Position, + //) + canSell := true + canBuy := true + + //predMidPrice := (askPrice + bidPrice) / 2. + + //if midPrice.Float64() > predMidPrice.Float64() { + // bidPrice = predMidPrice.Mul(fixedpoint.NewFromFloat(0.999)) + //} + // + //if midPrice.Float64() < predMidPrice.Float64() { + // askPrice = predMidPrice.Mul(fixedpoint.NewFromFloat(1.001)) + //} + // + //if midPrice.Float64() > askPrice.Float64() { + // canBuy = false + // askPrice = midPrice.Mul(fixedpoint.NewFromFloat(1.001)) + //} + // + //if midPrice.Float64() < bidPrice.Float64() { + // canSell = false + // bidPrice = midPrice.Mul(fixedpoint.NewFromFloat(0.999)) + //} + + sellQuantity := s.QuantityOrAmount.CalculateQuantity(askPrice) + buyQuantity := s.QuantityOrAmount.CalculateQuantity(bidPrice) + + sellOrder := types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeSell, + Type: types.OrderTypeLimitMaker, + Quantity: sellQuantity, + Price: askPrice, + Market: s.Market, + GroupID: s.groupID, + } + buyOrder := types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeLimitMaker, + Quantity: buyQuantity, + Price: bidPrice, + Market: s.Market, + GroupID: s.groupID, + } + + var submitBuyOrders []types.SubmitOrder + var submitSellOrders []types.SubmitOrder + + //baseBalance, hasBaseBalance := balances[s.Market.BaseCurrency] + //quoteBalance, hasQuoteBalance := balances[s.Market.QuoteCurrency] + + downBand := s.defaultBoll.LastDownBand() + upBand := s.defaultBoll.LastUpBand() + sma := s.defaultBoll.LastSMA() + log.Infof("bollinger band: up %f sma %f down %f", upBand, sma, downBand) + + bandPercentage := calculateBandPercentage(upBand, downBand, sma, midPrice.Float64()) + log.Infof("mid price band percentage: %v", bandPercentage) + + maxExposurePosition, err := s.getCurrentAllowedExposurePosition(bandPercentage) + if err != nil { + log.WithError(err).Errorf("can not calculate CurrentAllowedExposurePosition") + return + } + + log.Infof("calculated max exposure position: %v", maxExposurePosition) + + if maxExposurePosition.Sign() > 0 && base.Compare(maxExposurePosition) > 0 { + canBuy = false + } + + if maxExposurePosition.Sign() > 0 { + if s.Long != nil && *s.Long && base.Sign() < 0 { + canSell = false + } else if base.Compare(maxExposurePosition.Neg()) < 0 { + canSell = false + } + } + + //if s.ShadowProtection && kline != nil { + // switch kline.Direction() { + // case types.DirectionDown: + // shadowHeight := kline.GetLowerShadowHeight() + // shadowRatio := kline.GetLowerShadowRatio() + // if shadowHeight.IsZero() && shadowRatio.Compare(s.ShadowProtectionRatio) < 0 { + // log.Infof("%s shadow protection enabled, lower shadow ratio %v < %v", s.Symbol, shadowRatio, s.ShadowProtectionRatio) + // canBuy = false + // } + // case types.DirectionUp: + // shadowHeight := kline.GetUpperShadowHeight() + // shadowRatio := kline.GetUpperShadowRatio() + // if shadowHeight.IsZero() || shadowRatio.Compare(s.ShadowProtectionRatio) < 0 { + // log.Infof("%s shadow protection enabled, upper shadow ratio %v < %v", s.Symbol, shadowRatio, s.ShadowProtectionRatio) + // canSell = false + // } + // } + //} + + // Apply quantity skew + // CASE #1: + // WHEN: price is in the neutral bollginer band (window 1) == neutral + // THEN: we don't apply skew + // CASE #2: + // WHEN: price is in the upper band (window 2 > price > window 1) == upTrend + // THEN: we apply upTrend skew + // CASE #3: + // WHEN: price is in the lower band (window 2 < price < window 1) == downTrend + // THEN: we apply downTrend skew + // CASE #4: + // WHEN: price breaks the lower band (price < window 2) == strongDownTrend + // THEN: we apply strongDownTrend skew + // CASE #5: + // WHEN: price breaks the upper band (price > window 2) == strongUpTrend + // THEN: we apply strongUpTrend skew + //if s.TradeInBand { + // if !inBetween(midPrice.Float64(), s.neutralBoll.LastDownBand(), s.neutralBoll.LastUpBand()) { + // log.Infof("tradeInBand is set, skip placing orders when the price is outside of the band") + // return + // } + //} + + //revmacd := s.detectPriceTrend(s.neutralBoll, midPrice.Float64()) + //switch revmacd { + //case NeutralTrend: + // // do nothing + // + //case UpTrend: + // skew := s.UptrendSkew + // buyOrder.Quantity = fixedpoint.Max(s.Market.MinQuantity, sellOrder.Quantity.Mul(skew)) + // + //case DownTrend: + // skew := s.DowntrendSkew + // ratio := fixedpoint.One.Div(skew) + // sellOrder.Quantity = fixedpoint.Max(s.Market.MinQuantity, buyOrder.Quantity.Mul(ratio)) + // + //} + + //if !hasQuoteBalance || buyOrder.Quantity.Mul(buyOrder.Price).Compare(quoteBalance.Available) > 0 { + // canBuy = false + //} + // + //if !hasBaseBalance || sellOrder.Quantity.Compare(baseBalance.Available) > 0 { + // canSell = false + //} + + //if midPrice.Compare(s.state.Position.AverageCost.Mul(fixedpoint.One.Add(s.MinProfitSpread))) < 0 { + // canSell = false + //} + + //if s.Long != nil && *s.Long && base.Sub(sellOrder.Quantity).Sign() < 0 { + // canSell = false + //} + // + //if s.BuyBelowNeutralSMA && midPrice.Float64() > s.neutralBoll.LastSMA() { + // canBuy = false + //} + + if canSell { + submitSellOrders = append(submitSellOrders, sellOrder) + //sellOrder = s.adjustOrderPrice(sellOrder, false) + //submitSellOrders = append(submitSellOrders, sellOrder) + //sellOrder = s.adjustOrderPrice(sellOrder, false) + //submitSellOrders = append(submitSellOrders, sellOrder) + } + if canBuy { + submitBuyOrders = append(submitBuyOrders, buyOrder) + //buyOrder = s.adjustOrderPrice(buyOrder, true) + //submitBuyOrders = append(submitBuyOrders, buyOrder) + //buyOrder = s.adjustOrderPrice(buyOrder, true) + //submitBuyOrders = append(submitBuyOrders, buyOrder) + } + + // condition for lower the average cost + /* + if midPrice < s.state.Position.AverageCost.MulFloat64(1.0-s.MinProfitSpread.Float64()) && canBuy { + submitOrders = append(submitOrders, buyOrder) + } + */ + + for i := range submitBuyOrders { + submitBuyOrders[i] = s.adjustOrderQuantity(submitBuyOrders[i]) + } + + for i := range submitSellOrders { + submitSellOrders[i] = s.adjustOrderQuantity(submitSellOrders[i]) + } + + createdBuyOrders, err := orderExecutor.SubmitOrders(ctx, submitBuyOrders...) + if err != nil { + log.WithError(err).Errorf("can not place ping pong orders") + } + s.orderStore.Add(createdBuyOrders...) + s.activeMakerOrders.Add(createdBuyOrders...) + + createdSellOrders, err := orderExecutor.SubmitOrders(ctx, submitSellOrders...) + if err != nil { + log.WithError(err).Errorf("can not place ping pong orders") + } + s.orderStore.Add(createdSellOrders...) + s.activeMakerOrders.Add(createdSellOrders...) +} + +type PriceTrend string + +const ( + NeutralTrend PriceTrend = "neutral" + UpTrend PriceTrend = "upTrend" + DownTrend PriceTrend = "downTrend" + UnknownTrend PriceTrend = "unknown" +) + +func (s *Strategy) detectPriceTrend(inc *indicator.BOLL, price float64) PriceTrend { + if inBetween(price, inc.LastDownBand(), inc.LastUpBand()) { + return NeutralTrend + } + + if price < inc.LastDownBand() { + return DownTrend + } + + if price > inc.LastUpBand() { + return UpTrend + } + + return UnknownTrend +} + +func (s *Strategy) adjustOrderQuantity(submitOrder types.SubmitOrder) types.SubmitOrder { + if submitOrder.Quantity.Mul(submitOrder.Price).Compare(s.Market.MinNotional) < 0 { + submitOrder.Quantity = bbgo.AdjustFloatQuantityByMinAmount(submitOrder.Quantity, submitOrder.Price, s.Market.MinNotional.Mul(notionModifier)) + } + + if submitOrder.Quantity.Compare(s.Market.MinQuantity) < 0 { + submitOrder.Quantity = fixedpoint.Max(submitOrder.Quantity, s.Market.MinQuantity) + } + + return submitOrder +} + +func (s *Strategy) adjustOrderPrice(submitOrder types.SubmitOrder, side bool) types.SubmitOrder { + + if side { + submitOrder.Price = submitOrder.Price.Mul(fixedpoint.NewFromFloat(0.995)) + } else { + submitOrder.Price = submitOrder.Price.Mul(fixedpoint.NewFromFloat(1.005)) + } + + return submitOrder +} + +func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + // StrategyController + s.status = types.StrategyStatusRunning + + //if s.DisableShort { + // s.Long = &[]bool{true}[0] + //} + // + //if s.MinProfitSpread.IsZero() { + // s.MinProfitSpread = fixedpoint.NewFromFloat(0.001) + //} + // + //if s.UptrendSkew.IsZero() { + // s.UptrendSkew = fixedpoint.NewFromFloat(1.0 / 1.2) + //} + // + //if s.DowntrendSkew.IsZero() { + // s.DowntrendSkew = fixedpoint.NewFromFloat(1.2) + //} + // + //if s.ShadowProtectionRatio.IsZero() { + // s.ShadowProtectionRatio = fixedpoint.NewFromFloat(0.01) + //} + + // initial required information + s.session = session + s.neutralBoll = s.StandardIndicatorSet.BOLL(s.NeutralBollinger.IntervalWindow, s.NeutralBollinger.BandWidth) + s.defaultBoll = s.StandardIndicatorSet.BOLL(s.DefaultBollinger.IntervalWindow, s.DefaultBollinger.BandWidth) + + // calculate group id for orders + instanceID := fmt.Sprintf("%s-%s", ID, s.Symbol) + //s.groupID = max.GenerateGroupID(instanceID) + log.Infof("using group id %d from fnv(%s)", s.groupID, instanceID) + + // restore state + if err := s.LoadState(); err != nil { + return err + } + + s.state.Position.Strategy = ID + s.state.Position.StrategyInstanceID = instanceID + + //s.stopC = make(chan struct{}) + + s.activeMakerOrders = bbgo.NewActiveOrderBook(s.Symbol) + s.activeMakerOrders.BindStream(session.UserDataStream) + + s.orderStore = bbgo.NewOrderStore(s.Symbol) + s.orderStore.BindStream(session.UserDataStream) + + s.tradeCollector = bbgo.NewTradeCollector(s.Symbol, s.state.Position, s.orderStore) + + //s.tradeCollector.OnTrade(func(trade types.Trade, profit, netProfit fixedpoint.Value) { + // // StrategyController + // if s.status != types.StrategyStatusRunning { + // return + // } + // + // s.Notifiability.Notify(trade) + // s.state.ProfitStats.AddTrade(trade) + // + // if profit.Compare(fixedpoint.Zero) == 0 { + // s.Environment.RecordPosition(s.state.Position, trade, nil) + // } else { + // log.Infof("%s generated profit: %v", s.Symbol, profit) + // p := s.state.Position.NewProfit(trade, profit, netProfit) + // p.Strategy = ID + // p.StrategyInstanceID = instanceID + // s.Notify(&p) + // + // s.state.ProfitStats.AddProfit(p) + // s.Notify(&s.state.ProfitStats) + // + // s.Environment.RecordPosition(s.state.Position, trade, &p) + // } + //}) + // + //s.tradeCollector.OnPositionUpdate(func(position *types.Position) { + // log.Infof("position changed: %s", s.state.Position) + // s.Notify(s.state.Position) + //}) + + s.tradeCollector.BindStream(session.UserDataStream) + + //s.SmartStops.RunStopControllers(ctx, session, s.tradeCollector) + + //session.UserDataStream.OnStart(func() { + //if s.UseTickerPrice { + // ticker, err := s.session.Exchange.QueryTicker(ctx, s.Symbol) + // if err != nil { + // return + // } + // + // midPrice := ticker.Buy.Add(ticker.Sell).Div(two) + // s.placeOrders(ctx, orderExecutor, midPrice, nil) + //} else { + // if price, ok := session.LastPrice(s.Symbol); ok { + // s.placeOrders(ctx, orderExecutor, price, nil) + // } + //} + //}) + + var klines []*types.KLine + + session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { + // StrategyController + if s.status != types.StrategyStatusRunning { + return + } + + //if kline.Symbol != s.Symbol || kline.Interval != s.Interval { + // return + //} + + if kline.Interval == s.Interval { + klines = append(klines, &kline) + } + if len(klines) > 50 { + //if s.UseTickerPrice { + // ticker, err := s.session.Exchange.QueryTicker(ctx, s.Symbol) + // if err != nil { + // return + // } + // + // midPrice := ticker.Buy.Add(ticker.Sell).Div(two) + // log.Infof("using ticker price: bid %v / ask %v, mid price %v", ticker.Buy, ticker.Sell, midPrice) + // s.placeOrders(ctx, orderExecutor, midPrice, klines[len(klines)-100:]) + // s.tradeCollector.Process() + //} + //else { + if kline.Interval == s.Interval { + + //if s.state.Position.AverageCost.Div(kline.Close).Float64() < 0.999 { + // s.ClosePosition(ctx, fixedpoint.One) + // s.tradeCollector.Process() + //} + + if err := s.activeMakerOrders.GracefulCancel(ctx, s.session.Exchange); err != nil { + log.WithError(err).Errorf("graceful cancel order error") + } + + // check if there is a canceled order had partially filled. + s.tradeCollector.Process() + + s.placeOrders(ctx, orderExecutor, kline.Close, klines[len(klines)-50:]) + s.tradeCollector.Process() + } + //} + } + + }) + + // s.book = types.NewStreamBook(s.Symbol) + // s.book.BindStreamForBackground(session.MarketDataStream) + + //s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + // //defer wg.Done() + // //close(s.stopC) + // + // if err := s.activeMakerOrders.GracefulCancel(ctx, s.session.Exchange); err != nil { + // log.WithError(err).Errorf("graceful cancel order error") + // } + // + // s.tradeCollector.Process() + // + // if err := s.SaveState(); err != nil { + // log.WithError(err).Errorf("can not save state: %+v", s.state) + // } + //}) + + return nil +} + +func calculateBandPercentage(up, down, sma, midPrice float64) float64 { + if midPrice < sma { + // should be negative percentage + return (midPrice - sma) / math.Abs(sma-down) + } else if midPrice > sma { + // should be positive percentage + return (midPrice - sma) / math.Abs(up-sma) + } + + return 0.0 +} + +func inBetween(x, a, b float64) bool { + return a < x && x < b +}