diff --git a/apps/backtest-report/components/TradingViewChart.tsx b/apps/backtest-report/components/TradingViewChart.tsx index 0532127f4..31ed484ad 100644 --- a/apps/backtest-report/components/TradingViewChart.tsx +++ b/apps/backtest-report/components/TradingViewChart.tsx @@ -71,7 +71,7 @@ const parseOrder = () => { case "update_time": case "creation_time": case "time": - d[key] = new Date(d[key]); + d[key] = moment(d[key], 'dddd, DD MMM YYYY h:mm:ss').toDate(); break; } } diff --git a/config/drift.yaml b/config/drift.yaml index c5c684b56..b2459d22e 100644 --- a/config/drift.yaml +++ b/config/drift.yaml @@ -31,16 +31,25 @@ exchangeStrategies: stoploss: 0.3% source: close predictOffset: 2 - # the init value of takeProfitFactor Series, position avg +- takeProfitFactor * atr as take profit price - takeProfitFactor: 6 - profitFactorWindow: 8 noTrailingStopLoss: false + trailingStopLossType: kline # stddev on high/low-source - hlVarianceMultiplier: 0.22 + hlVarianceMultiplier: 0.23 hlRangeWindow: 5 - smootherWindow: 2 - fisherTransformWindow: 8 + window1m: 24 + smootherWindow1m: 24 + fisherTransformWindow1m: 162 + smootherWindow: 1 + fisherTransformWindow: 9 atrWindow: 14 + # orders not been traded will be canceled after `pendingMinutes` minutes + pendingMinutes: 3 + noRebalance: true + trendWindow: 12 + rebalanceFilter: 1.5 + + trailingActivationRatio: [0.004] + trailingCallbackRate: [0.001] generateGraph: true graphPNLDeductFee: true @@ -79,7 +88,7 @@ sync: backtest: startTime: "2022-01-01" - endTime: "2022-07-29" + endTime: "2022-08-30" symbols: - ETHBUSD sessions: [binance] diff --git a/config/driftBTC.yaml b/config/driftBTC.yaml index c199e0b0c..93e272fe3 100644 --- a/config/driftBTC.yaml +++ b/config/driftBTC.yaml @@ -24,7 +24,7 @@ exchangeStrategies: - on: binance drift: canvasPath: "./output.png" - symbol: BTCBUSD + symbol: BTCUSDT # kline interval for indicators interval: 15m window: 2 @@ -32,18 +32,33 @@ exchangeStrategies: source: close predictOffset: 2 noTrailingStopLoss: false + trailingStopLossType: kline # stddev on high/low-source hlVarianceMultiplier: 0.22 hlRangeWindow: 5 - smootherWindow: 2 + smootherWindow: 1 fisherTransformWindow: 9 - # the init value of takeProfitFactor Series, the coefficient of ATR as TP - takeProfitFactor: 6 - profitFactorWindow: 8 + window1m: 22 + smootherWindow1m: 18 + fisherTransformWindow1m: 162 atrWindow: 14 + # orders not been traded will be canceled after `pendingMinutes` minutes + pendingMinutes: 5 + noRebalance: true + trendWindow: 576 + rebalanceFilter: 0 + + # ActivationRatio should be increasing order + # when farest price from entry goes over that ratio, start using the callback ratio accordingly to do trailingstop + #trailingActivationRatio: [0.01, 0.016, 0.05] + trailingActivationRatio: [0.0012, 0.002, 0.01, 0.016] + #trailingActivationRatio: [] + #trailingCallbackRate: [] + #trailingCallbackRate: [0.002, 0.01, 0.1] + trailingCallbackRate: [0.0004, 0.0008, 0.002, 0.01] generateGraph: true - graphPNLDeductFee: true + graphPNLDeductFee: false graphPNLPath: "./pnl.png" graphCumPNLPath: "./cumpnl.png" #exits: @@ -100,18 +115,18 @@ sync: sessions: - binance symbols: - - BTCBUSD + - BTCUSDT backtest: startTime: "2022-01-01" - endTime: "2022-07-29" + endTime: "2022-08-30" symbols: - - BTCBUSD + - BTCUSDT sessions: [binance] accounts: binance: makerFeeRate: 0.000 - takerFeeRate: 0.00075 + #takerFeeRate: 0.000 balances: BTC: 1 - BUSD: 5000.0 + USDT: 5000 diff --git a/go.mod b/go.mod index 0c0e6d6ae..f6904f458 100644 --- a/go.mod +++ b/go.mod @@ -25,6 +25,7 @@ require ( github.com/golang/mock v1.6.0 github.com/google/uuid v1.3.0 github.com/gorilla/websocket v1.5.0 + github.com/jedib0t/go-pretty/v6 v6.3.6 github.com/jmoiron/sqlx v1.3.4 github.com/joho/godotenv v1.3.0 github.com/leekchan/accounting v0.0.0-20191218023648-17a4ce5f94d4 @@ -43,7 +44,7 @@ require ( github.com/spf13/cobra v1.1.1 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.7.1 - github.com/stretchr/testify v1.7.0 + github.com/stretchr/testify v1.7.4 github.com/valyala/fastjson v1.5.1 github.com/wcharczuk/go-chart/v2 v2.1.0 github.com/webview/webview v0.0.0-20210216142346-e0bfdf0e5d90 @@ -93,7 +94,7 @@ require ( github.com/magiconair/properties v1.8.4 // indirect github.com/mattn/go-colorable v0.1.9 // indirect github.com/mattn/go-isatty v0.0.14 // indirect - github.com/mattn/go-runewidth v0.0.12 // indirect + github.com/mattn/go-runewidth v0.0.13 // indirect github.com/mattn/go-sqlite3 v1.14.13 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect diff --git a/go.sum b/go.sum index e04a6df9c..5feae444a 100644 --- a/go.sum +++ b/go.sum @@ -81,7 +81,6 @@ github.com/c9s/requestgen v1.3.0/go.mod h1:5n9FU3hr5307IiXAmbMiZbHYaPiys1u9jCWYe github.com/c9s/rockhopper v1.2.2-0.20220617053729-ffdc87df194b h1:wT8c03PHLv7+nZUIGqxAzRvIfYHNxMCNVWwvdGkOXTs= github.com/c9s/rockhopper v1.2.2-0.20220617053729-ffdc87df194b/go.mod h1:EKObf66Cp7erWxym2de+07qNN5T1N9PXxHdh97N44EQ= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= @@ -149,7 +148,6 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= @@ -351,6 +349,8 @@ github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0f github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jedib0t/go-pretty/v6 v6.3.6 h1:A6w2BuyPMtf7M82BGRBys9bAba2C26ZX9lrlrZ7uH6U= +github.com/jedib0t/go-pretty/v6 v6.3.6/go.mod h1:MgmISkTWDSFu0xOqiZ0mKNntMQ2mDgOcwOkwBEkMDJI= github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869 h1:IPJ3dvxmJ4uczJe5YQdrYB16oTJlGSC/OyZDqUk9xX4= github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869/go.mod h1:cJ6Cj7dQo+O6GJNiMx+Pa94qKj+TG8ONdKHgMNIyyag= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= @@ -439,8 +439,9 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/mattn/go-runewidth v0.0.12 h1:Y41i/hVW3Pgwr8gV+J23B9YEY0zxjptBuCWEaxmAOow= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= @@ -502,6 +503,7 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -598,6 +600,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.1.4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.0/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= @@ -605,15 +609,16 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.4 h1:wZRexSlwd7ZXfKINDLsO4r7WBt3gTKONc6K/VesHvHM= +github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tebeka/strftime v0.1.3 h1:5HQXOqWKYRFfNyBMNVc9z5+QzuBtIXy03psIhtdJYto= github.com/tebeka/strftime v0.1.3/go.mod h1:7wJm3dZlpr4l/oVK0t1HYIc4rMzQ2XJlOMIUJUJH6XQ= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= -github.com/ugorji/go v1.2.3 h1:WbFSXLxDFKVN69Sk8t+XHGzVCD7R8UoAATR8NqZgTbk= github.com/ugorji/go v1.2.3/go.mod h1:5l8GZ8hZvmL4uMdy+mhCO1LjswGRYco9Q3HfuisB21A= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ugorji/go/codec v1.2.3 h1:/mVYEV+Jo3IZKeA5gBngN0AvNnQltEDkR+eQikkWQu0= @@ -633,7 +638,6 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= @@ -766,8 +770,6 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220403103023-749bd193bc2b h1:vI32FkLJNAWtGD4BwkThwEy6XS7ZLLMHkSkYfF8M0W0= golang.org/x/net v0.0.0-20220403103023-749bd193bc2b/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= @@ -846,7 +848,6 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c h1:aFV+BgZ4svzjfabn8ERpuB4JI4N6/rdy1iusx77G3oU= golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -919,7 +920,6 @@ golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.9 h1:j9KsMiaP1c3B0OTQGth0/k+miLGTgLsAFUCrF2vLcF8= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/pkg/bbgo/interact.go b/pkg/bbgo/interact.go index 3ace4cf72..820097219 100644 --- a/pkg/bbgo/interact.go +++ b/pkg/bbgo/interact.go @@ -51,6 +51,27 @@ func NewCoreInteraction(environment *Environment, trader *Trader) *CoreInteracti } } +type SimpleInteraction struct { + Command string + Description string + F interface{} + Cmd *interact.Command +} + +func (it *SimpleInteraction) Commands(i *interact.Interact) { + it.Cmd = i.PrivateCommand(it.Command, it.Description, it.F) +} + +func RegisterCommand(command, desc string, f interface{}) *interact.Command { + it := &SimpleInteraction{ + Command: command, + Description: desc, + F: f, + } + interact.AddCustomInteraction(it) + return it.Cmd +} + func getStrategySignatures(exchangeStrategies map[string]SingleExchangeStrategy) []string { var strategies []string for signature := range exchangeStrategies { diff --git a/pkg/bbgo/notification.go b/pkg/bbgo/notification.go index db63d7448..2d8420297 100644 --- a/pkg/bbgo/notification.go +++ b/pkg/bbgo/notification.go @@ -1,6 +1,8 @@ package bbgo import ( + "bytes" + "github.com/sirupsen/logrus" "github.com/c9s/bbgo/pkg/util" @@ -20,9 +22,19 @@ func NotifyTo(channel string, obj interface{}, args ...interface{}) { Notification.NotifyTo(channel, obj, args...) } +func SendPhoto(buffer *bytes.Buffer) { + Notification.SendPhoto(buffer) +} + +func SendPhotoTo(channel string, buffer *bytes.Buffer) { + Notification.SendPhotoTo(channel, buffer) +} + type Notifier interface { NotifyTo(channel string, obj interface{}, args ...interface{}) Notify(obj interface{}, args ...interface{}) + SendPhotoTo(channel string, buffer *bytes.Buffer) + SendPhoto(buffer *bytes.Buffer) } type NullNotifier struct{} @@ -31,6 +43,10 @@ func (n *NullNotifier) NotifyTo(channel string, obj interface{}, args ...interfa func (n *NullNotifier) Notify(obj interface{}, args ...interface{}) {} +func (n *NullNotifier) SendPhoto(buffer *bytes.Buffer) {} + +func (n *NullNotifier) SendPhotoTo(channel string, buffer *bytes.Buffer) {} + type Notifiability struct { notifiers []Notifier SessionChannelRouter *PatternChannelRouter `json:"-"` @@ -83,3 +99,15 @@ func (m *Notifiability) NotifyTo(channel string, obj interface{}, args ...interf n.NotifyTo(channel, obj, args...) } } + +func (m *Notifiability) SendPhoto(buffer *bytes.Buffer) { + for _, n := range m.notifiers { + n.SendPhoto(buffer) + } +} + +func (m *Notifiability) SendPhotoTo(channel string, buffer *bytes.Buffer) { + for _, n := range m.notifiers { + n.SendPhotoTo(channel, buffer) + } +} diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index 4ac14a803..d8d8347ff 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -117,6 +117,10 @@ func (e *GeneralOrderExecutor) SubmitOrders(ctx context.Context, submitOrders .. err = fmt.Errorf("can not place orders: %w", err) } } + // FIXME: map by price and volume + for i := 0; i < len(createdOrders); i++ { + createdOrders[i].Tag = formattedOrders[i].Tag + } e.orderStore.Add(createdOrders...) e.activeMakerOrders.Add(createdOrders...) diff --git a/pkg/bbgo/order_store.go b/pkg/bbgo/order_store.go index 46e4911c7..473473e09 100644 --- a/pkg/bbgo/order_store.go +++ b/pkg/bbgo/order_store.go @@ -104,8 +104,9 @@ func (s *OrderStore) Update(o types.Order) bool { s.mu.Lock() defer s.mu.Unlock() - _, ok := s.orders[o.OrderID] + old, ok := s.orders[o.OrderID] if ok { + o.Tag = old.Tag s.orders[o.OrderID] = o } return ok diff --git a/pkg/indicator/fisher.go b/pkg/indicator/fisher.go index b4b6430cd..cd9ce4dac 100644 --- a/pkg/indicator/fisher.go +++ b/pkg/indicator/fisher.go @@ -34,6 +34,10 @@ func (inc *FisherTransform) Update(value float64) { inc.prices.Update(value) highest := inc.prices.Highest(inc.Window) lowest := inc.prices.Lowest(inc.Window) + if highest == lowest { + inc.Values.Update(0) + return + } x := 2*((value-lowest)/(highest-lowest)) - 1 if x == 1 { x = 0.9999 diff --git a/pkg/notifier/slacknotifier/slack.go b/pkg/notifier/slacknotifier/slack.go index 69e229257..333e3f1e5 100644 --- a/pkg/notifier/slacknotifier/slack.go +++ b/pkg/notifier/slacknotifier/slack.go @@ -1,6 +1,7 @@ package slacknotifier import ( + "bytes" "context" "fmt" "time" @@ -150,6 +151,14 @@ func (n *Notifier) NotifyTo(channel string, obj interface{}, args ...interface{} } } +func (n *Notifier) SendPhoto(buffer *bytes.Buffer) { + n.SendPhotoTo(n.channel, buffer) +} + +func (n *Notifier) SendPhotoTo(channel string, buffer *bytes.Buffer) { + // TODO +} + /* func (n *Notifier) NotifyTrade(trade *types.Trade) { _, _, err := n.client.PostMessageContext(context.Background(), n.TradeChannel, diff --git a/pkg/notifier/telegramnotifier/telegram.go b/pkg/notifier/telegramnotifier/telegram.go index 36927319e..0afe4566f 100644 --- a/pkg/notifier/telegramnotifier/telegram.go +++ b/pkg/notifier/telegramnotifier/telegram.go @@ -1,6 +1,7 @@ package telegramnotifier import ( + "bytes" "fmt" "reflect" "strconv" @@ -129,6 +130,48 @@ func (n *Notifier) NotifyTo(channel string, obj interface{}, args ...interface{} } } +func (n *Notifier) SendPhoto(buffer *bytes.Buffer) { + n.SendPhotoTo("", buffer) +} + +func photoFromBuffer(buffer *bytes.Buffer) telebot.InputMedia { + reader := bytes.NewReader(buffer.Bytes()) + return &telebot.Photo{ + File: telebot.FromReader(reader), + } +} + +func (n *Notifier) SendPhotoTo(channel string, buffer *bytes.Buffer) { + if n.broadcast { + if n.Subscribers == nil { + return + } + + for chatID := range n.Subscribers { + chat, err := n.bot.ChatByID(strconv.FormatInt(chatID, 10)) + if err != nil { + log.WithError(err).Error("can not get chat by ID") + continue + } + album := telebot.Album{ + photoFromBuffer(buffer), + } + if _, err := n.bot.SendAlbum(chat, album); err != nil { + log.WithError(err).Error("failed to send message") + } + } + } else if n.Chats != nil { + for _, chat := range n.Chats { + album := telebot.Album{ + photoFromBuffer(buffer), + } + if _, err := n.bot.SendAlbum(chat, album); err != nil { + log.WithError(err).Error("telegram send error") + } + } + } +} + func (n *Notifier) AddChat(c *telebot.Chat) { if n.Chats == nil { n.Chats = make(map[int64]*telebot.Chat) diff --git a/pkg/strategy/drift/driftma.go b/pkg/strategy/drift/driftma.go new file mode 100644 index 000000000..6660371c5 --- /dev/null +++ b/pkg/strategy/drift/driftma.go @@ -0,0 +1,51 @@ +package drift + +import ( + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +type DriftMA struct { + types.SeriesBase + ma1 types.UpdatableSeriesExtend + drift *indicator.Drift + ma2 types.UpdatableSeriesExtend +} + +func (s *DriftMA) Update(value float64) { + s.ma1.Update(value) + s.drift.Update(s.ma1.Last()) + s.ma2.Update(s.drift.Last()) +} + +func (s *DriftMA) Last() float64 { + return s.ma2.Last() +} + +func (s *DriftMA) Index(i int) float64 { + return s.ma2.Index(i) +} + +func (s *DriftMA) Length() int { + return s.ma2.Length() +} + +func (s *DriftMA) ZeroPoint() float64 { + return s.drift.ZeroPoint() +} + +func (s *DriftMA) Clone() *DriftMA { + out := DriftMA{ + ma1: types.Clone(s.ma1), + drift: s.drift.Clone(), + ma2: types.Clone(s.ma2), + } + out.SeriesBase.Series = &out + return &out +} + +func (s *DriftMA) TestUpdate(v float64) *DriftMA { + out := s.Clone() + out.Update(v) + return out +} diff --git a/pkg/strategy/drift/output.go b/pkg/strategy/drift/output.go new file mode 100644 index 000000000..1043c3e1e --- /dev/null +++ b/pkg/strategy/drift/output.go @@ -0,0 +1,210 @@ +package drift + +import ( + "fmt" + "io" + "reflect" + "sort" + "strings" + "unsafe" + + "github.com/c9s/bbgo/pkg/util" + "github.com/fatih/color" + "github.com/jedib0t/go-pretty/v6/table" + "github.com/jedib0t/go-pretty/v6/text" +) + +type jsonStruct struct { + key string + json string + tp string + value interface{} +} +type jsonArr []jsonStruct + +func (a jsonArr) Len() int { return len(a) } +func (a jsonArr) Less(i, j int) bool { return a[i].key < a[j].key } +func (a jsonArr) Swap(i, j int) { a[i], a[j] = a[j], a[i] } + +func (s *Strategy) ParamDump(f io.Writer, seriesLength ...int) { + length := 1 + if len(seriesLength) > 0 && seriesLength[0] > 0 { + length = seriesLength[0] + } + val := reflect.ValueOf(s).Elem() + for i := 0; i < val.Type().NumField(); i++ { + t := val.Type().Field(i) + if ig := t.Tag.Get("ignore"); ig == "true" { + continue + } + field := val.Field(i) + if t.IsExported() || t.Anonymous || t.Type.Kind() == reflect.Func || t.Type.Kind() == reflect.Chan { + continue + } + fieldName := t.Name + typeName := field.Type().String() + log.Infof("fieldName %s typeName %s", fieldName, typeName) + value := reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem() + isSeries := true + lastFunc := value.MethodByName("Last") + isSeries = isSeries && lastFunc.IsValid() + lengthFunc := value.MethodByName("Length") + isSeries = isSeries && lengthFunc.IsValid() + indexFunc := value.MethodByName("Index") + isSeries = isSeries && indexFunc.IsValid() + + stringFunc := value.MethodByName("String") + canString := stringFunc.IsValid() + + if isSeries { + l := int(lengthFunc.Call(nil)[0].Int()) + if l >= length { + fmt.Fprintf(f, "%s: Series[..., %.4f", fieldName, indexFunc.Call([]reflect.Value{reflect.ValueOf(length - 1)})[0].Float()) + for j := length - 2; j >= 0; j-- { + fmt.Fprintf(f, ", %.4f", indexFunc.Call([]reflect.Value{reflect.ValueOf(j)})[0].Float()) + } + fmt.Fprintf(f, "]\n") + } else if l > 0 { + fmt.Fprintf(f, "%s: Series[%.4f", fieldName, indexFunc.Call([]reflect.Value{reflect.ValueOf(l - 1)})[0].Float()) + for j := l - 2; j >= 0; j-- { + fmt.Fprintf(f, ", %.4f", indexFunc.Call([]reflect.Value{reflect.ValueOf(j)})[0].Float()) + } + fmt.Fprintf(f, "]\n") + } else { + fmt.Fprintf(f, "%s: Series[]\n", fieldName) + } + } else if canString { + fmt.Fprintf(f, "%s: %s\n", fieldName, stringFunc.Call(nil)[0].String()) + } else if field.CanConvert(reflect.TypeOf(int(0))) { + fmt.Fprintf(f, "%s: %d\n", fieldName, field.Int()) + } else if field.CanConvert(reflect.TypeOf(float64(0))) { + fmt.Fprintf(f, "%s: %.4f\n", fieldName, field.Float()) + } else if field.CanInterface() { + fmt.Fprintf(f, "%s: %v", fieldName, field.Interface()) + } else if field.Type().Kind() == reflect.Map { + fmt.Fprintf(f, "%s: {", fieldName) + iter := value.MapRange() + for iter.Next() { + k := iter.Key().Interface() + v := iter.Value().Interface() + fmt.Fprintf(f, "%v: %v, ", k, v) + } + fmt.Fprintf(f, "}\n") + } else if field.Type().Kind() == reflect.Slice { + fmt.Fprintf(f, "%s: [", fieldName) + l := field.Len() + if l > 0 { + fmt.Fprintf(f, "%v", field.Index(0)) + } + for j := 1; j < field.Len(); j++ { + fmt.Fprintf(f, ", %v", field.Index(j)) + } + fmt.Fprintf(f, "]\n") + } else { + fmt.Fprintf(f, "%s(%s): %s\n", fieldName, typeName, field.String()) + } + } +} + +func (s *Strategy) Print(f io.Writer, pretty bool, withColor ...bool) { + //b, _ := json.MarshalIndent(s.ExitMethods, " ", " ") + + t := table.NewWriter() + style := table.Style{ + Name: "StyleRounded", + Box: table.StyleBoxRounded, + Color: table.ColorOptionsDefault, + Format: table.FormatOptionsDefault, + HTML: table.DefaultHTMLOptions, + Options: table.OptionsDefault, + Title: table.TitleOptionsDefault, + } + var hiyellow func(io.Writer, string, ...interface{}) + if len(withColor) > 0 && withColor[0] { + if pretty { + style.Color = table.ColorOptionsYellowWhiteOnBlack + style.Color.Row = text.Colors{text.FgHiYellow, text.BgHiBlack} + style.Color.RowAlternate = text.Colors{text.FgYellow, text.BgBlack} + } + hiyellow = color.New(color.FgHiYellow).FprintfFunc() + } else { + hiyellow = func(a io.Writer, format string, args ...interface{}) { + fmt.Fprintf(a, format, args...) + } + } + if pretty { + t.SetOutputMirror(f) + t.SetStyle(style) + t.AppendHeader(table.Row{"json", "struct field name", "type", "value"}) + } + hiyellow(f, "------ %s Settings ------\n", s.InstanceID()) + + embeddedWhiteSet := map[string]struct{}{"Window": {}, "Interval": {}, "Symbol": {}} + redundantSet := map[string]struct{}{} + var rows []table.Row + val := reflect.ValueOf(*s) + var values jsonArr + for i := 0; i < val.Type().NumField(); i++ { + t := val.Type().Field(i) + if !t.IsExported() { + continue + } + fieldName := t.Name + switch jsonTag := t.Tag.Get("json"); jsonTag { + case "-": + case "": + if t.Anonymous { + var target reflect.Type + if t.Type.Kind() == util.Pointer { + target = t.Type.Elem() + } else { + target = t.Type + } + for j := 0; j < target.NumField(); j++ { + tt := target.Field(j) + if !tt.IsExported() { + continue + } + fieldName := tt.Name + if _, ok := embeddedWhiteSet[fieldName]; !ok { + continue + } + + if jtag := tt.Tag.Get("json"); jtag != "" && jtag != "-" { + name := strings.Split(jtag, ",")[0] + if _, ok := redundantSet[name]; ok { + continue + } + redundantSet[name] = struct{}{} + var value interface{} + if t.Type.Kind() == util.Pointer { + value = val.Field(i).Elem().Field(j).Interface() + } else { + value = val.Field(i).Field(j).Interface() + } + values = append(values, jsonStruct{key: fieldName, json: name, tp: tt.Type.String(), value: value}) + } + } + } + default: + name := strings.Split(jsonTag, ",")[0] + if _, ok := redundantSet[name]; ok { + continue + } + redundantSet[name] = struct{}{} + values = append(values, jsonStruct{key: fieldName, json: name, tp: t.Type.String(), value: val.Field(i).Interface()}) + } + } + sort.Sort(values) + for _, value := range values { + if pretty { + rows = append(rows, table.Row{value.json, value.key, value.tp, value.value}) + } else { + hiyellow(f, "%s: %v\n", value.json, value.value) + } + } + if pretty { + t.AppendRows(rows) + t.Render() + } +} diff --git a/pkg/strategy/drift/strategy.go b/pkg/strategy/drift/strategy.go index f3a31f137..c0bd79ae4 100644 --- a/pkg/strategy/drift/strategy.go +++ b/pkg/strategy/drift/strategy.go @@ -1,35 +1,40 @@ package drift import ( - "bufio" + "bytes" "context" - "encoding/json" "errors" "fmt" "math" "os" + "strconv" "strings" "sync" - "github.com/fatih/color" "github.com/sirupsen/logrus" "github.com/wcharczuk/go-chart/v2" "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/interact" "github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/util" ) const ID = "drift" +const DDriftFilterNeg = -0.7 +const DDriftFilterPos = 0.7 +const DriftFilterNeg = -1.85 +const DriftFilterPos = 1.85 + var log = logrus.WithField("strategy", ID) var Four fixedpoint.Value = fixedpoint.NewFromInt(4) var Three fixedpoint.Value = fixedpoint.NewFromInt(3) var Two fixedpoint.Value = fixedpoint.NewFromInt(2) var Delta fixedpoint.Value = fixedpoint.NewFromFloat(0.01) -var Fee = fixedpoint.NewFromFloat(0.0008) // taker fee % * 2, for upper bound +var Fee = 0.0008 // taker fee % * 2, for upper bound func init() { bbgo.RegisterStrategy(ID, &Strategy{}) @@ -49,34 +54,50 @@ type Strategy struct { *types.ProfitStats `persistence:"profit_stats"` *types.TradeStats `persistence:"trade_stats"` - ma types.UpdatableSeriesExtend - stdevHigh *indicator.StdDev - stdevLow *indicator.StdDev - drift *DriftMA - atr *indicator.ATR - midPrice fixedpoint.Value - lock sync.RWMutex + p *types.Position - // This stores the maximum TP coefficient of ATR multiplier of each entry point - takeProfitFactor types.UpdatableSeriesExtend + trendLine types.UpdatableSeriesExtend + ma types.UpdatableSeriesExtend + stdevHigh *indicator.StdDev + stdevLow *indicator.StdDev + drift *DriftMA + drift1m *DriftMA + atr *indicator.ATR + midPrice fixedpoint.Value + lock sync.RWMutex `ignore:"true"` + positionLock sync.RWMutex `ignore:"true"` + minutesCounter int + orderPendingCounter map[uint64]int + frameKLine *types.KLine + kline1m *types.KLine + + beta float64 Source string `json:"source,omitempty"` - TakeProfitFactor float64 `json:"takeProfitFactor"` - ProfitFactorWindow int `json:"profitFactorWindow"` StopLoss fixedpoint.Value `json:"stoploss"` CanvasPath string `json:"canvasPath"` PredictOffset int `json:"predictOffset"` HighLowVarianceMultiplier float64 `json:"hlVarianceMultiplier"` NoTrailingStopLoss bool `json:"noTrailingStopLoss"` + TrailingStopLossType string `json:"trailingStopLossType"` // trailing stop sources. Possible options are `kline` for 1m kline and `realtime` from order updates HLRangeWindow int `json:"hlRangeWindow"` + Window1m int `json:"window1m"` + SmootherWindow1m int `json:"smootherWindow1m"` + FisherTransformWindow1m int `json:"fisherTransformWindow1m"` SmootherWindow int `json:"smootherWindow"` FisherTransformWindow int `json:"fisherTransformWindow"` ATRWindow int `json:"atrWindow"` + PendingMinutes int `json:"pendingMinutes"` // if order not be traded for pendingMinutes of time, cancel it. + NoRebalance bool `json:"noRebalance"` // disable rebalance + TrendWindow int `json:"trendWindow"` // trendLine is used for rebalancing the position. When trendLine goes up, hold base, otherwise hold quote + RebalanceFilter float64 `json:"rebalanceFilter"` // beta filter on the Linear Regression of trendLine + TrailingCallbackRate []float64 `json:"trailingCallbackRate"` + TrailingActivationRatio []float64 `json:"trailingActivationRatio"` - buyPrice float64 - sellPrice float64 - highestPrice float64 - lowestPrice float64 + buyPrice float64 `persistence:"buy_price"` + sellPrice float64 `persistence:"sell_price"` + highestPrice float64 `persistence:"highest_price"` + lowestPrice float64 `persistence:"lowest_price"` // This is not related to trade but for statistics graph generation // Will deduct fee in percentage from every trade @@ -94,40 +115,12 @@ type Strategy struct { getSource SourceFunc } -func (s *Strategy) Print(o *os.File) { - f := bufio.NewWriter(o) - defer f.Flush() - b, _ := json.MarshalIndent(s.ExitMethods, " ", " ") - hiyellow := color.New(color.FgHiYellow).FprintfFunc() - hiyellow(f, "------ %s Settings ------\n", s.InstanceID()) - hiyellow(f, "generateGraph: %v\n", s.GenerateGraph) - hiyellow(f, "canvasPath: %s\n", s.CanvasPath) - hiyellow(f, "graphPNLPath: %s\n", s.GraphPNLPath) - hiyellow(f, "graphCumPNLPath: %s\n", s.GraphCumPNLPath) - hiyellow(f, "source: %s\n", s.Source) - hiyellow(f, "stoploss: %v\n", s.StopLoss) - hiyellow(f, "takeProfitFactor(last): %f, (init): %f\n", s.takeProfitFactor.Last(), s.TakeProfitFactor) - hiyellow(f, "profitFactorWindow: %d\n", s.ProfitFactorWindow) - hiyellow(f, "predictOffset: %d\n", s.PredictOffset) - hiyellow(f, "exits:\n %s\n", string(b)) - hiyellow(f, "symbol: %s\n", s.Symbol) - hiyellow(f, "interval: %s\n", s.Interval) - hiyellow(f, "window: %d\n", s.Window) - hiyellow(f, "noTrailingStopLoss: %v\n", s.NoTrailingStopLoss) - hiyellow(f, "hlVarianceMutiplier: %f\n", s.HighLowVarianceMultiplier) - hiyellow(f, "hlRangeWindow: %d\n", s.HLRangeWindow) - hiyellow(f, "smootherWindow: %d\n", s.SmootherWindow) - hiyellow(f, "fisherTransformWindow: %d\n", s.FisherTransformWindow) - hiyellow(f, "atrWindow: %d\n", s.ATRWindow) - hiyellow(f, "\n") -} - func (s *Strategy) ID() string { return ID } func (s *Strategy) InstanceID() string { - return fmt.Sprintf("%s:%s", ID, s.Symbol) + return fmt.Sprintf("%s:%s:%v", ID, s.Symbol, bbgo.IsBackTesting) } func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { @@ -149,13 +142,13 @@ func (s *Strategy) CurrentPosition() *types.Position { } func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Value) error { - order := s.Position.NewMarketCloseOrder(percentage) + order := s.p.NewMarketCloseOrder(percentage) if order == nil { return nil } order.Tag = "close" order.TimeInForce = "" - balances := s.Session.GetAccount().Balances() + balances := s.GeneralOrderExecutor.Session().GetAccount().Balances() baseBalance := balances[s.Market.BaseCurrency].Available price := s.getLastPrice() if order.Side == types.SideTypeBuy { @@ -211,52 +204,7 @@ func (s *Strategy) SourceFuncGenerator() SourceFunc { } } -type DriftMA struct { - types.SeriesBase - ma1 types.UpdatableSeriesExtend - drift *indicator.Drift - ma2 types.UpdatableSeriesExtend -} - -func (s *DriftMA) Update(value float64) { - s.ma1.Update(value) - s.drift.Update(s.ma1.Last()) - s.ma2.Update(s.drift.Last()) -} - -func (s *DriftMA) Last() float64 { - return s.ma2.Last() -} - -func (s *DriftMA) Index(i int) float64 { - return s.ma2.Index(i) -} - -func (s *DriftMA) Length() int { - return s.ma2.Length() -} - -func (s *DriftMA) ZeroPoint() float64 { - return s.drift.ZeroPoint() -} - -func (s *DriftMA) Clone() *DriftMA { - out := DriftMA{ - ma1: types.Clone(s.ma1), - drift: s.drift.Clone(), - ma2: types.Clone(s.ma2), - } - out.SeriesBase.Series = &out - return &out -} - -func (s *DriftMA) TestUpdate(v float64) *DriftMA { - out := s.Clone() - out.Update(v) - return out -} - -func (s *Strategy) initIndicators() error { +func (s *Strategy) initIndicators(priceLines *types.Queue) error { s.ma = &indicator.SMA{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.HLRangeWindow}} s.stdevHigh = &indicator.StdDev{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.HLRangeWindow}} s.stdevLow = &indicator.StdDev{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.HLRangeWindow}} @@ -273,11 +221,22 @@ func (s *Strategy) initIndicators() error { }, } s.drift.SeriesBase.Series = s.drift - s.atr = &indicator.ATR{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.ATRWindow}} - s.takeProfitFactor = &indicator.SMA{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.ProfitFactorWindow}} - for i := 0; i < s.ProfitFactorWindow; i++ { - s.takeProfitFactor.Update(s.TakeProfitFactor) + s.drift1m = &DriftMA{ + drift: &indicator.Drift{ + MA: &indicator.SMA{IntervalWindow: types.IntervalWindow{Interval: types.Interval1m, Window: s.Window1m}}, + IntervalWindow: types.IntervalWindow{Interval: types.Interval1m, Window: s.Window1m}, + }, + ma1: &indicator.EWMA{ + IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.SmootherWindow1m}, + }, + ma2: &indicator.FisherTransform{ + IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.FisherTransformWindow1m}, + }, } + s.drift1m.SeriesBase.Series = s.drift1m + s.atr = &indicator.ATR{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.ATRWindow}} + s.trendLine = &indicator.EWMA{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.TrendWindow}} + store, _ := s.Session.MarketDataStore(s.Symbol) klines, ok := store.KLinesOfInterval(s.Interval) if !ok { @@ -292,11 +251,101 @@ func (s *Strategy) initIndicators() error { s.stdevHigh.Update(high - s.ma.Last()) s.stdevLow.Update(s.ma.Last() - low) s.drift.Update(source) + s.trendLine.Update(source) s.atr.PushK(kline) + priceLines.Update(source) + } + if s.frameKLine != nil && klines != nil { + s.frameKLine.Set(&(*klines)[len(*klines)-1]) + } + klines, ok = store.KLinesOfInterval(types.Interval1m) + if !ok { + return errors.New("klines not exists") + } + for _, kline := range *klines { + source := s.getSource(&kline).Float64() + s.drift1m.Update(source) + if s.drift1m.Last() != s.drift1m.Last() { + panic(fmt.Sprintf("%f %v %f %f", source, s.drift1m.drift.Values.Index(1), s.drift1m.ma2.Last(), s.drift1m.drift.LastValue)) + } + } + if s.kline1m != nil && klines != nil { + s.kline1m.Set(&(*klines)[len(*klines)-1]) } return nil } +func (s *Strategy) smartCancel(ctx context.Context, pricef, atr float64) (int, error) { + nonTraded := s.GeneralOrderExecutor.ActiveMakerOrders().Orders() + if len(nonTraded) > 0 { + if len(nonTraded) > 1 { + log.Errorf("should only have one order to cancel, got %d", len(nonTraded)) + } + toCancel := false + + drift := s.drift1m.Array(2) + for _, order := range nonTraded { + log.Warnf("%v", order) + if s.minutesCounter-s.orderPendingCounter[order.OrderID] > s.PendingMinutes { + if order.Side == types.SideTypeBuy && drift[1] < drift[0] { + continue + } else if order.Side == types.SideTypeSell && drift[1] > drift[0] { + continue + } + toCancel = true + } else if order.Side == types.SideTypeBuy { + // 75% of the probability + if order.Price.Float64()+s.stdevHigh.Last()*2 <= pricef { + toCancel = true + } + } else if order.Side == types.SideTypeSell { + // 75% of the probability + if order.Price.Float64()-s.stdevLow.Last()*2 >= pricef { + toCancel = true + } + } else { + panic("not supported side for the order") + } + } + if toCancel { + err := s.GeneralOrderExecutor.GracefulCancel(ctx) + // TODO: clean orderPendingCounter on cancel/trade + if err == nil { + for _, order := range nonTraded { + delete(s.orderPendingCounter, order.OrderID) + } + } + log.Warnf("cancel all %v", err) + return 0, err + } + } + return len(nonTraded), nil +} + +func (s *Strategy) trailingCheck(price float64, direction string) bool { + if s.highestPrice > 0 && s.highestPrice < price { + s.highestPrice = price + } + if s.lowestPrice > 0 && s.lowestPrice > price { + s.lowestPrice = price + } + isShort := direction == "short" + for i := len(s.TrailingCallbackRate) - 1; i >= 0; i-- { + trailingCallbackRate := s.TrailingCallbackRate[i] + trailingActivationRatio := s.TrailingActivationRatio[i] + if isShort { + if (s.sellPrice-s.lowestPrice)/s.lowestPrice > trailingActivationRatio { + return (price-s.lowestPrice)/s.lowestPrice > trailingCallbackRate + } + } else { + if (s.highestPrice-s.buyPrice)/s.buyPrice > trailingActivationRatio { + return (s.highestPrice-price)/price > trailingCallbackRate + } + } + } + return false +} + func (s *Strategy) initTickerFunctions(ctx context.Context) { if s.IsBackTesting() { s.getLastPrice = func() fixedpoint.Value { @@ -311,59 +360,67 @@ func (s *Strategy) initTickerFunctions(ctx context.Context) { bestBid := ticker.Buy bestAsk := ticker.Sell - var pricef, atr, avg float64 - var price fixedpoint.Value - if util.TryLock(&s.lock) { - if !bestAsk.IsZero() && !bestBid.IsZero() { - s.midPrice = bestAsk.Add(bestBid).Div(Two) - } else if !bestAsk.IsZero() { - s.midPrice = bestAsk - } else { - s.midPrice = bestBid - } - price = s.midPrice - pricef = s.midPrice.Float64() - } else { + var pricef, atr float64 + if !util.TryLock(&s.lock) { return } + if !bestAsk.IsZero() && !bestBid.IsZero() { + s.midPrice = bestAsk.Add(bestBid).Div(Two) + } else if !bestAsk.IsZero() { + s.midPrice = bestAsk + } else { + s.midPrice = bestBid + } + pricef = s.midPrice.Float64() + + s.lock.Unlock() + + if !util.TryLock(&s.positionLock) { + return + } + if s.highestPrice > 0 && s.highestPrice < pricef { s.highestPrice = pricef } if s.lowestPrice > 0 && s.lowestPrice > pricef { s.lowestPrice = pricef } - // for trailing stoploss during the realtime - if s.NoTrailingStopLoss || s.GeneralOrderExecutor.ActiveMakerOrders().NumOfOrders() > 0 { - s.lock.Unlock() + if s.NoTrailingStopLoss || s.TrailingStopLossType == "kline" { + s.positionLock.Unlock() return } - atr = s.atr.Last() - avg = s.buyPrice + s.sellPrice - d := s.drift.TestUpdate(pricef) - drift := d.Last() - ddrift := d.drift.Last() - takeProfitFactor := s.takeProfitFactor.Predict(2) - exitShortCondition := ( /*avg+atr/2 <= pricef || avg*(1.+stoploss) <= pricef ||*/ (drift > 0 && ddrift > 0.6) || avg-atr*takeProfitFactor >= pricef || - ((pricef-s.lowestPrice)/s.lowestPrice > 0.003 && (avg-s.lowestPrice)/s.lowestPrice > 0.015)) && - (s.Position.IsShort() && !s.Position.IsDust(price)) - exitLongCondition := ( /*avg-atr/2 >= pricef || avg*(1.-stoploss) >= pricef ||*/ (drift < 0 && ddrift < -0.6) || avg+atr*takeProfitFactor <= pricef || - ((s.highestPrice-pricef)/pricef > 0.003 && (s.highestPrice-avg)/avg > 0.015)) && - (!s.Position.IsLong() && !s.Position.IsDust(price)) - if exitShortCondition || exitLongCondition { - if exitLongCondition && s.highestPrice > avg { - s.takeProfitFactor.Update((s.highestPrice - avg) / atr * 4) - } else if exitShortCondition && avg > s.lowestPrice { - s.takeProfitFactor.Update((avg - s.lowestPrice) / atr * 4) - } - _ = s.ClosePosition(ctx, fixedpoint.One) - } - s.lock.Unlock() + stoploss := s.StopLoss.Float64() + atr = s.atr.Last() + numPending := 0 + var err error + if numPending, err = s.smartCancel(ctx, pricef, atr); err != nil { + log.WithError(err).Errorf("cannot cancel orders") + s.positionLock.Unlock() + return + } + if numPending > 0 { + s.positionLock.Unlock() + return + } + + exitShortCondition := s.sellPrice > 0 && (s.sellPrice*(1.+stoploss) <= pricef || + s.trailingCheck(pricef, "short")) + exitLongCondition := s.buyPrice > 0 && (s.buyPrice*(1.-stoploss) >= pricef || + s.trailingCheck(pricef, "long")) + if exitShortCondition || exitLongCondition { + log.Infof("Close position by orderbook changes") + s.positionLock.Unlock() + _ = s.ClosePosition(ctx, fixedpoint.One) + } else { + s.positionLock.Unlock() + } }) s.getLastPrice = func() (lastPrice fixedpoint.Value) { var ok bool s.lock.RLock() + defer s.lock.RUnlock() if s.midPrice.IsZero() { lastPrice, ok = s.Session.LastPrice(s.Symbol) if !ok { @@ -373,32 +430,75 @@ func (s *Strategy) initTickerFunctions(ctx context.Context) { } else { lastPrice = s.midPrice } - s.lock.RUnlock() return lastPrice } } } -func (s *Strategy) Draw(time types.Time, priceLine types.SeriesExtend, profit types.Series, cumProfit types.Series, zeroPoints types.Series) { +func (s *Strategy) DrawIndicators(time types.Time, priceLine types.SeriesExtend, zeroPoints types.Series) *types.Canvas { canvas := types.NewCanvas(s.InstanceID(), s.Interval) Length := priceLine.Length() if Length > 300 { Length = 300 } + log.Infof("draw indicators with %d data", Length) mean := priceLine.Mean(Length) highestPrice := priceLine.Minus(mean).Abs().Highest(Length) highestDrift := s.drift.Abs().Highest(Length) hi := s.drift.drift.Abs().Highest(Length) + h1m := s.drift1m.Abs().Highest(Length * s.Interval.Minutes()) ratio := highestPrice / highestDrift + canvas.Plot("upband", s.ma.Add(s.stdevHigh), time, Length) canvas.Plot("ma", s.ma, time, Length) canvas.Plot("downband", s.ma.Minus(s.stdevLow), time, Length) canvas.Plot("drift", s.drift.Mul(ratio).Add(mean), time, Length) canvas.Plot("driftOrig", s.drift.drift.Mul(highestPrice/hi).Add(mean), time, Length) + canvas.Plot("drift1m", s.drift1m.Mul(highestPrice/h1m).Add(mean), time, Length*s.Interval.Minutes(), types.Interval1m) canvas.Plot("zero", types.NumberSeries(mean), time, Length) canvas.Plot("price", priceLine, time, Length) canvas.Plot("zeroPoint", zeroPoints, time, Length) + return canvas +} + +func (s *Strategy) DrawPNL(profit types.Series) *types.Canvas { + canvas := types.NewCanvas(s.InstanceID()) + log.Errorf("pnl Highest: %f, Lowest: %f", types.Highest(profit, profit.Length()), types.Lowest(profit, profit.Length())) + length := profit.Length() + if s.GraphPNLDeductFee { + canvas.PlotRaw("pnl % (with Fee Deducted)", profit, length) + } else { + canvas.PlotRaw("pnl %", profit, length) + } + canvas.YAxis = chart.YAxis{ + ValueFormatter: func(v interface{}) string { + if vf, isFloat := v.(float64); isFloat { + return fmt.Sprintf("%.4f", vf) + } + return "" + }, + } + canvas.PlotRaw("1", types.NumberSeries(1), length) + return canvas +} + +func (s *Strategy) DrawCumPNL(cumProfit types.Series) *types.Canvas { + canvas := types.NewCanvas(s.InstanceID()) + canvas.PlotRaw("cummulative pnl", cumProfit, cumProfit.Length()) + canvas.YAxis = chart.YAxis{ + ValueFormatter: func(v interface{}) string { + if vf, isFloat := v.(float64); isFloat { + return fmt.Sprintf("%.4f", vf) + } + return "" + }, + } + return canvas +} + +func (s *Strategy) Draw(time types.Time, priceLine types.SeriesExtend, profit types.Series, cumProfit types.Series, zeroPoints types.Series) { + canvas := s.DrawIndicators(time, priceLine, zeroPoints) f, err := os.Create(s.CanvasPath) if err != nil { log.WithError(err).Errorf("cannot create on %s", s.CanvasPath) @@ -409,12 +509,7 @@ func (s *Strategy) Draw(time types.Time, priceLine types.SeriesExtend, profit ty log.WithError(err).Errorf("cannot render in drift") } - canvas = types.NewCanvas(s.InstanceID()) - if s.GraphPNLDeductFee { - canvas.PlotRaw("pnl % (with Fee Deducted)", profit, profit.Length()) - } else { - canvas.PlotRaw("pnl %", profit, profit.Length()) - } + canvas = s.DrawPNL(profit) f, err = os.Create(s.GraphPNLPath) if err != nil { log.WithError(err).Errorf("open pnl") @@ -425,12 +520,7 @@ func (s *Strategy) Draw(time types.Time, priceLine types.SeriesExtend, profit ty log.WithError(err).Errorf("render pnl") } - canvas = types.NewCanvas(s.InstanceID()) - if s.GraphPNLDeductFee { - canvas.PlotRaw("cummulative pnl % (with Fee Deducted)", cumProfit, cumProfit.Length()) - } else { - canvas.PlotRaw("cummulative pnl %", cumProfit, cumProfit.Length()) - } + canvas = s.DrawCumPNL(cumProfit) f, err = os.Create(s.GraphCumPNLPath) if err != nil { log.WithError(err).Errorf("open cumpnl") @@ -442,11 +532,79 @@ func (s *Strategy) Draw(time types.Time, priceLine types.SeriesExtend, profit ty } } +// Sending new rebalance orders cost too much. +// Modify the position instead to expect the strategy itself rebalance on Close +func (s *Strategy) Rebalance(ctx context.Context) { + price := s.getLastPrice() + _, beta := types.LinearRegression(s.trendLine, 3) + if math.Abs(beta) > s.RebalanceFilter && math.Abs(s.beta) > s.RebalanceFilter || math.Abs(s.beta) < s.RebalanceFilter && math.Abs(beta) < s.RebalanceFilter { + return + } + balances := s.GeneralOrderExecutor.Session().GetAccount().Balances() + baseBalance := balances[s.Market.BaseCurrency].Total() + quoteBalance := balances[s.Market.QuoteCurrency].Total() + total := baseBalance.Add(quoteBalance.Div(price)) + percentage := fixedpoint.One.Sub(Delta) + log.Infof("rebalance beta %f %v", beta, s.p) + if beta > s.RebalanceFilter { + if total.Mul(percentage).Compare(baseBalance) > 0 { + q := total.Mul(percentage).Sub(baseBalance) + s.p.Lock() + defer s.p.Unlock() + s.p.Base = q.Neg() + s.p.Quote = q.Mul(price) + s.p.AverageCost = price + } + } else if beta <= -s.RebalanceFilter { + if total.Mul(percentage).Compare(quoteBalance.Div(price)) > 0 { + q := total.Mul(percentage).Sub(quoteBalance.Div(price)) + s.p.Lock() + defer s.p.Unlock() + s.p.Base = q + s.p.Quote = q.Mul(price).Neg() + s.p.AverageCost = price + } + } else { + if total.Div(Two).Compare(quoteBalance.Div(price)) > 0 { + q := total.Div(Two).Sub(quoteBalance.Div(price)) + s.p.Lock() + defer s.p.Unlock() + s.p.Base = q + s.p.Quote = q.Mul(price).Neg() + s.p.AverageCost = price + } else if total.Div(Two).Compare(baseBalance) > 0 { + q := total.Div(Two).Sub(baseBalance) + s.p.Lock() + defer s.p.Unlock() + s.p.Base = q.Neg() + s.p.Quote = q.Mul(price) + s.p.AverageCost = price + } else { + s.p.Lock() + defer s.p.Unlock() + s.p.Reset() + } + } + log.Infof("rebalanceafter %v %v %v", baseBalance, quoteBalance, s.p) + s.beta = beta +} + +func (s *Strategy) CalcAssetValue(price fixedpoint.Value) fixedpoint.Value { + balances := s.Session.GetAccount().Balances() + return balances[s.Market.BaseCurrency].Total().Mul(price).Add(balances[s.Market.QuoteCurrency].Total()) +} + func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { instanceID := s.InstanceID() // Will be set by persistence if there's any from DB if s.Position == nil { s.Position = types.NewPositionFromMarket(s.Market) + s.p = types.NewPositionFromMarket(s.Market) + } else { + s.p = types.NewPositionFromMarket(s.Market) + s.p.Base = s.Position.Base + s.p.Quote = s.Position.Quote + s.p.AverageCost = s.Position.AverageCost } if s.ProfitStats == nil { s.ProfitStats = types.NewProfitStats(s.Market) @@ -481,121 +639,166 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se }) s.GeneralOrderExecutor.Bind() + s.orderPendingCounter = make(map[uint64]int) + s.minutesCounter = 0 + // Exit methods from config for _, method := range s.ExitMethods { method.Bind(session, s.GeneralOrderExecutor) } - buyPrice := fixedpoint.Zero - sellPrice := fixedpoint.Zero - Volume := fixedpoint.Zero - profit := types.Float64Slice{} - cumProfit := types.Float64Slice{1.} - orderTagHistory := make(map[uint64]string) - s.buyPrice = 0 - s.sellPrice = 0 - s.Session.UserDataStream.OnOrderUpdate(func(order types.Order) { - orderTagHistory[order.OrderID] = order.Tag - }) - modify := func(p fixedpoint.Value) fixedpoint.Value { + + profit := types.Float64Slice{1., 1.} + price, _ := s.Session.LastPrice(s.Symbol) + initAsset := s.CalcAssetValue(price).Float64() + cumProfit := types.Float64Slice{initAsset, initAsset} + modify := func(p float64) float64 { return p } if s.GraphPNLDeductFee { - modify = func(p fixedpoint.Value) fixedpoint.Value { - return p.Mul(fixedpoint.One.Sub(Fee)) + modify = func(p float64) float64 { + return p * (1. - Fee) } } - s.Session.UserDataStream.OnTradeUpdate(func(trade types.Trade) { - tag, ok := orderTagHistory[trade.OrderID] + s.GeneralOrderExecutor.TradeCollector().OnTrade(func(trade types.Trade, _profit, _netProfit fixedpoint.Value) { + s.p.AddTrade(trade) + order, ok := s.GeneralOrderExecutor.TradeCollector().OrderStore().Get(trade.OrderID) if !ok { panic(fmt.Sprintf("cannot find order: %v", trade)) } + tag := order.Tag + + price := trade.Price.Float64() + + if s.buyPrice > 0 { + profit.Update(modify(price / s.buyPrice)) + cumProfit.Update(s.CalcAssetValue(trade.Price).Float64()) + } else if s.sellPrice > 0 { + profit.Update(modify(s.sellPrice / price)) + cumProfit.Update(s.CalcAssetValue(trade.Price).Float64()) + } + s.positionLock.Lock() + defer s.positionLock.Unlock() if tag == "close" { - if !buyPrice.IsZero() { - profit.Update(modify(trade.Price.Div(buyPrice)). - Sub(fixedpoint.One). - Mul(trade.Quantity). - Div(Volume). - Add(fixedpoint.One). - Float64()) - cumProfit.Update(cumProfit.Last() * profit.Last()) - Volume = Volume.Sub(trade.Quantity) - if Volume.IsZero() { - buyPrice = fixedpoint.Zero - } - if !sellPrice.IsZero() { - panic("sellprice shouldn't be zero") - } - } else if !sellPrice.IsZero() { - profit.Update(modify(sellPrice.Div(trade.Price)). - Sub(fixedpoint.One). - Mul(trade.Quantity). - Div(Volume). - Neg(). - Add(fixedpoint.One). - Float64()) - cumProfit.Update(cumProfit.Last() * profit.Last()) - Volume = Volume.Add(trade.Quantity) - if Volume.IsZero() { - sellPrice = fixedpoint.Zero - } - if !buyPrice.IsZero() { - panic("buyprice shouldn't be zero") - } + if s.p.IsDust(trade.Price) { + s.buyPrice = 0 + s.sellPrice = 0 + s.highestPrice = 0 + s.lowestPrice = 0 + } else if s.p.IsLong() { + + s.buyPrice = trade.Price.Float64() + s.sellPrice = 0 + s.highestPrice = s.buyPrice + s.lowestPrice = 0 } else { - panic("no price available") + s.sellPrice = trade.Price.Float64() + s.buyPrice = 0 + s.highestPrice = 0 + s.lowestPrice = s.sellPrice + } + } else if tag == "long" { + if s.p.IsDust(trade.Price) { + s.buyPrice = 0 + s.sellPrice = 0 + s.highestPrice = 0 + s.lowestPrice = 0 + } else if s.p.IsLong() { + s.buyPrice = trade.Price.Float64() + s.sellPrice = 0 + s.highestPrice = s.buyPrice + s.lowestPrice = 0 } } else if tag == "short" { - if buyPrice.IsZero() { - if !sellPrice.IsZero() { - sellPrice = sellPrice.Mul(Volume).Sub(trade.Price.Mul(trade.Quantity)).Div(Volume.Sub(trade.Quantity)) - } else { - sellPrice = trade.Price - } - } else { - profit.Update(modify(trade.Price.Div(buyPrice)).Float64()) - cumProfit.Update(cumProfit.Last() * profit.Last()) - buyPrice = fixedpoint.Zero - Volume = fixedpoint.Zero - sellPrice = trade.Price + if s.p.IsDust(trade.Price) { + s.sellPrice = 0 + s.buyPrice = 0 + s.highestPrice = 0 + s.lowestPrice = 0 + } else if s.p.IsShort() { + s.sellPrice = trade.Price.Float64() + s.buyPrice = 0 + s.highestPrice = 0 + s.lowestPrice = s.sellPrice } - Volume = Volume.Sub(trade.Quantity) - } else if tag == "long" { - if sellPrice.IsZero() { - if !buyPrice.IsZero() { - buyPrice = buyPrice.Mul(Volume).Add(trade.Price.Mul(trade.Quantity)).Div(Volume.Add(trade.Quantity)) - } else { - buyPrice = trade.Price - } - } else { - profit.Update(modify(sellPrice.Div(trade.Price)).Float64()) - cumProfit.Update(cumProfit.Last() * profit.Last()) - sellPrice = fixedpoint.Zero - buyPrice = trade.Price - Volume = fixedpoint.Zero - } - Volume = Volume.Add(trade.Quantity) + } else { + panic("tag unknown") } - s.buyPrice = buyPrice.Float64() - s.highestPrice = s.buyPrice - s.sellPrice = sellPrice.Float64() - s.lowestPrice = s.sellPrice + bbgo.Notify("tag: %s, sp: %.4f bp: %.4f hp: %.4f lp: %.4f, trade: %s, pos: %s", tag, s.sellPrice, s.buyPrice, s.highestPrice, s.lowestPrice, trade.String(), s.p.String()) }) - if err := s.initIndicators(); err != nil { + s.frameKLine = &types.KLine{} + s.kline1m = &types.KLine{} + priceLine := types.NewQueue(300) + if err := s.initIndicators(priceLine); err != nil { log.WithError(err).Errorf("initIndicator failed") return nil } s.initTickerFunctions(ctx) - dynamicKLine := &types.KLine{} - priceLine := types.NewQueue(300) zeroPoints := types.NewQueue(300) stoploss := s.StopLoss.Float64() + // default value: use 1m kline + if !s.NoTrailingStopLoss && s.IsBackTesting() || s.TrailingStopLossType == "" { + s.TrailingStopLossType = "kline" + } - session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { - if s.Status != types.StrategyStatusRunning { + bbgo.RegisterCommand("/draw", "Draw Indicators", func(reply interact.Reply) { + canvas := s.DrawIndicators(s.frameKLine.StartTime, priceLine, zeroPoints) + var buffer bytes.Buffer + if err := canvas.Render(chart.PNG, &buffer); err != nil { + log.WithError(err).Errorf("cannot render indicators in drift") + reply.Message(fmt.Sprintf("[error] cannot render indicators in drift: %v", err)) return } + bbgo.SendPhoto(&buffer) + }) + + bbgo.RegisterCommand("/pnl", "Draw PNL(%) per trade", func(reply interact.Reply) { + canvas := s.DrawPNL(&profit) + var buffer bytes.Buffer + if err := canvas.Render(chart.PNG, &buffer); err != nil { + log.WithError(err).Errorf("cannot render pnl in drift") + reply.Message(fmt.Sprintf("[error] cannot render pnl in drift: %v", err)) + return + } + bbgo.SendPhoto(&buffer) + }) + + bbgo.RegisterCommand("/cumpnl", "Draw Cummulative PNL(Quote)", func(reply interact.Reply) { + canvas := s.DrawCumPNL(&cumProfit) + var buffer bytes.Buffer + if err := canvas.Render(chart.PNG, &buffer); err != nil { + log.WithError(err).Errorf("cannot render cumpnl in drift") + reply.Message(fmt.Sprintf("[error] canot render cumpnl in drift: %v", err)) + return + } + bbgo.SendPhoto(&buffer) + }) + + bbgo.RegisterCommand("/config", "Show latest config", func(reply interact.Reply) { + var buffer bytes.Buffer + s.Print(&buffer, false) + reply.Message(buffer.String()) + }) + + bbgo.RegisterCommand("/pos", "Show internal position", func(reply interact.Reply) { + reply.Message(s.p.String()) + }) + + bbgo.RegisterCommand("/dump", "Dump internal params", func(reply interact.Reply) { + reply.Message("Please enter series output length:") + }).Next(func(length string, reply interact.Reply) { + var buffer bytes.Buffer + l, err := strconv.Atoi(length) + if err != nil { + s.ParamDump(&buffer) + } else { + s.ParamDump(&buffer, l) + } + reply.Message(buffer.String()) + }) + + session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { if kline.Symbol != s.Symbol { return } @@ -606,55 +809,64 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return } if kline.Interval == types.Interval1m { - if s.NoTrailingStopLoss || !s.IsBackTesting() { + s.kline1m.Set(&kline) + s.drift1m.Update(s.getSource(&kline).Float64()) + s.minutesCounter += 1 + if s.Status != types.StrategyStatusRunning { + return + } + if s.NoTrailingStopLoss || s.TrailingStopLossType == "realtime" { return } // for doing the trailing stoploss during backtesting atr = s.atr.Last() price := s.getLastPrice() pricef := price.Float64() + + var err error + numPending := 0 + if numPending, err = s.smartCancel(ctx, pricef, atr); err != nil { + log.WithError(err).Errorf("cannot cancel orders") + return + } + if numPending > 0 { + log.Infof("pending orders: %d, exit", numPending) + return + } + lowf := math.Min(kline.Low.Float64(), pricef) highf := math.Max(kline.High.Float64(), pricef) - d := s.drift.TestUpdate(pricef) - drift := d.Last() - ddrift := d.drift.Last() - + s.positionLock.Lock() if s.lowestPrice > 0 && lowf < s.lowestPrice { s.lowestPrice = lowf } if s.highestPrice > 0 && highf > s.highestPrice { s.highestPrice = highf } - avg := s.buyPrice + s.sellPrice - - if s.GeneralOrderExecutor.ActiveMakerOrders().NumOfOrders() > 0 { - return - } - - takeProfitFactor := s.takeProfitFactor.Predict(2) - exitShortCondition := ( /*avg+atr/2 <= highf || avg*(1.+stoploss) <= pricef ||*/ (drift > 0 && ddrift > 0.6) || avg-atr*takeProfitFactor >= pricef || - ((highf-s.lowestPrice)/s.lowestPrice > 0.003 && (avg-s.lowestPrice)/s.lowestPrice > 0.015)) && - (s.Position.IsShort() && !s.Position.IsDust(price)) - exitLongCondition := ( /*avg-atr/2 >= lowf || avg*(1.-stoploss) >= pricef || */ (drift < 0 && ddrift < -0.6) || avg+atr*takeProfitFactor <= pricef || - ((s.highestPrice-lowf)/lowf > 0.003 && (s.highestPrice-avg)/avg > 0.015)) && - (s.Position.IsLong() && !s.Position.IsDust(price)) + exitShortCondition := s.sellPrice > 0 && (s.sellPrice*(1.+stoploss) <= highf || + s.trailingCheck(highf, "short") || s.drift1m.Last() > 0) + exitLongCondition := s.buyPrice > 0 && (s.buyPrice*(1.-stoploss) >= lowf || + s.trailingCheck(lowf, "long") || s.drift1m.Last() < 0) if exitShortCondition || exitLongCondition { - if exitLongCondition && s.highestPrice > avg { - s.takeProfitFactor.Update((s.highestPrice - avg) / atr * 4) - } else if exitShortCondition && avg > s.lowestPrice { - s.takeProfitFactor.Update((avg - s.lowestPrice) / atr * 4) - } + s.positionLock.Unlock() _ = s.ClosePosition(ctx, fixedpoint.One) + } else { + s.positionLock.Unlock() } return } - dynamicKLine.Set(&kline) + if kline.Interval != s.Interval { + return + } + s.frameKLine.Set(&kline) - source := s.getSource(dynamicKLine) + source := s.getSource(s.frameKLine) sourcef := source.Float64() priceLine.Update(sourcef) s.ma.Update(sourcef) + s.trendLine.Update(sourcef) s.drift.Update(sourcef) + zeroPoint := s.drift.ZeroPoint() zeroPoints.Update(zeroPoint) s.atr.PushK(kline) @@ -671,62 +883,111 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.stdevLow.Update(lowdiff) highdiff := highf - s.ma.Last() s.stdevHigh.Update(highdiff) + + if s.Status != types.StrategyStatusRunning { + return + } + + s.positionLock.Lock() + log.Errorf("highdiff: %3.2f ma: %.2f, close: %8v, high: %8v, low: %8v, time: %v", s.stdevHigh.Last(), s.ma.Last(), kline.Close, kline.High, kline.Low, kline.StartTime) if s.lowestPrice > 0 && lowf < s.lowestPrice { s.lowestPrice = lowf } if s.highestPrice > 0 && highf > s.highestPrice { s.highestPrice = highf } - avg := s.buyPrice + s.sellPrice - takeProfitFactor := s.takeProfitFactor.Predict(2) - if !s.IsBackTesting() { - balances := s.Session.GetAccount().Balances() - bbgo.Notify("zeroPoint: %.4f, source: %.4f, price: %.4f, driftPred: %.4f, drift: %.4f, drift[1]: %.4f, atr: %.4f, avg: %.4f", - zeroPoint, sourcef, pricef, driftPred, drift[0], drift[1], atr, avg) - // Notify will parse args to strings and process separately - bbgo.Notify("balances: [Base] %s [Quote] %s", balances[s.Market.BaseCurrency].String(), balances[s.Market.QuoteCurrency].String()) + if !s.NoRebalance { + s.Rebalance(ctx) } - //shortCondition := (sourcef <= zeroPoint && driftPred <= drift[0] && drift[0] <= 0 && drift[1] > 0 && drift[2] > drift[1]) - //longCondition := (sourcef >= zeroPoint && driftPred >= drift[0] && drift[0] >= 0 && drift[1] < 0 && drift[2] < drift[1]) - //bothUp := ddrift[1] < ddrift[0] && drift[1] < drift[0] - //bothDown := ddrift[1] > ddrift[0] && drift[1] > drift[0] - shortCondition := (drift[1] >= -0.9 || ddrift[1] >= 0) && (driftPred <= -0.6 || ddriftPred <= 0) - longCondition := (drift[1] <= 0.9 || ddrift[1] <= 0) && (driftPred >= 0.6 || ddriftPred >= 0) - exitShortCondition := ((drift[0] >= 0.6 && ddrift[0] >= 0) || - avg*(1.+stoploss) <= pricef || - avg-atr*takeProfitFactor >= pricef) && - s.Position.IsShort() && !longCondition && !shortCondition - exitLongCondition := ((drift[0] <= -0.6 && ddrift[0] <= 0) || - avg*(1.-stoploss) >= pricef || - avg+atr*takeProfitFactor <= pricef) && - s.Position.IsLong() && !shortCondition && !longCondition + balances := s.GeneralOrderExecutor.Session().GetAccount().Balances() + bbgo.Notify("source: %.4f, price: %.4f, driftPred: %.4f, ddriftPred: %.4f, drift[1]: %.4f, ddrift[1]: %.4f, atr: %.4f, lowf %.4f, highf: %.4f lowest: %.4f highest: %.4f sp %.4f bp %.4f", + sourcef, pricef, driftPred, ddriftPred, drift[1], ddrift[1], atr, lowf, highf, s.lowestPrice, s.highestPrice, s.sellPrice, s.buyPrice) + // Notify will parse args to strings and process separately + bbgo.Notify("balances: [Base] %s(%v %s) [Quote] %s [Total] %v %s", + balances[s.Market.BaseCurrency].String(), + balances[s.Market.BaseCurrency].Total().Mul(price), + s.Market.QuoteCurrency, + balances[s.Market.QuoteCurrency].String(), + s.CalcAssetValue(price), + s.Market.QuoteCurrency, + ) - if (exitShortCondition || exitLongCondition) && s.Position.IsOpened(price) { + shortCondition := (drift[1] >= DriftFilterNeg || ddrift[1] >= 0) && (driftPred <= DDriftFilterNeg || ddriftPred <= 0) || drift[1] < 0 && drift[0] < 0 + longCondition := (drift[1] <= DriftFilterPos || ddrift[1] <= 0) && (driftPred >= DDriftFilterPos || ddriftPred >= 0) || drift[1] > 0 && drift[0] > 0 + if shortCondition && longCondition { + if drift[1] > drift[0] { + longCondition = false + } else { + shortCondition = false + } + } + exitShortCondition := s.sellPrice > 0 && !shortCondition && !longCondition && (s.sellPrice*(1.+stoploss) <= highf || + s.trailingCheck(pricef, "short")) + exitLongCondition := s.buyPrice > 0 && !longCondition && !shortCondition && (s.buyPrice*(1.-stoploss) >= lowf || + s.trailingCheck(pricef, "long")) + + if exitShortCondition || exitLongCondition { if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil { log.WithError(err).Errorf("cannot cancel orders") + s.positionLock.Unlock() return } - if exitShortCondition && avg > s.lowestPrice { - s.takeProfitFactor.Update((avg - s.lowestPrice) / atr * 4) - } else if exitLongCondition && avg < s.highestPrice { - s.takeProfitFactor.Update((s.highestPrice - avg) / atr * 4) - } - if s.takeProfitFactor.Last() == 0 { - log.Errorf("exit %f %f %f %v", s.highestPrice, s.lowestPrice, avg, s.takeProfitFactor.Array(10)) - } + s.positionLock.Unlock() _ = s.ClosePosition(ctx, fixedpoint.One) return } + if longCondition { + if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil { + log.WithError(err).Errorf("cannot cancel orders") + s.positionLock.Unlock() + return + } + source = source.Sub(fixedpoint.NewFromFloat(s.stdevLow.Last() * s.HighLowVarianceMultiplier)) + if source.Compare(price) > 0 { + source = price + } + sourcef = source.Float64() + + quoteBalance, ok := s.Session.GetAccount().Balance(s.Market.QuoteCurrency) + if !ok { + log.Errorf("unable to get quoteCurrency") + s.positionLock.Unlock() + return + } + if s.Market.IsDustQuantity( + quoteBalance.Available.Div(source), source) { + s.positionLock.Unlock() + return + } + s.positionLock.Unlock() + quantity := quoteBalance.Available.Div(source) + createdOrders, err := s.GeneralOrderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeLimit, + Price: source, + Quantity: quantity, + Tag: "long", + }) + if err != nil { + log.WithError(err).Errorf("cannot place buy order") + return + } + s.orderPendingCounter[createdOrders[0].OrderID] = s.minutesCounter + return + } if shortCondition { if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil { log.WithError(err).Errorf("cannot cancel orders") + s.positionLock.Unlock() return } baseBalance, ok := s.Session.GetAccount().Balance(s.Market.BaseCurrency) if !ok { log.Errorf("unable to get baseBalance") + s.positionLock.Unlock() return } source = source.Add(fixedpoint.NewFromFloat(s.stdevHigh.Last() * s.HighLowVarianceMultiplier)) @@ -736,14 +997,10 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se sourcef = source.Float64() if s.Market.IsDustQuantity(baseBalance.Available, source) { + s.positionLock.Unlock() return } - if avg < s.highestPrice && avg > 0 && s.Position.IsLong() { - s.takeProfitFactor.Update((s.highestPrice - avg) / atr * 4) - if s.takeProfitFactor.Last() == 0 { - log.Errorf("short %f %f", s.highestPrice, avg) - } - } + s.positionLock.Unlock() // Cleanup pending StopOrders quantity := baseBalance.Available createdOrders, err := s.GeneralOrderExecutor.SubmitOrders(ctx, types.SubmitOrder{ @@ -758,60 +1015,28 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se log.WithError(err).Errorf("cannot place sell order") return } - orderTagHistory[createdOrders[0].OrderID] = "short" - } - if longCondition { - if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil { - log.WithError(err).Errorf("cannot cancel orders") - return - } - source = source.Sub(fixedpoint.NewFromFloat(s.stdevLow.Last() * s.HighLowVarianceMultiplier)) - if source.Compare(price) > 0 { - source = price - } - sourcef = source.Float64() - - quoteBalance, ok := s.Session.GetAccount().Balance(s.Market.QuoteCurrency) - if !ok { - log.Errorf("unable to get quoteCurrency") - return - } - if s.Market.IsDustQuantity( - quoteBalance.Available.Div(source), source) { - return - } - if avg > s.lowestPrice && s.Position.IsShort() { - s.takeProfitFactor.Update((avg - s.lowestPrice) / atr * 4) - if s.takeProfitFactor.Last() == 0 { - log.Errorf("long %f %f", s.lowestPrice, avg) - } - - } - quantity := quoteBalance.Available.Div(source) - createdOrders, err := s.GeneralOrderExecutor.SubmitOrders(ctx, types.SubmitOrder{ - Symbol: s.Symbol, - Side: types.SideTypeBuy, - Type: types.OrderTypeLimit, - Price: source, - Quantity: quantity, - Tag: "long", - }) - if err != nil { - log.WithError(err).Errorf("cannot place buy order") - return - } - orderTagHistory[createdOrders[0].OrderID] = "long" + s.orderPendingCounter[createdOrders[0].OrderID] = s.minutesCounter + return } + s.positionLock.Unlock() }) bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { - defer s.Print(os.Stdout) + var buffer bytes.Buffer - defer fmt.Fprintln(os.Stdout, s.TradeStats.BriefString()) + s.Print(&buffer, true, true) + + fmt.Fprintln(&buffer, "--- NonProfitable Dates ---") + for _, daypnl := range s.TradeStats.IntervalProfits[types.Interval1d].GetNonProfitableIntervals() { + fmt.Fprintf(&buffer, "%s\n", daypnl) + } + fmt.Fprintln(&buffer, s.TradeStats.BriefString()) + + os.Stdout.Write(buffer.Bytes()) if s.GenerateGraph { - s.Draw(dynamicKLine.StartTime, priceLine, &profit, &cumProfit, zeroPoints) + s.Draw(s.frameKLine.StartTime, priceLine, &profit, &cumProfit, zeroPoints) } wg.Done() diff --git a/pkg/types/indicator.go b/pkg/types/indicator.go index 5c67fbc21..ca1822135 100644 --- a/pkg/types/indicator.go +++ b/pkg/types/indicator.go @@ -1192,16 +1192,35 @@ func NewCanvas(title string, intervals ...Interval) *Canvas { return out } -func (canvas *Canvas) Plot(tag string, a Series, endTime Time, length int) { +func expand(a []float64, length int, defaultVal float64) []float64 { + l := len(a) + if l >= length { + return a + } + for i := 0; i < length-l; i++ { + a = append([]float64{defaultVal}, a...) + } + return a +} + +func (canvas *Canvas) Plot(tag string, a Series, endTime Time, length int, intervals ...Interval) { var timeline []time.Time e := endTime.Time() + if a.Length() == 0 { + return + } + oldest := a.Index(a.Length() - 1) + interval := canvas.Interval + if len(intervals) > 0 { + interval = intervals[0] + } for i := length - 1; i >= 0; i-- { - shiftedT := e.Add(-time.Duration(i*canvas.Interval.Minutes()) * time.Minute) + shiftedT := e.Add(-time.Duration(i*interval.Minutes()) * time.Minute) timeline = append(timeline, shiftedT) } canvas.Series = append(canvas.Series, chart.TimeSeries{ Name: tag, - YValues: Reverse(a, length), + YValues: expand(Reverse(a, length), length, oldest), XValues: timeline, }) } @@ -1211,10 +1230,14 @@ func (canvas *Canvas) PlotRaw(tag string, a Series, length int) { for i := 0; i < length; i++ { x = append(x, float64(i)) } + if a.Length() == 0 { + return + } + oldest := a.Index(a.Length() - 1) canvas.Series = append(canvas.Series, chart.ContinuousSeries{ Name: tag, XValues: x, - YValues: Reverse(a, length), + YValues: expand(Reverse(a, length), length, oldest), }) } diff --git a/pkg/types/trade_stats.go b/pkg/types/trade_stats.go index c4aab1c51..de6f203a4 100644 --- a/pkg/types/trade_stats.go +++ b/pkg/types/trade_stats.go @@ -1,6 +1,8 @@ package types import ( + "encoding/json" + "log" "math" "time" @@ -10,13 +12,14 @@ import ( ) type IntervalProfitCollector struct { - Interval Interval `json:"interval"` - Profits *Float64Slice `json:"profits"` - tmpTime time.Time `json:"tmpTime"` + Interval Interval `json:"interval"` + Profits *Float64Slice `json:"profits"` + Timestamp *Float64Slice `json:"timestamp"` + tmpTime time.Time `json:"tmpTime"` } func NewIntervalProfitCollector(i Interval, startTime time.Time) *IntervalProfitCollector { - return &IntervalProfitCollector{Interval: i, tmpTime: startTime, Profits: &Float64Slice{1.}} + return &IntervalProfitCollector{Interval: i, tmpTime: startTime, Profits: &Float64Slice{1.}, Timestamp: &Float64Slice{float64(startTime.Unix())}} } // Update the collector by every traded profit @@ -31,6 +34,7 @@ func (s *IntervalProfitCollector) Update(profit *Profit) { 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 @@ -40,6 +44,48 @@ func (s *IntervalProfitCollector) Update(profit *Profit) { } } +type ProfitReport struct { + StartTime time.Time `json:"startTime"` + Profit float64 `json:"profit"` + Interval Interval `json:"interval"` +} + +func (s ProfitReport) String() string { + b, err := json.MarshalIndent(s, "", "\t") + if err != nil { + log.Fatal(err) + } + return string(b) +} + +// Get all none-profitable intervals +func (s *IntervalProfitCollector) GetNonProfitableIntervals() (result []ProfitReport) { + if s.Profits == nil { + return result + } + l := s.Profits.Length() + for i := 0; i < l; i++ { + if s.Profits.Index(i) <= 1. { + result = append(result, ProfitReport{StartTime: time.Unix(int64(s.Timestamp.Index(i)), 0), Profit: s.Profits.Index(i), Interval: s.Interval}) + } + } + return result +} + +// Get all profitable intervals +func (s *IntervalProfitCollector) GetProfitableIntervals() (result []ProfitReport) { + if s.Profits == nil { + return result + } + l := s.Profits.Length() + for i := 0; i < l; i++ { + if s.Profits.Index(i) > 1. { + result = append(result, ProfitReport{StartTime: time.Unix(int64(s.Timestamp.Index(i)), 0), Profit: s.Profits.Index(i), Interval: s.Interval}) + } + } + return result +} + // Get number of profitable traded intervals func (s *IntervalProfitCollector) GetNumOfProfitableIntervals() (profit int) { if s.Profits == nil { @@ -226,6 +272,8 @@ func (s *TradeStats) BriefString() string { GrossLoss: s.GrossLoss, LargestProfitTrade: s.LargestProfitTrade, LargestLossTrade: s.LargestLossTrade, + AverageProfitTrade: s.AverageProfitTrade, + AverageLossTrade: s.AverageLossTrade, ProfitFactor: s.ProfitFactor, TotalNetProfit: s.TotalNetProfit, IntervalProfits: s.IntervalProfits, diff --git a/pkg/util/pointer.go b/pkg/util/pointer.go new file mode 100644 index 000000000..35d469c6e --- /dev/null +++ b/pkg/util/pointer.go @@ -0,0 +1,8 @@ +//go:build !go1.18 +// +build !go1.18 + +package util + +import "reflect" + +const Pointer = reflect.Ptr diff --git a/pkg/util/pointer_18.go b/pkg/util/pointer_18.go new file mode 100644 index 000000000..d2f172734 --- /dev/null +++ b/pkg/util/pointer_18.go @@ -0,0 +1,8 @@ +//go:build go1.18 +// +build go1.18 + +package util + +import "reflect" + +const Pointer = reflect.Pointer