From cb1c5634a2ceffbdeca963e571010287a0874cb9 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 29 Jun 2022 15:14:24 +0800 Subject: [PATCH 01/92] pivotshort: remove redundant notification --- pkg/bbgo/order_executor_general.go | 1 - pkg/strategy/pivotshort/strategy.go | 1 - 2 files changed, 2 deletions(-) diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index 540dce477..44ab8bb94 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -125,7 +125,6 @@ func (e *GeneralOrderExecutor) ClosePosition(ctx context.Context, percentage fix } submitOrder.Tag = strings.Join(tags, ",") - _, err := e.SubmitOrders(ctx, *submitOrder) return err } diff --git a/pkg/strategy/pivotshort/strategy.go b/pkg/strategy/pivotshort/strategy.go index 69cfa6cdb..a1c5dcdb4 100644 --- a/pkg/strategy/pivotshort/strategy.go +++ b/pkg/strategy/pivotshort/strategy.go @@ -174,7 +174,6 @@ func (s *Strategy) CurrentPosition() *types.Position { } func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Value) error { - bbgo.Notify("Closing position", s.Position) return s.orderExecutor.ClosePosition(ctx, percentage) } From 84083f56b78784f3314a82160fddecee04c3b446 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 29 Jun 2022 15:16:56 +0800 Subject: [PATCH 02/92] bbgo: add ExchangeSession param to the subscribe method --- pkg/bbgo/exit.go | 6 ++++-- pkg/bbgo/exit_test.go | 2 +- pkg/bbgo/reflect.go | 8 ++++++++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/pkg/bbgo/exit.go b/pkg/bbgo/exit.go index b931de880..98404ddbf 100644 --- a/pkg/bbgo/exit.go +++ b/pkg/bbgo/exit.go @@ -14,7 +14,7 @@ type ExitMethod struct { CumulatedVolumeTakeProfit *CumulatedVolumeTakeProfit `json:"cumulatedVolumeTakeProfit"` } -func (m *ExitMethod) Subscribe() { +func (m *ExitMethod) Subscribe(session *ExchangeSession) { // TODO: pull out this implementation as a simple function to reflect.go rv := reflect.ValueOf(m) rt := reflect.TypeOf(m) @@ -22,11 +22,13 @@ func (m *ExitMethod) Subscribe() { rv = rv.Elem() rt = rt.Elem() infType := reflect.TypeOf((*types.Subscriber)(nil)).Elem() + + argValues := toReflectValues(session) for i := 0; i < rt.NumField(); i++ { fieldType := rt.Field(i) if fieldType.Type.Implements(infType) { method := rv.Field(i).MethodByName("Subscribe") - method.Call(nil) + method.Call(argValues) } } } diff --git a/pkg/bbgo/exit_test.go b/pkg/bbgo/exit_test.go index 607eda489..12fda5bec 100644 --- a/pkg/bbgo/exit_test.go +++ b/pkg/bbgo/exit_test.go @@ -4,5 +4,5 @@ import "testing" func TestExitMethod(t *testing.T) { em := &ExitMethod{} - em.Subscribe() + em.Subscribe(&ExchangeSession{}) } diff --git a/pkg/bbgo/reflect.go b/pkg/bbgo/reflect.go index c9cb5a33b..f83da274d 100644 --- a/pkg/bbgo/reflect.go +++ b/pkg/bbgo/reflect.go @@ -110,3 +110,11 @@ func newTypeValueInterface(typ reflect.Type) interface{} { dst := reflect.New(typ) return dst.Interface() } + +func toReflectValues(args ...interface{}) (values []reflect.Value) { + for _, arg := range args { + values = append(values, reflect.ValueOf(arg)) + } + + return values +} From 83d6f4764cf9f1a00ce9efbbd588ac91e3d6b6fa Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 29 Jun 2022 15:37:18 +0800 Subject: [PATCH 03/92] types: fix profit factor calculation Signed-off-by: c9s --- pkg/types/trade_stats.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/types/trade_stats.go b/pkg/types/trade_stats.go index ddc31a8b2..ed459e00a 100644 --- a/pkg/types/trade_stats.go +++ b/pkg/types/trade_stats.go @@ -45,7 +45,7 @@ func (s *TradeStats) Add(pnl fixedpoint.Value) { s.WinningRatio = fixedpoint.NewFromFloat(float64(s.NumOfProfitTrade) / float64(s.NumOfLossTrade)) } - s.ProfitFactor = s.GrossProfit.Div(s.GrossLoss) + s.ProfitFactor = s.GrossProfit.Div(s.GrossLoss.Abs()) } func (s *TradeStats) String() string { From 4bb2e4a25fe87449fedbb9ce2c894aee28075df2 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 29 Jun 2022 16:59:50 +0800 Subject: [PATCH 04/92] fix stopEMA range check --- pkg/strategy/pivotshort/strategy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/pivotshort/strategy.go b/pkg/strategy/pivotshort/strategy.go index a1c5dcdb4..3fd239cb6 100644 --- a/pkg/strategy/pivotshort/strategy.go +++ b/pkg/strategy/pivotshort/strategy.go @@ -337,7 +337,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se log.Infof("%s breakLow signal detected, closed price %f < breakPrice %f", kline.Symbol, closePrice.Float64(), breakPrice.Float64()) // stop EMA protection - if s.stopEWMA != nil && !s.BreakLow.StopEMARange.IsZero() { + if s.stopEWMA != nil { ema := fixedpoint.NewFromFloat(s.stopEWMA.Last()) if ema.IsZero() { return From b26d3005a3491c7188b61ff0759a9f8a7afd4d5b Mon Sep 17 00:00:00 2001 From: zenix Date: Thu, 16 Jun 2022 19:54:56 +0900 Subject: [PATCH 05/92] feature: add pct_change implementation in indicator --- pkg/types/indicator.go | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/pkg/types/indicator.go b/pkg/types/indicator.go index fb7bf8b32..f6733cdeb 100644 --- a/pkg/types/indicator.go +++ b/pkg/types/indicator.go @@ -595,6 +595,44 @@ func Change(a Series, offset ...int) SeriesExtend { return NewSeries(&ChangeResult{a, o}) } +type PercentageChangeResult struct { + a Series + offset int +} + +func (c *PercentageChangeResult) Last() float64 { + if c.offset >= c.a.Length() { + return 0 + } + return c.a.Last()/c.a.Index(c.offset) - 1 +} + +func (c *PercentageChangeResult) Index(i int) float64 { + if i+c.offset >= c.a.Length() { + return 0 + } + return c.a.Index(i)/c.a.Index(i+c.offset) - 1 +} + +func (c *PercentageChangeResult) Length() int { + length := c.a.Length() + if length >= c.offset { + return length - c.offset + } + return 0 +} + +// Percentage change between current and a prior element, a / a[offset] - 1. +// offset: if not give, offset is 1. +func PercentageChange(a Series, offset ...int) Series { + o := 1 + if len(offset) > 0 { + o = offset[0] + } + + return &PercentageChangeResult{a, o} +} + func Stdev(a Series, length int) float64 { avg := Mean(a, length) s := .0 From 36127a63325269fae7a98e71a13d2b61bea3bee0 Mon Sep 17 00:00:00 2001 From: zenix Date: Thu, 16 Jun 2022 20:04:04 +0900 Subject: [PATCH 06/92] feature: implement omega, sharp, sortino related functions --- pkg/statistics/omega.go | 1 + pkg/statistics/sharp.go | 14 ++++++++++++++ pkg/statistics/sortino.go | 1 + 3 files changed, 16 insertions(+) create mode 100644 pkg/statistics/omega.go create mode 100644 pkg/statistics/sharp.go create mode 100644 pkg/statistics/sortino.go diff --git a/pkg/statistics/omega.go b/pkg/statistics/omega.go new file mode 100644 index 000000000..12c0dbc25 --- /dev/null +++ b/pkg/statistics/omega.go @@ -0,0 +1 @@ +package statistics diff --git a/pkg/statistics/sharp.go b/pkg/statistics/sharp.go new file mode 100644 index 000000000..ca8c81750 --- /dev/null +++ b/pkg/statistics/sharp.go @@ -0,0 +1,14 @@ +package statistics + +import ( + "github.com/c9s/bbgo/pkg/types" +) + +// Sharpe: Calcluates the sharpe ratio of access returns +// +// @param rf (float): Risk-free rate expressed as a yearly (annualized) return +// @param periods (int): Freq. of returns (252/365 for daily, 12 for monthy) +// @param annualize (bool): return annualize sharpe? +// @param smart (bool): return smart sharpe ratio +func Sharpe(returns types.Series, rf float64, periods int, annualize bool, smart bool) { +} diff --git a/pkg/statistics/sortino.go b/pkg/statistics/sortino.go new file mode 100644 index 000000000..12c0dbc25 --- /dev/null +++ b/pkg/statistics/sortino.go @@ -0,0 +1 @@ +package statistics From 1e31c4fb045a12b5fcb085e723e8b245db830f2d Mon Sep 17 00:00:00 2001 From: zenix Date: Mon, 20 Jun 2022 22:59:47 +0900 Subject: [PATCH 07/92] feature: add correlation for series --- pkg/types/indicator.go | 90 +++++++++++++++++++++++++++++++++++++ pkg/types/indicator_test.go | 23 ++++++++++ 2 files changed, 113 insertions(+) diff --git a/pkg/types/indicator.go b/pkg/types/indicator.go index f6733cdeb..a525343a0 100644 --- a/pkg/types/indicator.go +++ b/pkg/types/indicator.go @@ -643,4 +643,94 @@ func Stdev(a Series, length int) float64 { return math.Sqrt(s / float64(length)) } +type CorrFunc func(Series, Series, int) float64 + +func Kendall(a, b Series, length int) float64 { + if a.Length() < length { + length = a.Length() + } + if b.Length() < length { + length = b.Length() + } + aRanks := Rank(a, length) + bRanks := Rank(b, length) + concordant, discordant := 0, 0 + for i := 0; i < length; i++ { + for j := i + 1; j < length; j++ { + value := (aRanks.Index(i) - aRanks.Index(j)) * (bRanks.Index(i) - bRanks.Index(j)) + if value > 0 { + concordant++ + } else { + discordant++ + } + } + } + return float64(concordant-discordant) * 2.0 / float64(length*(length-1)) +} + +func Rank(a Series, length int) Series { + if length > a.Length() { + length = a.Length() + } + rank := make([]float64, length) + mapper := make([]float64, length+1) + for i := length - 1; i >= 0; i-- { + ii := a.Index(i) + counter := 0. + for j := 0; j < length; j++ { + if a.Index(j) <= ii { + counter += 1. + } + } + rank[i] = counter + mapper[int(counter)] += 1. + } + output := NewQueue(length) + for i := length - 1; i >= 0; i-- { + output.Update(rank[i] - (mapper[int(rank[i])]-1.)/2) + } + return output +} + +func Pearson(a, b Series, length int) float64 { + if a.Length() < length { + length = a.Length() + } + if b.Length() < length { + length = b.Length() + } + x := make([]float64, length) + y := make([]float64, length) + for i := 0; i < length; i++ { + x[i] = a.Index(i) + y[i] = b.Index(i) + } + return stat.Correlation(x, y, nil) +} + +func Spearman(a, b Series, length int) float64 { + if a.Length() < length { + length = a.Length() + } + if b.Length() < length { + length = b.Length() + } + aRank := Rank(a, length) + bRank := Rank(b, length) + return Pearson(aRank, bRank, length) +} + +// similar to pandas.Series.corr() function. +// +// method could either be `types.Pearson`, `types.Spearman` or `types.Kendall` +func Correlation(a Series, b Series, length int, method ...CorrFunc) float64 { + var runner CorrFunc + if len(method) == 0 { + runner = Pearson + } else { + runner = method[0] + } + return runner(a, b, length) +} + // TODO: ta.linreg diff --git a/pkg/types/indicator_test.go b/pkg/types/indicator_test.go index 8919c7866..89e63f7aa 100644 --- a/pkg/types/indicator_test.go +++ b/pkg/types/indicator_test.go @@ -33,3 +33,26 @@ func TestFloat64Slice(t *testing.T) { b = append(b, 3.0) assert.Equal(t, c.Last(), 1.) } + +/* +python + +import pandas as pd +s1 = pd.Series([.2, 0., .6, .2, .2]) +s2 = pd.Series([.3, .6, .0, .1]) +print(s1.corr(s2, method='pearson')) +print(s1.corr(s2, method='spearman') +print(s1.corr(s2, method='kendall')) +print(s1.rank()) +*/ +func TestCorr(t *testing.T) { + var a = Float64Slice{.2, .0, .6, .2} + var b = Float64Slice{.3, .6, .0, .1} + corr := Correlation(&a, &b, 4, Pearson) + assert.InDelta(t, corr, -0.8510644, 0.001) + out := Rank(&a, 4) + assert.Equal(t, out.Index(0), 2.5) + assert.Equal(t, out.Index(1), 4.0) + corr = Correlation(&a, &b, 4, Spearman) + assert.InDelta(t, corr, -0.94868, 0.001) +} From d8d77cec1eb385eb255167ee5c874b60eb26ebe5 Mon Sep 17 00:00:00 2001 From: zenix Date: Tue, 21 Jun 2022 15:38:46 +0900 Subject: [PATCH 08/92] feature: add skew, covariance and variance --- pkg/types/indicator.go | 47 +++++++++++++++++++++++++++++++++++++ pkg/types/indicator_test.go | 28 ++++++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/pkg/types/indicator.go b/pkg/types/indicator.go index a525343a0..3648a1d8b 100644 --- a/pkg/types/indicator.go +++ b/pkg/types/indicator.go @@ -733,4 +733,51 @@ func Correlation(a Series, b Series, length int, method ...CorrFunc) float64 { return runner(a, b, length) } +// similar to pandas.Series.cov() function with ddof=0 +// +// Compute covariance with Series +func Covariance(a Series, b Series, length int) float64 { + if a.Length() < length { + length = a.Length() + } + if b.Length() < length { + length = b.Length() + } + + meana := Mean(a, length) + meanb := Mean(b, length) + sum := 0.0 + for i := 0; i < length; i++ { + sum += (a.Index(i) - meana) * (b.Index(i) - meanb) + } + sum /= float64(length) + return sum +} + +func Variance(a Series, length int) float64 { + return Covariance(a, a, length) +} + +// similar to pandas.Series.skew() function. +// +// Return unbiased skew over input series +func Skew(a Series, length int) float64 { + if length > a.Length() { + length = a.Length() + } + mean := Mean(a, length) + sum2 := 0.0 + sum3 := 0.0 + for i := 0; i < length; i++ { + diff := a.Index(i) - mean + sum2 += diff * diff + sum3 += diff * diff * diff + } + if length <= 2 || sum2 == 0 { + return math.NaN() + } + l := float64(length) + return l * math.Sqrt(l-1) / (l - 2) * sum3 / math.Pow(sum2, 1.5) +} + // TODO: ta.linreg diff --git a/pkg/types/indicator_test.go b/pkg/types/indicator_test.go index 89e63f7aa..82517b449 100644 --- a/pkg/types/indicator_test.go +++ b/pkg/types/indicator_test.go @@ -56,3 +56,31 @@ func TestCorr(t *testing.T) { corr = Correlation(&a, &b, 4, Spearman) assert.InDelta(t, corr, -0.94868, 0.001) } + +/* +python + +import pandas as pd +s1 = pd.Series([.2, 0., .6, .2, .2]) +s2 = pd.Series([.3, .6, .0, .1]) +print(s1.cov(s2, ddof=0)) +*/ +func TestCov(t *testing.T) { + var a = Float64Slice{.2, .0, .6, .2} + var b = Float64Slice{.3, .6, .0, .1} + cov := Covariance(&a, &b, 4) + assert.InDelta(t, cov, -0.042499, 0.001) +} + +/* +python + +import pandas as pd +s1 = pd.Series([.2, 0., .6, .2, .2]) +print(s1.skew()) +*/ +func TestSkew(t *testing.T) { + var a = Float64Slice{.2, .0, .6, .2} + sk := Skew(&a, 4) + assert.InDelta(t, sk, 1.129338, 0.001) +} From 69533c0397b75d6bfec126c9fbc1a3a05a44e952 Mon Sep 17 00:00:00 2001 From: zenix Date: Tue, 21 Jun 2022 19:47:14 +0900 Subject: [PATCH 09/92] feature: add sharpe function implementation --- pkg/statistics/sharp.go | 24 ++++++++- pkg/statistics/sharp_test.go | 27 ++++++++++ pkg/types/indicator.go | 96 +++++++++++++++++++++++++++++++++++- 3 files changed, 143 insertions(+), 4 deletions(-) create mode 100644 pkg/statistics/sharp_test.go diff --git a/pkg/statistics/sharp.go b/pkg/statistics/sharp.go index ca8c81750..bf2114249 100644 --- a/pkg/statistics/sharp.go +++ b/pkg/statistics/sharp.go @@ -1,14 +1,34 @@ package statistics import ( + "math" + "github.com/c9s/bbgo/pkg/types" ) // Sharpe: Calcluates the sharpe ratio of access returns // -// @param rf (float): Risk-free rate expressed as a yearly (annualized) return // @param periods (int): Freq. of returns (252/365 for daily, 12 for monthy) // @param annualize (bool): return annualize sharpe? // @param smart (bool): return smart sharpe ratio -func Sharpe(returns types.Series, rf float64, periods int, annualize bool, smart bool) { +func Sharpe(returns types.Series, periods int, annualize bool, smart bool) float64 { + data := returns + num := data.Length() + if types.Lowest(data, num) >= 0 && types.Highest(data, num) > 1 { + data = types.PercentageChange(returns) + } + divisor := types.Stdev(data, data.Length(), 1) + if smart { + sum := 0. + coef := math.Abs(types.Correlation(data, types.Shift(data, 1), num-1)) + for i := 1; i < num; i++ { + sum += float64(num-i) / float64(num) * math.Pow(coef, float64(i)) + } + divisor = divisor * math.Sqrt(1.+2.*sum) + } + result := types.Mean(data) / divisor + if annualize { + return result * math.Sqrt(float64(periods)) + } + return result } diff --git a/pkg/statistics/sharp_test.go b/pkg/statistics/sharp_test.go new file mode 100644 index 000000000..7d373301c --- /dev/null +++ b/pkg/statistics/sharp_test.go @@ -0,0 +1,27 @@ +package statistics + +import ( + "github.com/c9s/bbgo/pkg/types" + "github.com/stretchr/testify/assert" + "testing" +) + +/* +python + +import quantstats as qx +import pandas as pd + +print(qx.stats.sharpe(pd.Series([0.01, 0.1, 0.001]), 0, 0, False, False)) +print(qx.stats.sharpe(pd.Series([0.01, 0.1, 0.001]), 0, 252, False, False)) +print(qx.stats.sharpe(pd.Series([0.01, 0.1, 0.001]), 0, 252, True, False)) +*/ +func TestSharpe(t *testing.T) { + var a types.Series = &types.Float64Slice{0.01, 0.1, 0.001} + output := Sharpe(a, 0, false, false) + assert.InDelta(t, output, 0.67586, 0.0001) + output = Sharpe(a, 252, false, false) + assert.InDelta(t, output, 0.67586, 0.0001) + output = Sharpe(a, 252, true, false) + assert.InDelta(t, output, 10.7289, 0.0001) +} diff --git a/pkg/types/indicator.go b/pkg/types/indicator.go index 3648a1d8b..3790c6075 100644 --- a/pkg/types/indicator.go +++ b/pkg/types/indicator.go @@ -633,14 +633,24 @@ func PercentageChange(a Series, offset ...int) Series { return &PercentageChangeResult{a, o} } -func Stdev(a Series, length int) float64 { +func Stdev(a Series, params ...int) float64 { + length := a.Length() + if len(params) > 0 { + if params[0] < length { + length = params[0] + } + } + ddof := 0 + if len(params) > 1 { + ddof = params[1] + } avg := Mean(a, length) s := .0 for i := 0; i < length; i++ { diff := a.Index(i) - avg s += diff * diff } - return math.Sqrt(s / float64(length)) + return math.Sqrt(s / float64(length - ddof)) } type CorrFunc func(Series, Series, int) float64 @@ -780,4 +790,86 @@ func Skew(a Series, length int) float64 { return l * math.Sqrt(l-1) / (l - 2) * sum3 / math.Pow(sum2, 1.5) } +type ShiftResult struct { + a Series + offset int +} + +func (inc *ShiftResult) Last() float64 { + if inc.offset < 0 { + return 0 + } + if inc.offset > inc.a.Length() { + return 0 + } + return inc.a.Index(inc.offset) +} +func (inc *ShiftResult) Index(i int) float64 { + if inc.offset + i < 0 { + return 0 + } + if inc.offset + i > inc.a.Length() { + return 0 + } + return inc.a.Index(inc.offset + i) +} + +func (inc *ShiftResult) Length() int { + return inc.a.Length() - inc.offset +} + +func Shift(a Series, offset int) Series { + return &ShiftResult{a, offset} +} + +type RollingResult struct { + a Series + window int +} + +type SliceView struct { + a Series + start int + length int +} + +func (s *SliceView) Last() float64 { + return s.a.Index(s.start) +} +func (s *SliceView) Index(i int) float64 { + if i >= s.length { + return 0 + } + return s.a.Index(i+s.start) +} + +func (s *SliceView) Length() int { + return s.length +} + +var _ Series = &SliceView{} + +func (r *RollingResult) Last() Series { + return &SliceView{r.a, 0, r.window} +} + +func (r *RollingResult) Index(i int) Series { + if i * r.window > r.a.Length() { + return nil + } + return &SliceView{r.a, i*r.window, r.window} +} + +func (r *RollingResult) Length() int { + mod := r.a.Length() % r.window + if mod > 0 { + return r.a.Length() / r.window + 1 + } else { + return r.a.Length() / r.window + } +} + +func Rolling(a Series, window int) *RollingResult { + return &RollingResult{a, window} +} // TODO: ta.linreg From 70f4676340467930bbca9a657685058f2554f84f Mon Sep 17 00:00:00 2001 From: zenix Date: Wed, 29 Jun 2022 21:49:02 +0900 Subject: [PATCH 10/92] feature: extend indicators, extend seriesbase methods --- pkg/indicator/ad.go | 6 ++- pkg/indicator/alma.go | 22 ++++++---- pkg/indicator/atr.go | 4 +- pkg/indicator/boll.go | 16 +++---- pkg/indicator/cci.go | 4 +- pkg/indicator/cma.go | 7 ++- pkg/indicator/dema.go | 4 +- pkg/indicator/dmi.go | 12 +++--- pkg/indicator/drift.go | 4 +- pkg/indicator/emv.go | 4 +- pkg/indicator/ewma.go | 4 +- pkg/indicator/hull.go | 4 +- pkg/indicator/line.go | 7 ++- pkg/indicator/macd.go | 9 ++-- pkg/indicator/obv.go | 11 +++++ pkg/indicator/rma.go | 4 +- pkg/indicator/rsi.go | 6 ++- pkg/indicator/sma.go | 6 ++- pkg/indicator/ssf.go | 5 ++- pkg/indicator/supertrend.go | 7 ++- pkg/indicator/tema.go | 4 +- pkg/indicator/till.go | 2 + pkg/indicator/tma.go | 4 +- pkg/indicator/vidya.go | 4 +- pkg/indicator/volatility.go | 17 ++++++++ pkg/indicator/vwap.go | 6 ++- pkg/indicator/vwma.go | 7 ++- pkg/indicator/wwma.go | 4 +- pkg/indicator/zlema.go | 4 +- pkg/strategy/ewoDgtrd/strategy.go | 36 ++++++++-------- pkg/types/indicator.go | 71 +++++++++++++++++-------------- pkg/types/seriesbase_imp.go | 51 +++++++++++++++++++--- 32 files changed, 253 insertions(+), 103 deletions(-) diff --git a/pkg/indicator/ad.go b/pkg/indicator/ad.go index d7263a5ab..6cdf0e3d9 100644 --- a/pkg/indicator/ad.go +++ b/pkg/indicator/ad.go @@ -14,6 +14,7 @@ Accumulation/Distribution Indicator (A/D) */ //go:generate callbackgen -type AD type AD struct { + types.SeriesBase types.IntervalWindow Values types.Float64Slice PrePrice float64 @@ -23,6 +24,9 @@ type AD struct { } func (inc *AD) Update(high, low, cloze, volume float64) { + if len(inc.Values) == 0 { + inc.SeriesBase.Series = inc + } var moneyFlowVolume float64 if high == low { moneyFlowVolume = 0 @@ -53,7 +57,7 @@ func (inc *AD) Length() int { return len(inc.Values) } -var _ types.Series = &AD{} +var _ types.SeriesExtend = &AD{} func (inc *AD) calculateAndUpdate(kLines []types.KLine) { for _, k := range kLines { diff --git a/pkg/indicator/alma.go b/pkg/indicator/alma.go index 44e0f18c7..d9a03ad0a 100644 --- a/pkg/indicator/alma.go +++ b/pkg/indicator/alma.go @@ -13,11 +13,12 @@ import ( // @param sigma: the standard deviation applied to the combo line. This makes the combo line sharper //go:generate callbackgen -type ALMA type ALMA struct { + types.SeriesBase types.IntervalWindow // required Offset float64 // required: recommend to be 5 Sigma int // required: recommend to be 0.5 - Weight []float64 - Sum float64 + weight []float64 + sum float64 input []float64 Values types.Float64Slice UpdateCallbacks []func(value float64) @@ -27,16 +28,17 @@ const MaxNumOfALMA = 5_000 const MaxNumOfALMATruncateSize = 100 func (inc *ALMA) Update(value float64) { - if inc.Weight == nil { - inc.Weight = make([]float64, inc.Window) + if inc.weight == nil { + inc.SeriesBase.Series = inc + inc.weight = make([]float64, inc.Window) m := inc.Offset * (float64(inc.Window) - 1.) s := float64(inc.Window) / float64(inc.Sigma) - inc.Sum = 0. + inc.sum = 0. for i := 0; i < inc.Window; i++ { diff := float64(i) - m wt := math.Exp(-diff * diff / 2. / s / s) - inc.Sum += wt - inc.Weight[i] = wt + inc.sum += wt + inc.weight[i] = wt } } inc.input = append(inc.input, value) @@ -44,9 +46,9 @@ func (inc *ALMA) Update(value float64) { weightedSum := 0.0 inc.input = inc.input[len(inc.input)-inc.Window:] for i := 0; i < inc.Window; i++ { - weightedSum += inc.Weight[inc.Window-i-1] * inc.input[i] + weightedSum += inc.weight[inc.Window-i-1] * inc.input[i] } - inc.Values.Push(weightedSum / inc.Sum) + inc.Values.Push(weightedSum / inc.sum) if len(inc.Values) > MaxNumOfALMA { inc.Values = inc.Values[MaxNumOfALMATruncateSize-1:] } @@ -71,6 +73,8 @@ func (inc *ALMA) Length() int { return len(inc.Values) } +var _ types.SeriesExtend = &ALMA{} + func (inc *ALMA) calculateAndUpdate(allKLines []types.KLine) { if inc.input == nil { for _, k := range allKLines { diff --git a/pkg/indicator/atr.go b/pkg/indicator/atr.go index 327a30263..759cffebe 100644 --- a/pkg/indicator/atr.go +++ b/pkg/indicator/atr.go @@ -9,6 +9,7 @@ import ( //go:generate callbackgen -type ATR type ATR struct { + types.SeriesBase types.IntervalWindow PercentageVolatility types.Float64Slice @@ -25,6 +26,7 @@ func (inc *ATR) Update(high, low, cloze float64) { } if inc.RMA == nil { + inc.SeriesBase.Series = inc inc.RMA = &RMA{ IntervalWindow: types.IntervalWindow{Window: inc.Window}, Adjust: true, @@ -73,7 +75,7 @@ func (inc *ATR) Length() int { return inc.RMA.Length() } -var _ types.Series = &ATR{} +var _ types.SeriesExtend = &ATR{} func (inc *ATR) CalculateAndUpdate(kLines []types.KLine) { for _, k := range kLines { diff --git a/pkg/indicator/boll.go b/pkg/indicator/boll.go index 70338be04..30ebf8466 100644 --- a/pkg/indicator/boll.go +++ b/pkg/indicator/boll.go @@ -41,20 +41,20 @@ type BOLL struct { type BandType int -func (inc *BOLL) GetUpBand() types.Series { - return &inc.UpBand +func (inc *BOLL) GetUpBand() types.SeriesExtend { + return types.NewSeries(&inc.UpBand) } -func (inc *BOLL) GetDownBand() types.Series { - return &inc.DownBand +func (inc *BOLL) GetDownBand() types.SeriesExtend { + return types.NewSeries(&inc.DownBand) } -func (inc *BOLL) GetSMA() types.Series { - return &inc.SMA +func (inc *BOLL) GetSMA() types.SeriesExtend { + return types.NewSeries(&inc.SMA) } -func (inc *BOLL) GetStdDev() types.Series { - return &inc.StdDev +func (inc *BOLL) GetStdDev() types.SeriesExtend { + return types.NewSeries(&inc.StdDev) } func (inc *BOLL) LastUpBand() float64 { diff --git a/pkg/indicator/cci.go b/pkg/indicator/cci.go index 9380ad816..ef0ec558d 100644 --- a/pkg/indicator/cci.go +++ b/pkg/indicator/cci.go @@ -12,6 +12,7 @@ import ( // with modification of ddof=0 to let standard deviation to be divided by N instead of N-1 //go:generate callbackgen -type CCI type CCI struct { + types.SeriesBase types.IntervalWindow Input types.Float64Slice TypicalPrice types.Float64Slice @@ -23,6 +24,7 @@ type CCI struct { func (inc *CCI) Update(value float64) { if len(inc.TypicalPrice) == 0 { + inc.SeriesBase.Series = inc inc.TypicalPrice.Push(value) inc.Input.Push(value) return @@ -75,7 +77,7 @@ func (inc *CCI) Length() int { return len(inc.Values) } -var _ types.Series = &CCI{} +var _ types.SeriesExtend = &CCI{} var three = fixedpoint.NewFromInt(3) diff --git a/pkg/indicator/cma.go b/pkg/indicator/cma.go index 8040c8707..fbdf35734 100644 --- a/pkg/indicator/cma.go +++ b/pkg/indicator/cma.go @@ -8,6 +8,7 @@ import ( // Refer: https://en.wikipedia.org/wiki/Moving_average //go:generate callbackgen -type CA type CA struct { + types.SeriesBase Interval types.Interval Values types.Float64Slice length float64 @@ -15,11 +16,15 @@ type CA struct { } func (inc *CA) Update(x float64) { + if len(inc.Values) == 0 { + inc.SeriesBase.Series = inc + } newVal := (inc.Values.Last()*inc.length + x) / (inc.length + 1.) inc.length += 1 inc.Values.Push(newVal) if len(inc.Values) > MaxNumOfEWMA { inc.Values = inc.Values[MaxNumOfEWMATruncateSize-1:] + inc.length = float64(len(inc.Values)) } } @@ -41,7 +46,7 @@ func (inc *CA) Length() int { return len(inc.Values) } -var _ types.Series = &CA{} +var _ types.SeriesExtend = &CA{} func (inc *CA) calculateAndUpdate(allKLines []types.KLine) { for _, k := range allKLines { diff --git a/pkg/indicator/dema.go b/pkg/indicator/dema.go index bc476134a..d9152d279 100644 --- a/pkg/indicator/dema.go +++ b/pkg/indicator/dema.go @@ -10,6 +10,7 @@ import ( //go:generate callbackgen -type DEMA type DEMA struct { types.IntervalWindow + types.SeriesBase Values types.Float64Slice a1 *EWMA a2 *EWMA @@ -19,6 +20,7 @@ type DEMA struct { func (inc *DEMA) Update(value float64) { if len(inc.Values) == 0 { + inc.SeriesBase.Series = inc inc.a1 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}} inc.a2 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}} } @@ -46,7 +48,7 @@ func (inc *DEMA) Length() int { return len(inc.Values) } -var _ types.Series = &DEMA{} +var _ types.SeriesExtend = &DEMA{} func (inc *DEMA) calculateAndUpdate(allKLines []types.KLine) { if inc.a1 == nil { diff --git a/pkg/indicator/dmi.go b/pkg/indicator/dmi.go index cb0fc7169..74ea75eaa 100644 --- a/pkg/indicator/dmi.go +++ b/pkg/indicator/dmi.go @@ -17,11 +17,11 @@ type DMI struct { types.IntervalWindow ADXSmoothing int atr *ATR - DMP types.UpdatableSeries - DMN types.UpdatableSeries + DMP types.UpdatableSeriesExtend + DMN types.UpdatableSeriesExtend DIPlus *types.Queue DIMinus *types.Queue - ADX types.UpdatableSeries + ADX types.UpdatableSeriesExtend PrevHigh, PrevLow float64 UpdateCallbacks []func(diplus, diminus, adx float64) } @@ -71,15 +71,15 @@ func (inc *DMI) Update(high, low, cloze float64) { } -func (inc *DMI) GetDIPlus() types.Series { +func (inc *DMI) GetDIPlus() types.SeriesExtend { return inc.DIPlus } -func (inc *DMI) GetDIMinus() types.Series { +func (inc *DMI) GetDIMinus() types.SeriesExtend { return inc.DIMinus } -func (inc *DMI) GetADX() types.Series { +func (inc *DMI) GetADX() types.SeriesExtend { return inc.ADX } diff --git a/pkg/indicator/drift.go b/pkg/indicator/drift.go index bda5b51d5..8494acc39 100644 --- a/pkg/indicator/drift.go +++ b/pkg/indicator/drift.go @@ -11,6 +11,7 @@ import ( // could be used in Monte Carlo Simulations //go:generate callbackgen -type Drift type Drift struct { + types.SeriesBase types.IntervalWindow chng *types.Queue Values types.Float64Slice @@ -22,6 +23,7 @@ type Drift struct { func (inc *Drift) Update(value float64) { if inc.chng == nil { + inc.SeriesBase.Series = inc inc.SMA = &SMA{IntervalWindow: types.IntervalWindow{Interval: inc.Interval, Window: inc.Window}} inc.chng = types.NewQueue(inc.Window) inc.LastValue = value @@ -64,7 +66,7 @@ func (inc *Drift) Length() int { return inc.Values.Length() } -var _ types.Series = &Drift{} +var _ types.SeriesExtend = &Drift{} func (inc *Drift) calculateAndUpdate(allKLines []types.KLine) { if inc.chng == nil { diff --git a/pkg/indicator/emv.go b/pkg/indicator/emv.go index 08d439e45..f626ad70e 100644 --- a/pkg/indicator/emv.go +++ b/pkg/indicator/emv.go @@ -9,6 +9,7 @@ import ( //go:generate callbackgen -type EMV type EMV struct { + types.SeriesBase types.IntervalWindow prevH float64 prevL float64 @@ -25,6 +26,7 @@ func (inc *EMV) Update(high, low, vol float64) { inc.EMVScale = DefaultEMVScale } if inc.prevH == 0 || inc.Values == nil { + inc.SeriesBase.Series = inc inc.prevH = high inc.prevL = low inc.Values = &SMA{IntervalWindow: inc.IntervalWindow} @@ -59,7 +61,7 @@ func (inc *EMV) Length() int { return inc.Values.Length() } -var _ types.Series = &EMV{} +var _ types.SeriesExtend = &EMV{} func (inc *EMV) calculateAndUpdate(allKLines []types.KLine) { if inc.Values == nil { diff --git a/pkg/indicator/ewma.go b/pkg/indicator/ewma.go index d94fb7953..4335c1316 100644 --- a/pkg/indicator/ewma.go +++ b/pkg/indicator/ewma.go @@ -16,6 +16,7 @@ const MaxNumOfEWMATruncateSize = 100 //go:generate callbackgen -type EWMA type EWMA struct { types.IntervalWindow + types.SeriesBase Values types.Float64Slice LastOpenTime time.Time @@ -26,6 +27,7 @@ func (inc *EWMA) Update(value float64) { var multiplier = 2.0 / float64(1+inc.Window) if len(inc.Values) == 0 { + inc.SeriesBase.Series = inc inc.Values.Push(value) return } else if len(inc.Values) > MaxNumOfEWMA { @@ -136,4 +138,4 @@ func (inc *EWMA) Bind(updater KLineWindowUpdater) { updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) } -var _ types.Series = &EWMA{} +var _ types.SeriesExtend = &EWMA{} diff --git a/pkg/indicator/hull.go b/pkg/indicator/hull.go index 0c8347f9b..7eaf8ad70 100644 --- a/pkg/indicator/hull.go +++ b/pkg/indicator/hull.go @@ -10,6 +10,7 @@ import ( // Refer URL: https://fidelity.com/learning-center/trading-investing/technical-analysis/technical-indicator-guide/hull-moving-average //go:generate callbackgen -type HULL type HULL struct { + types.SeriesBase types.IntervalWindow ma1 *EWMA ma2 *EWMA @@ -20,6 +21,7 @@ type HULL struct { func (inc *HULL) Update(value float64) { if inc.result == nil { + inc.SeriesBase.Series = inc inc.ma1 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window / 2}} inc.ma2 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}} inc.result = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, int(math.Sqrt(float64(inc.Window)))}} @@ -50,7 +52,7 @@ func (inc *HULL) Length() int { return inc.result.Length() } -var _ types.Series = &HULL{} +var _ types.SeriesExtend = &HULL{} // TODO: should we just ignore the possible overlapping? func (inc *HULL) calculateAndUpdate(allKLines []types.KLine) { diff --git a/pkg/indicator/line.go b/pkg/indicator/line.go index 763d58f89..a3932ac1c 100644 --- a/pkg/indicator/line.go +++ b/pkg/indicator/line.go @@ -12,6 +12,7 @@ import ( // 3. resistance // of the market data, defined with series interface type Line struct { + types.SeriesBase types.IntervalWindow start float64 end float64 @@ -63,7 +64,7 @@ func (l *Line) SetXY2(index int, value float64) { } func NewLine(startIndex int, startValue float64, endIndex int, endValue float64, interval types.Interval) *Line { - return &Line{ + line := &Line{ start: startValue, end: endValue, startIndex: startIndex, @@ -71,6 +72,8 @@ func NewLine(startIndex int, startValue float64, endIndex int, endValue float64, currentTime: time.Time{}, Interval: interval, } + line.SeriesBase.Series = line + return line } -var _ types.Series = &Line{} +var _ types.SeriesExtend = &Line{} diff --git a/pkg/indicator/macd.go b/pkg/indicator/macd.go index 3dfbd6d45..43e94589b 100644 --- a/pkg/indicator/macd.go +++ b/pkg/indicator/macd.go @@ -87,6 +87,7 @@ func (inc *MACD) Bind(updater KLineWindowUpdater) { } type MACDValues struct { + types.SeriesBase *MACD } @@ -109,10 +110,12 @@ func (inc *MACDValues) Length() int { return len(inc.Values) } -func (inc *MACD) MACD() types.Series { - return &MACDValues{inc} +func (inc *MACD) MACD() types.SeriesExtend { + out := &MACDValues{MACD: inc} + out.SeriesBase.Series = out + return out } -func (inc *MACD) Singals() types.Series { +func (inc *MACD) Singals() types.SeriesExtend { return &inc.SignalLine } diff --git a/pkg/indicator/obv.go b/pkg/indicator/obv.go index 3ea11772d..52321892f 100644 --- a/pkg/indicator/obv.go +++ b/pkg/indicator/obv.go @@ -14,6 +14,7 @@ On-Balance Volume (OBV) Definition */ //go:generate callbackgen -type OBV type OBV struct { + types.SeriesBase types.IntervalWindow Values types.Float64Slice PrePrice float64 @@ -24,6 +25,7 @@ type OBV struct { func (inc *OBV) Update(price, volume float64) { if len(inc.Values) == 0 { + inc.SeriesBase.Series = inc inc.PrePrice = price inc.Values.Push(volume) return @@ -43,6 +45,15 @@ func (inc *OBV) Last() float64 { return inc.Values[len(inc.Values)-1] } +func (inc *OBV) Index(i int) float64 { + if len(inc.Values)-i <= 0 { + return 0.0 + } + return inc.Values[len(inc.Values)-i-1] +} + +var _ types.SeriesExtend = &OBV{} + func (inc *OBV) calculateAndUpdate(kLines []types.KLine) { for _, k := range kLines { if inc.EndTime != zeroTime && !k.EndTime.After(inc.EndTime) { diff --git a/pkg/indicator/rma.go b/pkg/indicator/rma.go index 8fee7a128..857261ecc 100644 --- a/pkg/indicator/rma.go +++ b/pkg/indicator/rma.go @@ -11,6 +11,7 @@ import ( // Refer: https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.ewm.html#pandas-dataframe-ewm //go:generate callbackgen -type RMA type RMA struct { + types.SeriesBase types.IntervalWindow Values types.Float64Slice counter int @@ -24,6 +25,7 @@ type RMA struct { func (inc *RMA) Update(x float64) { lambda := 1 / float64(inc.Window) if inc.counter == 0 { + inc.SeriesBase.Series = inc inc.sum = 1 inc.tmp = x } else { @@ -60,7 +62,7 @@ func (inc *RMA) Length() int { return len(inc.Values) } -var _ types.Series = &RMA{} +var _ types.SeriesExtend = &RMA{} func (inc *RMA) calculateAndUpdate(kLines []types.KLine) { for _, k := range kLines { diff --git a/pkg/indicator/rsi.go b/pkg/indicator/rsi.go index b9eabd6f4..8a76322ab 100644 --- a/pkg/indicator/rsi.go +++ b/pkg/indicator/rsi.go @@ -14,6 +14,7 @@ https://www.investopedia.com/terms/r/rsi.asp */ //go:generate callbackgen -type RSI type RSI struct { + types.SeriesBase types.IntervalWindow Values types.Float64Slice Prices types.Float64Slice @@ -25,6 +26,9 @@ type RSI struct { } func (inc *RSI) Update(price float64) { + if len(inc.Prices) == 0 { + inc.SeriesBase.Series = inc + } inc.Prices.Push(price) if len(inc.Prices) < inc.Window+1 { @@ -74,7 +78,7 @@ func (inc *RSI) Length() int { return len(inc.Values) } -var _ types.Series = &RSI{} +var _ types.SeriesExtend = &RSI{} func (inc *RSI) calculateAndUpdate(kLines []types.KLine) { for _, k := range kLines { diff --git a/pkg/indicator/sma.go b/pkg/indicator/sma.go index d500c5d6f..c7e11ac09 100644 --- a/pkg/indicator/sma.go +++ b/pkg/indicator/sma.go @@ -16,6 +16,7 @@ var zeroTime time.Time //go:generate callbackgen -type SMA type SMA struct { + types.SeriesBase types.IntervalWindow Values types.Float64Slice Cache types.Float64Slice @@ -44,10 +45,13 @@ func (inc *SMA) Length() int { return len(inc.Values) } -var _ types.Series = &SMA{} +var _ types.SeriesExtend = &SMA{} func (inc *SMA) Update(value float64) { if len(inc.Cache) < inc.Window { + if len(inc.Cache) == 0 { + inc.SeriesBase.Series = inc + } inc.Cache = append(inc.Cache, value) if len(inc.Cache) == inc.Window { inc.Values = append(inc.Values, types.Mean(&inc.Cache)) diff --git a/pkg/indicator/ssf.go b/pkg/indicator/ssf.go index d8c1340d4..28871a041 100644 --- a/pkg/indicator/ssf.go +++ b/pkg/indicator/ssf.go @@ -20,6 +20,7 @@ import ( // //go:generate callbackgen -type SSF type SSF struct { + types.SeriesBase types.IntervalWindow Poles int c1 float64 @@ -34,6 +35,7 @@ type SSF struct { func (inc *SSF) Update(value float64) { if inc.Poles == 3 { if inc.Values == nil { + inc.SeriesBase.Series = inc x := math.Pi / float64(inc.Window) a0 := math.Exp(-x) b0 := 2. * a0 * math.Cos(math.Sqrt(3.)*x) @@ -53,6 +55,7 @@ func (inc *SSF) Update(value float64) { inc.Values.Push(result) } else { // poles == 2 if inc.Values == nil { + inc.SeriesBase.Series = inc x := math.Pi * math.Sqrt(2.) / float64(inc.Window) a0 := math.Exp(-x) inc.c3 = -a0 * a0 @@ -88,7 +91,7 @@ func (inc *SSF) Last() float64 { return inc.Values.Last() } -var _ types.Series = &SSF{} +var _ types.SeriesExtend = &SSF{} func (inc *SSF) calculateAndUpdate(allKLines []types.KLine) { if inc.Values != nil { diff --git a/pkg/indicator/supertrend.go b/pkg/indicator/supertrend.go index c195dc4e6..d87b41b59 100644 --- a/pkg/indicator/supertrend.go +++ b/pkg/indicator/supertrend.go @@ -12,6 +12,7 @@ var logst = logrus.WithField("indicator", "supertrend") //go:generate callbackgen -type Supertrend type Supertrend struct { + types.SeriesBase types.IntervalWindow ATRMultiplier float64 `json:"atrMultiplier"` @@ -54,6 +55,10 @@ func (inc *Supertrend) Update(highPrice, lowPrice, closePrice float64) { panic("window must be greater than 0") } + if inc.AverageTrueRange == nil { + inc.SeriesBase.Series = inc + } + // Start with DirectionUp if inc.trend != types.DirectionUp && inc.trend != types.DirectionDown { inc.trend = types.DirectionUp @@ -120,7 +125,7 @@ func (inc *Supertrend) GetSignal() types.Direction { return inc.tradeSignal } -var _ types.Series = &Supertrend{} +var _ types.SeriesExtend = &Supertrend{} func (inc *Supertrend) calculateAndUpdate(kLines []types.KLine) { for _, k := range kLines { diff --git a/pkg/indicator/tema.go b/pkg/indicator/tema.go index 91d53a63d..8d1fc3fd3 100644 --- a/pkg/indicator/tema.go +++ b/pkg/indicator/tema.go @@ -9,6 +9,7 @@ import ( //go:generate callbackgen -type TEMA type TEMA struct { + types.SeriesBase types.IntervalWindow Values types.Float64Slice A1 *EWMA @@ -20,6 +21,7 @@ type TEMA struct { func (inc *TEMA) Update(value float64) { if len(inc.Values) == 0 { + inc.SeriesBase.Series = inc inc.A1 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}} inc.A2 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}} inc.A3 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}} @@ -51,7 +53,7 @@ func (inc *TEMA) Length() int { return len(inc.Values) } -var _ types.Series = &TEMA{} +var _ types.SeriesExtend = &TEMA{} func (inc *TEMA) calculateAndUpdate(allKLines []types.KLine) { if inc.A1 == nil { diff --git a/pkg/indicator/till.go b/pkg/indicator/till.go index 73f97ead5..795194e5c 100644 --- a/pkg/indicator/till.go +++ b/pkg/indicator/till.go @@ -10,6 +10,7 @@ const defaultVolumeFactor = 0.7 // Refer URL: https://tradingpedia.com/forex-trading-indicator/t3-moving-average-indicator/ //go:generate callbackgen -type TILL type TILL struct { + types.SeriesBase types.IntervalWindow VolumeFactor float64 e1 *EWMA @@ -30,6 +31,7 @@ func (inc *TILL) Update(value float64) { if inc.VolumeFactor == 0 { inc.VolumeFactor = defaultVolumeFactor } + inc.SeriesBase.Series = inc inc.e1 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}} inc.e2 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}} inc.e3 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}} diff --git a/pkg/indicator/tma.go b/pkg/indicator/tma.go index 482f3936c..c100a987d 100644 --- a/pkg/indicator/tma.go +++ b/pkg/indicator/tma.go @@ -8,6 +8,7 @@ import ( // Refer URL: https://ja.wikipedia.org/wiki/移動平均 //go:generate callbackgen -type TMA type TMA struct { + types.SeriesBase types.IntervalWindow s1 *SMA s2 *SMA @@ -16,6 +17,7 @@ type TMA struct { func (inc *TMA) Update(value float64) { if inc.s1 == nil { + inc.SeriesBase.Series = inc w := (inc.Window + 1) / 2 inc.s1 = &SMA{IntervalWindow: types.IntervalWindow{inc.Interval, w}} inc.s2 = &SMA{IntervalWindow: types.IntervalWindow{inc.Interval, w}} @@ -46,7 +48,7 @@ func (inc *TMA) Length() int { return inc.s2.Length() } -var _ types.Series = &TMA{} +var _ types.SeriesExtend = &TMA{} func (inc *TMA) calculateAndUpdate(allKLines []types.KLine) { if inc.s1 == nil { diff --git a/pkg/indicator/vidya.go b/pkg/indicator/vidya.go index 658e89ac1..cda286e00 100644 --- a/pkg/indicator/vidya.go +++ b/pkg/indicator/vidya.go @@ -10,6 +10,7 @@ import ( // Refer URL: https://metatrader5.com/en/terminal/help/indicators/trend_indicators/vida //go:generate callbackgen -type VIDYA type VIDYA struct { + types.SeriesBase types.IntervalWindow Values types.Float64Slice input types.Float64Slice @@ -19,6 +20,7 @@ type VIDYA struct { func (inc *VIDYA) Update(value float64) { if inc.Values.Length() == 0 { + inc.SeriesBase.Series = inc inc.Values.Push(value) inc.input.Push(value) return @@ -66,7 +68,7 @@ func (inc *VIDYA) Length() int { return inc.Values.Length() } -var _ types.Series = &VIDYA{} +var _ types.SeriesExtend = &VIDYA{} func (inc *VIDYA) calculateAndUpdate(allKLines []types.KLine) { if inc.input.Length() == 0 { diff --git a/pkg/indicator/volatility.go b/pkg/indicator/volatility.go index aae62e283..9f4571408 100644 --- a/pkg/indicator/volatility.go +++ b/pkg/indicator/volatility.go @@ -17,6 +17,7 @@ const MaxNumOfVOLTruncateSize = 100 //go:generate callbackgen -type VOLATILITY type VOLATILITY struct { + types.SeriesBase types.IntervalWindow Values types.Float64Slice EndTime time.Time @@ -31,6 +32,19 @@ func (inc *VOLATILITY) Last() float64 { return inc.Values[len(inc.Values)-1] } +func (inc *VOLATILITY) Index(i int) float64 { + if len(inc.Values)-i <= 0 { + return 0.0 + } + return inc.Values[len(inc.Values)-i-1] +} + +func (inc *VOLATILITY) Length() int { + return len(inc.Values) +} + +var _ types.SeriesExtend = &VOLATILITY{} + func (inc *VOLATILITY) calculateAndUpdate(klines []types.KLine) { if len(klines) < inc.Window { return @@ -42,6 +56,9 @@ func (inc *VOLATILITY) calculateAndUpdate(klines []types.KLine) { if inc.EndTime != zeroTime && lastKLine.GetEndTime().Before(inc.EndTime) { return } + if len(inc.Values) == 0 { + inc.SeriesBase.Series = inc + } var recentT = klines[end-(inc.Window-1) : end+1] diff --git a/pkg/indicator/vwap.go b/pkg/indicator/vwap.go index 7fcac717a..89e3b28e9 100644 --- a/pkg/indicator/vwap.go +++ b/pkg/indicator/vwap.go @@ -17,6 +17,7 @@ Volume-Weighted Average Price (VWAP) Explained */ //go:generate callbackgen -type VWAP type VWAP struct { + types.SeriesBase types.IntervalWindow Values types.Float64Slice Prices types.Float64Slice @@ -29,6 +30,9 @@ type VWAP struct { } func (inc *VWAP) Update(price, volume float64) { + if len(inc.Prices) == 0 { + inc.SeriesBase.Series = inc + } inc.Prices.Push(price) inc.Volumes.Push(volume) @@ -65,7 +69,7 @@ func (inc *VWAP) Length() int { return len(inc.Values) } -var _ types.Series = &VWAP{} +var _ types.SeriesExtend = &VWAP{} func (inc *VWAP) calculateAndUpdate(kLines []types.KLine) { var priceF = KLineTypicalPriceMapper diff --git a/pkg/indicator/vwma.go b/pkg/indicator/vwma.go index 131e2f5df..4ee1068c9 100644 --- a/pkg/indicator/vwma.go +++ b/pkg/indicator/vwma.go @@ -20,6 +20,7 @@ Volume Weighted Moving Average */ //go:generate callbackgen -type VWMA type VWMA struct { + types.SeriesBase types.IntervalWindow Values types.Float64Slice EndTime time.Time @@ -46,7 +47,7 @@ func (inc *VWMA) Length() int { return len(inc.Values) } -var _ types.Series = &VWMA{} +var _ types.SeriesExtend = &VWMA{} func KLinePriceVolumeMapper(k types.KLine) float64 { return k.Close.Mul(k.Volume).Float64() @@ -81,6 +82,10 @@ func (inc *VWMA) calculateAndUpdate(kLines []types.KLine) { return } + if len(inc.Values) == 0 { + inc.SeriesBase.Series = inc + } + vwma := pv / v inc.Values.Push(vwma) diff --git a/pkg/indicator/wwma.go b/pkg/indicator/wwma.go index 13fd1b8d1..be0ec0a7e 100644 --- a/pkg/indicator/wwma.go +++ b/pkg/indicator/wwma.go @@ -14,6 +14,7 @@ const MaxNumOfWWMATruncateSize = 100 //go:generate callbackgen -type WWMA type WWMA struct { + types.SeriesBase types.IntervalWindow Values types.Float64Slice LastOpenTime time.Time @@ -23,6 +24,7 @@ type WWMA struct { func (inc *WWMA) Update(value float64) { if len(inc.Values) == 0 { + inc.SeriesBase.Series = inc inc.Values.Push(value) return } else if len(inc.Values) > MaxNumOfWWMA { @@ -85,4 +87,4 @@ func (inc *WWMA) Bind(updater KLineWindowUpdater) { updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) } -var _ types.Series = &WWMA{} +var _ types.SeriesExtend = &WWMA{} diff --git a/pkg/indicator/zlema.go b/pkg/indicator/zlema.go index 4ed97d84a..f127c0008 100644 --- a/pkg/indicator/zlema.go +++ b/pkg/indicator/zlema.go @@ -9,6 +9,7 @@ import ( //go:generate callbackgen -type ZLEMA type ZLEMA struct { + types.SeriesBase types.IntervalWindow data types.Float64Slice @@ -41,6 +42,7 @@ func (inc *ZLEMA) Length() int { func (inc *ZLEMA) Update(value float64) { if inc.lag == 0 || inc.zlema == nil { + inc.SeriesBase.Series = inc inc.zlema = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}} inc.lag = int((float64(inc.Window)-1.)/2. + 0.5) } @@ -55,7 +57,7 @@ func (inc *ZLEMA) Update(value float64) { inc.zlema.Update(emaData) } -var _ types.Series = &ZLEMA{} +var _ types.SeriesExtend = &ZLEMA{} func (inc *ZLEMA) calculateAndUpdate(allKLines []types.KLine) { if inc.zlema == nil { diff --git a/pkg/strategy/ewoDgtrd/strategy.go b/pkg/strategy/ewoDgtrd/strategy.go index e777bcb9d..9188dc54c 100644 --- a/pkg/strategy/ewoDgtrd/strategy.go +++ b/pkg/strategy/ewoDgtrd/strategy.go @@ -63,11 +63,11 @@ type Strategy struct { atr *indicator.ATR emv *indicator.EMV ccis *CCISTOCH - ma5 types.Series - ma34 types.Series - ewo types.Series - ewoSignal types.Series - ewoHistogram types.Series + ma5 types.SeriesExtend + ma34 types.SeriesExtend + ewo types.SeriesExtend + ewoSignal types.SeriesExtend + ewoHistogram types.SeriesExtend ewoChangeRate float64 heikinAshi *HeikinAshi peakPrice fixedpoint.Value @@ -279,8 +279,8 @@ func (s *Strategy) SetupIndicators(store *bbgo.MarketDataStore) { }) - s.ma5 = ema5 - s.ma34 = ema34 + s.ma5 = types.NewSeries(ema5) + s.ma34 = types.NewSeries(ema34) } else if s.UseSma { sma5 := &indicator.SMA{IntervalWindow: window5} sma34 := &indicator.SMA{IntervalWindow: window34} @@ -300,8 +300,8 @@ func (s *Strategy) SetupIndicators(store *bbgo.MarketDataStore) { sma34.Update(cloze) } }) - s.ma5 = sma5 - s.ma34 = sma34 + s.ma5 = types.NewSeries(sma5) + s.ma34 = types.NewSeries(sma34) } else { evwma5 := &VWEMA{ PV: &indicator.EWMA{IntervalWindow: window5}, @@ -331,12 +331,12 @@ func (s *Strategy) SetupIndicators(store *bbgo.MarketDataStore) { evwma34.UpdateVal(price, vol) } }) - s.ma5 = evwma5 - s.ma34 = evwma34 + s.ma5 = types.NewSeries(evwma5) + s.ma34 = types.NewSeries(evwma34) } - s.ewo = types.Mul(types.Minus(types.Div(s.ma5, s.ma34), 1.0), 100.) - s.ewoHistogram = types.Minus(s.ma5, s.ma34) + s.ewo = s.ma5.Div(s.ma34).Minus(1.0).Mul(100.) + s.ewoHistogram = s.ma5.Minus(s.ma34) windowSignal := types.IntervalWindow{Interval: s.Interval, Window: s.SignalWindow} if s.UseEma { sig := &indicator.EWMA{IntervalWindow: windowSignal} @@ -355,7 +355,7 @@ func (s *Strategy) SetupIndicators(store *bbgo.MarketDataStore) { sig.Update(s.ewo.Last()) } }) - s.ewoSignal = sig + s.ewoSignal = types.NewSeries(sig) } else if s.UseSma { sig := &indicator.SMA{IntervalWindow: windowSignal} store.OnKLineWindowUpdate(func(interval types.Interval, _ types.KLineWindow) { @@ -365,7 +365,7 @@ func (s *Strategy) SetupIndicators(store *bbgo.MarketDataStore) { if sig.Length() == 0 { // lazy init - ewoVals := types.Reverse(s.ewo) + ewoVals := s.ewo.Reverse() for _, ewoValue := range ewoVals { sig.Update(ewoValue) } @@ -373,7 +373,7 @@ func (s *Strategy) SetupIndicators(store *bbgo.MarketDataStore) { sig.Update(s.ewo.Last()) } }) - s.ewoSignal = sig + s.ewoSignal = types.NewSeries(sig) } else { sig := &VWEMA{ PV: &indicator.EWMA{IntervalWindow: windowSignal}, @@ -385,7 +385,7 @@ func (s *Strategy) SetupIndicators(store *bbgo.MarketDataStore) { } if sig.Length() == 0 { // lazy init - ewoVals := types.Reverse(s.ewo) + ewoVals := s.ewo.Reverse() for i, ewoValue := range ewoVals { vol := window.Volume().Index(i) sig.PV.Update(ewoValue * vol) @@ -397,7 +397,7 @@ func (s *Strategy) SetupIndicators(store *bbgo.MarketDataStore) { sig.V.Update(vol) } }) - s.ewoSignal = sig + s.ewoSignal = types.NewSeries(sig) } } diff --git a/pkg/types/indicator.go b/pkg/types/indicator.go index 3790c6075..a6dea330f 100644 --- a/pkg/types/indicator.go +++ b/pkg/types/indicator.go @@ -11,15 +11,18 @@ import ( // Super basic Series type that simply holds the float64 data // with size limit (the only difference compare to float64slice) type Queue struct { + SeriesBase arr []float64 size int } func NewQueue(size int) *Queue { - return &Queue{ + out := &Queue{ arr: make([]float64, 0, size), size: size, } + out.SeriesBase.Series = out + return out } func (inc *Queue) Last() float64 { @@ -47,7 +50,7 @@ func (inc *Queue) Update(v float64) { } } -var _ Series = &Queue{} +var _ SeriesExtend = &Queue{} // Float64Indicator is the indicators (SMA and EWMA) that we want to use are returning float64 data. type Float64Indicator interface { @@ -82,24 +85,24 @@ type SeriesExtend interface { Array(limit ...int) (result []float64) Reverse(limit ...int) (result Float64Slice) Change(offset ...int) SeriesExtend - Stdev(length int) float64 + PercentageChange(offset ...int) SeriesExtend + Stdev(params ...int) float64 + Rolling(window int) *RollingResult + Shift(offset int) SeriesExtend + Skew(length int) float64 + Variance(length int) float64 + Covariance(b Series, length int) float64 + Correlation(b Series, length int, method ...CorrFunc) float64 + Rank(length int) SeriesExtend } -type IndexFuncType func(int) float64 -type LastFuncType func() float64 -type LengthFuncType func() int - type SeriesBase struct { - index IndexFuncType - last LastFuncType - length LengthFuncType + Series } func NewSeries(a Series) SeriesExtend { return &SeriesBase{ - index: a.Index, - last: a.Last, - length: a.Length, + Series: a, } } @@ -108,6 +111,11 @@ type UpdatableSeries interface { Update(float64) } +type UpdatableSeriesExtend interface { + SeriesExtend + Update(float64) +} + // The interface maps to pinescript basic type `series` for bool type // Access the internal historical data from the latest to the oldest // Index(0) always maps to Last() @@ -624,13 +632,13 @@ func (c *PercentageChangeResult) Length() int { // Percentage change between current and a prior element, a / a[offset] - 1. // offset: if not give, offset is 1. -func PercentageChange(a Series, offset ...int) Series { +func PercentageChange(a Series, offset ...int) SeriesExtend { o := 1 if len(offset) > 0 { o = offset[0] } - return &PercentageChangeResult{a, o} + return NewSeries(&PercentageChangeResult{a, o}) } func Stdev(a Series, params ...int) float64 { @@ -650,7 +658,7 @@ func Stdev(a Series, params ...int) float64 { diff := a.Index(i) - avg s += diff * diff } - return math.Sqrt(s / float64(length - ddof)) + return math.Sqrt(s / float64(length-ddof)) } type CorrFunc func(Series, Series, int) float64 @@ -678,7 +686,7 @@ func Kendall(a, b Series, length int) float64 { return float64(concordant-discordant) * 2.0 / float64(length*(length-1)) } -func Rank(a Series, length int) Series { +func Rank(a Series, length int) SeriesExtend { if length > a.Length() { length = a.Length() } @@ -805,10 +813,10 @@ func (inc *ShiftResult) Last() float64 { return inc.a.Index(inc.offset) } func (inc *ShiftResult) Index(i int) float64 { - if inc.offset + i < 0 { + if inc.offset+i < 0 { return 0 } - if inc.offset + i > inc.a.Length() { + if inc.offset+i > inc.a.Length() { return 0 } return inc.a.Index(inc.offset + i) @@ -818,18 +826,18 @@ func (inc *ShiftResult) Length() int { return inc.a.Length() - inc.offset } -func Shift(a Series, offset int) Series { - return &ShiftResult{a, offset} +func Shift(a Series, offset int) SeriesExtend { + return NewSeries(&ShiftResult{a, offset}) } type RollingResult struct { - a Series + a Series window int } type SliceView struct { - a Series - start int + a Series + start int length int } @@ -840,7 +848,7 @@ func (s *SliceView) Index(i int) float64 { if i >= s.length { return 0 } - return s.a.Index(i+s.start) + return s.a.Index(i + s.start) } func (s *SliceView) Length() int { @@ -849,21 +857,21 @@ func (s *SliceView) Length() int { var _ Series = &SliceView{} -func (r *RollingResult) Last() Series { - return &SliceView{r.a, 0, r.window} +func (r *RollingResult) Last() SeriesExtend { + return NewSeries(&SliceView{r.a, 0, r.window}) } -func (r *RollingResult) Index(i int) Series { - if i * r.window > r.a.Length() { +func (r *RollingResult) Index(i int) SeriesExtend { + if i*r.window > r.a.Length() { return nil } - return &SliceView{r.a, i*r.window, r.window} + return NewSeries(&SliceView{r.a, i * r.window, r.window}) } func (r *RollingResult) Length() int { mod := r.a.Length() % r.window if mod > 0 { - return r.a.Length() / r.window + 1 + return r.a.Length()/r.window + 1 } else { return r.a.Length() / r.window } @@ -872,4 +880,5 @@ func (r *RollingResult) Length() int { func Rolling(a Series, window int) *RollingResult { return &RollingResult{a, window} } + // TODO: ta.linreg diff --git a/pkg/types/seriesbase_imp.go b/pkg/types/seriesbase_imp.go index 021da9aca..f15317a72 100644 --- a/pkg/types/seriesbase_imp.go +++ b/pkg/types/seriesbase_imp.go @@ -1,15 +1,24 @@ package types func (s *SeriesBase) Index(i int) float64 { - return s.index(i) + if s.Series == nil { + return 0 + } + return s.Series.Index(i) } func (s *SeriesBase) Last() float64 { - return s.last() + if s.Series == nil { + return 0 + } + return s.Series.Last() } func (s *SeriesBase) Length() int { - return s.length() + if s.Series == nil { + return 0 + } + return s.Series.Length() } func (s *SeriesBase) Sum(limit ...int) float64 { @@ -80,6 +89,38 @@ func (s *SeriesBase) Change(offset ...int) SeriesExtend { return Change(s, offset...) } -func (s *SeriesBase) Stdev(length int) float64 { - return Stdev(s, length) +func (s *SeriesBase) PercentageChange(offset ...int) SeriesExtend { + return PercentageChange(s, offset...) +} + +func (s *SeriesBase) Stdev(params ...int) float64 { + return Stdev(s, params...) +} + +func (s *SeriesBase) Rolling(window int) *RollingResult { + return Rolling(s, window) +} + +func (s *SeriesBase) Shift(offset int) SeriesExtend { + return Shift(s, offset) +} + +func (s *SeriesBase) Skew(length int) float64 { + return Skew(s, length) +} + +func (s *SeriesBase) Variance(length int) float64 { + return Variance(s, length) +} + +func (s *SeriesBase) Covariance(b Series, length int) float64 { + return Covariance(s, b, length) +} + +func (s *SeriesBase) Correlation(b Series, length int, method ...CorrFunc) float64 { + return Correlation(s, b, length, method...) +} + +func (s *SeriesBase) Rank(length int) SeriesExtend { + return Rank(s, length) } From 0141f81086c4102525efbd35099cca41345b0344 Mon Sep 17 00:00:00 2001 From: zenix Date: Wed, 29 Jun 2022 22:02:50 +0900 Subject: [PATCH 11/92] refactor: ewo use SeriesExtend --- pkg/strategy/ewoDgtrd/strategy.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/strategy/ewoDgtrd/strategy.go b/pkg/strategy/ewoDgtrd/strategy.go index 9188dc54c..bce4cf07a 100644 --- a/pkg/strategy/ewoDgtrd/strategy.go +++ b/pkg/strategy/ewoDgtrd/strategy.go @@ -279,8 +279,8 @@ func (s *Strategy) SetupIndicators(store *bbgo.MarketDataStore) { }) - s.ma5 = types.NewSeries(ema5) - s.ma34 = types.NewSeries(ema34) + s.ma5 = ema5 + s.ma34 = ema34 } else if s.UseSma { sma5 := &indicator.SMA{IntervalWindow: window5} sma34 := &indicator.SMA{IntervalWindow: window34} @@ -300,8 +300,8 @@ func (s *Strategy) SetupIndicators(store *bbgo.MarketDataStore) { sma34.Update(cloze) } }) - s.ma5 = types.NewSeries(sma5) - s.ma34 = types.NewSeries(sma34) + s.ma5 = sma5 + s.ma34 = sma34 } else { evwma5 := &VWEMA{ PV: &indicator.EWMA{IntervalWindow: window5}, @@ -355,7 +355,7 @@ func (s *Strategy) SetupIndicators(store *bbgo.MarketDataStore) { sig.Update(s.ewo.Last()) } }) - s.ewoSignal = types.NewSeries(sig) + s.ewoSignal = sig } else if s.UseSma { sig := &indicator.SMA{IntervalWindow: windowSignal} store.OnKLineWindowUpdate(func(interval types.Interval, _ types.KLineWindow) { @@ -373,7 +373,7 @@ func (s *Strategy) SetupIndicators(store *bbgo.MarketDataStore) { sig.Update(s.ewo.Last()) } }) - s.ewoSignal = types.NewSeries(sig) + s.ewoSignal = sig } else { sig := &VWEMA{ PV: &indicator.EWMA{IntervalWindow: windowSignal}, From ee45f154a1ddcfc605c005936ee9c4a796005e7e Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 29 Jun 2022 17:58:43 +0800 Subject: [PATCH 12/92] pivotshort: rename bounce short to resistance short --- pkg/strategy/pivotshort/strategy.go | 34 ++++++++++++++++------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/pkg/strategy/pivotshort/strategy.go b/pkg/strategy/pivotshort/strategy.go index 3fd239cb6..91f7baa3c 100644 --- a/pkg/strategy/pivotshort/strategy.go +++ b/pkg/strategy/pivotshort/strategy.go @@ -47,7 +47,7 @@ type BreakLow struct { StopEMA *types.IntervalWindow `json:"stopEMA"` } -type BounceShort struct { +type ResistanceShort struct { Enabled bool `json:"enabled"` types.IntervalWindow @@ -85,7 +85,7 @@ type Strategy struct { BreakLow BreakLow `json:"breakLow"` - BounceShort *BounceShort `json:"bounceShort"` + ResistanceShort *ResistanceShort `json:"resistanceShort"` Entry Entry `json:"entry"` ExitMethods []bbgo.ExitMethod `json:"exits"` @@ -114,13 +114,17 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: types.Interval1m}) - if s.BounceShort != nil && s.BounceShort.Enabled { - session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.BounceShort.Interval}) + if s.ResistanceShort != nil && s.ResistanceShort.Enabled { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.ResistanceShort.Interval}) } if !bbgo.IsBackTesting { session.Subscribe(types.MarketTradeChannel, s.Symbol, types.SubscribeOptions{}) } + + for _, m := range s.ExitMethods { + m.Subscribe(session) + } } func (s *Strategy) useQuantityOrBaseBalance(quantity fixedpoint.Value) fixedpoint.Value { @@ -247,8 +251,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.pivotLowPrices = append(s.pivotLowPrices, s.lastLow) }) - if s.BounceShort != nil && s.BounceShort.Enabled { - s.resistancePivot = &indicator.Pivot{IntervalWindow: s.BounceShort.IntervalWindow} + if s.ResistanceShort != nil && s.ResistanceShort.Enabled { + s.resistancePivot = &indicator.Pivot{IntervalWindow: s.ResistanceShort.IntervalWindow} s.resistancePivot.Bind(store) } @@ -260,7 +264,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se method.Bind(session, s.orderExecutor) } - if s.BounceShort != nil && s.BounceShort.Enabled { + if s.ResistanceShort != nil && s.ResistanceShort.Enabled { if s.resistancePivot != nil { s.preloadPivot(s.resistancePivot, store) } @@ -272,7 +276,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se if s.resistancePivot != nil { lows := s.resistancePivot.Lows - minDistance := s.BounceShort.MinDistance.Float64() + minDistance := s.ResistanceShort.MinDistance.Float64() closePrice := lastKLine.Close.Float64() s.resistancePrices = findPossibleResistancePrices(closePrice, minDistance, lows) log.Infof("last price: %f, possible resistance prices: %+v", closePrice, s.resistancePrices) @@ -370,17 +374,17 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return } - if s.BounceShort == nil || !s.BounceShort.Enabled { + if s.ResistanceShort == nil || !s.ResistanceShort.Enabled { return } - if kline.Symbol != s.Symbol || kline.Interval != s.BounceShort.Interval { + if kline.Symbol != s.Symbol || kline.Interval != s.ResistanceShort.Interval { return } if s.resistancePivot != nil { closePrice := kline.Close.Float64() - minDistance := s.BounceShort.MinDistance.Float64() + minDistance := s.ResistanceShort.MinDistance.Float64() lows := s.resistancePivot.Lows s.resistancePrices = findPossibleResistancePrices(closePrice, minDistance, lows) @@ -425,15 +429,15 @@ func (s *Strategy) findHigherPivotLow(price fixedpoint.Value) (fixedpoint.Value, func (s *Strategy) placeBounceSellOrders(ctx context.Context, resistancePrice fixedpoint.Value) { futuresMode := s.session.Futures || s.session.IsolatedFutures - totalQuantity := s.BounceShort.Quantity - numLayers := s.BounceShort.NumLayers + totalQuantity := s.ResistanceShort.Quantity + numLayers := s.ResistanceShort.NumLayers if numLayers == 0 { numLayers = 1 } numLayersF := fixedpoint.NewFromInt(int64(numLayers)) - layerSpread := s.BounceShort.LayerSpread + layerSpread := s.ResistanceShort.LayerSpread quantity := totalQuantity.Div(numLayersF) log.Infof("placing bounce short orders: resistance price = %f, layer quantity = %f, num of layers = %d", resistancePrice.Float64(), quantity.Float64(), numLayers) @@ -444,7 +448,7 @@ func (s *Strategy) placeBounceSellOrders(ctx context.Context, resistancePrice fi baseBalance := balances[s.Market.BaseCurrency] // price = (resistance_price * (1.0 + ratio)) * ((1.0 + layerSpread) * i) - price := resistancePrice.Mul(fixedpoint.One.Add(s.BounceShort.Ratio)) + price := resistancePrice.Mul(fixedpoint.One.Add(s.ResistanceShort.Ratio)) spread := layerSpread.Mul(fixedpoint.NewFromInt(int64(i))) price = price.Add(spread) log.Infof("price = %f", price.Float64()) From 38767cd2df2974d5587aee405692c349fd03551f Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 29 Jun 2022 17:59:11 +0800 Subject: [PATCH 13/92] move private methods to the bottom --- pkg/strategy/pivotshort/strategy.go | 86 +++++++++++++++-------------- 1 file changed, 44 insertions(+), 42 deletions(-) diff --git a/pkg/strategy/pivotshort/strategy.go b/pkg/strategy/pivotshort/strategy.go index 91f7baa3c..7141c0483 100644 --- a/pkg/strategy/pivotshort/strategy.go +++ b/pkg/strategy/pivotshort/strategy.go @@ -127,48 +127,6 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { } } -func (s *Strategy) useQuantityOrBaseBalance(quantity fixedpoint.Value) fixedpoint.Value { - balance, hasBalance := s.session.Account.Balance(s.Market.BaseCurrency) - - if hasBalance { - if quantity.IsZero() { - bbgo.Notify("sell quantity is not set, submitting sell with all base balance: %s", balance.Available.String()) - quantity = balance.Available - } else { - quantity = fixedpoint.Min(quantity, balance.Available) - } - } - - if quantity.IsZero() { - log.Errorf("quantity is zero, can not submit sell order, please check settings") - } - - return quantity -} - -func (s *Strategy) placeLimitSell(ctx context.Context, price, quantity fixedpoint.Value, tag string) { - _, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ - Symbol: s.Symbol, - Price: price, - Side: types.SideTypeSell, - Type: types.OrderTypeLimit, - Quantity: quantity, - MarginSideEffect: types.SideEffectTypeMarginBuy, - Tag: tag, - }) -} - -func (s *Strategy) placeMarketSell(ctx context.Context, quantity fixedpoint.Value, tag string) { - _, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ - Symbol: s.Symbol, - Side: types.SideTypeSell, - Type: types.OrderTypeMarket, - Quantity: quantity, - MarginSideEffect: types.SideEffectTypeMarginBuy, - Tag: tag, - }) -} - func (s *Strategy) InstanceID() string { return fmt.Sprintf("%s:%s", ID, s.Symbol) } @@ -518,3 +476,47 @@ func findPossibleResistancePrices(closePrice float64, minDistance float64, lows return resistancePrices } + + +func (s *Strategy) useQuantityOrBaseBalance(quantity fixedpoint.Value) fixedpoint.Value { + balance, hasBalance := s.session.Account.Balance(s.Market.BaseCurrency) + + if hasBalance { + if quantity.IsZero() { + bbgo.Notify("sell quantity is not set, submitting sell with all base balance: %s", balance.Available.String()) + quantity = balance.Available + } else { + quantity = fixedpoint.Min(quantity, balance.Available) + } + } + + if quantity.IsZero() { + log.Errorf("quantity is zero, can not submit sell order, please check settings") + } + + return quantity +} + +func (s *Strategy) placeLimitSell(ctx context.Context, price, quantity fixedpoint.Value, tag string) { + _, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: s.Symbol, + Price: price, + Side: types.SideTypeSell, + Type: types.OrderTypeLimit, + Quantity: quantity, + MarginSideEffect: types.SideEffectTypeMarginBuy, + Tag: tag, + }) +} + +func (s *Strategy) placeMarketSell(ctx context.Context, quantity fixedpoint.Value, tag string) { + _, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeSell, + Type: types.OrderTypeMarket, + Quantity: quantity, + MarginSideEffect: types.SideEffectTypeMarginBuy, + Tag: tag, + }) +} + From 9733eec28072ea7dfc5ac20e8829fbacc1f03fb3 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 29 Jun 2022 17:59:33 +0800 Subject: [PATCH 14/92] pivotshort: move pure funcs to the bottom --- pkg/strategy/pivotshort/strategy.go | 48 ++++++++++++++--------------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/pkg/strategy/pivotshort/strategy.go b/pkg/strategy/pivotshort/strategy.go index 7141c0483..ed0042f7c 100644 --- a/pkg/strategy/pivotshort/strategy.go +++ b/pkg/strategy/pivotshort/strategy.go @@ -453,31 +453,6 @@ func (s *Strategy) preloadPivot(pivot *indicator.Pivot, store *bbgo.MarketDataSt return &last } -func findPossibleResistancePrices(closePrice float64, minDistance float64, lows []float64) []float64 { - // sort float64 in increasing order - sort.Float64s(lows) - - var resistancePrices []float64 - for _, low := range lows { - if low < closePrice { - continue - } - - last := closePrice - if len(resistancePrices) > 0 { - last = resistancePrices[len(resistancePrices)-1] - } - - if (low / last) < (1.0 + minDistance) { - continue - } - resistancePrices = append(resistancePrices, low) - } - - return resistancePrices -} - - func (s *Strategy) useQuantityOrBaseBalance(quantity fixedpoint.Value) fixedpoint.Value { balance, hasBalance := s.session.Account.Balance(s.Market.BaseCurrency) @@ -520,3 +495,26 @@ func (s *Strategy) placeMarketSell(ctx context.Context, quantity fixedpoint.Valu }) } +func findPossibleResistancePrices(closePrice float64, minDistance float64, lows []float64) []float64 { + // sort float64 in increasing order + sort.Float64s(lows) + + var resistancePrices []float64 + for _, low := range lows { + if low < closePrice { + continue + } + + last := closePrice + if len(resistancePrices) > 0 { + last = resistancePrices[len(resistancePrices)-1] + } + + if (low / last) < (1.0 + minDistance) { + continue + } + resistancePrices = append(resistancePrices, low) + } + + return resistancePrices +} From ab3341d5ae5b093bdfbd437251121295ab18032b Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 29 Jun 2022 18:00:50 +0800 Subject: [PATCH 15/92] pivotshort: make preload pivot as a pure function --- pkg/strategy/pivotshort/strategy.go | 39 +++++++++++++++-------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/pkg/strategy/pivotshort/strategy.go b/pkg/strategy/pivotshort/strategy.go index ed0042f7c..2ee5a7551 100644 --- a/pkg/strategy/pivotshort/strategy.go +++ b/pkg/strategy/pivotshort/strategy.go @@ -188,7 +188,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.pivot = &indicator.Pivot{IntervalWindow: s.IntervalWindow} s.pivot.Bind(store) - lastKLine := s.preloadPivot(s.pivot, store) + lastKLine := preloadPivot(s.pivot, store) // update pivot low data session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { @@ -224,7 +224,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se if s.ResistanceShort != nil && s.ResistanceShort.Enabled { if s.resistancePivot != nil { - s.preloadPivot(s.resistancePivot, store) + preloadPivot(s.resistancePivot, store) } session.UserDataStream.OnStart(func() { @@ -435,23 +435,6 @@ func (s *Strategy) placeOrder(ctx context.Context, price fixedpoint.Value, quant }) } -func (s *Strategy) preloadPivot(pivot *indicator.Pivot, store *bbgo.MarketDataStore) *types.KLine { - klines, ok := store.KLinesOfInterval(pivot.Interval) - if !ok { - return nil - } - - last := (*klines)[len(*klines)-1] - log.Debugf("updating pivot indicator: %d klines", len(*klines)) - - for i := pivot.Window; i < len(*klines); i++ { - pivot.Update((*klines)[0 : i+1]) - } - - log.Infof("found %s %v previous lows: %v", s.Symbol, pivot.IntervalWindow, pivot.Lows) - log.Infof("found %s %v previous highs: %v", s.Symbol, pivot.IntervalWindow, pivot.Highs) - return &last -} func (s *Strategy) useQuantityOrBaseBalance(quantity fixedpoint.Value) fixedpoint.Value { balance, hasBalance := s.session.Account.Balance(s.Market.BaseCurrency) @@ -518,3 +501,21 @@ func findPossibleResistancePrices(closePrice float64, minDistance float64, lows return resistancePrices } + +func preloadPivot(pivot *indicator.Pivot, store *bbgo.MarketDataStore) *types.KLine { + klines, ok := store.KLinesOfInterval(pivot.Interval) + if !ok { + return nil + } + + last := (*klines)[len(*klines)-1] + log.Debugf("updating pivot indicator: %d klines", len(*klines)) + + for i := pivot.Window; i < len(*klines); i++ { + pivot.Update((*klines)[0 : i+1]) + } + + log.Debugf("found %v previous lows: %v", pivot.IntervalWindow, pivot.Lows) + log.Debugf("found %v previous highs: %v", pivot.IntervalWindow, pivot.Highs) + return &last +} From fa917b0b77bd89f1d55ad3f5491892dbec29e2c8 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 29 Jun 2022 18:39:16 +0800 Subject: [PATCH 16/92] bbgo: implmenet reflectMergeStructFields so that we can merge field values --- pkg/bbgo/exit_cumulated_volume_take_profit.go | 49 +++++++++++------- pkg/bbgo/reflect.go | 23 +++++++++ pkg/bbgo/reflect_test.go | 51 +++++++++++++++++++ 3 files changed, 105 insertions(+), 18 deletions(-) create mode 100644 pkg/bbgo/reflect_test.go diff --git a/pkg/bbgo/exit_cumulated_volume_take_profit.go b/pkg/bbgo/exit_cumulated_volume_take_profit.go index 77c855c4b..864e949ab 100644 --- a/pkg/bbgo/exit_cumulated_volume_take_profit.go +++ b/pkg/bbgo/exit_cumulated_volume_take_profit.go @@ -3,6 +3,8 @@ package bbgo import ( "context" + log "github.com/sirupsen/logrus" + "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) @@ -15,7 +17,10 @@ import ( // > SELECT start_time, `interval`, quote_volume, open, close FROM binance_klines WHERE symbol = 'ETHUSDT' AND `interval` = '5m' ORDER BY quote_volume DESC LIMIT 20; // type CumulatedVolumeTakeProfit struct { + Symbol string `json:"symbol"` + types.IntervalWindow + Ratio fixedpoint.Value `json:"ratio"` MinQuoteVolume fixedpoint.Value `json:"minQuoteVolume"` @@ -32,7 +37,7 @@ func (s *CumulatedVolumeTakeProfit) Bind(session *ExchangeSession, orderExecutor store, _ := session.MarketDataStore(position.Symbol) session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { - if kline.Symbol != position.Symbol || kline.Interval != types.Interval1m { + if kline.Symbol != position.Symbol || kline.Interval != s.Interval { return } @@ -46,25 +51,33 @@ func (s *CumulatedVolumeTakeProfit) Bind(session *ExchangeSession, orderExecutor return } - if klines, ok := store.KLinesOfInterval(s.Interval); ok { - var cbv = fixedpoint.Zero - var cqv = fixedpoint.Zero - for i := 0; i < s.Window; i++ { - last := (*klines)[len(*klines)-1-i] - cqv = cqv.Add(last.QuoteVolume) - cbv = cbv.Add(last.Volume) - } + klines, ok := store.KLinesOfInterval(s.Interval) + if !ok { + log.Warnf("history kline not found") + return + } - if cqv.Compare(s.MinQuoteVolume) > 0 { - Notify("%s TakeProfit triggered by cumulated volume (window: %d) %f > %f, price = %f", - position.Symbol, - s.Window, - cqv.Float64(), - s.MinQuoteVolume.Float64(), kline.Close.Float64()) + if len(*klines) < s.Window { + return + } - _ = orderExecutor.ClosePosition(context.Background(), fixedpoint.One, "cumulatedVolumeTakeProfit") - return - } + var cbv = fixedpoint.Zero + var cqv = fixedpoint.Zero + for i := 0; i < s.Window; i++ { + last := (*klines)[len(*klines)-1-i] + cqv = cqv.Add(last.QuoteVolume) + cbv = cbv.Add(last.Volume) + } + + if cqv.Compare(s.MinQuoteVolume) > 0 { + Notify("%s TakeProfit triggered by cumulated volume (window: %d) %f > %f, price = %f", + position.Symbol, + s.Window, + cqv.Float64(), + s.MinQuoteVolume.Float64(), kline.Close.Float64()) + + _ = orderExecutor.ClosePosition(context.Background(), fixedpoint.One, "cumulatedVolumeTakeProfit") + return } }) } diff --git a/pkg/bbgo/reflect.go b/pkg/bbgo/reflect.go index f83da274d..13ed95be1 100644 --- a/pkg/bbgo/reflect.go +++ b/pkg/bbgo/reflect.go @@ -111,6 +111,7 @@ func newTypeValueInterface(typ reflect.Type) interface{} { return dst.Interface() } +// toReflectValues convert the go objects into reflect.Value slice func toReflectValues(args ...interface{}) (values []reflect.Value) { for _, arg := range args { values = append(values, reflect.ValueOf(arg)) @@ -118,3 +119,25 @@ func toReflectValues(args ...interface{}) (values []reflect.Value) { return values } + +func reflectMergeStructFields(dst, src interface{}) { + rtA := reflect.TypeOf(dst) + srcStructType := reflect.TypeOf(src) + + rtA = rtA.Elem() + srcStructType = srcStructType.Elem() + + for i := 0; i < rtA.NumField(); i++ { + fieldType := rtA.Field(i) + fieldName := fieldType.Name + if fieldSrcType, ok := srcStructType.FieldByName(fieldName); ok { + if fieldSrcType.Type == fieldType.Type { + srcValue := reflect.ValueOf(src).Elem().FieldByName(fieldName) + dstValue := reflect.ValueOf(dst).Elem().FieldByName(fieldName) + if (fieldType.Type.Kind() == reflect.Ptr && dstValue.IsNil()) || dstValue.IsZero() { + dstValue.Set(srcValue) + } + } + } + } +} diff --git a/pkg/bbgo/reflect_test.go b/pkg/bbgo/reflect_test.go new file mode 100644 index 000000000..43b548a1d --- /dev/null +++ b/pkg/bbgo/reflect_test.go @@ -0,0 +1,51 @@ +package bbgo + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/types" +) + +func Test_reflectMergeStructFields(t *testing.T) { + t.Run("zero value", func(t *testing.T) { + a := &TestStrategy{Symbol: "BTCUSDT"} + b := &CumulatedVolumeTakeProfit{Symbol: ""} + reflectMergeStructFields(b, a) + assert.Equal(t, "BTCUSDT", b.Symbol) + }) + + t.Run("non-zero value", func(t *testing.T) { + a := &TestStrategy{Symbol: "BTCUSDT"} + b := &CumulatedVolumeTakeProfit{Symbol: "ETHUSDT"} + reflectMergeStructFields(b, a) + assert.Equal(t, "ETHUSDT", b.Symbol, "should be the original value") + }) + + t.Run("zero embedded struct", func(t *testing.T) { + iw := types.IntervalWindow{Interval: types.Interval1h, Window: 30} + a := &struct { + types.IntervalWindow + }{ + IntervalWindow: iw, + } + b := &CumulatedVolumeTakeProfit{} + reflectMergeStructFields(b, a) + assert.Equal(t, iw, b.IntervalWindow) + }) + + t.Run("non-zero embedded struct", func(t *testing.T) { + iw := types.IntervalWindow{Interval: types.Interval1h, Window: 30} + a := &struct { + types.IntervalWindow + }{ + IntervalWindow: iw, + } + b := &CumulatedVolumeTakeProfit{ + IntervalWindow: types.IntervalWindow{Interval: types.Interval5m, Window: 9}, + } + reflectMergeStructFields(b, a) + assert.Equal(t, types.IntervalWindow{Interval: types.Interval5m, Window: 9}, b.IntervalWindow) + }) +} From a74decc47d0d597266c511419b0b5a1e716c7548 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 29 Jun 2022 18:41:13 +0800 Subject: [PATCH 17/92] add more test case for reflect --- pkg/bbgo/reflect_test.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pkg/bbgo/reflect_test.go b/pkg/bbgo/reflect_test.go index 43b548a1d..60f32cc32 100644 --- a/pkg/bbgo/reflect_test.go +++ b/pkg/bbgo/reflect_test.go @@ -48,4 +48,19 @@ func Test_reflectMergeStructFields(t *testing.T) { reflectMergeStructFields(b, a) assert.Equal(t, types.IntervalWindow{Interval: types.Interval5m, Window: 9}, b.IntervalWindow) }) + + t.Run("skip different type but the same name", func(t *testing.T) { + a := &struct { + A float64 + }{ + A: 1.99, + } + b := &struct { + A string + }{} + reflectMergeStructFields(b, a) + assert.Equal(t, "", b.A) + assert.Equal(t, 1.99, a.A) + }) + } From 3013eeccc7773125074f5d71b81e1f24437f8ef6 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 29 Jun 2022 18:49:42 +0800 Subject: [PATCH 18/92] move dynamic stuff to the pkg/dynamic package --- pkg/bbgo/exit.go | 3 +- pkg/bbgo/injection_test.go | 3 +- pkg/bbgo/persistence.go | 7 +-- pkg/bbgo/persistence_test.go | 3 +- pkg/bbgo/reflect.go | 95 ------------------------------------ pkg/bbgo/reflect_test.go | 64 ------------------------ pkg/bbgo/trader.go | 3 +- pkg/dynamic/field.go | 8 +++ pkg/dynamic/iterate.go | 54 ++++++++++++++++++++ pkg/dynamic/merge.go | 25 ++++++++++ pkg/dynamic/merge_test.go | 75 ++++++++++++++++++++++++++++ pkg/dynamic/typevalue.go | 24 +++++++++ 12 files changed, 198 insertions(+), 166 deletions(-) create mode 100644 pkg/dynamic/field.go create mode 100644 pkg/dynamic/iterate.go create mode 100644 pkg/dynamic/merge.go create mode 100644 pkg/dynamic/merge_test.go create mode 100644 pkg/dynamic/typevalue.go diff --git a/pkg/bbgo/exit.go b/pkg/bbgo/exit.go index 98404ddbf..11f97d8a8 100644 --- a/pkg/bbgo/exit.go +++ b/pkg/bbgo/exit.go @@ -3,6 +3,7 @@ package bbgo import ( "reflect" + "github.com/c9s/bbgo/pkg/dynamic" "github.com/c9s/bbgo/pkg/types" ) @@ -23,7 +24,7 @@ func (m *ExitMethod) Subscribe(session *ExchangeSession) { rt = rt.Elem() infType := reflect.TypeOf((*types.Subscriber)(nil)).Elem() - argValues := toReflectValues(session) + argValues := dynamic.ToReflectValues(session) for i := 0; i < rt.NumField(); i++ { fieldType := rt.Field(i) if fieldType.Type.Implements(infType) { diff --git a/pkg/bbgo/injection_test.go b/pkg/bbgo/injection_test.go index dd6370320..69a8b5f72 100644 --- a/pkg/bbgo/injection_test.go +++ b/pkg/bbgo/injection_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/assert" + "github.com/c9s/bbgo/pkg/dynamic" "github.com/c9s/bbgo/pkg/service" "github.com/c9s/bbgo/pkg/types" ) @@ -22,7 +23,7 @@ func Test_injectField(t *testing.T) { // get the value of the pointer, or it can not be set. var rv = reflect.ValueOf(tt).Elem() - _, ret := hasField(rv, "TradeService") + _, ret := dynamic.HasField(rv, "TradeService") assert.True(t, ret) ts := &service.TradeService{} diff --git a/pkg/bbgo/persistence.go b/pkg/bbgo/persistence.go index b435c8f07..89b4179df 100644 --- a/pkg/bbgo/persistence.go +++ b/pkg/bbgo/persistence.go @@ -6,6 +6,7 @@ import ( log "github.com/sirupsen/logrus" + "github.com/c9s/bbgo/pkg/dynamic" "github.com/c9s/bbgo/pkg/service" ) @@ -106,10 +107,10 @@ func Sync(obj interface{}) { } func loadPersistenceFields(obj interface{}, id string, persistence service.PersistenceService) error { - return iterateFieldsByTag(obj, "persistence", func(tag string, field reflect.StructField, value reflect.Value) error { + return dynamic.IterateFieldsByTag(obj, "persistence", func(tag string, field reflect.StructField, value reflect.Value) error { log.Debugf("[loadPersistenceFields] loading value into field %v, tag = %s, original value = %v", field, tag, value) - newValueInf := newTypeValueInterface(value.Type()) + newValueInf := dynamic.NewTypeValueInterface(value.Type()) // inf := value.Interface() store := persistence.NewStore("state", id, tag) if err := store.Load(&newValueInf); err != nil { @@ -134,7 +135,7 @@ func loadPersistenceFields(obj interface{}, id string, persistence service.Persi } func storePersistenceFields(obj interface{}, id string, persistence service.PersistenceService) error { - return iterateFieldsByTag(obj, "persistence", func(tag string, ft reflect.StructField, fv reflect.Value) error { + return dynamic.IterateFieldsByTag(obj, "persistence", func(tag string, ft reflect.StructField, fv reflect.Value) error { log.Debugf("[storePersistenceFields] storing value from field %v, tag = %s, original value = %v", ft, tag, fv) inf := fv.Interface() diff --git a/pkg/bbgo/persistence_test.go b/pkg/bbgo/persistence_test.go index ebc5314f0..1c14eb2e9 100644 --- a/pkg/bbgo/persistence_test.go +++ b/pkg/bbgo/persistence_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/assert" + "github.com/c9s/bbgo/pkg/dynamic" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/service" "github.com/c9s/bbgo/pkg/types" @@ -83,7 +84,7 @@ func Test_loadPersistenceFields(t *testing.T) { t.Run(psName+"/nil", func(t *testing.T) { var b *TestStruct = nil err := loadPersistenceFields(b, "test-nil", ps) - assert.Equal(t, errCanNotIterateNilPointer, err) + assert.Equal(t, dynamic.ErrCanNotIterateNilPointer, err) }) t.Run(psName+"/pointer-field", func(t *testing.T) { diff --git a/pkg/bbgo/reflect.go b/pkg/bbgo/reflect.go index 13ed95be1..aad404e1b 100644 --- a/pkg/bbgo/reflect.go +++ b/pkg/bbgo/reflect.go @@ -1,8 +1,6 @@ package bbgo import ( - "errors" - "fmt" "reflect" ) @@ -48,96 +46,3 @@ func isSymbolBasedStrategy(rs reflect.Value) (string, bool) { return field.String(), true } -func hasField(rs reflect.Value, fieldName string) (field reflect.Value, ok bool) { - field = rs.FieldByName(fieldName) - return field, field.IsValid() -} - -type StructFieldIterator func(tag string, ft reflect.StructField, fv reflect.Value) error - -var errCanNotIterateNilPointer = errors.New("can not iterate struct on a nil pointer") - -func iterateFieldsByTag(obj interface{}, tagName string, cb StructFieldIterator) error { - sv := reflect.ValueOf(obj) - st := reflect.TypeOf(obj) - - if st.Kind() != reflect.Ptr { - return fmt.Errorf("f should be a pointer of a struct, %s given", st) - } - - // for pointer, check if it's nil - if sv.IsNil() { - return errCanNotIterateNilPointer - } - - // solve the reference - st = st.Elem() - sv = sv.Elem() - - if st.Kind() != reflect.Struct { - return fmt.Errorf("f should be a struct, %s given", st) - } - - for i := 0; i < sv.NumField(); i++ { - fv := sv.Field(i) - ft := st.Field(i) - - // skip unexported fields - if !st.Field(i).IsExported() { - continue - } - - tag, ok := ft.Tag.Lookup(tagName) - if !ok { - continue - } - - if err := cb(tag, ft, fv); err != nil { - return err - } - } - - return nil -} - -// https://github.com/xiaojun207/go-base-utils/blob/master/utils/Clone.go -func newTypeValueInterface(typ reflect.Type) interface{} { - if typ.Kind() == reflect.Ptr { - typ = typ.Elem() - dst := reflect.New(typ).Elem() - return dst.Addr().Interface() - } - dst := reflect.New(typ) - return dst.Interface() -} - -// toReflectValues convert the go objects into reflect.Value slice -func toReflectValues(args ...interface{}) (values []reflect.Value) { - for _, arg := range args { - values = append(values, reflect.ValueOf(arg)) - } - - return values -} - -func reflectMergeStructFields(dst, src interface{}) { - rtA := reflect.TypeOf(dst) - srcStructType := reflect.TypeOf(src) - - rtA = rtA.Elem() - srcStructType = srcStructType.Elem() - - for i := 0; i < rtA.NumField(); i++ { - fieldType := rtA.Field(i) - fieldName := fieldType.Name - if fieldSrcType, ok := srcStructType.FieldByName(fieldName); ok { - if fieldSrcType.Type == fieldType.Type { - srcValue := reflect.ValueOf(src).Elem().FieldByName(fieldName) - dstValue := reflect.ValueOf(dst).Elem().FieldByName(fieldName) - if (fieldType.Type.Kind() == reflect.Ptr && dstValue.IsNil()) || dstValue.IsZero() { - dstValue.Set(srcValue) - } - } - } - } -} diff --git a/pkg/bbgo/reflect_test.go b/pkg/bbgo/reflect_test.go index 60f32cc32..920078f66 100644 --- a/pkg/bbgo/reflect_test.go +++ b/pkg/bbgo/reflect_test.go @@ -1,66 +1,2 @@ package bbgo -import ( - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/c9s/bbgo/pkg/types" -) - -func Test_reflectMergeStructFields(t *testing.T) { - t.Run("zero value", func(t *testing.T) { - a := &TestStrategy{Symbol: "BTCUSDT"} - b := &CumulatedVolumeTakeProfit{Symbol: ""} - reflectMergeStructFields(b, a) - assert.Equal(t, "BTCUSDT", b.Symbol) - }) - - t.Run("non-zero value", func(t *testing.T) { - a := &TestStrategy{Symbol: "BTCUSDT"} - b := &CumulatedVolumeTakeProfit{Symbol: "ETHUSDT"} - reflectMergeStructFields(b, a) - assert.Equal(t, "ETHUSDT", b.Symbol, "should be the original value") - }) - - t.Run("zero embedded struct", func(t *testing.T) { - iw := types.IntervalWindow{Interval: types.Interval1h, Window: 30} - a := &struct { - types.IntervalWindow - }{ - IntervalWindow: iw, - } - b := &CumulatedVolumeTakeProfit{} - reflectMergeStructFields(b, a) - assert.Equal(t, iw, b.IntervalWindow) - }) - - t.Run("non-zero embedded struct", func(t *testing.T) { - iw := types.IntervalWindow{Interval: types.Interval1h, Window: 30} - a := &struct { - types.IntervalWindow - }{ - IntervalWindow: iw, - } - b := &CumulatedVolumeTakeProfit{ - IntervalWindow: types.IntervalWindow{Interval: types.Interval5m, Window: 9}, - } - reflectMergeStructFields(b, a) - assert.Equal(t, types.IntervalWindow{Interval: types.Interval5m, Window: 9}, b.IntervalWindow) - }) - - t.Run("skip different type but the same name", func(t *testing.T) { - a := &struct { - A float64 - }{ - A: 1.99, - } - b := &struct { - A string - }{} - reflectMergeStructFields(b, a) - assert.Equal(t, "", b.A) - assert.Equal(t, 1.99, a.A) - }) - -} diff --git a/pkg/bbgo/trader.go b/pkg/bbgo/trader.go index 4fc35e08e..f05e8f9b7 100644 --- a/pkg/bbgo/trader.go +++ b/pkg/bbgo/trader.go @@ -10,6 +10,7 @@ import ( _ "github.com/go-sql-driver/mysql" + "github.com/c9s/bbgo/pkg/dynamic" "github.com/c9s/bbgo/pkg/interact" ) @@ -394,7 +395,7 @@ func (trader *Trader) injectCommonServices(s interface{}) error { // a special injection for persistence selector: // if user defined the selector, the facade pointer will be nil, hence we need to update the persistence facade pointer sv := reflect.ValueOf(s).Elem() - if field, ok := hasField(sv, "Persistence"); ok { + if field, ok := dynamic.HasField(sv, "Persistence"); ok { // the selector is set, but we need to update the facade pointer if !field.IsNil() { elem := field.Elem() diff --git a/pkg/dynamic/field.go b/pkg/dynamic/field.go new file mode 100644 index 000000000..baccd563a --- /dev/null +++ b/pkg/dynamic/field.go @@ -0,0 +1,8 @@ +package dynamic + +import "reflect" + +func HasField(rs reflect.Value, fieldName string) (field reflect.Value, ok bool) { + field = rs.FieldByName(fieldName) + return field, field.IsValid() +} diff --git a/pkg/dynamic/iterate.go b/pkg/dynamic/iterate.go new file mode 100644 index 000000000..12d6e2842 --- /dev/null +++ b/pkg/dynamic/iterate.go @@ -0,0 +1,54 @@ +package dynamic + +import ( + "errors" + "fmt" + "reflect" +) + +type StructFieldIterator func(tag string, ft reflect.StructField, fv reflect.Value) error + +var ErrCanNotIterateNilPointer = errors.New("can not iterate struct on a nil pointer") + +func IterateFieldsByTag(obj interface{}, tagName string, cb StructFieldIterator) error { + sv := reflect.ValueOf(obj) + st := reflect.TypeOf(obj) + + if st.Kind() != reflect.Ptr { + return fmt.Errorf("f should be a pointer of a struct, %s given", st) + } + + // for pointer, check if it's nil + if sv.IsNil() { + return ErrCanNotIterateNilPointer + } + + // solve the reference + st = st.Elem() + sv = sv.Elem() + + if st.Kind() != reflect.Struct { + return fmt.Errorf("f should be a struct, %s given", st) + } + + for i := 0; i < sv.NumField(); i++ { + fv := sv.Field(i) + ft := st.Field(i) + + // skip unexported fields + if !st.Field(i).IsExported() { + continue + } + + tag, ok := ft.Tag.Lookup(tagName) + if !ok { + continue + } + + if err := cb(tag, ft, fv); err != nil { + return err + } + } + + return nil +} diff --git a/pkg/dynamic/merge.go b/pkg/dynamic/merge.go new file mode 100644 index 000000000..1f0472c88 --- /dev/null +++ b/pkg/dynamic/merge.go @@ -0,0 +1,25 @@ +package dynamic + +import "reflect" + +func MergeStructValues(dst, src interface{}) { + rtA := reflect.TypeOf(dst) + srcStructType := reflect.TypeOf(src) + + rtA = rtA.Elem() + srcStructType = srcStructType.Elem() + + for i := 0; i < rtA.NumField(); i++ { + fieldType := rtA.Field(i) + fieldName := fieldType.Name + if fieldSrcType, ok := srcStructType.FieldByName(fieldName); ok { + if fieldSrcType.Type == fieldType.Type { + srcValue := reflect.ValueOf(src).Elem().FieldByName(fieldName) + dstValue := reflect.ValueOf(dst).Elem().FieldByName(fieldName) + if (fieldType.Type.Kind() == reflect.Ptr && dstValue.IsNil()) || dstValue.IsZero() { + dstValue.Set(srcValue) + } + } + } + } +} diff --git a/pkg/dynamic/merge_test.go b/pkg/dynamic/merge_test.go new file mode 100644 index 000000000..2e8929ff0 --- /dev/null +++ b/pkg/dynamic/merge_test.go @@ -0,0 +1,75 @@ +package dynamic + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +type TestStrategy struct { + Symbol string `json:"symbol"` + Interval string `json:"interval"` + BaseQuantity fixedpoint.Value `json:"baseQuantity"` + MaxAssetQuantity fixedpoint.Value `json:"maxAssetQuantity"` + MinDropPercentage fixedpoint.Value `json:"minDropPercentage"` +} + +func Test_reflectMergeStructFields(t *testing.T) { + t.Run("zero value", func(t *testing.T) { + a := &TestStrategy{Symbol: "BTCUSDT"} + b := &bbgo.CumulatedVolumeTakeProfit{Symbol: ""} + MergeStructValues(b, a) + assert.Equal(t, "BTCUSDT", b.Symbol) + }) + + t.Run("non-zero value", func(t *testing.T) { + a := &TestStrategy{Symbol: "BTCUSDT"} + b := &bbgo.CumulatedVolumeTakeProfit{Symbol: "ETHUSDT"} + MergeStructValues(b, a) + assert.Equal(t, "ETHUSDT", b.Symbol, "should be the original value") + }) + + t.Run("zero embedded struct", func(t *testing.T) { + iw := types.IntervalWindow{Interval: types.Interval1h, Window: 30} + a := &struct { + types.IntervalWindow + }{ + IntervalWindow: iw, + } + b := &bbgo.CumulatedVolumeTakeProfit{} + MergeStructValues(b, a) + assert.Equal(t, iw, b.IntervalWindow) + }) + + t.Run("non-zero embedded struct", func(t *testing.T) { + iw := types.IntervalWindow{Interval: types.Interval1h, Window: 30} + a := &struct { + types.IntervalWindow + }{ + IntervalWindow: iw, + } + b := &bbgo.CumulatedVolumeTakeProfit{ + IntervalWindow: types.IntervalWindow{Interval: types.Interval5m, Window: 9}, + } + MergeStructValues(b, a) + assert.Equal(t, types.IntervalWindow{Interval: types.Interval5m, Window: 9}, b.IntervalWindow) + }) + + t.Run("skip different type but the same name", func(t *testing.T) { + a := &struct { + A float64 + }{ + A: 1.99, + } + b := &struct { + A string + }{} + MergeStructValues(b, a) + assert.Equal(t, "", b.A) + assert.Equal(t, 1.99, a.A) + }) +} diff --git a/pkg/dynamic/typevalue.go b/pkg/dynamic/typevalue.go new file mode 100644 index 000000000..a12ccf416 --- /dev/null +++ b/pkg/dynamic/typevalue.go @@ -0,0 +1,24 @@ +package dynamic + +import "reflect" + +// https://github.com/xiaojun207/go-base-utils/blob/master/utils/Clone.go +func NewTypeValueInterface(typ reflect.Type) interface{} { + if typ.Kind() == reflect.Ptr { + typ = typ.Elem() + dst := reflect.New(typ).Elem() + return dst.Addr().Interface() + } + dst := reflect.New(typ) + return dst.Interface() +} + +// ToReflectValues convert the go objects into reflect.Value slice +func ToReflectValues(args ...interface{}) (values []reflect.Value) { + for _, arg := range args { + values = append(values, reflect.ValueOf(arg)) + } + + return values +} + From cf0ca70d24e682f769e05defccc6f3c3a732b8f6 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 29 Jun 2022 18:51:22 +0800 Subject: [PATCH 19/92] move and rename isSymbolBasedStrategy --- pkg/bbgo/config.go | 3 ++- pkg/bbgo/reflect.go | 21 +++------------------ pkg/bbgo/trader.go | 2 +- pkg/dynamic/field.go | 18 ++++++++++++++++++ 4 files changed, 24 insertions(+), 20 deletions(-) diff --git a/pkg/bbgo/config.go b/pkg/bbgo/config.go index 8a2eeb3e6..8c34c6232 100644 --- a/pkg/bbgo/config.go +++ b/pkg/bbgo/config.go @@ -13,6 +13,7 @@ import ( "gopkg.in/yaml.v3" "github.com/c9s/bbgo/pkg/datatype" + "github.com/c9s/bbgo/pkg/dynamic" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/service" "github.com/c9s/bbgo/pkg/types" @@ -387,7 +388,7 @@ func (c *Config) GetSignature() string { id := strategy.ID() ps = append(ps, id) - if symbol, ok := isSymbolBasedStrategy(reflect.ValueOf(strategy)); ok { + if symbol, ok := dynamic.LookupSymbolField(reflect.ValueOf(strategy)); ok { ps = append(ps, symbol) } } diff --git a/pkg/bbgo/reflect.go b/pkg/bbgo/reflect.go index aad404e1b..263c2cc87 100644 --- a/pkg/bbgo/reflect.go +++ b/pkg/bbgo/reflect.go @@ -2,6 +2,8 @@ package bbgo import ( "reflect" + + "github.com/c9s/bbgo/pkg/dynamic" ) type InstanceIDProvider interface { @@ -17,7 +19,7 @@ func callID(obj interface{}) string { return ret[0].String() } - if symbol, ok := isSymbolBasedStrategy(sv); ok { + if symbol, ok := dynamic.LookupSymbolField(sv); ok { m := sv.MethodByName("ID") ret := m.Call(nil) return ret[0].String() + ":" + symbol @@ -29,20 +31,3 @@ func callID(obj interface{}) string { return ret[0].String() + ":" } -func isSymbolBasedStrategy(rs reflect.Value) (string, bool) { - if rs.Kind() == reflect.Ptr { - rs = rs.Elem() - } - - field := rs.FieldByName("Symbol") - if !field.IsValid() { - return "", false - } - - if field.Kind() != reflect.String { - return "", false - } - - return field.String(), true -} - diff --git a/pkg/bbgo/trader.go b/pkg/bbgo/trader.go index f05e8f9b7..f8dbc7203 100644 --- a/pkg/bbgo/trader.go +++ b/pkg/bbgo/trader.go @@ -202,7 +202,7 @@ func (trader *Trader) RunSingleExchangeStrategy(ctx context.Context, strategy Si return errors.Wrapf(err, "failed to inject OrderExecutor on %T", strategy) } - if symbol, ok := isSymbolBasedStrategy(rs); ok { + if symbol, ok := dynamic.LookupSymbolField(rs); ok { log.Infof("found symbol based strategy from %s", rs.Type()) market, ok := session.Market(symbol) diff --git a/pkg/dynamic/field.go b/pkg/dynamic/field.go index baccd563a..5fc222be1 100644 --- a/pkg/dynamic/field.go +++ b/pkg/dynamic/field.go @@ -6,3 +6,21 @@ func HasField(rs reflect.Value, fieldName string) (field reflect.Value, ok bool) field = rs.FieldByName(fieldName) return field, field.IsValid() } + +func LookupSymbolField(rs reflect.Value) (string, bool) { + if rs.Kind() == reflect.Ptr { + rs = rs.Elem() + } + + field := rs.FieldByName("Symbol") + if !field.IsValid() { + return "", false + } + + if field.Kind() != reflect.String { + return "", false + } + + return field.String(), true +} + From 7d5474e3dda08f95ad7ddeb5d9971c2bbbd7c4f6 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 30 Jun 2022 12:37:54 +0800 Subject: [PATCH 20/92] pivotshort: call MergeStructValues to update the field value --- config/pivotshort.yaml | 1 + pkg/dynamic/merge.go | 4 ++++ pkg/strategy/pivotshort/strategy.go | 7 ++++--- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/config/pivotshort.yaml b/config/pivotshort.yaml index 473df2cbc..76a7fdbe6 100644 --- a/config/pivotshort.yaml +++ b/config/pivotshort.yaml @@ -91,6 +91,7 @@ exchangeStrategies: # (5) cumulatedVolumeTakeProfit is used to take profit when the cumulated quote volume from the klines exceeded a threshold - cumulatedVolumeTakeProfit: minQuoteVolume: 100_000_000 + interval: 5m window: 2 backtest: diff --git a/pkg/dynamic/merge.go b/pkg/dynamic/merge.go index 1f0472c88..ac20f8152 100644 --- a/pkg/dynamic/merge.go +++ b/pkg/dynamic/merge.go @@ -2,6 +2,8 @@ package dynamic import "reflect" +// MergeStructValues merges the field value from the source struct to the dest struct. +// Only fields with the same type and the same name will be updated. func MergeStructValues(dst, src interface{}) { rtA := reflect.TypeOf(dst) srcStructType := reflect.TypeOf(src) @@ -12,7 +14,9 @@ func MergeStructValues(dst, src interface{}) { for i := 0; i < rtA.NumField(); i++ { fieldType := rtA.Field(i) fieldName := fieldType.Name + // if there is a field with the same name if fieldSrcType, ok := srcStructType.FieldByName(fieldName); ok { + // ensure that the type is the same if fieldSrcType.Type == fieldType.Type { srcValue := reflect.ValueOf(src).Elem().FieldByName(fieldName) dstValue := reflect.ValueOf(dst).Elem().FieldByName(fieldName) diff --git a/pkg/strategy/pivotshort/strategy.go b/pkg/strategy/pivotshort/strategy.go index 2ee5a7551..1ce58c7a2 100644 --- a/pkg/strategy/pivotshort/strategy.go +++ b/pkg/strategy/pivotshort/strategy.go @@ -10,6 +10,7 @@ import ( "github.com/sirupsen/logrus" "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/dynamic" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/indicator" "github.com/c9s/bbgo/pkg/types" @@ -93,7 +94,6 @@ type Strategy struct { session *bbgo.ExchangeSession orderExecutor *bbgo.GeneralOrderExecutor - stopLossPrice fixedpoint.Value lastLow fixedpoint.Value pivot *indicator.Pivot resistancePivot *indicator.Pivot @@ -122,7 +122,9 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { session.Subscribe(types.MarketTradeChannel, s.Symbol, types.SubscribeOptions{}) } - for _, m := range s.ExitMethods { + for i := range s.ExitMethods { + m := s.ExitMethods[i] + dynamic.MergeStructValues(&m, s) m.Subscribe(session) } } @@ -435,7 +437,6 @@ func (s *Strategy) placeOrder(ctx context.Context, price fixedpoint.Value, quant }) } - func (s *Strategy) useQuantityOrBaseBalance(quantity fixedpoint.Value) fixedpoint.Value { balance, hasBalance := s.session.Account.Balance(s.Market.BaseCurrency) From 527070d13d9b0b410f4392fda7525f4e9a315951 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 30 Jun 2022 13:48:04 +0800 Subject: [PATCH 21/92] all: rewrite and clean up graceful shutdown api --- pkg/bbgo/graceful_shutdown.go | 24 +++++++++++++++++++++++- pkg/bbgo/persistence_test.go | 1 - pkg/bbgo/trader.go | 3 --- pkg/cmd/backtest.go | 4 +--- pkg/cmd/run.go | 13 ++----------- pkg/dynamic/merge_test.go | 14 +++++++++----- pkg/strategy/bollgrid/strategy.go | 5 +---- pkg/strategy/bollmaker/strategy.go | 3 +-- pkg/strategy/dca/strategy.go | 1 - pkg/strategy/emastop/strategy.go | 5 ++--- pkg/strategy/ewoDgtrd/strategy.go | 4 ++-- pkg/strategy/flashcrash/strategy.go | 6 +----- pkg/strategy/fmaker/strategy.go | 1 - pkg/strategy/grid/strategy.go | 4 +--- pkg/strategy/pivotshort/strategy.go | 6 +++--- pkg/strategy/rsmaker/strategy.go | 3 --- pkg/strategy/supertrend/strategy.go | 3 +-- pkg/strategy/support/strategy.go | 3 +-- pkg/strategy/wall/strategy.go | 3 +-- pkg/strategy/xbalance/strategy.go | 4 +--- pkg/strategy/xgap/strategy.go | 3 +-- pkg/strategy/xmaker/strategy.go | 3 +-- pkg/strategy/xnav/strategy.go | 3 +-- 23 files changed, 53 insertions(+), 66 deletions(-) diff --git a/pkg/bbgo/graceful_shutdown.go b/pkg/bbgo/graceful_shutdown.go index b35482ce2..c3248b0c8 100644 --- a/pkg/bbgo/graceful_shutdown.go +++ b/pkg/bbgo/graceful_shutdown.go @@ -3,18 +3,40 @@ package bbgo import ( "context" "sync" + "time" + + "github.com/sirupsen/logrus" ) +var graceful = &Graceful{} + //go:generate callbackgen -type Graceful type Graceful struct { shutdownCallbacks []func(ctx context.Context, wg *sync.WaitGroup) } +// Shutdown is a blocking call to emit all shutdown callbacks at the same time. func (g *Graceful) Shutdown(ctx context.Context) { var wg sync.WaitGroup wg.Add(len(g.shutdownCallbacks)) - go g.EmitShutdown(ctx, &wg) + // for each shutdown callback, we give them 10 second + shtCtx, cancel := context.WithTimeout(ctx, 10*time.Second) + + go g.EmitShutdown(shtCtx, &wg) wg.Wait() + cancel() +} + +func OnShutdown(f func(ctx context.Context, wg *sync.WaitGroup)) { + graceful.OnShutdown(f) +} + +func Shutdown() { + logrus.Infof("shutting down...") + + ctx, cancel := context.WithTimeout(context.TODO(), 30*time.Second) + graceful.Shutdown(ctx) + cancel() } diff --git a/pkg/bbgo/persistence_test.go b/pkg/bbgo/persistence_test.go index 1c14eb2e9..0eea57ed5 100644 --- a/pkg/bbgo/persistence_test.go +++ b/pkg/bbgo/persistence_test.go @@ -24,7 +24,6 @@ func (s *TestStructWithoutInstanceID) ID() string { type TestStruct struct { *Environment - *Graceful Position *types.Position `persistence:"position"` Integer int64 `persistence:"integer"` diff --git a/pkg/bbgo/trader.go b/pkg/bbgo/trader.go index f8dbc7203..86371a53b 100644 --- a/pkg/bbgo/trader.go +++ b/pkg/bbgo/trader.go @@ -73,8 +73,6 @@ type Trader struct { exchangeStrategies map[string][]SingleExchangeStrategy logger Logger - - Graceful Graceful } func NewTrader(environ *Environment) *Trader { @@ -416,7 +414,6 @@ func (trader *Trader) injectCommonServices(s interface{}) error { } return parseStructAndInject(s, - &trader.Graceful, &trader.logger, Notification, trader.environment.TradeService, diff --git a/pkg/cmd/backtest.go b/pkg/cmd/backtest.go index 363d2d282..1c1ac11a4 100644 --- a/pkg/cmd/backtest.go +++ b/pkg/cmd/backtest.go @@ -443,9 +443,7 @@ var BacktestCmd = &cobra.Command{ cmdutil.WaitForSignal(runCtx, syscall.SIGINT, syscall.SIGTERM) log.Infof("shutting down trader...") - shutdownCtx, cancelShutdown := context.WithDeadline(runCtx, time.Now().Add(10*time.Second)) - trader.Graceful.Shutdown(shutdownCtx) - cancelShutdown() + bbgo.Shutdown() // put the logger back to print the pnl log.SetLevel(log.InfoLevel) diff --git a/pkg/cmd/run.go b/pkg/cmd/run.go index ca634c0a5..c6f1841d6 100644 --- a/pkg/cmd/run.go +++ b/pkg/cmd/run.go @@ -8,7 +8,6 @@ import ( "path/filepath" "runtime/pprof" "syscall" - "time" "github.com/pkg/errors" log "github.com/sirupsen/logrus" @@ -78,12 +77,7 @@ func runSetup(baseCtx context.Context, userConfig *bbgo.Config, enableApiServer cmdutil.WaitForSignal(ctx, syscall.SIGINT, syscall.SIGTERM) cancelTrading() - // graceful period = 15 second - shutdownCtx, cancelShutdown := context.WithDeadline(ctx, time.Now().Add(15*time.Second)) - - log.Infof("shutting down...") - trader.Graceful.Shutdown(shutdownCtx) - cancelShutdown() + bbgo.Shutdown() return nil } @@ -216,10 +210,7 @@ func runConfig(basectx context.Context, cmd *cobra.Command, userConfig *bbgo.Con cmdutil.WaitForSignal(ctx, syscall.SIGINT, syscall.SIGTERM) cancelTrading() - log.Infof("shutting down...") - shutdownCtx, cancelShutdown := context.WithDeadline(ctx, time.Now().Add(30*time.Second)) - trader.Graceful.Shutdown(shutdownCtx) - cancelShutdown() + bbgo.Shutdown() if err := trader.SaveState(); err != nil { log.WithError(err).Errorf("can not save strategy states") diff --git a/pkg/dynamic/merge_test.go b/pkg/dynamic/merge_test.go index 2e8929ff0..bc65e49c8 100644 --- a/pkg/dynamic/merge_test.go +++ b/pkg/dynamic/merge_test.go @@ -5,7 +5,6 @@ import ( "github.com/stretchr/testify/assert" - "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) @@ -21,14 +20,14 @@ type TestStrategy struct { func Test_reflectMergeStructFields(t *testing.T) { t.Run("zero value", func(t *testing.T) { a := &TestStrategy{Symbol: "BTCUSDT"} - b := &bbgo.CumulatedVolumeTakeProfit{Symbol: ""} + b := &struct{ Symbol string }{Symbol: ""} MergeStructValues(b, a) assert.Equal(t, "BTCUSDT", b.Symbol) }) t.Run("non-zero value", func(t *testing.T) { a := &TestStrategy{Symbol: "BTCUSDT"} - b := &bbgo.CumulatedVolumeTakeProfit{Symbol: "ETHUSDT"} + b := &struct{ Symbol string }{Symbol: "ETHUSDT"} MergeStructValues(b, a) assert.Equal(t, "ETHUSDT", b.Symbol, "should be the original value") }) @@ -40,7 +39,10 @@ func Test_reflectMergeStructFields(t *testing.T) { }{ IntervalWindow: iw, } - b := &bbgo.CumulatedVolumeTakeProfit{} + b := &struct { + Symbol string + types.IntervalWindow + }{} MergeStructValues(b, a) assert.Equal(t, iw, b.IntervalWindow) }) @@ -52,7 +54,9 @@ func Test_reflectMergeStructFields(t *testing.T) { }{ IntervalWindow: iw, } - b := &bbgo.CumulatedVolumeTakeProfit{ + b := &struct { + types.IntervalWindow + }{ IntervalWindow: types.IntervalWindow{Interval: types.Interval5m, Window: 9}, } MergeStructValues(b, a) diff --git a/pkg/strategy/bollgrid/strategy.go b/pkg/strategy/bollgrid/strategy.go index 8d689f7d4..676c8fea0 100644 --- a/pkg/strategy/bollgrid/strategy.go +++ b/pkg/strategy/bollgrid/strategy.go @@ -41,9 +41,6 @@ type Strategy struct { // This field will be injected automatically since we defined the Symbol field. *bbgo.StandardIndicatorSet - // Graceful let you define the graceful shutdown handler - *bbgo.Graceful - // Market stores the configuration of the market, for example, VolumePrecision, PricePrecision, MinLotSize... etc // This field will be injected automatically since we defined the Symbol field. types.Market @@ -350,7 +347,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.profitOrders.BindStream(session.UserDataStream) // setup graceful shutting down handler - s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { // call Done to notify the main process. defer wg.Done() log.Infof("canceling active orders...") diff --git a/pkg/strategy/bollmaker/strategy.go b/pkg/strategy/bollmaker/strategy.go index 93b135b9d..2bcf6e2e9 100644 --- a/pkg/strategy/bollmaker/strategy.go +++ b/pkg/strategy/bollmaker/strategy.go @@ -49,7 +49,6 @@ type BollingerSetting struct { } type Strategy struct { - *bbgo.Graceful *bbgo.Persistence Environment *bbgo.Environment @@ -616,7 +615,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // s.book = types.NewStreamBook(s.Symbol) // s.book.BindStreamForBackground(session.MarketDataStream) - s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() _ = s.orderExecutor.GracefulCancel(ctx) diff --git a/pkg/strategy/dca/strategy.go b/pkg/strategy/dca/strategy.go index f0e86aa53..38e6c9d14 100644 --- a/pkg/strategy/dca/strategy.go +++ b/pkg/strategy/dca/strategy.go @@ -47,7 +47,6 @@ func (b BudgetPeriod) Duration() time.Duration { // Strategy is the Dollar-Cost-Average strategy type Strategy struct { - *bbgo.Graceful Environment *bbgo.Environment Symbol string `json:"symbol"` diff --git a/pkg/strategy/emastop/strategy.go b/pkg/strategy/emastop/strategy.go index 89c837b37..7c9c4a190 100644 --- a/pkg/strategy/emastop/strategy.go +++ b/pkg/strategy/emastop/strategy.go @@ -25,7 +25,6 @@ func init() { } type Strategy struct { - *bbgo.Graceful SourceExchangeName string `json:"sourceExchange"` @@ -217,7 +216,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.place(ctx, orderExecutor, session, indicator, closePrice) }) - s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() log.Infof("canceling trailingstop order...") s.clear(ctx, orderExecutor) @@ -261,7 +260,7 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se s.place(ctx, &orderExecutor, session, indicator, closePrice) }) - s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() log.Infof("canceling trailingstop order...") s.clear(ctx, &orderExecutor) diff --git a/pkg/strategy/ewoDgtrd/strategy.go b/pkg/strategy/ewoDgtrd/strategy.go index bce4cf07a..4a926c901 100644 --- a/pkg/strategy/ewoDgtrd/strategy.go +++ b/pkg/strategy/ewoDgtrd/strategy.go @@ -51,7 +51,6 @@ type Strategy struct { KLineEndTime types.Time *bbgo.Environment - *bbgo.Graceful bbgo.StrategyController activeMakerOrders *bbgo.ActiveOrderBook @@ -1221,7 +1220,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } } }) - s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + + bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() log.Infof("canceling active orders...") s.CancelAll(ctx) diff --git a/pkg/strategy/flashcrash/strategy.go b/pkg/strategy/flashcrash/strategy.go index b15fcbfb7..4b5c80577 100644 --- a/pkg/strategy/flashcrash/strategy.go +++ b/pkg/strategy/flashcrash/strategy.go @@ -49,10 +49,6 @@ type Strategy struct { // This field will be injected automatically since we defined the Symbol field. *bbgo.StandardIndicatorSet - // Graceful shutdown function - *bbgo.Graceful - // -------------------------- - // ewma is the exponential weighted moving average indicator ewma *indicator.EWMA } @@ -114,7 +110,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.activeOrders = bbgo.NewActiveOrderBook(s.Symbol) s.activeOrders.BindStream(session.UserDataStream) - s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() log.Infof("canceling active orders...") diff --git a/pkg/strategy/fmaker/strategy.go b/pkg/strategy/fmaker/strategy.go index d67367569..c6f7068be 100644 --- a/pkg/strategy/fmaker/strategy.go +++ b/pkg/strategy/fmaker/strategy.go @@ -31,7 +31,6 @@ type IntervalWindowSetting struct { } type Strategy struct { - *bbgo.Graceful *bbgo.Persistence Environment *bbgo.Environment diff --git a/pkg/strategy/grid/strategy.go b/pkg/strategy/grid/strategy.go index 75e781783..7fc899576 100644 --- a/pkg/strategy/grid/strategy.go +++ b/pkg/strategy/grid/strategy.go @@ -45,8 +45,6 @@ type State struct { } type Strategy struct { - *bbgo.Graceful `json:"-" yaml:"-"` - *bbgo.Persistence // OrderExecutor is an interface for submitting order. @@ -621,7 +619,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se }) s.tradeCollector.BindStream(session.UserDataStream) - s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() if err := s.SaveState(); err != nil { diff --git a/pkg/strategy/pivotshort/strategy.go b/pkg/strategy/pivotshort/strategy.go index 1ce58c7a2..4d8f87338 100644 --- a/pkg/strategy/pivotshort/strategy.go +++ b/pkg/strategy/pivotshort/strategy.go @@ -70,8 +70,6 @@ type Entry struct { } type Strategy struct { - *bbgo.Graceful - Environment *bbgo.Environment Symbol string `json:"symbol"` Market types.Market @@ -124,6 +122,8 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { for i := range s.ExitMethods { m := s.ExitMethods[i] + + // we need to pass some information from the strategy configuration to the exit methods, like symbol, interval and window dynamic.MergeStructValues(&m, s) m.Subscribe(session) } @@ -369,7 +369,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se }) } - s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { _, _ = fmt.Fprintln(os.Stderr, s.TradeStats.String()) wg.Done() }) diff --git a/pkg/strategy/rsmaker/strategy.go b/pkg/strategy/rsmaker/strategy.go index 0d0c5a8fe..0704f8ae8 100644 --- a/pkg/strategy/rsmaker/strategy.go +++ b/pkg/strategy/rsmaker/strategy.go @@ -29,9 +29,6 @@ func init() { } type Strategy struct { - *bbgo.Graceful - *bbgo.Notifiability - Environment *bbgo.Environment StandardIndicatorSet *bbgo.StandardIndicatorSet Market types.Market diff --git a/pkg/strategy/supertrend/strategy.go b/pkg/strategy/supertrend/strategy.go index e91313be3..2f456a7d2 100644 --- a/pkg/strategy/supertrend/strategy.go +++ b/pkg/strategy/supertrend/strategy.go @@ -30,7 +30,6 @@ func init() { } type Strategy struct { - *bbgo.Graceful *bbgo.Persistence Environment *bbgo.Environment @@ -391,7 +390,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se }) // Graceful shutdown - s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() close(s.stopC) diff --git a/pkg/strategy/support/strategy.go b/pkg/strategy/support/strategy.go index 4a02e443a..d9a37b135 100644 --- a/pkg/strategy/support/strategy.go +++ b/pkg/strategy/support/strategy.go @@ -134,7 +134,6 @@ func (control *TrailingStopControl) GenerateStopOrder(quantity fixedpoint.Value) type Strategy struct { *bbgo.Persistence `json:"-"` *bbgo.Environment `json:"-"` - *bbgo.Graceful `json:"-"` session *bbgo.ExchangeSession @@ -582,7 +581,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } }) - s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() // Cancel trailing stop order diff --git a/pkg/strategy/wall/strategy.go b/pkg/strategy/wall/strategy.go index 824cc28cf..ffc9cef4a 100644 --- a/pkg/strategy/wall/strategy.go +++ b/pkg/strategy/wall/strategy.go @@ -30,7 +30,6 @@ func init() { } type Strategy struct { - *bbgo.Graceful *bbgo.Persistence Environment *bbgo.Environment @@ -377,7 +376,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } }() - s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() close(s.stopC) diff --git a/pkg/strategy/xbalance/strategy.go b/pkg/strategy/xbalance/strategy.go index 60f0ac6ae..ea4aa1e04 100644 --- a/pkg/strategy/xbalance/strategy.go +++ b/pkg/strategy/xbalance/strategy.go @@ -135,8 +135,6 @@ func (a *Address) UnmarshalJSON(body []byte) error { } type Strategy struct { - *bbgo.Graceful - Interval types.Duration `json:"interval"` Addresses map[string]Address `json:"addresses"` @@ -342,7 +340,7 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se s.State = s.newDefaultState() } - s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() }) diff --git a/pkg/strategy/xgap/strategy.go b/pkg/strategy/xgap/strategy.go index 102ea2a42..7406e5551 100644 --- a/pkg/strategy/xgap/strategy.go +++ b/pkg/strategy/xgap/strategy.go @@ -57,7 +57,6 @@ func (s *State) Reset() { } type Strategy struct { - *bbgo.Graceful *bbgo.Persistence Symbol string `json:"symbol"` @@ -193,7 +192,7 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se } } - s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() close(s.stopC) diff --git a/pkg/strategy/xmaker/strategy.go b/pkg/strategy/xmaker/strategy.go index 289b94208..aca499413 100644 --- a/pkg/strategy/xmaker/strategy.go +++ b/pkg/strategy/xmaker/strategy.go @@ -33,7 +33,6 @@ func init() { } type Strategy struct { - *bbgo.Graceful *bbgo.Persistence Environment *bbgo.Environment @@ -879,7 +878,7 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order } }() - s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() close(s.stopC) diff --git a/pkg/strategy/xnav/strategy.go b/pkg/strategy/xnav/strategy.go index f11582efb..ea9526f47 100644 --- a/pkg/strategy/xnav/strategy.go +++ b/pkg/strategy/xnav/strategy.go @@ -58,7 +58,6 @@ func (s *State) Reset() { } type Strategy struct { - *bbgo.Graceful *bbgo.Persistence *bbgo.Environment @@ -180,7 +179,7 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se return err } - s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() s.SaveState() From e2ab363e648ec28b512ba67dc1f2ce83de700eff Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 30 Jun 2022 14:32:33 +0800 Subject: [PATCH 22/92] dynamic: add CallStructFieldsMethod for map struct field call Signed-off-by: c9s --- pkg/bbgo/exit.go | 36 +++++++++++++++-------------------- pkg/dynamic/call.go | 41 ++++++++++++++++++++++++++++++++++++++++ pkg/dynamic/call_test.go | 29 ++++++++++++++++++++++++++++ pkg/types/subscribe.go | 5 ----- 4 files changed, 85 insertions(+), 26 deletions(-) create mode 100644 pkg/dynamic/call.go create mode 100644 pkg/dynamic/call_test.go delete mode 100644 pkg/types/subscribe.go diff --git a/pkg/bbgo/exit.go b/pkg/bbgo/exit.go index 11f97d8a8..a0880cf6d 100644 --- a/pkg/bbgo/exit.go +++ b/pkg/bbgo/exit.go @@ -1,10 +1,9 @@ package bbgo import ( - "reflect" + "github.com/pkg/errors" "github.com/c9s/bbgo/pkg/dynamic" - "github.com/c9s/bbgo/pkg/types" ) type ExitMethod struct { @@ -16,34 +15,29 @@ type ExitMethod struct { } func (m *ExitMethod) Subscribe(session *ExchangeSession) { - // TODO: pull out this implementation as a simple function to reflect.go - rv := reflect.ValueOf(m) - rt := reflect.TypeOf(m) - - rv = rv.Elem() - rt = rt.Elem() - infType := reflect.TypeOf((*types.Subscriber)(nil)).Elem() - - argValues := dynamic.ToReflectValues(session) - for i := 0; i < rt.NumField(); i++ { - fieldType := rt.Field(i) - if fieldType.Type.Implements(infType) { - method := rv.Field(i).MethodByName("Subscribe") - method.Call(argValues) - } + if err := dynamic.CallStructFieldsMethod(m, "Subscribe", session); err != nil { + panic(errors.Wrap(err, "dynamic call failed")) } } func (m *ExitMethod) Bind(session *ExchangeSession, orderExecutor *GeneralOrderExecutor) { if m.ProtectiveStopLoss != nil { m.ProtectiveStopLoss.Bind(session, orderExecutor) - } else if m.RoiStopLoss != nil { + } + + if m.RoiStopLoss != nil { m.RoiStopLoss.Bind(session, orderExecutor) - } else if m.RoiTakeProfit != nil { + } + + if m.RoiTakeProfit != nil { m.RoiTakeProfit.Bind(session, orderExecutor) - } else if m.LowerShadowTakeProfit != nil { + } + + if m.LowerShadowTakeProfit != nil { m.LowerShadowTakeProfit.Bind(session, orderExecutor) - } else if m.CumulatedVolumeTakeProfit != nil { + } + + if m.CumulatedVolumeTakeProfit != nil { m.CumulatedVolumeTakeProfit.Bind(session, orderExecutor) } } diff --git a/pkg/dynamic/call.go b/pkg/dynamic/call.go new file mode 100644 index 000000000..aa43c6f9d --- /dev/null +++ b/pkg/dynamic/call.go @@ -0,0 +1,41 @@ +package dynamic + +import ( + "errors" + "reflect" +) + +// CallStructFieldsMethod iterates field from the given struct object +// check if the field object implements the interface, if it's implemented, then we call a specific method +func CallStructFieldsMethod(m interface{}, method string, args ...interface{}) error { + rv := reflect.ValueOf(m) + rt := reflect.TypeOf(m) + + if rt.Kind() != reflect.Ptr { + return errors.New("the given object needs to be a pointer") + } + + rv = rv.Elem() + rt = rt.Elem() + + if rt.Kind() != reflect.Struct { + return errors.New("the given object needs to be struct") + } + + argValues := ToReflectValues(args...) + for i := 0; i < rt.NumField(); i++ { + fieldType := rt.Field(i) + + // skip non-exported fields + if !fieldType.IsExported() { + continue + } + + if _, ok := fieldType.Type.MethodByName(method); ok { + refMethod := rv.Field(i).MethodByName(method) + refMethod.Call(argValues) + } + } + + return nil +} diff --git a/pkg/dynamic/call_test.go b/pkg/dynamic/call_test.go new file mode 100644 index 000000000..5324218f9 --- /dev/null +++ b/pkg/dynamic/call_test.go @@ -0,0 +1,29 @@ +package dynamic + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +type callTest struct { + ChildCall1 *childCall1 + ChildCall2 *childCall2 +} + +type childCall1 struct{} + +func (c *childCall1) Subscribe(a int) {} + +type childCall2 struct{} + +func (c *childCall2) Subscribe(a int) {} + +func TestCallStructFieldsMethod(t *testing.T) { + c := &callTest{ + ChildCall1: &childCall1{}, + ChildCall2: &childCall2{}, + } + err := CallStructFieldsMethod(c, "Subscribe", 10) + assert.NoError(t, err) +} diff --git a/pkg/types/subscribe.go b/pkg/types/subscribe.go deleted file mode 100644 index 324dd04cb..000000000 --- a/pkg/types/subscribe.go +++ /dev/null @@ -1,5 +0,0 @@ -package types - -type Subscriber interface { - Subscribe() -} From b15e8d0ce4614b5e494007ce9ef57a01b2354fd2 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 30 Jun 2022 15:13:42 +0800 Subject: [PATCH 23/92] all: refactor exit method set and fix dynamic call/merge Signed-off-by: c9s --- pkg/bbgo/exit.go | 36 ++++++++++++++++++++++- pkg/bbgo/exit_lower_shadow_take_profit.go | 11 ++++++- pkg/dynamic/call.go | 18 ++++++++++-- pkg/dynamic/merge.go | 9 ++++++ pkg/dynamic/merge_test.go | 3 ++ pkg/dynamic/typevalue.go | 4 +-- pkg/strategy/pivotshort/strategy.go | 13 ++------ 7 files changed, 77 insertions(+), 17 deletions(-) diff --git a/pkg/bbgo/exit.go b/pkg/bbgo/exit.go index a0880cf6d..5b99ce982 100644 --- a/pkg/bbgo/exit.go +++ b/pkg/bbgo/exit.go @@ -1,11 +1,25 @@ package bbgo import ( + "reflect" + "github.com/pkg/errors" "github.com/c9s/bbgo/pkg/dynamic" ) +type ExitMethodSet []ExitMethod + +func (s *ExitMethodSet) SetAndSubscribe(session *ExchangeSession, parent interface{}) { + for i := range *s { + m := (*s)[i] + + // manually inherit configuration from strategy + m.Inherit(parent) + m.Subscribe(session) + } +} + type ExitMethod struct { RoiStopLoss *RoiStopLoss `json:"roiStopLoss"` ProtectiveStopLoss *ProtectiveStopLoss `json:"protectiveStopLoss"` @@ -14,9 +28,29 @@ type ExitMethod struct { CumulatedVolumeTakeProfit *CumulatedVolumeTakeProfit `json:"cumulatedVolumeTakeProfit"` } +// Inherit is used for inheriting properties from the given strategy struct +// for example, some exit method requires the default interval and symbol name from the strategy param object +func (m *ExitMethod) Inherit(parent interface{}) { + // we need to pass some information from the strategy configuration to the exit methods, like symbol, interval and window + rt := reflect.TypeOf(m).Elem() + rv := reflect.ValueOf(m).Elem() + for j := 0; j < rv.NumField(); j++ { + if !rt.Field(j).IsExported() { + continue + } + + fieldValue := rv.Field(j) + if fieldValue.Kind() == reflect.Ptr && fieldValue.IsNil() { + continue + } + + dynamic.MergeStructValues(fieldValue.Interface(), parent) + } +} + func (m *ExitMethod) Subscribe(session *ExchangeSession) { if err := dynamic.CallStructFieldsMethod(m, "Subscribe", session); err != nil { - panic(errors.Wrap(err, "dynamic call failed")) + panic(errors.Wrap(err, "dynamic Subscribe call failed")) } } diff --git a/pkg/bbgo/exit_lower_shadow_take_profit.go b/pkg/bbgo/exit_lower_shadow_take_profit.go index a6008223e..677bbd908 100644 --- a/pkg/bbgo/exit_lower_shadow_take_profit.go +++ b/pkg/bbgo/exit_lower_shadow_take_profit.go @@ -8,12 +8,21 @@ import ( ) type LowerShadowTakeProfit struct { - Ratio fixedpoint.Value `json:"ratio"` + // inherit from the strategy + types.IntervalWindow + // inherit from the strategy + Symbol string `json:"symbol"` + + Ratio fixedpoint.Value `json:"ratio"` session *ExchangeSession orderExecutor *GeneralOrderExecutor } +func (s *LowerShadowTakeProfit) Subscribe(session *ExchangeSession) { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) +} + func (s *LowerShadowTakeProfit) Bind(session *ExchangeSession, orderExecutor *GeneralOrderExecutor) { s.session = session s.orderExecutor = orderExecutor diff --git a/pkg/dynamic/call.go b/pkg/dynamic/call.go index aa43c6f9d..a3122faad 100644 --- a/pkg/dynamic/call.go +++ b/pkg/dynamic/call.go @@ -25,16 +25,28 @@ func CallStructFieldsMethod(m interface{}, method string, args ...interface{}) e argValues := ToReflectValues(args...) for i := 0; i < rt.NumField(); i++ { fieldType := rt.Field(i) + fieldValue := rv.Field(i) // skip non-exported fields if !fieldType.IsExported() { continue } - if _, ok := fieldType.Type.MethodByName(method); ok { - refMethod := rv.Field(i).MethodByName(method) - refMethod.Call(argValues) + if fieldType.Type.Kind() == reflect.Ptr && fieldValue.IsNil() { + continue } + + methodType, ok := fieldType.Type.MethodByName(method) + if !ok { + continue + } + + if len(argValues) < methodType.Type.NumIn() { + // return fmt.Errorf("method %v require %d args, %d given", methodType, methodType.Type.NumIn(), len(argValues)) + } + + refMethod := fieldValue.MethodByName(method) + refMethod.Call(argValues) } return nil diff --git a/pkg/dynamic/merge.go b/pkg/dynamic/merge.go index ac20f8152..7d87939e8 100644 --- a/pkg/dynamic/merge.go +++ b/pkg/dynamic/merge.go @@ -5,6 +5,10 @@ import "reflect" // MergeStructValues merges the field value from the source struct to the dest struct. // Only fields with the same type and the same name will be updated. func MergeStructValues(dst, src interface{}) { + if dst == nil { + return + } + rtA := reflect.TypeOf(dst) srcStructType := reflect.TypeOf(src) @@ -14,6 +18,11 @@ func MergeStructValues(dst, src interface{}) { for i := 0; i < rtA.NumField(); i++ { fieldType := rtA.Field(i) fieldName := fieldType.Name + + if !fieldType.IsExported() { + continue + } + // if there is a field with the same name if fieldSrcType, ok := srcStructType.FieldByName(fieldName); ok { // ensure that the type is the same diff --git a/pkg/dynamic/merge_test.go b/pkg/dynamic/merge_test.go index bc65e49c8..be7c2ee25 100644 --- a/pkg/dynamic/merge_test.go +++ b/pkg/dynamic/merge_test.go @@ -36,8 +36,10 @@ func Test_reflectMergeStructFields(t *testing.T) { iw := types.IntervalWindow{Interval: types.Interval1h, Window: 30} a := &struct { types.IntervalWindow + Symbol string }{ IntervalWindow: iw, + Symbol: "BTCUSDT", } b := &struct { Symbol string @@ -45,6 +47,7 @@ func Test_reflectMergeStructFields(t *testing.T) { }{} MergeStructValues(b, a) assert.Equal(t, iw, b.IntervalWindow) + assert.Equal(t, "BTCUSDT", b.Symbol) }) t.Run("non-zero embedded struct", func(t *testing.T) { diff --git a/pkg/dynamic/typevalue.go b/pkg/dynamic/typevalue.go index a12ccf416..3ca3f1c83 100644 --- a/pkg/dynamic/typevalue.go +++ b/pkg/dynamic/typevalue.go @@ -15,10 +15,10 @@ func NewTypeValueInterface(typ reflect.Type) interface{} { // ToReflectValues convert the go objects into reflect.Value slice func ToReflectValues(args ...interface{}) (values []reflect.Value) { - for _, arg := range args { + for i := range args { + arg := args[i] values = append(values, reflect.ValueOf(arg)) } return values } - diff --git a/pkg/strategy/pivotshort/strategy.go b/pkg/strategy/pivotshort/strategy.go index 4d8f87338..5cdb40661 100644 --- a/pkg/strategy/pivotshort/strategy.go +++ b/pkg/strategy/pivotshort/strategy.go @@ -10,7 +10,6 @@ import ( "github.com/sirupsen/logrus" "github.com/c9s/bbgo/pkg/bbgo" - "github.com/c9s/bbgo/pkg/dynamic" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/indicator" "github.com/c9s/bbgo/pkg/types" @@ -86,8 +85,8 @@ type Strategy struct { ResistanceShort *ResistanceShort `json:"resistanceShort"` - Entry Entry `json:"entry"` - ExitMethods []bbgo.ExitMethod `json:"exits"` + Entry Entry `json:"entry"` + ExitMethods bbgo.ExitMethodSet `json:"exits"` session *bbgo.ExchangeSession orderExecutor *bbgo.GeneralOrderExecutor @@ -120,13 +119,7 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { session.Subscribe(types.MarketTradeChannel, s.Symbol, types.SubscribeOptions{}) } - for i := range s.ExitMethods { - m := s.ExitMethods[i] - - // we need to pass some information from the strategy configuration to the exit methods, like symbol, interval and window - dynamic.MergeStructValues(&m, s) - m.Subscribe(session) - } + s.ExitMethods.SetAndSubscribe(session, s) } func (s *Strategy) InstanceID() string { From 903d773025cb1163593010d4a0a5be46c848dd32 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 30 Jun 2022 15:48:06 +0800 Subject: [PATCH 24/92] dynamic: invert if Signed-off-by: c9s --- pkg/dynamic/merge.go | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/pkg/dynamic/merge.go b/pkg/dynamic/merge.go index 7d87939e8..aed5a442f 100644 --- a/pkg/dynamic/merge.go +++ b/pkg/dynamic/merge.go @@ -24,14 +24,17 @@ func MergeStructValues(dst, src interface{}) { } // if there is a field with the same name - if fieldSrcType, ok := srcStructType.FieldByName(fieldName); ok { - // ensure that the type is the same - if fieldSrcType.Type == fieldType.Type { - srcValue := reflect.ValueOf(src).Elem().FieldByName(fieldName) - dstValue := reflect.ValueOf(dst).Elem().FieldByName(fieldName) - if (fieldType.Type.Kind() == reflect.Ptr && dstValue.IsNil()) || dstValue.IsZero() { - dstValue.Set(srcValue) - } + fieldSrcType, found := srcStructType.FieldByName(fieldName) + if !found { + continue + } + + // ensure that the type is the same + if fieldSrcType.Type == fieldType.Type { + srcValue := reflect.ValueOf(src).Elem().FieldByName(fieldName) + dstValue := reflect.ValueOf(dst).Elem().FieldByName(fieldName) + if (fieldType.Type.Kind() == reflect.Ptr && dstValue.IsNil()) || dstValue.IsZero() { + dstValue.Set(srcValue) } } } From 6aa6e57d96cf861f2ddaad58a9783286adda31d4 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 30 Jun 2022 17:42:23 +0800 Subject: [PATCH 25/92] add ema condition to the lower shadow take profit --- config/pivotshort.yaml | 9 ++++++--- pkg/bbgo/exit_lower_shadow_take_profit.go | 9 +++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/config/pivotshort.yaml b/config/pivotshort.yaml index 76a7fdbe6..e1bfa89c1 100644 --- a/config/pivotshort.yaml +++ b/config/pivotshort.yaml @@ -39,12 +39,13 @@ exchangeStrategies: # stopEMARange is the price range we allow short. # Short-allowed price range = [current price] > [EMA] * (1 - [stopEMARange]) - stopEMARange: 0% + # Higher the stopEMARange than higher the chance to open a short + stopEMARange: 1% stopEMA: interval: 1h window: 99 - bounceShort: + resistanceShort: enabled: false interval: 1h window: 10 @@ -86,11 +87,13 @@ exchangeStrategies: # you can grab a simple stats by the following SQL: # SELECT ((close - low) / close) AS shadow_ratio FROM binance_klines WHERE symbol = 'ETHUSDT' AND `interval` = '5m' AND start_time > '2022-01-01' ORDER BY shadow_ratio DESC LIMIT 20; - lowerShadowTakeProfit: + interval: 30m + window: 99 ratio: 3% # (5) cumulatedVolumeTakeProfit is used to take profit when the cumulated quote volume from the klines exceeded a threshold - cumulatedVolumeTakeProfit: - minQuoteVolume: 100_000_000 + minQuoteVolume: 200_000_000 interval: 5m window: 2 diff --git a/pkg/bbgo/exit_lower_shadow_take_profit.go b/pkg/bbgo/exit_lower_shadow_take_profit.go index 677bbd908..ba598a69e 100644 --- a/pkg/bbgo/exit_lower_shadow_take_profit.go +++ b/pkg/bbgo/exit_lower_shadow_take_profit.go @@ -27,6 +27,10 @@ func (s *LowerShadowTakeProfit) Bind(session *ExchangeSession, orderExecutor *Ge s.session = session s.orderExecutor = orderExecutor + stdIndicatorSet, _ := session.StandardIndicatorSet(s.Symbol) + ewma := stdIndicatorSet.EWMA(s.IntervalWindow) + + position := orderExecutor.Position() session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { if kline.Symbol != position.Symbol || kline.Interval != types.Interval1m { @@ -47,6 +51,11 @@ func (s *LowerShadowTakeProfit) Bind(session *ExchangeSession, orderExecutor *Ge return } + // skip close price higher than the ewma + if closePrice.Float64() > ewma.Last() { + return + } + if kline.GetLowerShadowHeight().Div(kline.Close).Compare(s.Ratio) > 0 { Notify("%s TakeProfit triggered by shadow ratio %f, price = %f", position.Symbol, From bfcbf8566e5b55c8ace9591c2ed193fe6be3b01f Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 30 Jun 2022 17:46:42 +0800 Subject: [PATCH 26/92] config: adjust default stop ema range --- config/pivotshort.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/pivotshort.yaml b/config/pivotshort.yaml index e1bfa89c1..3a9bb4ad0 100644 --- a/config/pivotshort.yaml +++ b/config/pivotshort.yaml @@ -40,7 +40,7 @@ exchangeStrategies: # stopEMARange is the price range we allow short. # Short-allowed price range = [current price] > [EMA] * (1 - [stopEMARange]) # Higher the stopEMARange than higher the chance to open a short - stopEMARange: 1% + stopEMARange: 2% stopEMA: interval: 1h window: 99 @@ -93,9 +93,9 @@ exchangeStrategies: # (5) cumulatedVolumeTakeProfit is used to take profit when the cumulated quote volume from the klines exceeded a threshold - cumulatedVolumeTakeProfit: - minQuoteVolume: 200_000_000 interval: 5m window: 2 + minQuoteVolume: 200_000_000 backtest: sessions: From 3e6b975c2c89b84089c7384bbe88948afd019cc4 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 30 Jun 2022 18:29:02 +0800 Subject: [PATCH 27/92] pivotshort: refactor ResistanceShort entry method --- config/pivotshort.yaml | 8 +- pkg/bbgo/exit.go | 2 +- pkg/dynamic/merge.go | 4 +- pkg/dynamic/merge_test.go | 10 +- pkg/strategy/pivotshort/strategy.go | 246 ++++++++++++++-------------- pkg/types/position.go | 4 + 6 files changed, 144 insertions(+), 130 deletions(-) diff --git a/config/pivotshort.yaml b/config/pivotshort.yaml index 3a9bb4ad0..ea5aa6f61 100644 --- a/config/pivotshort.yaml +++ b/config/pivotshort.yaml @@ -46,12 +46,14 @@ exchangeStrategies: window: 99 resistanceShort: - enabled: false + enabled: true interval: 1h - window: 10 + window: 8 + quantity: 10.0 + + # minDistance is used to ignore the place that is too near to the current price minDistance: 3% - # stopLossPercentage: 1% # ratio is the ratio of the resistance price, # higher the ratio, lower the price diff --git a/pkg/bbgo/exit.go b/pkg/bbgo/exit.go index 5b99ce982..2701fdb94 100644 --- a/pkg/bbgo/exit.go +++ b/pkg/bbgo/exit.go @@ -44,7 +44,7 @@ func (m *ExitMethod) Inherit(parent interface{}) { continue } - dynamic.MergeStructValues(fieldValue.Interface(), parent) + dynamic.InheritStructValues(fieldValue.Interface(), parent) } } diff --git a/pkg/dynamic/merge.go b/pkg/dynamic/merge.go index aed5a442f..8e44bd333 100644 --- a/pkg/dynamic/merge.go +++ b/pkg/dynamic/merge.go @@ -2,9 +2,9 @@ package dynamic import "reflect" -// MergeStructValues merges the field value from the source struct to the dest struct. +// InheritStructValues merges the field value from the source struct to the dest struct. // Only fields with the same type and the same name will be updated. -func MergeStructValues(dst, src interface{}) { +func InheritStructValues(dst, src interface{}) { if dst == nil { return } diff --git a/pkg/dynamic/merge_test.go b/pkg/dynamic/merge_test.go index be7c2ee25..ca61355b0 100644 --- a/pkg/dynamic/merge_test.go +++ b/pkg/dynamic/merge_test.go @@ -21,14 +21,14 @@ func Test_reflectMergeStructFields(t *testing.T) { t.Run("zero value", func(t *testing.T) { a := &TestStrategy{Symbol: "BTCUSDT"} b := &struct{ Symbol string }{Symbol: ""} - MergeStructValues(b, a) + InheritStructValues(b, a) assert.Equal(t, "BTCUSDT", b.Symbol) }) t.Run("non-zero value", func(t *testing.T) { a := &TestStrategy{Symbol: "BTCUSDT"} b := &struct{ Symbol string }{Symbol: "ETHUSDT"} - MergeStructValues(b, a) + InheritStructValues(b, a) assert.Equal(t, "ETHUSDT", b.Symbol, "should be the original value") }) @@ -45,7 +45,7 @@ func Test_reflectMergeStructFields(t *testing.T) { Symbol string types.IntervalWindow }{} - MergeStructValues(b, a) + InheritStructValues(b, a) assert.Equal(t, iw, b.IntervalWindow) assert.Equal(t, "BTCUSDT", b.Symbol) }) @@ -62,7 +62,7 @@ func Test_reflectMergeStructFields(t *testing.T) { }{ IntervalWindow: types.IntervalWindow{Interval: types.Interval5m, Window: 9}, } - MergeStructValues(b, a) + InheritStructValues(b, a) assert.Equal(t, types.IntervalWindow{Interval: types.Interval5m, Window: 9}, b.IntervalWindow) }) @@ -75,7 +75,7 @@ func Test_reflectMergeStructFields(t *testing.T) { b := &struct { A string }{} - MergeStructValues(b, a) + InheritStructValues(b, a) assert.Equal(t, "", b.A) assert.Equal(t, 1.99, a.A) }) diff --git a/pkg/strategy/pivotshort/strategy.go b/pkg/strategy/pivotshort/strategy.go index 5cdb40661..a55094833 100644 --- a/pkg/strategy/pivotshort/strategy.go +++ b/pkg/strategy/pivotshort/strategy.go @@ -10,6 +10,7 @@ import ( "github.com/sirupsen/logrus" "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/dynamic" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/indicator" "github.com/c9s/bbgo/pkg/types" @@ -48,7 +49,9 @@ type BreakLow struct { } type ResistanceShort struct { - Enabled bool `json:"enabled"` + Enabled bool `json:"enabled"` + Symbol string `json:"-"` + Market types.Market `json:"-"` types.IntervalWindow @@ -57,15 +60,126 @@ type ResistanceShort struct { LayerSpread fixedpoint.Value `json:"layerSpread"` Quantity fixedpoint.Value `json:"quantity"` Ratio fixedpoint.Value `json:"ratio"` + + session *bbgo.ExchangeSession + orderExecutor *bbgo.GeneralOrderExecutor + + resistancePivot *indicator.Pivot + resistancePrices []float64 + nextResistancePrice fixedpoint.Value + + resistanceOrders []types.Order } -type Entry struct { - CatBounceRatio fixedpoint.Value `json:"catBounceRatio"` - NumLayers int `json:"numLayers"` - TotalQuantity fixedpoint.Value `json:"totalQuantity"` +func (s *ResistanceShort) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor) { + s.session = session + s.orderExecutor = orderExecutor - Quantity fixedpoint.Value `json:"quantity"` - MarginSideEffect types.MarginOrderSideEffectType `json:"marginOrderSideEffect"` + position := orderExecutor.Position() + symbol := position.Symbol + store, _ := session.MarketDataStore(symbol) + + s.resistancePivot = &indicator.Pivot{IntervalWindow: s.IntervalWindow} + s.resistancePivot.Bind(store) + + // preload history kline data to the resistance pivot indicator + // we use the last kline to find the higher lows + lastKLine := preloadPivot(s.resistancePivot, store) + + // use the last kline from the history before we get the next closed kline + s.findNextResistancePriceAndPlaceOrders(lastKLine.Close) + + session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { + if kline.Symbol != s.Symbol || kline.Interval != s.Interval { + return + } + + s.findNextResistancePriceAndPlaceOrders(kline.Close) + }) +} + +func (s *ResistanceShort) findNextResistancePriceAndPlaceOrders(closePrice fixedpoint.Value) { + position := s.orderExecutor.Position() + if position.IsOpened(closePrice) { + return + } + + minDistance := s.MinDistance.Float64() + lows := s.resistancePivot.Lows + resistancePrices := findPossibleResistancePrices(closePrice.Float64(), minDistance, lows) + + log.Infof("last price: %f, possible resistance prices: %+v", closePrice.Float64(), resistancePrices) + + ctx := context.Background() + if len(resistancePrices) > 0 { + nextResistancePrice := fixedpoint.NewFromFloat(resistancePrices[0]) + if nextResistancePrice.Compare(s.nextResistancePrice) != 0 { + s.nextResistancePrice = nextResistancePrice + s.placeResistanceOrders(ctx, nextResistancePrice) + } + } +} + +func (s *ResistanceShort) placeResistanceOrders(ctx context.Context, resistancePrice fixedpoint.Value) { + futuresMode := s.session.Futures || s.session.IsolatedFutures + _ = futuresMode + + totalQuantity := s.Quantity + numLayers := s.NumLayers + if numLayers == 0 { + numLayers = 1 + } + + numLayersF := fixedpoint.NewFromInt(int64(numLayers)) + layerSpread := s.LayerSpread + quantity := totalQuantity.Div(numLayersF) + + if err := s.orderExecutor.CancelOrders(ctx, s.resistanceOrders...); err != nil { + log.WithError(err).Errorf("can not cancel resistance orders: %+v", s.resistanceOrders) + } + s.resistanceOrders = nil + + log.Infof("placing resistance orders: resistance price = %f, layer quantity = %f, num of layers = %d", resistancePrice.Float64(), quantity.Float64(), numLayers) + + var orderForms []types.SubmitOrder + for i := 0; i < numLayers; i++ { + balances := s.session.GetAccount().Balances() + quoteBalance := balances[s.Market.QuoteCurrency] + baseBalance := balances[s.Market.BaseCurrency] + _ = quoteBalance + _ = baseBalance + + // price = (resistance_price * (1.0 + ratio)) * ((1.0 + layerSpread) * i) + price := resistancePrice.Mul(fixedpoint.One.Add(s.Ratio)) + spread := layerSpread.Mul(fixedpoint.NewFromInt(int64(i))) + price = price.Add(spread) + log.Infof("price = %f", price.Float64()) + + log.Infof("placing bounce short order #%d: price = %f, quantity = %f", i, price.Float64(), quantity.Float64()) + + orderForms = append(orderForms, types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeSell, + Type: types.OrderTypeLimitMaker, + Price: price, + Quantity: quantity, + Tag: "resistanceShort", + }) + + // TODO: fix futures mode later + /* + if futuresMode { + if quantity.Mul(price).Compare(quoteBalance.Available) <= 0 { + } + } + */ + } + + createdOrders, err := s.orderExecutor.SubmitOrders(ctx, orderForms...) + if err != nil { + log.WithError(err).Errorf("can not place resistance order") + } + s.resistanceOrders = createdOrders } type Strategy struct { @@ -81,11 +195,12 @@ type Strategy struct { ProfitStats *types.ProfitStats `persistence:"profit_stats"` TradeStats *types.TradeStats `persistence:"trade_stats"` + // BreakLow is one of the entry method BreakLow BreakLow `json:"breakLow"` + // ResistanceShort is one of the entry method ResistanceShort *ResistanceShort `json:"resistanceShort"` - Entry Entry `json:"entry"` ExitMethods bbgo.ExitMethodSet `json:"exits"` session *bbgo.ExchangeSession @@ -96,7 +211,6 @@ type Strategy struct { resistancePivot *indicator.Pivot stopEWMA *indicator.EWMA pivotLowPrices []fixedpoint.Value - resistancePrices []float64 currentBounceShortPrice fixedpoint.Value // StrategyController @@ -112,6 +226,7 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: types.Interval1m}) if s.ResistanceShort != nil && s.ResistanceShort.Enabled { + dynamic.InheritStructValues(s.ResistanceShort, s) session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.ResistanceShort.Interval}) } @@ -182,8 +297,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.pivot = &indicator.Pivot{IntervalWindow: s.IntervalWindow} s.pivot.Bind(store) - - lastKLine := preloadPivot(s.pivot, store) + preloadPivot(s.pivot, store) // update pivot low data session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { @@ -204,11 +318,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.pivotLowPrices = append(s.pivotLowPrices, s.lastLow) }) - if s.ResistanceShort != nil && s.ResistanceShort.Enabled { - s.resistancePivot = &indicator.Pivot{IntervalWindow: s.ResistanceShort.IntervalWindow} - s.resistancePivot.Bind(store) - } - if s.BreakLow.StopEMA != nil { s.stopEWMA = standardIndicator.EWMA(*s.BreakLow.StopEMA) } @@ -218,35 +327,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } if s.ResistanceShort != nil && s.ResistanceShort.Enabled { - if s.resistancePivot != nil { - preloadPivot(s.resistancePivot, store) - } - - session.UserDataStream.OnStart(func() { - if lastKLine == nil { - return - } - - if s.resistancePivot != nil { - lows := s.resistancePivot.Lows - minDistance := s.ResistanceShort.MinDistance.Float64() - closePrice := lastKLine.Close.Float64() - s.resistancePrices = findPossibleResistancePrices(closePrice, minDistance, lows) - log.Infof("last price: %f, possible resistance prices: %+v", closePrice, s.resistancePrices) - - if len(s.resistancePrices) > 0 { - resistancePrice := fixedpoint.NewFromFloat(s.resistancePrices[0]) - if resistancePrice.Compare(s.currentBounceShortPrice) != 0 { - log.Infof("updating resistance price... possible resistance prices: %+v", s.resistancePrices) - - _ = s.orderExecutor.GracefulCancel(ctx) - - s.currentBounceShortPrice = resistancePrice - s.placeBounceSellOrders(ctx, s.currentBounceShortPrice) - } - } - } - }) + s.ResistanceShort.Bind(session, s.orderExecutor) } // Always check whether you can open a short position or not @@ -321,40 +402,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } }) - session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { - // StrategyController - if s.Status != types.StrategyStatusRunning { - return - } - - if s.ResistanceShort == nil || !s.ResistanceShort.Enabled { - return - } - - if kline.Symbol != s.Symbol || kline.Interval != s.ResistanceShort.Interval { - return - } - - if s.resistancePivot != nil { - closePrice := kline.Close.Float64() - minDistance := s.ResistanceShort.MinDistance.Float64() - lows := s.resistancePivot.Lows - s.resistancePrices = findPossibleResistancePrices(closePrice, minDistance, lows) - - if len(s.resistancePrices) > 0 { - resistancePrice := fixedpoint.NewFromFloat(s.resistancePrices[0]) - if resistancePrice.Compare(s.currentBounceShortPrice) != 0 { - log.Infof("updating resistance price... possible resistance prices: %+v", s.resistancePrices) - - _ = s.orderExecutor.GracefulCancel(ctx) - - s.currentBounceShortPrice = resistancePrice - s.placeBounceSellOrders(ctx, s.currentBounceShortPrice) - } - } - } - }) - if !bbgo.IsBackTesting { // use market trade to submit short order session.MarketDataStream.OnMarketTrade(func(trade types.Trade) { @@ -380,46 +427,6 @@ func (s *Strategy) findHigherPivotLow(price fixedpoint.Value) (fixedpoint.Value, return price, false } -func (s *Strategy) placeBounceSellOrders(ctx context.Context, resistancePrice fixedpoint.Value) { - futuresMode := s.session.Futures || s.session.IsolatedFutures - totalQuantity := s.ResistanceShort.Quantity - numLayers := s.ResistanceShort.NumLayers - if numLayers == 0 { - numLayers = 1 - } - - numLayersF := fixedpoint.NewFromInt(int64(numLayers)) - - layerSpread := s.ResistanceShort.LayerSpread - quantity := totalQuantity.Div(numLayersF) - - log.Infof("placing bounce short orders: resistance price = %f, layer quantity = %f, num of layers = %d", resistancePrice.Float64(), quantity.Float64(), numLayers) - - for i := 0; i < numLayers; i++ { - balances := s.session.GetAccount().Balances() - quoteBalance := balances[s.Market.QuoteCurrency] - baseBalance := balances[s.Market.BaseCurrency] - - // price = (resistance_price * (1.0 + ratio)) * ((1.0 + layerSpread) * i) - price := resistancePrice.Mul(fixedpoint.One.Add(s.ResistanceShort.Ratio)) - spread := layerSpread.Mul(fixedpoint.NewFromInt(int64(i))) - price = price.Add(spread) - log.Infof("price = %f", price.Float64()) - - log.Infof("placing bounce short order #%d: price = %f, quantity = %f", i, price.Float64(), quantity.Float64()) - - if futuresMode { - if quantity.Mul(price).Compare(quoteBalance.Available) <= 0 { - s.placeOrder(ctx, price, quantity) - } - } else { - if quantity.Compare(baseBalance.Available) <= 0 { - s.placeOrder(ctx, price, quantity) - } - } - } -} - func (s *Strategy) placeOrder(ctx context.Context, price fixedpoint.Value, quantity fixedpoint.Value) { _, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ Symbol: s.Symbol, @@ -474,6 +481,7 @@ func (s *Strategy) placeMarketSell(ctx context.Context, quantity fixedpoint.Valu func findPossibleResistancePrices(closePrice float64, minDistance float64, lows []float64) []float64 { // sort float64 in increasing order + // lower to higher prices sort.Float64s(lows) var resistancePrices []float64 diff --git a/pkg/types/position.go b/pkg/types/position.go index 185106572..e96423006 100644 --- a/pkg/types/position.go +++ b/pkg/types/position.go @@ -273,6 +273,10 @@ func (p *Position) IsClosed() bool { return p.Base.Sign() == 0 } +func (p *Position) IsOpened(currentPrice fixedpoint.Value) bool { + return p.IsClosed() || !p.IsDust(currentPrice) +} + func (p *Position) Type() PositionType { if p.Base.Sign() > 0 { return PositionLong From fa98f3fda2fddef3ee023e26233908d1fdf640e7 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 30 Jun 2022 18:29:59 +0800 Subject: [PATCH 28/92] fix position.IsOpened method Signed-off-by: c9s --- pkg/types/position.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/types/position.go b/pkg/types/position.go index e96423006..a85d09057 100644 --- a/pkg/types/position.go +++ b/pkg/types/position.go @@ -274,7 +274,7 @@ func (p *Position) IsClosed() bool { } func (p *Position) IsOpened(currentPrice fixedpoint.Value) bool { - return p.IsClosed() || !p.IsDust(currentPrice) + return !p.IsClosed() && !p.IsDust(currentPrice) } func (p *Position) Type() PositionType { From a4af4776d218274a667f5cb511cee22eab7296ab Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 1 Jul 2022 00:57:19 +0800 Subject: [PATCH 29/92] pivotshort: use active orderbook to maintain the resistance orders --- pkg/bbgo/order_executor_general.go | 14 +++++++--- pkg/strategy/pivotshort/strategy.go | 40 +++++++++++++++-------------- 2 files changed, 31 insertions(+), 23 deletions(-) diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index 44ab8bb94..ebe01c42b 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -86,9 +86,9 @@ func (e *GeneralOrderExecutor) Bind() { e.tradeCollector.BindStream(e.session.UserDataStream) } +// CancelOrders cancels the given order objects directly func (e *GeneralOrderExecutor) CancelOrders(ctx context.Context, orders ...types.Order) error { - err := e.session.Exchange.CancelOrders(ctx, orders...) - return err + return e.session.Exchange.CancelOrders(ctx, orders...) } func (e *GeneralOrderExecutor) SubmitOrders(ctx context.Context, submitOrders ...types.SubmitOrder) (types.OrderSlice, error) { @@ -108,8 +108,9 @@ func (e *GeneralOrderExecutor) SubmitOrders(ctx context.Context, submitOrders .. return createdOrders, err } -func (e *GeneralOrderExecutor) GracefulCancel(ctx context.Context) error { - if err := e.activeMakerOrders.GracefulCancel(ctx, e.session.Exchange); err != nil { +// GracefulCancelActiveOrderBook cancels the orders from the active orderbook. +func (e *GeneralOrderExecutor) GracefulCancelActiveOrderBook(ctx context.Context, activeOrders *ActiveOrderBook) error { + if err := activeOrders.GracefulCancel(ctx, e.session.Exchange); err != nil { log.WithError(err).Errorf("graceful cancel order error") return err } @@ -118,6 +119,11 @@ func (e *GeneralOrderExecutor) GracefulCancel(ctx context.Context) error { return nil } +// GracefulCancel cancels all active maker orders +func (e *GeneralOrderExecutor) GracefulCancel(ctx context.Context) error { + return e.GracefulCancelActiveOrderBook(ctx, e.activeMakerOrders) +} + func (e *GeneralOrderExecutor) ClosePosition(ctx context.Context, percentage fixedpoint.Value, tags ...string) error { submitOrder := e.position.NewMarketCloseOrder(percentage) if submitOrder == nil { diff --git a/pkg/strategy/pivotshort/strategy.go b/pkg/strategy/pivotshort/strategy.go index a55094833..f99562e68 100644 --- a/pkg/strategy/pivotshort/strategy.go +++ b/pkg/strategy/pivotshort/strategy.go @@ -68,16 +68,16 @@ type ResistanceShort struct { resistancePrices []float64 nextResistancePrice fixedpoint.Value - resistanceOrders []types.Order + activeOrders *bbgo.ActiveOrderBook } func (s *ResistanceShort) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor) { s.session = session s.orderExecutor = orderExecutor + s.activeOrders = bbgo.NewActiveOrderBook(s.Symbol) + s.activeOrders.BindStream(session.UserDataStream) - position := orderExecutor.Position() - symbol := position.Symbol - store, _ := session.MarketDataStore(symbol) + store, _ := session.MarketDataStore(s.Symbol) s.resistancePivot = &indicator.Pivot{IntervalWindow: s.IntervalWindow} s.resistancePivot.Bind(store) @@ -87,22 +87,25 @@ func (s *ResistanceShort) Bind(session *bbgo.ExchangeSession, orderExecutor *bbg lastKLine := preloadPivot(s.resistancePivot, store) // use the last kline from the history before we get the next closed kline - s.findNextResistancePriceAndPlaceOrders(lastKLine.Close) + if lastKLine != nil { + s.findNextResistancePriceAndPlaceOrders(lastKLine.Close) + } session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { if kline.Symbol != s.Symbol || kline.Interval != s.Interval { return } + position := s.orderExecutor.Position() + if position.IsOpened(kline.Close) { + return + } + s.findNextResistancePriceAndPlaceOrders(kline.Close) }) } func (s *ResistanceShort) findNextResistancePriceAndPlaceOrders(closePrice fixedpoint.Value) { - position := s.orderExecutor.Position() - if position.IsOpened(closePrice) { - return - } minDistance := s.MinDistance.Float64() lows := s.resistancePivot.Lows @@ -114,6 +117,7 @@ func (s *ResistanceShort) findNextResistancePriceAndPlaceOrders(closePrice fixed if len(resistancePrices) > 0 { nextResistancePrice := fixedpoint.NewFromFloat(resistancePrices[0]) if nextResistancePrice.Compare(s.nextResistancePrice) != 0 { + bbgo.Notify("Found next resistance price: %f", nextResistancePrice.Float64()) s.nextResistancePrice = nextResistancePrice s.placeResistanceOrders(ctx, nextResistancePrice) } @@ -134,10 +138,9 @@ func (s *ResistanceShort) placeResistanceOrders(ctx context.Context, resistanceP layerSpread := s.LayerSpread quantity := totalQuantity.Div(numLayersF) - if err := s.orderExecutor.CancelOrders(ctx, s.resistanceOrders...); err != nil { - log.WithError(err).Errorf("can not cancel resistance orders: %+v", s.resistanceOrders) + if err := s.orderExecutor.CancelOrders(ctx, s.activeOrders.Orders()...); err != nil { + log.WithError(err).Errorf("can not cancel resistance orders: %+v", s.activeOrders.Orders()) } - s.resistanceOrders = nil log.Infof("placing resistance orders: resistance price = %f, layer quantity = %f, num of layers = %d", resistancePrice.Float64(), quantity.Float64(), numLayers) @@ -179,7 +182,7 @@ func (s *ResistanceShort) placeResistanceOrders(ctx context.Context, resistanceP if err != nil { log.WithError(err).Errorf("can not place resistance order") } - s.resistanceOrders = createdOrders + s.activeOrders.Add(createdOrders...) } type Strategy struct { @@ -206,12 +209,11 @@ type Strategy struct { session *bbgo.ExchangeSession orderExecutor *bbgo.GeneralOrderExecutor - lastLow fixedpoint.Value - pivot *indicator.Pivot - resistancePivot *indicator.Pivot - stopEWMA *indicator.EWMA - pivotLowPrices []fixedpoint.Value - currentBounceShortPrice fixedpoint.Value + lastLow fixedpoint.Value + pivot *indicator.Pivot + resistancePivot *indicator.Pivot + stopEWMA *indicator.EWMA + pivotLowPrices []fixedpoint.Value // StrategyController bbgo.StrategyController From 454036b1668c86ed26b11aba590c008981cab625 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 1 Jul 2022 01:06:10 +0800 Subject: [PATCH 30/92] use types.KLineWith to wrap callbacks --- pkg/strategy/pivotshort/strategy.go | 26 +++++++------------------- pkg/types/kline.go | 11 +++++++++++ 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/pkg/strategy/pivotshort/strategy.go b/pkg/strategy/pivotshort/strategy.go index f99562e68..034eea81b 100644 --- a/pkg/strategy/pivotshort/strategy.go +++ b/pkg/strategy/pivotshort/strategy.go @@ -91,18 +91,14 @@ func (s *ResistanceShort) Bind(session *bbgo.ExchangeSession, orderExecutor *bbg s.findNextResistancePriceAndPlaceOrders(lastKLine.Close) } - session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { - if kline.Symbol != s.Symbol || kline.Interval != s.Interval { - return - } - + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { position := s.orderExecutor.Position() if position.IsOpened(kline.Close) { return } s.findNextResistancePriceAndPlaceOrders(kline.Close) - }) + })) } func (s *ResistanceShort) findNextResistancePriceAndPlaceOrders(closePrice fixedpoint.Value) { @@ -302,11 +298,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se preloadPivot(s.pivot, store) // update pivot low data - session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { - if kline.Symbol != s.Symbol || kline.Interval != s.Interval { - return - } - + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { lastLow := fixedpoint.NewFromFloat(s.pivot.LastLow()) if lastLow.IsZero() { return @@ -318,7 +310,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.lastLow = lastLow s.pivotLowPrices = append(s.pivotLowPrices, s.lastLow) - }) + })) if s.BreakLow.StopEMA != nil { s.stopEWMA = standardIndicator.EWMA(*s.BreakLow.StopEMA) @@ -333,16 +325,12 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } // Always check whether you can open a short position or not - session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { if s.Status != types.StrategyStatusRunning { return } - if kline.Symbol != s.Symbol || kline.Interval != types.Interval1m { - return - } - - if !s.Position.IsClosed() && !s.Position.IsDust(kline.Close) { + if s.Position.IsOpened(kline.Close) { return } @@ -402,7 +390,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se bbgo.Notify("%s price %f breaks the previous low %f with ratio %f, submitting limit sell @ %f", s.Symbol, kline.Close.Float64(), previousLow.Float64(), s.BreakLow.Ratio.Float64(), sellPrice.Float64()) s.placeLimitSell(ctx, sellPrice, quantity, "breakLowLimit") } - }) + })) if !bbgo.IsBackTesting { // use market trade to submit short order diff --git a/pkg/types/kline.go b/pkg/types/kline.go index 6a92ffdf3..529d30d95 100644 --- a/pkg/types/kline.go +++ b/pkg/types/kline.go @@ -604,3 +604,14 @@ func (k *KLineSeries) Length() int { } var _ Series = &KLineSeries{} + +type KLineCallBack func(k KLine) + +func KLineWith(symbol string, interval Interval, callback KLineCallBack) KLineCallBack { + return func(k KLine) { + if k.Symbol != symbol || k.Interval != interval { + return + } + callback(k) + } +} From 503d851c9d1add3e2e679d16c2e1dc048f40ad00 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 1 Jul 2022 01:24:34 +0800 Subject: [PATCH 31/92] pivotshort: move resistance short to a single file --- pkg/strategy/pivotshort/resistance.go | 145 ++++++++++++++++++++++++++ pkg/strategy/pivotshort/strategy.go | 137 +----------------------- 2 files changed, 147 insertions(+), 135 deletions(-) create mode 100644 pkg/strategy/pivotshort/resistance.go diff --git a/pkg/strategy/pivotshort/resistance.go b/pkg/strategy/pivotshort/resistance.go new file mode 100644 index 000000000..b32af2e42 --- /dev/null +++ b/pkg/strategy/pivotshort/resistance.go @@ -0,0 +1,145 @@ +package pivotshort + +import ( + "context" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +type ResistanceShort struct { + Enabled bool `json:"enabled"` + Symbol string `json:"-"` + Market types.Market `json:"-"` + + types.IntervalWindow + + MinDistance fixedpoint.Value `json:"minDistance"` + NumLayers int `json:"numLayers"` + LayerSpread fixedpoint.Value `json:"layerSpread"` + Quantity fixedpoint.Value `json:"quantity"` + Ratio fixedpoint.Value `json:"ratio"` + + session *bbgo.ExchangeSession + orderExecutor *bbgo.GeneralOrderExecutor + + resistancePivot *indicator.Pivot + resistancePrices []float64 + nextResistancePrice fixedpoint.Value + + activeOrders *bbgo.ActiveOrderBook +} + +func (s *ResistanceShort) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor) { + s.session = session + s.orderExecutor = orderExecutor + s.activeOrders = bbgo.NewActiveOrderBook(s.Symbol) + s.activeOrders.BindStream(session.UserDataStream) + + store, _ := session.MarketDataStore(s.Symbol) + + s.resistancePivot = &indicator.Pivot{IntervalWindow: s.IntervalWindow} + s.resistancePivot.Bind(store) + + // preload history kline data to the resistance pivot indicator + // we use the last kline to find the higher lows + lastKLine := preloadPivot(s.resistancePivot, store) + + // use the last kline from the history before we get the next closed kline + if lastKLine != nil { + s.findNextResistancePriceAndPlaceOrders(lastKLine.Close) + } + + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { + position := s.orderExecutor.Position() + if position.IsOpened(kline.Close) { + return + } + + s.findNextResistancePriceAndPlaceOrders(kline.Close) + })) +} + +func (s *ResistanceShort) findNextResistancePriceAndPlaceOrders(closePrice fixedpoint.Value) { + + minDistance := s.MinDistance.Float64() + lows := s.resistancePivot.Lows + resistancePrices := findPossibleResistancePrices(closePrice.Float64(), minDistance, lows) + + log.Infof("last price: %f, possible resistance prices: %+v", closePrice.Float64(), resistancePrices) + + ctx := context.Background() + if len(resistancePrices) > 0 { + nextResistancePrice := fixedpoint.NewFromFloat(resistancePrices[0]) + if nextResistancePrice.Compare(s.nextResistancePrice) != 0 { + bbgo.Notify("Found next resistance price: %f", nextResistancePrice.Float64()) + s.nextResistancePrice = nextResistancePrice + s.placeResistanceOrders(ctx, nextResistancePrice) + } + } +} + +func (s *ResistanceShort) placeResistanceOrders(ctx context.Context, resistancePrice fixedpoint.Value) { + futuresMode := s.session.Futures || s.session.IsolatedFutures + _ = futuresMode + + totalQuantity := s.Quantity + numLayers := s.NumLayers + if numLayers == 0 { + numLayers = 1 + } + + numLayersF := fixedpoint.NewFromInt(int64(numLayers)) + layerSpread := s.LayerSpread + quantity := totalQuantity.Div(numLayersF) + + if s.activeOrders.NumOfOrders() > 0 { + if err := s.orderExecutor.GracefulCancelActiveOrderBook(ctx, s.activeOrders); err != nil { + log.WithError(err).Errorf("can not cancel resistance orders: %+v", s.activeOrders.Orders()) + } + } + + log.Infof("placing resistance orders: resistance price = %f, layer quantity = %f, num of layers = %d", resistancePrice.Float64(), quantity.Float64(), numLayers) + + var orderForms []types.SubmitOrder + for i := 0; i < numLayers; i++ { + balances := s.session.GetAccount().Balances() + quoteBalance := balances[s.Market.QuoteCurrency] + baseBalance := balances[s.Market.BaseCurrency] + _ = quoteBalance + _ = baseBalance + + // price = (resistance_price * (1.0 + ratio)) * ((1.0 + layerSpread) * i) + price := resistancePrice.Mul(fixedpoint.One.Add(s.Ratio)) + spread := layerSpread.Mul(fixedpoint.NewFromInt(int64(i))) + price = price.Add(spread) + log.Infof("price = %f", price.Float64()) + + log.Infof("placing bounce short order #%d: price = %f, quantity = %f", i, price.Float64(), quantity.Float64()) + + orderForms = append(orderForms, types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeSell, + Type: types.OrderTypeLimitMaker, + Price: price, + Quantity: quantity, + Tag: "resistanceShort", + }) + + // TODO: fix futures mode later + /* + if futuresMode { + if quantity.Mul(price).Compare(quoteBalance.Available) <= 0 { + } + } + */ + } + + createdOrders, err := s.orderExecutor.SubmitOrders(ctx, orderForms...) + if err != nil { + log.WithError(err).Errorf("can not place resistance order") + } + s.activeOrders.Add(createdOrders...) +} diff --git a/pkg/strategy/pivotshort/strategy.go b/pkg/strategy/pivotshort/strategy.go index 034eea81b..814a3bca1 100644 --- a/pkg/strategy/pivotshort/strategy.go +++ b/pkg/strategy/pivotshort/strategy.go @@ -48,139 +48,6 @@ type BreakLow struct { StopEMA *types.IntervalWindow `json:"stopEMA"` } -type ResistanceShort struct { - Enabled bool `json:"enabled"` - Symbol string `json:"-"` - Market types.Market `json:"-"` - - types.IntervalWindow - - MinDistance fixedpoint.Value `json:"minDistance"` - NumLayers int `json:"numLayers"` - LayerSpread fixedpoint.Value `json:"layerSpread"` - Quantity fixedpoint.Value `json:"quantity"` - Ratio fixedpoint.Value `json:"ratio"` - - session *bbgo.ExchangeSession - orderExecutor *bbgo.GeneralOrderExecutor - - resistancePivot *indicator.Pivot - resistancePrices []float64 - nextResistancePrice fixedpoint.Value - - activeOrders *bbgo.ActiveOrderBook -} - -func (s *ResistanceShort) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor) { - s.session = session - s.orderExecutor = orderExecutor - s.activeOrders = bbgo.NewActiveOrderBook(s.Symbol) - s.activeOrders.BindStream(session.UserDataStream) - - store, _ := session.MarketDataStore(s.Symbol) - - s.resistancePivot = &indicator.Pivot{IntervalWindow: s.IntervalWindow} - s.resistancePivot.Bind(store) - - // preload history kline data to the resistance pivot indicator - // we use the last kline to find the higher lows - lastKLine := preloadPivot(s.resistancePivot, store) - - // use the last kline from the history before we get the next closed kline - if lastKLine != nil { - s.findNextResistancePriceAndPlaceOrders(lastKLine.Close) - } - - session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { - position := s.orderExecutor.Position() - if position.IsOpened(kline.Close) { - return - } - - s.findNextResistancePriceAndPlaceOrders(kline.Close) - })) -} - -func (s *ResistanceShort) findNextResistancePriceAndPlaceOrders(closePrice fixedpoint.Value) { - - minDistance := s.MinDistance.Float64() - lows := s.resistancePivot.Lows - resistancePrices := findPossibleResistancePrices(closePrice.Float64(), minDistance, lows) - - log.Infof("last price: %f, possible resistance prices: %+v", closePrice.Float64(), resistancePrices) - - ctx := context.Background() - if len(resistancePrices) > 0 { - nextResistancePrice := fixedpoint.NewFromFloat(resistancePrices[0]) - if nextResistancePrice.Compare(s.nextResistancePrice) != 0 { - bbgo.Notify("Found next resistance price: %f", nextResistancePrice.Float64()) - s.nextResistancePrice = nextResistancePrice - s.placeResistanceOrders(ctx, nextResistancePrice) - } - } -} - -func (s *ResistanceShort) placeResistanceOrders(ctx context.Context, resistancePrice fixedpoint.Value) { - futuresMode := s.session.Futures || s.session.IsolatedFutures - _ = futuresMode - - totalQuantity := s.Quantity - numLayers := s.NumLayers - if numLayers == 0 { - numLayers = 1 - } - - numLayersF := fixedpoint.NewFromInt(int64(numLayers)) - layerSpread := s.LayerSpread - quantity := totalQuantity.Div(numLayersF) - - if err := s.orderExecutor.CancelOrders(ctx, s.activeOrders.Orders()...); err != nil { - log.WithError(err).Errorf("can not cancel resistance orders: %+v", s.activeOrders.Orders()) - } - - log.Infof("placing resistance orders: resistance price = %f, layer quantity = %f, num of layers = %d", resistancePrice.Float64(), quantity.Float64(), numLayers) - - var orderForms []types.SubmitOrder - for i := 0; i < numLayers; i++ { - balances := s.session.GetAccount().Balances() - quoteBalance := balances[s.Market.QuoteCurrency] - baseBalance := balances[s.Market.BaseCurrency] - _ = quoteBalance - _ = baseBalance - - // price = (resistance_price * (1.0 + ratio)) * ((1.0 + layerSpread) * i) - price := resistancePrice.Mul(fixedpoint.One.Add(s.Ratio)) - spread := layerSpread.Mul(fixedpoint.NewFromInt(int64(i))) - price = price.Add(spread) - log.Infof("price = %f", price.Float64()) - - log.Infof("placing bounce short order #%d: price = %f, quantity = %f", i, price.Float64(), quantity.Float64()) - - orderForms = append(orderForms, types.SubmitOrder{ - Symbol: s.Symbol, - Side: types.SideTypeSell, - Type: types.OrderTypeLimitMaker, - Price: price, - Quantity: quantity, - Tag: "resistanceShort", - }) - - // TODO: fix futures mode later - /* - if futuresMode { - if quantity.Mul(price).Compare(quoteBalance.Available) <= 0 { - } - } - */ - } - - createdOrders, err := s.orderExecutor.SubmitOrders(ctx, orderForms...) - if err != nil { - log.WithError(err).Errorf("can not place resistance order") - } - s.activeOrders.Add(createdOrders...) -} - type Strategy struct { Environment *bbgo.Environment Symbol string `json:"symbol"` @@ -356,8 +223,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return } - // we need the price cross the break line - // or we do nothing + // we need the price cross the break line or we do nothing if !(openPrice.Compare(breakPrice) > 0 && closePrice.Compare(breakPrice) < 0) { return } @@ -378,6 +244,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } } + // graceful cancel all active orders _ = s.orderExecutor.GracefulCancel(ctx) quantity := s.useQuantityOrBaseBalance(s.BreakLow.Quantity) From 910c17a56732460b057da4173fe4d62aeb0be2ee Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 1 Jul 2022 13:09:30 +0800 Subject: [PATCH 32/92] dynamic: implement CallWithMatch for dynamic calls Signed-off-by: c9s --- pkg/bbgo/injection_test.go | 106 ------------------ pkg/bbgo/trader.go | 10 +- pkg/dynamic/call.go | 98 ++++++++++++++++ pkg/dynamic/call_test.go | 86 ++++++++++++++ pkg/{bbgo/injection.go => dynamic/inject.go} | 111 ++++++++++++++++++- pkg/interact/interact.go | 4 +- pkg/interact/interact_test.go | 6 +- pkg/interact/parse.go | 8 +- 8 files changed, 304 insertions(+), 125 deletions(-) delete mode 100644 pkg/bbgo/injection_test.go rename pkg/{bbgo/injection.go => dynamic/inject.go} (54%) diff --git a/pkg/bbgo/injection_test.go b/pkg/bbgo/injection_test.go deleted file mode 100644 index 69a8b5f72..000000000 --- a/pkg/bbgo/injection_test.go +++ /dev/null @@ -1,106 +0,0 @@ -package bbgo - -import ( - "reflect" - "testing" - "time" - - "github.com/stretchr/testify/assert" - - "github.com/c9s/bbgo/pkg/dynamic" - "github.com/c9s/bbgo/pkg/service" - "github.com/c9s/bbgo/pkg/types" -) - -func Test_injectField(t *testing.T) { - type TT struct { - TradeService *service.TradeService - } - - // only pointer object can be set. - var tt = &TT{} - - // get the value of the pointer, or it can not be set. - var rv = reflect.ValueOf(tt).Elem() - - _, ret := dynamic.HasField(rv, "TradeService") - assert.True(t, ret) - - ts := &service.TradeService{} - - err := injectField(rv, "TradeService", ts, true) - assert.NoError(t, err) -} - -func Test_parseStructAndInject(t *testing.T) { - t.Run("skip nil", func(t *testing.T) { - ss := struct { - a int - Env *Environment - }{ - a: 1, - Env: nil, - } - err := parseStructAndInject(&ss, nil) - assert.NoError(t, err) - assert.Nil(t, ss.Env) - }) - t.Run("pointer", func(t *testing.T) { - ss := struct { - a int - Env *Environment - }{ - a: 1, - Env: nil, - } - err := parseStructAndInject(&ss, &Environment{}) - assert.NoError(t, err) - assert.NotNil(t, ss.Env) - }) - - t.Run("composition", func(t *testing.T) { - type TT struct { - *service.TradeService - } - ss := TT{} - err := parseStructAndInject(&ss, &service.TradeService{}) - assert.NoError(t, err) - assert.NotNil(t, ss.TradeService) - }) - - t.Run("struct", func(t *testing.T) { - ss := struct { - a int - Env Environment - }{ - a: 1, - } - err := parseStructAndInject(&ss, Environment{ - startTime: time.Now(), - }) - assert.NoError(t, err) - assert.NotEqual(t, time.Time{}, ss.Env.startTime) - }) - t.Run("interface/any", func(t *testing.T) { - ss := struct { - Any interface{} // anything - }{ - Any: nil, - } - err := parseStructAndInject(&ss, &Environment{ - startTime: time.Now(), - }) - assert.NoError(t, err) - assert.NotNil(t, ss.Any) - }) - t.Run("interface/stringer", func(t *testing.T) { - ss := struct { - Stringer types.Stringer // stringer interface - }{ - Stringer: nil, - } - err := parseStructAndInject(&ss, &types.Trade{}) - assert.NoError(t, err) - assert.NotNil(t, ss.Stringer) - }) -} diff --git a/pkg/bbgo/trader.go b/pkg/bbgo/trader.go index 86371a53b..e8cbc13ed 100644 --- a/pkg/bbgo/trader.go +++ b/pkg/bbgo/trader.go @@ -196,7 +196,7 @@ func (trader *Trader) RunSingleExchangeStrategy(ctx context.Context, strategy Si return err } - if err := injectField(rs, "OrderExecutor", orderExecutor, false); err != nil { + if err := dynamic.InjectField(rs, "OrderExecutor", orderExecutor, false); err != nil { return errors.Wrapf(err, "failed to inject OrderExecutor on %T", strategy) } @@ -218,7 +218,7 @@ func (trader *Trader) RunSingleExchangeStrategy(ctx context.Context, strategy Si return fmt.Errorf("marketDataStore of symbol %s not found", symbol) } - if err := parseStructAndInject(strategy, + if err := dynamic.ParseStructAndInject(strategy, market, indicatorSet, store, @@ -401,19 +401,19 @@ func (trader *Trader) injectCommonServices(s interface{}) error { return fmt.Errorf("field Persistence is not a struct element, %s given", field) } - if err := injectField(elem, "Facade", PersistenceServiceFacade, true); err != nil { + if err := dynamic.InjectField(elem, "Facade", PersistenceServiceFacade, true); err != nil { return err } /* - if err := parseStructAndInject(field.Interface(), persistenceFacade); err != nil { + if err := ParseStructAndInject(field.Interface(), persistenceFacade); err != nil { return err } */ } } - return parseStructAndInject(s, + return dynamic.ParseStructAndInject(s, &trader.logger, Notification, trader.environment.TradeService, diff --git a/pkg/dynamic/call.go b/pkg/dynamic/call.go index a3122faad..a4c45a0f1 100644 --- a/pkg/dynamic/call.go +++ b/pkg/dynamic/call.go @@ -51,3 +51,101 @@ func CallStructFieldsMethod(m interface{}, method string, args ...interface{}) e return nil } + +// CallMatch calls the function with the matched argument automatically +func CallMatch(f interface{}, objects ...interface{}) ([]reflect.Value, error) { + fv := reflect.ValueOf(f) + ft := reflect.TypeOf(f) + + var startIndex = 0 + var fArgs []reflect.Value + + var factoryParams = findFactoryParams(objects...) + +nextDynamicInputArg: + for i := 0; i < ft.NumIn(); i++ { + at := ft.In(i) + + // uat == underlying argument type + uat := at + if at.Kind() == reflect.Ptr { + uat = at.Elem() + } + + for oi := startIndex; oi < len(objects); oi++ { + var obj = objects[oi] + var objT = reflect.TypeOf(obj) + if objT == at { + fArgs = append(fArgs, reflect.ValueOf(obj)) + startIndex = oi + 1 + continue nextDynamicInputArg + } + + // get the kind of argument + switch k := uat.Kind(); k { + + case reflect.Interface: + if objT.Implements(at) { + fArgs = append(fArgs, reflect.ValueOf(obj)) + startIndex = oi + 1 + continue nextDynamicInputArg + } + } + } + + // factory param can be reused + for _, fp := range factoryParams { + fpt := fp.Type() + outType := fpt.Out(0) + if outType == at { + fOut := fp.Call(nil) + fArgs = append(fArgs, fOut[0]) + continue nextDynamicInputArg + } + } + + fArgs = append(fArgs, reflect.Zero(at)) + } + + out := fv.Call(fArgs) + if ft.NumOut() == 0 { + return out, nil + } + + // try to get the error object from the return value (if any) + var err error + for i := 0; i < ft.NumOut(); i++ { + outType := ft.Out(i) + switch outType.Kind() { + case reflect.Interface: + o := out[i].Interface() + switch ov := o.(type) { + case error: + err = ov + + } + + } + } + return out, err +} + +func findFactoryParams(objs ...interface{}) (fs []reflect.Value) { + for i := range objs { + obj := objs[i] + + objT := reflect.TypeOf(obj) + + if objT.Kind() != reflect.Func { + continue + } + + if objT.NumOut() == 0 || objT.NumIn() > 0 { + continue + } + + fs = append(fs, reflect.ValueOf(obj)) + } + + return fs +} diff --git a/pkg/dynamic/call_test.go b/pkg/dynamic/call_test.go index 5324218f9..b65029ded 100644 --- a/pkg/dynamic/call_test.go +++ b/pkg/dynamic/call_test.go @@ -27,3 +27,89 @@ func TestCallStructFieldsMethod(t *testing.T) { err := CallStructFieldsMethod(c, "Subscribe", 10) assert.NoError(t, err) } + +type S struct { + ID string +} + +func (s *S) String() string { return s.ID } + +func TestCallMatch(t *testing.T) { + t.Run("simple", func(t *testing.T) { + f := func(a int, b int) { + assert.Equal(t, 1, a) + assert.Equal(t, 2, b) + } + _, err := CallMatch(f, 1, 2) + assert.NoError(t, err) + }) + + t.Run("interface", func(t *testing.T) { + type A interface { + String() string + } + f := func(foo int, a A) { + assert.Equal(t, "foo", a.String()) + } + _, err := CallMatch(f, 10, &S{ID: "foo"}) + assert.NoError(t, err) + }) + + t.Run("nil interface", func(t *testing.T) { + type A interface { + String() string + } + f := func(foo int, a A) { + assert.Equal(t, 10, foo) + assert.Nil(t, a) + } + _, err := CallMatch(f, 10) + assert.NoError(t, err) + }) + + t.Run("struct pointer", func(t *testing.T) { + f := func(foo int, s *S) { + assert.Equal(t, 10, foo) + assert.NotNil(t, s) + } + _, err := CallMatch(f, 10, &S{}) + assert.NoError(t, err) + }) + + t.Run("struct pointer x 2", func(t *testing.T) { + f := func(foo int, s1, s2 *S) { + assert.Equal(t, 10, foo) + assert.Equal(t, "s1", s1.String()) + assert.Equal(t, "s2", s2.String()) + } + _, err := CallMatch(f, 10, &S{ID: "s1"}, &S{ID: "s2"}) + assert.NoError(t, err) + }) + + t.Run("func factory", func(t *testing.T) { + f := func(s *S) { + assert.Equal(t, "factory", s.String()) + } + _, err := CallMatch(f, func() *S { + return &S{ID: "factory"} + }) + assert.NoError(t, err) + }) + + t.Run("nil", func(t *testing.T) { + f := func(s *S) { + assert.Nil(t, s) + } + _, err := CallMatch(f) + assert.NoError(t, err) + }) + + t.Run("zero struct", func(t *testing.T) { + f := func(s S) { + assert.Equal(t, S{}, s) + } + _, err := CallMatch(f) + assert.NoError(t, err) + }) + +} diff --git a/pkg/bbgo/injection.go b/pkg/dynamic/inject.go similarity index 54% rename from pkg/bbgo/injection.go rename to pkg/dynamic/inject.go index 0db8cf228..04a48599b 100644 --- a/pkg/bbgo/injection.go +++ b/pkg/dynamic/inject.go @@ -1,13 +1,23 @@ -package bbgo +package dynamic import ( "fmt" "reflect" + "testing" + "time" "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/service" + "github.com/c9s/bbgo/pkg/types" ) -func injectField(rs reflect.Value, fieldName string, obj interface{}, pointerOnly bool) error { +type testEnvironment struct { + startTime time.Time +} + +func InjectField(rs reflect.Value, fieldName string, obj interface{}, pointerOnly bool) error { field := rs.FieldByName(fieldName) if !field.IsValid() { return nil @@ -38,10 +48,10 @@ func injectField(rs reflect.Value, fieldName string, obj interface{}, pointerOnl return nil } -// parseStructAndInject parses the struct fields and injects the objects into the corresponding fields by its type. +// ParseStructAndInject parses the struct fields and injects the objects into the corresponding fields by its type. // if the given object is a reference of an object, the type of the target field MUST BE a pointer field. // if the given object is a struct value, the type of the target field CAN BE a pointer field or a struct value field. -func parseStructAndInject(f interface{}, objects ...interface{}) error { +func ParseStructAndInject(f interface{}, objects ...interface{}) error { sv := reflect.ValueOf(f) st := reflect.TypeOf(f) @@ -121,3 +131,96 @@ func parseStructAndInject(f interface{}, objects ...interface{}) error { return nil } + +func Test_injectField(t *testing.T) { + type TT struct { + TradeService *service.TradeService + } + + // only pointer object can be set. + var tt = &TT{} + + // get the value of the pointer, or it can not be set. + var rv = reflect.ValueOf(tt).Elem() + + _, ret := HasField(rv, "TradeService") + assert.True(t, ret) + + ts := &service.TradeService{} + + err := InjectField(rv, "TradeService", ts, true) + assert.NoError(t, err) +} + +func Test_parseStructAndInject(t *testing.T) { + t.Run("skip nil", func(t *testing.T) { + ss := struct { + a int + Env *testEnvironment + }{ + a: 1, + Env: nil, + } + err := ParseStructAndInject(&ss, nil) + assert.NoError(t, err) + assert.Nil(t, ss.Env) + }) + t.Run("pointer", func(t *testing.T) { + ss := struct { + a int + Env *testEnvironment + }{ + a: 1, + Env: nil, + } + err := ParseStructAndInject(&ss, &testEnvironment{}) + assert.NoError(t, err) + assert.NotNil(t, ss.Env) + }) + + t.Run("composition", func(t *testing.T) { + type TT struct { + *service.TradeService + } + ss := TT{} + err := ParseStructAndInject(&ss, &service.TradeService{}) + assert.NoError(t, err) + assert.NotNil(t, ss.TradeService) + }) + + t.Run("struct", func(t *testing.T) { + ss := struct { + a int + Env testEnvironment + }{ + a: 1, + } + err := ParseStructAndInject(&ss, testEnvironment{ + startTime: time.Now(), + }) + assert.NoError(t, err) + assert.NotEqual(t, time.Time{}, ss.Env.startTime) + }) + t.Run("interface/any", func(t *testing.T) { + ss := struct { + Any interface{} // anything + }{ + Any: nil, + } + err := ParseStructAndInject(&ss, &testEnvironment{ + startTime: time.Now(), + }) + assert.NoError(t, err) + assert.NotNil(t, ss.Any) + }) + t.Run("interface/stringer", func(t *testing.T) { + ss := struct { + Stringer types.Stringer // stringer interface + }{ + Stringer: nil, + } + err := ParseStructAndInject(&ss, &types.Trade{}) + assert.NoError(t, err) + assert.NotNil(t, ss.Stringer) + }) +} diff --git a/pkg/interact/interact.go b/pkg/interact/interact.go index a19b56210..820979cfe 100644 --- a/pkg/interact/interact.go +++ b/pkg/interact/interact.go @@ -112,7 +112,7 @@ func (it *Interact) handleResponse(session Session, text string, ctxObjects ...i } ctxObjects = append(ctxObjects, session) - _, err := parseFuncArgsAndCall(f, args, ctxObjects...) + _, err := ParseFuncArgsAndCall(f, args, ctxObjects...) if err != nil { return err } @@ -154,7 +154,7 @@ func (it *Interact) runCommand(session Session, command string, args []string, c ctxObjects = append(ctxObjects, session) session.SetState(cmd.initState) - if _, err := parseFuncArgsAndCall(cmd.F, args, ctxObjects...); err != nil { + if _, err := ParseFuncArgsAndCall(cmd.F, args, ctxObjects...); err != nil { return err } diff --git a/pkg/interact/interact_test.go b/pkg/interact/interact_test.go index bd0828240..8402ba1c8 100644 --- a/pkg/interact/interact_test.go +++ b/pkg/interact/interact_test.go @@ -18,7 +18,7 @@ func Test_parseFuncArgsAndCall_NoErrorFunction(t *testing.T) { return nil } - _, err := parseFuncArgsAndCall(noErrorFunc, []string{"BTCUSDT", "0.123", "true"}) + _, err := ParseFuncArgsAndCall(noErrorFunc, []string{"BTCUSDT", "0.123", "true"}) assert.NoError(t, err) } @@ -27,7 +27,7 @@ func Test_parseFuncArgsAndCall_ErrorFunction(t *testing.T) { return errors.New("error") } - _, err := parseFuncArgsAndCall(errorFunc, []string{"BTCUSDT", "0.123"}) + _, err := ParseFuncArgsAndCall(errorFunc, []string{"BTCUSDT", "0.123"}) assert.Error(t, err) } @@ -38,7 +38,7 @@ func Test_parseFuncArgsAndCall_InterfaceInjection(t *testing.T) { } buf := bytes.NewBuffer(nil) - _, err := parseFuncArgsAndCall(f, []string{"BTCUSDT", "0.123"}, buf) + _, err := ParseFuncArgsAndCall(f, []string{"BTCUSDT", "0.123"}, buf) assert.NoError(t, err) assert.Equal(t, "123", buf.String()) } diff --git a/pkg/interact/parse.go b/pkg/interact/parse.go index db4f3d1fd..64f55871b 100644 --- a/pkg/interact/parse.go +++ b/pkg/interact/parse.go @@ -10,21 +10,20 @@ import ( log "github.com/sirupsen/logrus" ) -func parseFuncArgsAndCall(f interface{}, args []string, objects ...interface{}) (State, error) { +func ParseFuncArgsAndCall(f interface{}, args []string, objects ...interface{}) (State, error) { fv := reflect.ValueOf(f) ft := reflect.TypeOf(f) - argIndex := 0 var rArgs []reflect.Value for i := 0; i < ft.NumIn(); i++ { at := ft.In(i) + // get the kind of argument switch k := at.Kind(); k { case reflect.Interface: found := false - for oi := 0; oi < len(objects); oi++ { obj := objects[oi] objT := reflect.TypeOf(obj) @@ -90,8 +89,8 @@ func parseFuncArgsAndCall(f interface{}, args []string, objects ...interface{}) } // try to get the error object from the return value - var state State var err error + var state State for i := 0; i < ft.NumOut(); i++ { outType := ft.Out(i) switch outType.Kind() { @@ -107,7 +106,6 @@ func parseFuncArgsAndCall(f interface{}, args []string, objects ...interface{}) err = ov } - } } return state, err From 8851e67356376cb7a550f167054c82bf5ae3e730 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 1 Jul 2022 13:10:53 +0800 Subject: [PATCH 33/92] dynamic: add doc comment to CallMatch Signed-off-by: c9s --- pkg/dynamic/call.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/dynamic/call.go b/pkg/dynamic/call.go index a4c45a0f1..4ba59484d 100644 --- a/pkg/dynamic/call.go +++ b/pkg/dynamic/call.go @@ -53,6 +53,10 @@ func CallStructFieldsMethod(m interface{}, method string, args ...interface{}) e } // CallMatch calls the function with the matched argument automatically +// you can define multiple parameter factory function to inject the return value as the function argument. +// e.g., +// CallMatch(targetFunction, 1, 10, true, func() *ParamType { .... }) +// func CallMatch(f interface{}, objects ...interface{}) ([]reflect.Value, error) { fv := reflect.ValueOf(f) ft := reflect.TypeOf(f) From 1af18a5fac5b30584bc26371ce255c3dfb346596 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 1 Jul 2022 15:30:06 +0800 Subject: [PATCH 34/92] pivotshort: fix breakLow handle event --- pkg/bbgo/exit_roi_stop_loss.go | 14 ++++++++------ pkg/strategy/pivotshort/strategy.go | 3 +-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/pkg/bbgo/exit_roi_stop_loss.go b/pkg/bbgo/exit_roi_stop_loss.go index 1cfb386a6..875934f41 100644 --- a/pkg/bbgo/exit_roi_stop_loss.go +++ b/pkg/bbgo/exit_roi_stop_loss.go @@ -8,24 +8,26 @@ import ( ) type RoiStopLoss struct { + Symbol string Percentage fixedpoint.Value `json:"percentage"` session *ExchangeSession orderExecutor *GeneralOrderExecutor } +func (s *RoiStopLoss) Subscribe(session *ExchangeSession) { + // use 1m kline to handle roi stop + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: types.Interval1m}) +} + func (s *RoiStopLoss) Bind(session *ExchangeSession, orderExecutor *GeneralOrderExecutor) { s.session = session s.orderExecutor = orderExecutor position := orderExecutor.Position() - session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { - if kline.Symbol != position.Symbol || kline.Interval != types.Interval1m { - return - } - + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, func(kline types.KLine) { s.checkStopPrice(kline.Close, position) - }) + })) if !IsBackTesting { session.MarketDataStream.OnMarketTrade(func(trade types.Trade) { diff --git a/pkg/strategy/pivotshort/strategy.go b/pkg/strategy/pivotshort/strategy.go index 814a3bca1..b70b3be9d 100644 --- a/pkg/strategy/pivotshort/strategy.go +++ b/pkg/strategy/pivotshort/strategy.go @@ -191,8 +191,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.ResistanceShort.Bind(session, s.orderExecutor) } - // Always check whether you can open a short position or not - session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, func(kline types.KLine) { if s.Status != types.StrategyStatusRunning { return } From 09ba2d31c3ccf271c6e1496f566a735b8197966a Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 1 Jul 2022 15:34:21 +0800 Subject: [PATCH 35/92] pivortshort: run placeResistanceOrders with margin borrow buy --- pkg/strategy/pivotshort/resistance.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/strategy/pivotshort/resistance.go b/pkg/strategy/pivotshort/resistance.go index b32af2e42..63f8a7492 100644 --- a/pkg/strategy/pivotshort/resistance.go +++ b/pkg/strategy/pivotshort/resistance.go @@ -126,6 +126,7 @@ func (s *ResistanceShort) placeResistanceOrders(ctx context.Context, resistanceP Price: price, Quantity: quantity, Tag: "resistanceShort", + MarginSideEffect: types.SideEffectTypeMarginBuy, }) // TODO: fix futures mode later From c792da21647c8a2edfd93daef9c7c6a49894adfc Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 1 Jul 2022 15:41:50 +0800 Subject: [PATCH 36/92] pivotshort: improve balance check for margin --- pkg/strategy/pivotshort/strategy.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/strategy/pivotshort/strategy.go b/pkg/strategy/pivotshort/strategy.go index b70b3be9d..34b5ae038 100644 --- a/pkg/strategy/pivotshort/strategy.go +++ b/pkg/strategy/pivotshort/strategy.go @@ -294,6 +294,10 @@ func (s *Strategy) placeOrder(ctx context.Context, price fixedpoint.Value, quant } func (s *Strategy) useQuantityOrBaseBalance(quantity fixedpoint.Value) fixedpoint.Value { + if s.session.Margin || s.session.IsolatedMargin || s.session.Futures || s.session.IsolatedFutures { + return quantity + } + balance, hasBalance := s.session.Account.Balance(s.Market.BaseCurrency) if hasBalance { From 7f5e92d1b52eb5ba8c30062ceaaa842b855b2dcc Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 1 Jul 2022 16:29:03 +0800 Subject: [PATCH 37/92] cancel order when shutdown --- pkg/strategy/pivotshort/strategy.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/pivotshort/strategy.go b/pkg/strategy/pivotshort/strategy.go index 34b5ae038..bd677127d 100644 --- a/pkg/strategy/pivotshort/strategy.go +++ b/pkg/strategy/pivotshort/strategy.go @@ -266,8 +266,10 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + defer wg.Done() + _, _ = fmt.Fprintln(os.Stderr, s.TradeStats.String()) - wg.Done() + _ = s.orderExecutor.GracefulCancel(ctx) }) return nil From 93741257123950aa909756b768358c1b123bec7e Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 1 Jul 2022 17:22:09 +0800 Subject: [PATCH 38/92] pivotshort: pull out break low logics Signed-off-by: c9s --- pkg/strategy/pivotshort/breaklow.go | 191 ++++++++++++++++++++++ pkg/strategy/pivotshort/resistance.go | 38 ++++- pkg/strategy/pivotshort/strategy.go | 225 +------------------------- 3 files changed, 231 insertions(+), 223 deletions(-) create mode 100644 pkg/strategy/pivotshort/breaklow.go diff --git a/pkg/strategy/pivotshort/breaklow.go b/pkg/strategy/pivotshort/breaklow.go new file mode 100644 index 000000000..b89c9b294 --- /dev/null +++ b/pkg/strategy/pivotshort/breaklow.go @@ -0,0 +1,191 @@ +package pivotshort + +import ( + "context" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +// BreakLow -- when price breaks the previous pivot low, we set a trade entry +type BreakLow struct { + Symbol string + Market types.Market + types.IntervalWindow + + // Ratio is a number less than 1.0, price * ratio will be the price triggers the short order. + Ratio fixedpoint.Value `json:"ratio"` + + // MarketOrder is the option to enable market order short. + MarketOrder bool `json:"marketOrder"` + + // BounceRatio is a ratio used for placing the limit order sell price + // limit sell price = breakLowPrice * (1 + BounceRatio) + BounceRatio fixedpoint.Value `json:"bounceRatio"` + + Quantity fixedpoint.Value `json:"quantity"` + StopEMARange fixedpoint.Value `json:"stopEMARange"` + StopEMA *types.IntervalWindow `json:"stopEMA"` + + lastLow fixedpoint.Value + pivot *indicator.Pivot + stopEWMA *indicator.EWMA + pivotLowPrices []fixedpoint.Value + + orderExecutor *bbgo.GeneralOrderExecutor + session *bbgo.ExchangeSession +} + +func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor) { + s.session = session + s.orderExecutor = orderExecutor + + position := orderExecutor.Position() + symbol := position.Symbol + store, _ := session.MarketDataStore(symbol) + standardIndicator, _ := session.StandardIndicatorSet(symbol) + + s.lastLow = fixedpoint.Zero + + s.pivot = &indicator.Pivot{IntervalWindow: s.IntervalWindow} + s.pivot.Bind(store) + preloadPivot(s.pivot, store) + + if s.StopEMA != nil { + s.stopEWMA = standardIndicator.EWMA(*s.StopEMA) + } + + // update pivot low data + session.MarketDataStream.OnKLineClosed(types.KLineWith(symbol, s.Interval, func(kline types.KLine) { + lastLow := fixedpoint.NewFromFloat(s.pivot.LastLow()) + if lastLow.IsZero() { + return + } + + if lastLow.Compare(s.lastLow) != 0 { + log.Infof("new pivot low detected: %f %s", s.pivot.LastLow(), kline.EndTime.Time()) + } + + s.lastLow = lastLow + s.pivotLowPrices = append(s.pivotLowPrices, s.lastLow) + })) + + session.MarketDataStream.OnKLineClosed(types.KLineWith(symbol, types.Interval1m, func(kline types.KLine) { + if position.IsOpened(kline.Close) { + return + } + + if len(s.pivotLowPrices) == 0 { + log.Infof("currently there is no pivot low prices, skip placing orders...") + return + } + + previousLow := s.pivotLowPrices[len(s.pivotLowPrices)-1] + + // truncate the pivot low prices + if len(s.pivotLowPrices) > 10 { + s.pivotLowPrices = s.pivotLowPrices[len(s.pivotLowPrices)-10:] + } + + ratio := fixedpoint.One.Add(s.Ratio) + breakPrice := previousLow.Mul(ratio) + + openPrice := kline.Open + closePrice := kline.Close + // if previous low is not break, skip + if closePrice.Compare(breakPrice) >= 0 { + return + } + + // we need the price cross the break line or we do nothing + if !(openPrice.Compare(breakPrice) > 0 && closePrice.Compare(breakPrice) < 0) { + return + } + + log.Infof("%s breakLow signal detected, closed price %f < breakPrice %f", kline.Symbol, closePrice.Float64(), breakPrice.Float64()) + + // stop EMA protection + if s.stopEWMA != nil { + ema := fixedpoint.NewFromFloat(s.stopEWMA.Last()) + if ema.IsZero() { + return + } + + emaStopShortPrice := ema.Mul(fixedpoint.One.Sub(s.StopEMARange)) + if closePrice.Compare(emaStopShortPrice) < 0 { + log.Infof("stopEMA protection: close price %f < EMA(%v) = %f", closePrice.Float64(), s.StopEMA, ema.Float64()) + return + } + } + + ctx := context.Background() + + // graceful cancel all active orders + _ = orderExecutor.GracefulCancel(ctx) + + quantity := s.useQuantityOrBaseBalance(s.Quantity) + if s.MarketOrder { + bbgo.Notify("%s price %f breaks the previous low %f with ratio %f, submitting market sell to open a short position", symbol, kline.Close.Float64(), previousLow.Float64(), s.Ratio.Float64()) + s.placeMarketSell(ctx, quantity, "breakLowMarket") + } else { + sellPrice := previousLow.Mul(fixedpoint.One.Add(s.BounceRatio)) + + bbgo.Notify("%s price %f breaks the previous low %f with ratio %f, submitting limit sell @ %f", symbol, kline.Close.Float64(), previousLow.Float64(), s.Ratio.Float64(), sellPrice.Float64()) + s.placeLimitSell(ctx, sellPrice, quantity, "breakLowLimit") + } + })) + + if !bbgo.IsBackTesting { + // use market trade to submit short order + session.MarketDataStream.OnMarketTrade(func(trade types.Trade) { + + }) + } +} + +func (s *BreakLow) useQuantityOrBaseBalance(quantity fixedpoint.Value) fixedpoint.Value { + if s.session.Margin || s.session.IsolatedMargin || s.session.Futures || s.session.IsolatedFutures { + return quantity + } + + balance, hasBalance := s.session.Account.Balance(s.Market.BaseCurrency) + if hasBalance { + if quantity.IsZero() { + bbgo.Notify("sell quantity is not set, submitting sell with all base balance: %s", balance.Available.String()) + quantity = balance.Available + } else { + quantity = fixedpoint.Min(quantity, balance.Available) + } + } + + if quantity.IsZero() { + log.Errorf("quantity is zero, can not submit sell order, please check settings") + } + + return quantity +} + +func (s *BreakLow) placeLimitSell(ctx context.Context, price, quantity fixedpoint.Value, tag string) { + _, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: s.Symbol, + Price: price, + Side: types.SideTypeSell, + Type: types.OrderTypeLimit, + Quantity: quantity, + MarginSideEffect: types.SideEffectTypeMarginBuy, + Tag: tag, + }) +} + +func (s *BreakLow) placeMarketSell(ctx context.Context, quantity fixedpoint.Value, tag string) { + _, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeSell, + Type: types.OrderTypeMarket, + Quantity: quantity, + MarginSideEffect: types.SideEffectTypeMarginBuy, + Tag: tag, + }) +} diff --git a/pkg/strategy/pivotshort/resistance.go b/pkg/strategy/pivotshort/resistance.go index 63f8a7492..39b18346a 100644 --- a/pkg/strategy/pivotshort/resistance.go +++ b/pkg/strategy/pivotshort/resistance.go @@ -2,6 +2,7 @@ package pivotshort import ( "context" + "sort" "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/fixedpoint" @@ -120,12 +121,12 @@ func (s *ResistanceShort) placeResistanceOrders(ctx context.Context, resistanceP log.Infof("placing bounce short order #%d: price = %f, quantity = %f", i, price.Float64(), quantity.Float64()) orderForms = append(orderForms, types.SubmitOrder{ - Symbol: s.Symbol, - Side: types.SideTypeSell, - Type: types.OrderTypeLimitMaker, - Price: price, - Quantity: quantity, - Tag: "resistanceShort", + Symbol: s.Symbol, + Side: types.SideTypeSell, + Type: types.OrderTypeLimitMaker, + Price: price, + Quantity: quantity, + Tag: "resistanceShort", MarginSideEffect: types.SideEffectTypeMarginBuy, }) @@ -144,3 +145,28 @@ func (s *ResistanceShort) placeResistanceOrders(ctx context.Context, resistanceP } s.activeOrders.Add(createdOrders...) } + +func findPossibleResistancePrices(closePrice float64, minDistance float64, lows []float64) []float64 { + // sort float64 in increasing order + // lower to higher prices + sort.Float64s(lows) + + var resistancePrices []float64 + for _, low := range lows { + if low < closePrice { + continue + } + + last := closePrice + if len(resistancePrices) > 0 { + last = resistancePrices[len(resistancePrices)-1] + } + + if (low / last) < (1.0 + minDistance) { + continue + } + resistancePrices = append(resistancePrices, low) + } + + return resistancePrices +} diff --git a/pkg/strategy/pivotshort/strategy.go b/pkg/strategy/pivotshort/strategy.go index bd677127d..2d5f41b6d 100644 --- a/pkg/strategy/pivotshort/strategy.go +++ b/pkg/strategy/pivotshort/strategy.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "os" - "sort" "sync" "github.com/sirupsen/logrus" @@ -31,23 +30,6 @@ type IntervalWindowSetting struct { types.IntervalWindow } -// BreakLow -- when price breaks the previous pivot low, we set a trade entry -type BreakLow struct { - // Ratio is a number less than 1.0, price * ratio will be the price triggers the short order. - Ratio fixedpoint.Value `json:"ratio"` - - // MarketOrder is the option to enable market order short. - MarketOrder bool `json:"marketOrder"` - - // BounceRatio is a ratio used for placing the limit order sell price - // limit sell price = breakLowPrice * (1 + BounceRatio) - BounceRatio fixedpoint.Value `json:"bounceRatio"` - - Quantity fixedpoint.Value `json:"quantity"` - StopEMARange fixedpoint.Value `json:"stopEMARange"` - StopEMA *types.IntervalWindow `json:"stopEMA"` -} - type Strategy struct { Environment *bbgo.Environment Symbol string `json:"symbol"` @@ -62,7 +44,7 @@ type Strategy struct { TradeStats *types.TradeStats `persistence:"trade_stats"` // BreakLow is one of the entry method - BreakLow BreakLow `json:"breakLow"` + BreakLow *BreakLow `json:"breakLow"` // ResistanceShort is one of the entry method ResistanceShort *ResistanceShort `json:"resistanceShort"` @@ -72,12 +54,6 @@ type Strategy struct { session *bbgo.ExchangeSession orderExecutor *bbgo.GeneralOrderExecutor - lastLow fixedpoint.Value - pivot *indicator.Pivot - resistancePivot *indicator.Pivot - stopEWMA *indicator.EWMA - pivotLowPrices []fixedpoint.Value - // StrategyController bbgo.StrategyController } @@ -95,6 +71,10 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.ResistanceShort.Interval}) } + if s.BreakLow != nil { + dynamic.InheritStructValues(s.BreakLow, s) + } + if !bbgo.IsBackTesting { session.Subscribe(types.MarketTradeChannel, s.Symbol, types.SubscribeOptions{}) } @@ -129,8 +109,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.TradeStats = &types.TradeStats{} } - s.lastLow = fixedpoint.Zero - // StrategyController s.Status = types.StrategyStatusRunning @@ -157,32 +135,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se }) s.orderExecutor.Bind() - store, _ := session.MarketDataStore(s.Symbol) - standardIndicator, _ := session.StandardIndicatorSet(s.Symbol) - - s.pivot = &indicator.Pivot{IntervalWindow: s.IntervalWindow} - s.pivot.Bind(store) - preloadPivot(s.pivot, store) - - // update pivot low data - session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { - lastLow := fixedpoint.NewFromFloat(s.pivot.LastLow()) - if lastLow.IsZero() { - return - } - - if lastLow.Compare(s.lastLow) != 0 { - log.Infof("new pivot low detected: %f %s", s.pivot.LastLow(), kline.EndTime.Time()) - } - - s.lastLow = lastLow - s.pivotLowPrices = append(s.pivotLowPrices, s.lastLow) - })) - - if s.BreakLow.StopEMA != nil { - s.stopEWMA = standardIndicator.EWMA(*s.BreakLow.StopEMA) - } - for _, method := range s.ExitMethods { method.Bind(session, s.orderExecutor) } @@ -191,83 +143,13 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.ResistanceShort.Bind(session, s.orderExecutor) } - session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, func(kline types.KLine) { - if s.Status != types.StrategyStatusRunning { - return - } - - if s.Position.IsOpened(kline.Close) { - return - } - - if len(s.pivotLowPrices) == 0 { - log.Infof("currently there is no pivot low prices, skip placing orders...") - return - } - - previousLow := s.pivotLowPrices[len(s.pivotLowPrices)-1] - - // truncate the pivot low prices - if len(s.pivotLowPrices) > 10 { - s.pivotLowPrices = s.pivotLowPrices[len(s.pivotLowPrices)-10:] - } - - ratio := fixedpoint.One.Add(s.BreakLow.Ratio) - breakPrice := previousLow.Mul(ratio) - - openPrice := kline.Open - closePrice := kline.Close - // if previous low is not break, skip - if closePrice.Compare(breakPrice) >= 0 { - return - } - - // we need the price cross the break line or we do nothing - if !(openPrice.Compare(breakPrice) > 0 && closePrice.Compare(breakPrice) < 0) { - return - } - - log.Infof("%s breakLow signal detected, closed price %f < breakPrice %f", kline.Symbol, closePrice.Float64(), breakPrice.Float64()) - - // stop EMA protection - if s.stopEWMA != nil { - ema := fixedpoint.NewFromFloat(s.stopEWMA.Last()) - if ema.IsZero() { - return - } - - emaStopShortPrice := ema.Mul(fixedpoint.One.Sub(s.BreakLow.StopEMARange)) - if closePrice.Compare(emaStopShortPrice) < 0 { - log.Infof("stopEMA protection: close price %f < EMA(%v) = %f", closePrice.Float64(), s.BreakLow.StopEMA, ema.Float64()) - return - } - } - - // graceful cancel all active orders - _ = s.orderExecutor.GracefulCancel(ctx) - - quantity := s.useQuantityOrBaseBalance(s.BreakLow.Quantity) - if s.BreakLow.MarketOrder { - bbgo.Notify("%s price %f breaks the previous low %f with ratio %f, submitting market sell to open a short position", s.Symbol, kline.Close.Float64(), previousLow.Float64(), s.BreakLow.Ratio.Float64()) - s.placeMarketSell(ctx, quantity, "breakLowMarket") - } else { - sellPrice := previousLow.Mul(fixedpoint.One.Add(s.BreakLow.BounceRatio)) - - bbgo.Notify("%s price %f breaks the previous low %f with ratio %f, submitting limit sell @ %f", s.Symbol, kline.Close.Float64(), previousLow.Float64(), s.BreakLow.Ratio.Float64(), sellPrice.Float64()) - s.placeLimitSell(ctx, sellPrice, quantity, "breakLowLimit") - } - })) - - if !bbgo.IsBackTesting { - // use market trade to submit short order - session.MarketDataStream.OnMarketTrade(func(trade types.Trade) { - - }) + if s.BreakLow != nil { + s.BreakLow.Bind(session, s.orderExecutor) } bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() - + _, _ = fmt.Fprintln(os.Stderr, s.TradeStats.String()) _ = s.orderExecutor.GracefulCancel(ctx) }) @@ -275,97 +157,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return nil } -func (s *Strategy) findHigherPivotLow(price fixedpoint.Value) (fixedpoint.Value, bool) { - for l := len(s.pivotLowPrices) - 1; l > 0; l-- { - if s.pivotLowPrices[l].Compare(price) > 0 { - return s.pivotLowPrices[l], true - } - } - - return price, false -} - -func (s *Strategy) placeOrder(ctx context.Context, price fixedpoint.Value, quantity fixedpoint.Value) { - _, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ - Symbol: s.Symbol, - Side: types.SideTypeSell, - Type: types.OrderTypeLimitMaker, - Price: price, - Quantity: quantity, - }) -} - -func (s *Strategy) useQuantityOrBaseBalance(quantity fixedpoint.Value) fixedpoint.Value { - if s.session.Margin || s.session.IsolatedMargin || s.session.Futures || s.session.IsolatedFutures { - return quantity - } - - balance, hasBalance := s.session.Account.Balance(s.Market.BaseCurrency) - - if hasBalance { - if quantity.IsZero() { - bbgo.Notify("sell quantity is not set, submitting sell with all base balance: %s", balance.Available.String()) - quantity = balance.Available - } else { - quantity = fixedpoint.Min(quantity, balance.Available) - } - } - - if quantity.IsZero() { - log.Errorf("quantity is zero, can not submit sell order, please check settings") - } - - return quantity -} - -func (s *Strategy) placeLimitSell(ctx context.Context, price, quantity fixedpoint.Value, tag string) { - _, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ - Symbol: s.Symbol, - Price: price, - Side: types.SideTypeSell, - Type: types.OrderTypeLimit, - Quantity: quantity, - MarginSideEffect: types.SideEffectTypeMarginBuy, - Tag: tag, - }) -} - -func (s *Strategy) placeMarketSell(ctx context.Context, quantity fixedpoint.Value, tag string) { - _, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ - Symbol: s.Symbol, - Side: types.SideTypeSell, - Type: types.OrderTypeMarket, - Quantity: quantity, - MarginSideEffect: types.SideEffectTypeMarginBuy, - Tag: tag, - }) -} - -func findPossibleResistancePrices(closePrice float64, minDistance float64, lows []float64) []float64 { - // sort float64 in increasing order - // lower to higher prices - sort.Float64s(lows) - - var resistancePrices []float64 - for _, low := range lows { - if low < closePrice { - continue - } - - last := closePrice - if len(resistancePrices) > 0 { - last = resistancePrices[len(resistancePrices)-1] - } - - if (low / last) < (1.0 + minDistance) { - continue - } - resistancePrices = append(resistancePrices, low) - } - - return resistancePrices -} - func preloadPivot(pivot *indicator.Pivot, store *bbgo.MarketDataStore) *types.KLine { klines, ok := store.KLinesOfInterval(pivot.Interval) if !ok { From 04df515aea4c05feaee786bfa72ba3eebfc0d352 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 1 Jul 2022 17:26:45 +0800 Subject: [PATCH 39/92] pivotshort: clean up and force kline direction --- pkg/strategy/pivotshort/breaklow.go | 50 ++++++++++++++--------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/pkg/strategy/pivotshort/breaklow.go b/pkg/strategy/pivotshort/breaklow.go index b89c9b294..3201927cf 100644 --- a/pkg/strategy/pivotshort/breaklow.go +++ b/pkg/strategy/pivotshort/breaklow.go @@ -104,6 +104,13 @@ func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.Gener return } + // force direction to be down + if closePrice.Compare(openPrice) > 0 { + log.Infof("%s price %f is closed higher than the open price %f, skip this break", kline.Symbol, closePrice.Float64(), openPrice.Float64()) + // skip UP klines + return + } + log.Infof("%s breakLow signal detected, closed price %f < breakPrice %f", kline.Symbol, closePrice.Float64(), breakPrice.Float64()) // stop EMA protection @@ -128,12 +135,28 @@ func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.Gener quantity := s.useQuantityOrBaseBalance(s.Quantity) if s.MarketOrder { bbgo.Notify("%s price %f breaks the previous low %f with ratio %f, submitting market sell to open a short position", symbol, kline.Close.Float64(), previousLow.Float64(), s.Ratio.Float64()) - s.placeMarketSell(ctx, quantity, "breakLowMarket") + _, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeSell, + Type: types.OrderTypeMarket, + Quantity: quantity, + MarginSideEffect: types.SideEffectTypeMarginBuy, + Tag: "breakLowMarket", + }) + } else { sellPrice := previousLow.Mul(fixedpoint.One.Add(s.BounceRatio)) bbgo.Notify("%s price %f breaks the previous low %f with ratio %f, submitting limit sell @ %f", symbol, kline.Close.Float64(), previousLow.Float64(), s.Ratio.Float64(), sellPrice.Float64()) - s.placeLimitSell(ctx, sellPrice, quantity, "breakLowLimit") + _, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: kline.Symbol, + Side: types.SideTypeSell, + Type: types.OrderTypeLimit, + Price: sellPrice, + Quantity: quantity, + MarginSideEffect: types.SideEffectTypeMarginBuy, + Tag: "breakLowLimit", + }) } })) @@ -166,26 +189,3 @@ func (s *BreakLow) useQuantityOrBaseBalance(quantity fixedpoint.Value) fixedpoin return quantity } - -func (s *BreakLow) placeLimitSell(ctx context.Context, price, quantity fixedpoint.Value, tag string) { - _, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ - Symbol: s.Symbol, - Price: price, - Side: types.SideTypeSell, - Type: types.OrderTypeLimit, - Quantity: quantity, - MarginSideEffect: types.SideEffectTypeMarginBuy, - Tag: tag, - }) -} - -func (s *BreakLow) placeMarketSell(ctx context.Context, quantity fixedpoint.Value, tag string) { - _, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ - Symbol: s.Symbol, - Side: types.SideTypeSell, - Type: types.OrderTypeMarket, - Quantity: quantity, - MarginSideEffect: types.SideEffectTypeMarginBuy, - Tag: tag, - }) -} From 53204f47eaa54e4cdbd6a753757b3ad3d0c7490b Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 1 Jul 2022 17:28:48 +0800 Subject: [PATCH 40/92] bollmaker: remove legacy state loading --- pkg/strategy/bollmaker/strategy.go | 32 ++---------------------------- 1 file changed, 2 insertions(+), 30 deletions(-) diff --git a/pkg/strategy/bollmaker/strategy.go b/pkg/strategy/bollmaker/strategy.go index 2bcf6e2e9..ff3c31eee 100644 --- a/pkg/strategy/bollmaker/strategy.go +++ b/pkg/strategy/bollmaker/strategy.go @@ -215,18 +215,6 @@ func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Valu return s.orderExecutor.ClosePosition(ctx, percentage) } -// Deprecated: LoadState method is migrated to the persistence struct tag. -func (s *Strategy) LoadState() error { - var state State - - // load position - if err := s.Persistence.Load(&state, ID, s.Symbol, stateKey); err == nil { - s.state = &state - } - - return nil -} - func (s *Strategy) getCurrentAllowedExposurePosition(bandPercentage float64) (fixedpoint.Value, error) { if s.DynamicExposurePositionScale != nil { v, err := s.DynamicExposurePositionScale.Scale(bandPercentage) @@ -505,17 +493,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // If position is nil, we need to allocate a new position for calculation if s.Position == nil { - // restore state (legacy) - if err := s.LoadState(); err != nil { - return err - } - - // fallback to the legacy position struct in the state - if s.state != nil && s.state.Position != nil && !s.state.Position.Base.IsZero() { - s.Position = s.state.Position - } else { - s.Position = types.NewPositionFromMarket(s.Market) - } + s.Position = types.NewPositionFromMarket(s.Market) } if s.session.MakerFeeRate.Sign() > 0 || s.session.TakerFeeRate.Sign() > 0 { @@ -526,13 +504,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } if s.ProfitStats == nil { - if s.state != nil { - // copy profit stats - p2 := s.state.ProfitStats - s.ProfitStats = &p2 - } else { - s.ProfitStats = types.NewProfitStats(s.Market) - } + s.ProfitStats = types.NewProfitStats(s.Market) } // Always update the position fields From 4bb9fb7e1bfa2020b4639a55ae993a970a590329 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 1 Jul 2022 17:32:01 +0800 Subject: [PATCH 41/92] fix profit stats wording --- pkg/types/profit.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/types/profit.go b/pkg/types/profit.go index c0cd6a4d5..e82937ffd 100644 --- a/pkg/types/profit.go +++ b/pkg/types/profit.go @@ -279,11 +279,11 @@ func (s *ProfitStats) PlainText() string { return fmt.Sprintf("%s Profit Today\n"+ "Profit %s %s\n"+ "Net profit %s %s\n"+ - "Trade Loss %s %s\n"+ + "Gross Loss %s %s\n"+ "Summary:\n"+ "Accumulated Profit %s %s\n"+ "Accumulated Net Profit %s %s\n"+ - "Accumulated Trade Loss %s %s\n"+ + "Accumulated Gross Loss %s %s\n"+ "Since %s", s.Symbol, s.TodayPnL.String(), s.QuoteCurrency, From b158c44b95a4ea85d78d10a3b239db4ebd38c0fc Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 1 Jul 2022 17:32:40 +0800 Subject: [PATCH 42/92] fix profit stats notification --- pkg/bbgo/order_executor_general.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index ebe01c42b..cabed33f1 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -65,7 +65,7 @@ func (e *GeneralOrderExecutor) BindProfitStats(profitStats *types.ProfitStats) { } profitStats.AddProfit(*profit) - Notify(&profitStats) + Notify(profitStats) }) } From 178913dd1bf3ec6de6f64e374ea8ffd98a2f2a74 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 1 Jul 2022 17:32:59 +0800 Subject: [PATCH 43/92] reformat code --- pkg/types/profit.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/types/profit.go b/pkg/types/profit.go index e82937ffd..15771ee66 100644 --- a/pkg/types/profit.go +++ b/pkg/types/profit.go @@ -204,14 +204,14 @@ type ProfitStats struct { QuoteCurrency string `json:"quoteCurrency"` BaseCurrency string `json:"baseCurrency"` - AccumulatedPnL fixedpoint.Value `json:"accumulatedPnL,omitempty"` + AccumulatedPnL fixedpoint.Value `json:"accumulatedPnL,omitempty"` AccumulatedNetProfit fixedpoint.Value `json:"accumulatedNetProfit,omitempty"` AccumulatedGrossProfit fixedpoint.Value `json:"accumulatedProfit,omitempty"` AccumulatedGrossLoss fixedpoint.Value `json:"accumulatedLoss,omitempty"` AccumulatedVolume fixedpoint.Value `json:"accumulatedVolume,omitempty"` - AccumulatedSince int64 `json:"accumulatedSince,omitempty"` + AccumulatedSince int64 `json:"accumulatedSince,omitempty"` - TodayPnL fixedpoint.Value `json:"todayPnL,omitempty"` + TodayPnL fixedpoint.Value `json:"todayPnL,omitempty"` TodayNetProfit fixedpoint.Value `json:"todayNetProfit,omitempty"` TodayGrossProfit fixedpoint.Value `json:"todayProfit,omitempty"` TodayGrossLoss fixedpoint.Value `json:"todayLoss,omitempty"` From 9a11fd59ed0052a22e4ebb51298716e81f3bc8b4 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 1 Jul 2022 17:43:51 +0800 Subject: [PATCH 44/92] pivotshort: fix open close price compare --- pkg/strategy/pivotshort/breaklow.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/pivotshort/breaklow.go b/pkg/strategy/pivotshort/breaklow.go index 3201927cf..26c9703bc 100644 --- a/pkg/strategy/pivotshort/breaklow.go +++ b/pkg/strategy/pivotshort/breaklow.go @@ -94,6 +94,7 @@ func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.Gener openPrice := kline.Open closePrice := kline.Close + // if previous low is not break, skip if closePrice.Compare(breakPrice) >= 0 { return @@ -101,11 +102,12 @@ func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.Gener // we need the price cross the break line or we do nothing if !(openPrice.Compare(breakPrice) > 0 && closePrice.Compare(breakPrice) < 0) { + log.Infof("%s kline is not between the break low price %f", kline.Symbol, breakPrice.Float64()) return } // force direction to be down - if closePrice.Compare(openPrice) > 0 { + if closePrice.Compare(openPrice) >= 0 { log.Infof("%s price %f is closed higher than the open price %f, skip this break", kline.Symbol, closePrice.Float64(), openPrice.Float64()) // skip UP klines return From f1867b02c36185b744ce554a574055a49c670ef5 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 1 Jul 2022 18:10:39 +0800 Subject: [PATCH 45/92] pivotshort: fix message --- pkg/strategy/pivotshort/breaklow.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/pivotshort/breaklow.go b/pkg/strategy/pivotshort/breaklow.go index 26c9703bc..de98e9fcc 100644 --- a/pkg/strategy/pivotshort/breaklow.go +++ b/pkg/strategy/pivotshort/breaklow.go @@ -78,7 +78,7 @@ func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.Gener } if len(s.pivotLowPrices) == 0 { - log.Infof("currently there is no pivot low prices, skip placing orders...") + log.Infof("currently there is no pivot low prices, can not check break low...") return } From 004e6b0e0b3440d3c20caba8a8d821da02418988 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 2 Jul 2022 00:28:41 +0800 Subject: [PATCH 46/92] pivotshort: fix findNextResistancePriceAndPlaceOrders --- pkg/strategy/pivotshort/resistance.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/strategy/pivotshort/resistance.go b/pkg/strategy/pivotshort/resistance.go index 39b18346a..b26fc62d0 100644 --- a/pkg/strategy/pivotshort/resistance.go +++ b/pkg/strategy/pivotshort/resistance.go @@ -64,6 +64,10 @@ func (s *ResistanceShort) Bind(session *bbgo.ExchangeSession, orderExecutor *bbg } func (s *ResistanceShort) findNextResistancePriceAndPlaceOrders(closePrice fixedpoint.Value) { + // if the close price is still lower than the resistance price, then we don't have to update + if closePrice.Compare(s.nextResistancePrice) <= 0 { + return + } minDistance := s.MinDistance.Float64() lows := s.resistancePivot.Lows From ac1b5e4df45e79aacc032a43954250f37cba4e8e Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 2 Jul 2022 12:43:57 +0800 Subject: [PATCH 47/92] check market in the NewPositionFromMarket Signed-off-by: c9s --- pkg/types/position.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/types/position.go b/pkg/types/position.go index a85d09057..9ace9018e 100644 --- a/pkg/types/position.go +++ b/pkg/types/position.go @@ -218,6 +218,10 @@ type FuturesPosition struct { } func NewPositionFromMarket(market Market) *Position { + if len(market.BaseCurrency) == 0 || len(market.QuoteCurrency) == 0 { + panic("logical exception: missing market information, base currency or quote currency is empty") + } + return &Position{ Symbol: market.Symbol, BaseCurrency: market.BaseCurrency, From f940bb8e0a431002948895654496009fee3abb83 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 2 Jul 2022 13:21:27 +0800 Subject: [PATCH 48/92] implement SupportTakeProfit method --- pkg/strategy/pivotshort/breaklow.go | 4 ++ pkg/strategy/pivotshort/resistance.go | 43 ++++++++++++---- pkg/strategy/pivotshort/strategy.go | 71 +++++++++++++++++++++++++++ 3 files changed, 109 insertions(+), 9 deletions(-) diff --git a/pkg/strategy/pivotshort/breaklow.go b/pkg/strategy/pivotshort/breaklow.go index de98e9fcc..8b251e5dc 100644 --- a/pkg/strategy/pivotshort/breaklow.go +++ b/pkg/strategy/pivotshort/breaklow.go @@ -38,6 +38,10 @@ type BreakLow struct { session *bbgo.ExchangeSession } +func (s *BreakLow) Subscribe(session *bbgo.ExchangeSession) { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) +} + func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor) { s.session = session s.orderExecutor = orderExecutor diff --git a/pkg/strategy/pivotshort/resistance.go b/pkg/strategy/pivotshort/resistance.go index b26fc62d0..1842412e9 100644 --- a/pkg/strategy/pivotshort/resistance.go +++ b/pkg/strategy/pivotshort/resistance.go @@ -150,26 +150,51 @@ func (s *ResistanceShort) placeResistanceOrders(ctx context.Context, resistanceP s.activeOrders.Add(createdOrders...) } +func findPossibleSupportPrices(closePrice float64, minDistance float64, lows []float64) []float64 { + // sort float64 in increasing order + // lower to higher prices + sort.Float64s(lows) + + var supportPrices []float64 + var last = closePrice + for i := len(lows) - 1; i >= 0; i-- { + price := lows[i] + + // filter prices that are lower than the current closed price + if price > closePrice { + continue + } + + if (price / last) < (1.0 - minDistance) { + continue + } + + supportPrices = append(supportPrices, price) + last = price + } + + return supportPrices +} + func findPossibleResistancePrices(closePrice float64, minDistance float64, lows []float64) []float64 { // sort float64 in increasing order // lower to higher prices sort.Float64s(lows) var resistancePrices []float64 - for _, low := range lows { - if low < closePrice { + var last = closePrice + for _, price := range lows { + // filter prices that are lower than the current closed price + if price < closePrice { continue } - last := closePrice - if len(resistancePrices) > 0 { - last = resistancePrices[len(resistancePrices)-1] - } - - if (low / last) < (1.0 + minDistance) { + if (price / last) < (1.0 + minDistance) { continue } - resistancePrices = append(resistancePrices, low) + + resistancePrices = append(resistancePrices, price) + last = price } return resistancePrices diff --git a/pkg/strategy/pivotshort/strategy.go b/pkg/strategy/pivotshort/strategy.go index 2d5f41b6d..74cef7103 100644 --- a/pkg/strategy/pivotshort/strategy.go +++ b/pkg/strategy/pivotshort/strategy.go @@ -30,6 +30,69 @@ type IntervalWindowSetting struct { types.IntervalWindow } +type SupportTakeProfit struct { + Symbol string + types.IntervalWindow + Ratio fixedpoint.Value `json:"ratio"` + + pivot *indicator.Pivot + orderExecutor *bbgo.GeneralOrderExecutor + session *bbgo.ExchangeSession + activeOrders *bbgo.ActiveOrderBook +} + +func (s *SupportTakeProfit) Subscribe(session *bbgo.ExchangeSession) { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) +} + +func (s *SupportTakeProfit) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor) { + s.session = session + s.orderExecutor = orderExecutor + s.activeOrders = bbgo.NewActiveOrderBook(s.Symbol) + + position := orderExecutor.Position() + symbol := position.Symbol + store, _ := session.MarketDataStore(symbol) + s.pivot = &indicator.Pivot{IntervalWindow: s.IntervalWindow} + s.pivot.Bind(store) + preloadPivot(s.pivot, store) + + session.UserDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { + supportPrices := findPossibleSupportPrices(kline.Close.Float64(), 0.1, s.pivot.Lows) + // supportPrices are sorted in decreasing order + if len(supportPrices) == 0 { + log.Infof("support prices not found") + return + } + + if !position.IsOpened(kline.Close) { + return + } + + nextSupport := fixedpoint.NewFromFloat(supportPrices[0]) + buyPrice := nextSupport.Mul(one.Add(s.Ratio)) + quantity := position.GetQuantity() + + ctx := context.Background() + + if err := orderExecutor.GracefulCancelActiveOrderBook(ctx, s.activeOrders); err != nil { + log.WithError(err).Errorf("cancel order failed") + } + + createdOrders, err := orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: symbol, + Type: types.OrderTypeLimitMaker, + Price: buyPrice, + Quantity: quantity, + }) + if err != nil { + log.WithError(err).Errorf("can not submit orders: %+v", createdOrders) + } + + s.activeOrders.Add(createdOrders...) + })) +} + type Strategy struct { Environment *bbgo.Environment Symbol string `json:"symbol"` @@ -49,6 +112,8 @@ type Strategy struct { // ResistanceShort is one of the entry method ResistanceShort *ResistanceShort `json:"resistanceShort"` + SupportTakeProfit *SupportTakeProfit `json:"supportTakeProfit"` + ExitMethods bbgo.ExitMethodSet `json:"exits"` session *bbgo.ExchangeSession @@ -73,6 +138,12 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { if s.BreakLow != nil { dynamic.InheritStructValues(s.BreakLow, s) + s.BreakLow.Subscribe(session) + } + + if s.SupportTakeProfit != nil { + dynamic.InheritStructValues(s.SupportTakeProfit, s) + s.SupportTakeProfit.Subscribe(session) } if !bbgo.IsBackTesting { From a0e8359d230c6694e7b4456febeea62ceed654ce Mon Sep 17 00:00:00 2001 From: LarryLuTW Date: Sat, 2 Jul 2022 17:45:24 +0800 Subject: [PATCH 49/92] add market for calculator --- pkg/cmd/pnl.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/cmd/pnl.go b/pkg/cmd/pnl.go index 3474b2b34..8d3fd4231 100644 --- a/pkg/cmd/pnl.go +++ b/pkg/cmd/pnl.go @@ -184,9 +184,15 @@ var PnLCmd = &cobra.Command{ return errors.New("no ticker data for current price") } + market, ok := session.Market(symbol) + if !ok { + return fmt.Errorf("market not found: %s, %s", symbol, session.Exchange.Name()) + } + currentPrice := currentTick.Last calculator := &pnl.AverageCostCalculator{ TradingFeeCurrency: tradingFeeCurrency, + Market: market, } report := calculator.Calculate(symbol, trades, currentPrice) From 1e8ac0d08a6a26cadfa5b514a329e351137c95a7 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 2 Jul 2022 18:51:17 +0800 Subject: [PATCH 50/92] pivotshort: improve price grouping --- config/pivotshort.yaml | 1 + pkg/strategy/pivotshort/breaklow.go | 1 - pkg/strategy/pivotshort/resistance.go | 109 ++++++++++++--------- pkg/strategy/pivotshort/resistance_test.go | 28 ++++++ 4 files changed, 94 insertions(+), 45 deletions(-) create mode 100644 pkg/strategy/pivotshort/resistance_test.go diff --git a/config/pivotshort.yaml b/config/pivotshort.yaml index ea5aa6f61..869b1fb63 100644 --- a/config/pivotshort.yaml +++ b/config/pivotshort.yaml @@ -54,6 +54,7 @@ exchangeStrategies: # minDistance is used to ignore the place that is too near to the current price minDistance: 3% + groupDistance: 1% # ratio is the ratio of the resistance price, # higher the ratio, lower the price diff --git a/pkg/strategy/pivotshort/breaklow.go b/pkg/strategy/pivotshort/breaklow.go index 8b251e5dc..d19011cc7 100644 --- a/pkg/strategy/pivotshort/breaklow.go +++ b/pkg/strategy/pivotshort/breaklow.go @@ -106,7 +106,6 @@ func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.Gener // we need the price cross the break line or we do nothing if !(openPrice.Compare(breakPrice) > 0 && closePrice.Compare(breakPrice) < 0) { - log.Infof("%s kline is not between the break low price %f", kline.Symbol, breakPrice.Float64()) return } diff --git a/pkg/strategy/pivotshort/resistance.go b/pkg/strategy/pivotshort/resistance.go index 1842412e9..6f8139164 100644 --- a/pkg/strategy/pivotshort/resistance.go +++ b/pkg/strategy/pivotshort/resistance.go @@ -17,11 +17,12 @@ type ResistanceShort struct { types.IntervalWindow - MinDistance fixedpoint.Value `json:"minDistance"` - NumLayers int `json:"numLayers"` - LayerSpread fixedpoint.Value `json:"layerSpread"` - Quantity fixedpoint.Value `json:"quantity"` - Ratio fixedpoint.Value `json:"ratio"` + MinDistance fixedpoint.Value `json:"minDistance"` + GroupDistance fixedpoint.Value `json:"groupDistance"` + NumLayers int `json:"numLayers"` + LayerSpread fixedpoint.Value `json:"layerSpread"` + Quantity fixedpoint.Value `json:"quantity"` + Ratio fixedpoint.Value `json:"ratio"` session *bbgo.ExchangeSession orderExecutor *bbgo.GeneralOrderExecutor @@ -39,6 +40,10 @@ func (s *ResistanceShort) Bind(session *bbgo.ExchangeSession, orderExecutor *bbg s.activeOrders = bbgo.NewActiveOrderBook(s.Symbol) s.activeOrders.BindStream(session.UserDataStream) + if s.GroupDistance.IsZero() { + s.GroupDistance = fixedpoint.NewFromFloat(0.01) + } + store, _ := session.MarketDataStore(s.Symbol) s.resistancePivot = &indicator.Pivot{IntervalWindow: s.IntervalWindow} @@ -65,13 +70,12 @@ func (s *ResistanceShort) Bind(session *bbgo.ExchangeSession, orderExecutor *bbg func (s *ResistanceShort) findNextResistancePriceAndPlaceOrders(closePrice fixedpoint.Value) { // if the close price is still lower than the resistance price, then we don't have to update - if closePrice.Compare(s.nextResistancePrice) <= 0 { + if closePrice.Compare(s.nextResistancePrice.Mul(one.Add(s.Ratio))) <= 0 { return } minDistance := s.MinDistance.Float64() - lows := s.resistancePivot.Lows - resistancePrices := findPossibleResistancePrices(closePrice.Float64(), minDistance, lows) + resistancePrices := findPossibleResistancePrices(closePrice.Float64()*(1.0+minDistance), s.GroupDistance.Float64(), s.resistancePivot.Lows) log.Infof("last price: %f, possible resistance prices: %+v", closePrice.Float64(), resistancePrices) @@ -151,51 +155,68 @@ func (s *ResistanceShort) placeResistanceOrders(ctx context.Context, resistanceP } func findPossibleSupportPrices(closePrice float64, minDistance float64, lows []float64) []float64 { - // sort float64 in increasing order - // lower to higher prices - sort.Float64s(lows) + return group(lower(lows, closePrice), minDistance) +} - var supportPrices []float64 - var last = closePrice - for i := len(lows) - 1; i >= 0; i-- { - price := lows[i] +func lower(arr []float64, x float64) []float64 { + sort.Float64s(arr) + var rst []float64 + for _, a := range arr { // filter prices that are lower than the current closed price - if price > closePrice { + if a > x { continue } - if (price / last) < (1.0 - minDistance) { - continue - } - - supportPrices = append(supportPrices, price) - last = price + rst = append(rst, a) } - return supportPrices + return rst +} + +func higher(arr []float64, x float64) []float64 { + sort.Float64s(arr) + + var rst []float64 + for _, a := range arr { + // filter prices that are lower than the current closed price + if a < x { + continue + } + rst = append(rst, a) + } + + return rst +} + +func group(arr []float64, minDistance float64) []float64 { + var groups []float64 + var grp = []float64{arr[0]} + for _, price := range arr { + avg := average(grp) + if (price / avg) > (1.0 + minDistance) { + groups = append(groups, avg) + grp = []float64{price} + } else { + grp = append(grp, price) + } + } + + if len(grp) > 0 { + groups = append(groups, average(grp)) + } + + return groups } func findPossibleResistancePrices(closePrice float64, minDistance float64, lows []float64) []float64 { - // sort float64 in increasing order - // lower to higher prices - sort.Float64s(lows) - - var resistancePrices []float64 - var last = closePrice - for _, price := range lows { - // filter prices that are lower than the current closed price - if price < closePrice { - continue - } - - if (price / last) < (1.0 + minDistance) { - continue - } - - resistancePrices = append(resistancePrices, price) - last = price - } - - return resistancePrices + return group(higher(lows, closePrice), minDistance) +} + +func average(arr []float64) float64 { + s := 0.0 + for _, a := range arr { + s += a + } + return s / float64(len(arr)) } diff --git a/pkg/strategy/pivotshort/resistance_test.go b/pkg/strategy/pivotshort/resistance_test.go new file mode 100644 index 000000000..6a4601771 --- /dev/null +++ b/pkg/strategy/pivotshort/resistance_test.go @@ -0,0 +1,28 @@ +package pivotshort + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_findPossibleResistancePrices(t *testing.T) { + prices := findPossibleResistancePrices(19000.0, 0.01, []float64{ + 23020.0, + 23040.0, + 23060.0, + + 24020.0, + 24040.0, + 24060.0, + }) + assert.Equal(t, []float64{23035, 24040}, prices) + + + prices = findPossibleResistancePrices(19000.0, 0.01, []float64{ + 23020.0, + 23040.0, + 23060.0, + }) + assert.Equal(t, []float64{23035}, prices) +} From a408b20eda4ecff5e954e9a9f7cd4515c749840e Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 3 Jul 2022 15:26:05 +0800 Subject: [PATCH 51/92] fix resistance price calculation --- pkg/strategy/pivotshort/resistance.go | 64 ++++++++++++++++++++------- 1 file changed, 48 insertions(+), 16 deletions(-) diff --git a/pkg/strategy/pivotshort/resistance.go b/pkg/strategy/pivotshort/resistance.go index 6f8139164..195aeee69 100644 --- a/pkg/strategy/pivotshort/resistance.go +++ b/pkg/strategy/pivotshort/resistance.go @@ -27,9 +27,9 @@ type ResistanceShort struct { session *bbgo.ExchangeSession orderExecutor *bbgo.GeneralOrderExecutor - resistancePivot *indicator.Pivot - resistancePrices []float64 - nextResistancePrice fixedpoint.Value + resistancePivot *indicator.Pivot + resistancePrices []float64 + currentResistancePrice fixedpoint.Value activeOrders *bbgo.ActiveOrderBook } @@ -61,6 +61,7 @@ func (s *ResistanceShort) Bind(session *bbgo.ExchangeSession, orderExecutor *bbg session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { position := s.orderExecutor.Position() if position.IsOpened(kline.Close) { + log.Infof("position is already opened, skip placing resistance orders") return } @@ -68,25 +69,52 @@ func (s *ResistanceShort) Bind(session *bbgo.ExchangeSession, orderExecutor *bbg })) } -func (s *ResistanceShort) findNextResistancePriceAndPlaceOrders(closePrice fixedpoint.Value) { - // if the close price is still lower than the resistance price, then we don't have to update - if closePrice.Compare(s.nextResistancePrice.Mul(one.Add(s.Ratio))) <= 0 { - return +func tail(arr []float64, length int) []float64 { + if len(arr) < length { + return arr } + return arr[len(arr)-1-length:] +} + +func (s *ResistanceShort) updateNextResistancePrice(closePrice fixedpoint.Value) bool { minDistance := s.MinDistance.Float64() - resistancePrices := findPossibleResistancePrices(closePrice.Float64()*(1.0+minDistance), s.GroupDistance.Float64(), s.resistancePivot.Lows) + groupDistance := s.GroupDistance.Float64() + resistancePrices := findPossibleResistancePrices(closePrice.Float64()*(1.0+minDistance), groupDistance, tail(s.resistancePivot.Lows, 6)) - log.Infof("last price: %f, possible resistance prices: %+v", closePrice.Float64(), resistancePrices) + if len(resistancePrices) == 0 { + return false + } + log.Infof("%s close price: %f, min distance: %f, possible resistance prices: %+v", s.Symbol, closePrice.Float64(), minDistance, resistancePrices) + + nextResistancePrice := fixedpoint.NewFromFloat(resistancePrices[0]) + + // if currentResistancePrice is not set or the close price is already higher than the current resistance price, + // we should update the resistance price + // if the detected resistance price is lower than the current one, we should also update it too + if s.currentResistancePrice.IsZero() { + s.currentResistancePrice = nextResistancePrice + return true + } + + currentSellPrice := s.currentResistancePrice.Mul(one.Add(s.Ratio)) + if closePrice.Compare(currentSellPrice) > 0 || + nextResistancePrice.Compare(currentSellPrice) < 0 { + s.currentResistancePrice = nextResistancePrice + return true + } + + return false +} + +func (s *ResistanceShort) findNextResistancePriceAndPlaceOrders(closePrice fixedpoint.Value) { ctx := context.Background() - if len(resistancePrices) > 0 { - nextResistancePrice := fixedpoint.NewFromFloat(resistancePrices[0]) - if nextResistancePrice.Compare(s.nextResistancePrice) != 0 { - bbgo.Notify("Found next resistance price: %f", nextResistancePrice.Float64()) - s.nextResistancePrice = nextResistancePrice - s.placeResistanceOrders(ctx, nextResistancePrice) - } + resistanceUpdated := s.updateNextResistancePrice(closePrice) + if resistanceUpdated { + // TODO: consider s.activeOrders.NumOfOrders() > 0 + bbgo.Notify("Found next resistance price: %f, updating resistance order...", s.currentResistancePrice.Float64()) + s.placeResistanceOrders(ctx, s.currentResistancePrice) } } @@ -190,6 +218,10 @@ func higher(arr []float64, x float64) []float64 { } func group(arr []float64, minDistance float64) []float64 { + if len(arr) == 0 { + return nil + } + var groups []float64 var grp = []float64{arr[0]} for _, price := range arr { From 74cac6e97773828b67ccf9825c4ba3304312b332 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 3 Jul 2022 15:44:37 +0800 Subject: [PATCH 52/92] pivotshort: adjust layer price calculation --- pkg/strategy/pivotshort/resistance.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pkg/strategy/pivotshort/resistance.go b/pkg/strategy/pivotshort/resistance.go index 195aeee69..200152f1c 100644 --- a/pkg/strategy/pivotshort/resistance.go +++ b/pkg/strategy/pivotshort/resistance.go @@ -112,7 +112,6 @@ func (s *ResistanceShort) findNextResistancePriceAndPlaceOrders(closePrice fixed ctx := context.Background() resistanceUpdated := s.updateNextResistancePrice(closePrice) if resistanceUpdated { - // TODO: consider s.activeOrders.NumOfOrders() > 0 bbgo.Notify("Found next resistance price: %f, updating resistance order...", s.currentResistancePrice.Float64()) s.placeResistanceOrders(ctx, s.currentResistancePrice) } @@ -140,6 +139,7 @@ func (s *ResistanceShort) placeResistanceOrders(ctx context.Context, resistanceP log.Infof("placing resistance orders: resistance price = %f, layer quantity = %f, num of layers = %d", resistancePrice.Float64(), quantity.Float64(), numLayers) + var sellPriceStart = resistancePrice.Mul(fixedpoint.One.Add(s.Ratio)) var orderForms []types.SubmitOrder for i := 0; i < numLayers; i++ { balances := s.session.GetAccount().Balances() @@ -148,13 +148,11 @@ func (s *ResistanceShort) placeResistanceOrders(ctx context.Context, resistanceP _ = quoteBalance _ = baseBalance - // price = (resistance_price * (1.0 + ratio)) * ((1.0 + layerSpread) * i) - price := resistancePrice.Mul(fixedpoint.One.Add(s.Ratio)) spread := layerSpread.Mul(fixedpoint.NewFromInt(int64(i))) - price = price.Add(spread) + price := sellPriceStart.Mul(one.Add(spread)) log.Infof("price = %f", price.Float64()) - log.Infof("placing bounce short order #%d: price = %f, quantity = %f", i, price.Float64(), quantity.Float64()) + log.Infof("placing resistance short order #%d: price = %f, quantity = %f", i, price.Float64(), quantity.Float64()) orderForms = append(orderForms, types.SubmitOrder{ Symbol: s.Symbol, From 278fbb7b51e1ab7fa8b021b104275054f8c29032 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 3 Jul 2022 17:13:01 +0800 Subject: [PATCH 53/92] pivotshort: fix support take profit method --- config/pivotshort.yaml | 18 +++---- pkg/strategy/pivotshort/math.go | 66 ++++++++++++++++++++++++++ pkg/strategy/pivotshort/resistance.go | 67 ++------------------------- pkg/strategy/pivotshort/strategy.go | 58 ++++++++++++++++------- 4 files changed, 118 insertions(+), 91 deletions(-) create mode 100644 pkg/strategy/pivotshort/math.go diff --git a/config/pivotshort.yaml b/config/pivotshort.yaml index 869b1fb63..a7512ed53 100644 --- a/config/pivotshort.yaml +++ b/config/pivotshort.yaml @@ -47,22 +47,22 @@ exchangeStrategies: resistanceShort: enabled: true - interval: 1h - window: 8 + interval: 5m + window: 80 quantity: 10.0 # minDistance is used to ignore the place that is too near to the current price - minDistance: 3% + minDistance: 5% groupDistance: 1% # ratio is the ratio of the resistance price, - # higher the ratio, lower the price - # first_layer_price = resistance_price * (1 - ratio) - # second_layer_price = (resistance_price * (1 - ratio)) * (2 * layerSpread) - ratio: 0% - numOfLayers: 1 - layerSpread: 0.1% + # higher the ratio, higher the sell price + # first_layer_price = resistance_price * (1 + ratio) + # second_layer_price = (resistance_price * (1 + ratio)) * (2 * layerSpread) + ratio: 1.2% + numOfLayers: 3 + layerSpread: 0.4% exits: # (0) roiStopLoss is the stop loss percentage of the position ROI (currently the price change) diff --git a/pkg/strategy/pivotshort/math.go b/pkg/strategy/pivotshort/math.go new file mode 100644 index 000000000..9f5df82f2 --- /dev/null +++ b/pkg/strategy/pivotshort/math.go @@ -0,0 +1,66 @@ +package pivotshort + +import "sort" + +func lower(arr []float64, x float64) []float64 { + sort.Float64s(arr) + + var rst []float64 + for _, a := range arr { + // filter prices that are lower than the current closed price + if a > x { + continue + } + + rst = append(rst, a) + } + + return rst +} + +func higher(arr []float64, x float64) []float64 { + sort.Float64s(arr) + + var rst []float64 + for _, a := range arr { + // filter prices that are lower than the current closed price + if a < x { + continue + } + rst = append(rst, a) + } + + return rst +} + +func group(arr []float64, minDistance float64) []float64 { + if len(arr) == 0 { + return nil + } + + var groups []float64 + var grp = []float64{arr[0]} + for _, price := range arr { + avg := average(grp) + if (price / avg) > (1.0 + minDistance) { + groups = append(groups, avg) + grp = []float64{price} + } else { + grp = append(grp, price) + } + } + + if len(grp) > 0 { + groups = append(groups, average(grp)) + } + + return groups +} + +func average(arr []float64) float64 { + s := 0.0 + for _, a := range arr { + s += a + } + return s / float64(len(arr)) +} diff --git a/pkg/strategy/pivotshort/resistance.go b/pkg/strategy/pivotshort/resistance.go index 200152f1c..6c8aa21d6 100644 --- a/pkg/strategy/pivotshort/resistance.go +++ b/pkg/strategy/pivotshort/resistance.go @@ -2,7 +2,6 @@ package pivotshort import ( "context" - "sort" "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/fixedpoint" @@ -98,6 +97,9 @@ func (s *ResistanceShort) updateNextResistancePrice(closePrice fixedpoint.Value) return true } + // if the current sell price is out-dated + // or + // the next resistance is lower than the current one. currentSellPrice := s.currentResistancePrice.Mul(one.Add(s.Ratio)) if closePrice.Compare(currentSellPrice) > 0 || nextResistancePrice.Compare(currentSellPrice) < 0 { @@ -184,69 +186,6 @@ func findPossibleSupportPrices(closePrice float64, minDistance float64, lows []f return group(lower(lows, closePrice), minDistance) } -func lower(arr []float64, x float64) []float64 { - sort.Float64s(arr) - - var rst []float64 - for _, a := range arr { - // filter prices that are lower than the current closed price - if a > x { - continue - } - - rst = append(rst, a) - } - - return rst -} - -func higher(arr []float64, x float64) []float64 { - sort.Float64s(arr) - - var rst []float64 - for _, a := range arr { - // filter prices that are lower than the current closed price - if a < x { - continue - } - rst = append(rst, a) - } - - return rst -} - -func group(arr []float64, minDistance float64) []float64 { - if len(arr) == 0 { - return nil - } - - var groups []float64 - var grp = []float64{arr[0]} - for _, price := range arr { - avg := average(grp) - if (price / avg) > (1.0 + minDistance) { - groups = append(groups, avg) - grp = []float64{price} - } else { - grp = append(grp, price) - } - } - - if len(grp) > 0 { - groups = append(groups, average(grp)) - } - - return groups -} - func findPossibleResistancePrices(closePrice float64, minDistance float64, lows []float64) []float64 { return group(higher(lows, closePrice), minDistance) } - -func average(arr []float64) float64 { - s := 0.0 - for _, a := range arr { - s += a - } - return s / float64(len(arr)) -} diff --git a/pkg/strategy/pivotshort/strategy.go b/pkg/strategy/pivotshort/strategy.go index 74cef7103..1fc671c92 100644 --- a/pkg/strategy/pivotshort/strategy.go +++ b/pkg/strategy/pivotshort/strategy.go @@ -35,16 +35,42 @@ type SupportTakeProfit struct { types.IntervalWindow Ratio fixedpoint.Value `json:"ratio"` - pivot *indicator.Pivot - orderExecutor *bbgo.GeneralOrderExecutor - session *bbgo.ExchangeSession - activeOrders *bbgo.ActiveOrderBook + pivot *indicator.Pivot + orderExecutor *bbgo.GeneralOrderExecutor + session *bbgo.ExchangeSession + activeOrders *bbgo.ActiveOrderBook + currentSupportPrice fixedpoint.Value } func (s *SupportTakeProfit) Subscribe(session *bbgo.ExchangeSession) { session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) } +func (s *SupportTakeProfit) updateSupportPrice(closePrice fixedpoint.Value) bool { + supportPrices := findPossibleSupportPrices(closePrice.Float64(), 0.05, s.pivot.Lows) + + if len(supportPrices) == 0 { + return false + } + + // nextSupportPrice are sorted in decreasing order + nextSupportPrice := fixedpoint.NewFromFloat(supportPrices[0]) + currentBuyPrice := s.currentSupportPrice.Mul(one.Add(s.Ratio)) + + if s.currentSupportPrice.IsZero() { + s.currentSupportPrice = nextSupportPrice + return true + } + + // the close price is already lower than the support price, than we should update + if closePrice.Compare(currentBuyPrice) < 0 || nextSupportPrice.Compare(s.currentSupportPrice) > 0 { + s.currentSupportPrice = nextSupportPrice + return true + } + + return false +} + func (s *SupportTakeProfit) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor) { s.session = session s.orderExecutor = orderExecutor @@ -58,27 +84,23 @@ func (s *SupportTakeProfit) Bind(session *bbgo.ExchangeSession, orderExecutor *b preloadPivot(s.pivot, store) session.UserDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { - supportPrices := findPossibleSupportPrices(kline.Close.Float64(), 0.1, s.pivot.Lows) - // supportPrices are sorted in decreasing order - if len(supportPrices) == 0 { - log.Infof("support prices not found") - return - } - if !position.IsOpened(kline.Close) { return } - nextSupport := fixedpoint.NewFromFloat(supportPrices[0]) - buyPrice := nextSupport.Mul(one.Add(s.Ratio)) - quantity := position.GetQuantity() + if !s.updateSupportPrice(kline.Close) { + return + } + buyPrice := s.currentSupportPrice.Mul(one.Add(s.Ratio)) + quantity := position.GetQuantity() ctx := context.Background() if err := orderExecutor.GracefulCancelActiveOrderBook(ctx, s.activeOrders); err != nil { log.WithError(err).Errorf("cancel order failed") } + bbgo.Notify("placing %s take profit order at price %f", s.Symbol, buyPrice.Float64()) createdOrders, err := orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ Symbol: symbol, Type: types.OrderTypeLimitMaker, @@ -112,7 +134,7 @@ type Strategy struct { // ResistanceShort is one of the entry method ResistanceShort *ResistanceShort `json:"resistanceShort"` - SupportTakeProfit *SupportTakeProfit `json:"supportTakeProfit"` + SupportTakeProfit []SupportTakeProfit `json:"supportTakeProfit"` ExitMethods bbgo.ExitMethodSet `json:"exits"` @@ -141,9 +163,9 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { s.BreakLow.Subscribe(session) } - if s.SupportTakeProfit != nil { - dynamic.InheritStructValues(s.SupportTakeProfit, s) - s.SupportTakeProfit.Subscribe(session) + for i := range s.SupportTakeProfit { + dynamic.InheritStructValues(&s.SupportTakeProfit[i], s) + s.SupportTakeProfit[i].Subscribe(session) } if !bbgo.IsBackTesting { From 81f9639c8581c19854a7aa90461eb832d22569f0 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 3 Jul 2022 17:22:29 +0800 Subject: [PATCH 54/92] pivotshort: bind supportTakeProfit method --- pkg/strategy/pivotshort/strategy.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/pivotshort/strategy.go b/pkg/strategy/pivotshort/strategy.go index 1fc671c92..728aa822e 100644 --- a/pkg/strategy/pivotshort/strategy.go +++ b/pkg/strategy/pivotshort/strategy.go @@ -48,7 +48,6 @@ func (s *SupportTakeProfit) Subscribe(session *bbgo.ExchangeSession) { func (s *SupportTakeProfit) updateSupportPrice(closePrice fixedpoint.Value) bool { supportPrices := findPossibleSupportPrices(closePrice.Float64(), 0.05, s.pivot.Lows) - if len(supportPrices) == 0 { return false } @@ -58,12 +57,14 @@ func (s *SupportTakeProfit) updateSupportPrice(closePrice fixedpoint.Value) bool currentBuyPrice := s.currentSupportPrice.Mul(one.Add(s.Ratio)) if s.currentSupportPrice.IsZero() { + log.Infof("setup next support take profit price at %f", nextSupportPrice.Float64()) s.currentSupportPrice = nextSupportPrice return true } // the close price is already lower than the support price, than we should update if closePrice.Compare(currentBuyPrice) < 0 || nextSupportPrice.Compare(s.currentSupportPrice) > 0 { + log.Infof("setup next support take profit price at %f", nextSupportPrice.Float64()) s.currentSupportPrice = nextSupportPrice return true } @@ -106,7 +107,9 @@ func (s *SupportTakeProfit) Bind(session *bbgo.ExchangeSession, orderExecutor *b Type: types.OrderTypeLimitMaker, Price: buyPrice, Quantity: quantity, + Tag: "supportTakeProfit", }) + if err != nil { log.WithError(err).Errorf("can not submit orders: %+v", createdOrders) } @@ -240,6 +243,10 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.BreakLow.Bind(session, s.orderExecutor) } + for _, m := range s.SupportTakeProfit { + m.Bind(session, s.orderExecutor) + } + bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() From 771f578efd386277b8395619f7a2395890c2af74 Mon Sep 17 00:00:00 2001 From: Fredrik <35973823+frin1@users.noreply.github.com> Date: Sun, 3 Jul 2022 13:16:41 +0200 Subject: [PATCH 55/92] optimizer/fix: prevent from crashing if missing SummaryReport --- go.sum | 2 -- pkg/optimizer/grid.go | 11 +++++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/go.sum b/go.sum index 5aa950f06..7d562cc24 100644 --- a/go.sum +++ b/go.sum @@ -330,8 +330,6 @@ github.com/lestrrat-go/strftime v1.0.0/go.mod h1:E1nN3pCbtMSu1yjSVeyuRFVm/U0xoR7 github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lib/pq v1.10.5 h1:J+gdV2cUmX7ZqL2B0lFcW0m+egaHC2V3lpO8nWxyYiQ= -github.com/lib/pq v1.10.5/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs= github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s= diff --git a/pkg/optimizer/grid.go b/pkg/optimizer/grid.go index b9aec0f59..952e6e06a 100644 --- a/pkg/optimizer/grid.go +++ b/pkg/optimizer/grid.go @@ -4,10 +4,11 @@ import ( "context" "encoding/json" "fmt" - "github.com/cheggaaa/pb/v3" "sort" - "github.com/evanphx/json-patch/v5" + "github.com/cheggaaa/pb/v3" + + jsonpatch "github.com/evanphx/json-patch/v5" "github.com/c9s/bbgo/pkg/backtest" "github.com/c9s/bbgo/pkg/fixedpoint" @@ -16,6 +17,9 @@ import ( type MetricValueFunc func(summaryReport *backtest.SummaryReport) fixedpoint.Value var TotalProfitMetricValueFunc = func(summaryReport *backtest.SummaryReport) fixedpoint.Value { + if summaryReport == nil { + return fixedpoint.Zero + } return summaryReport.TotalProfit } @@ -217,6 +221,9 @@ func (o *GridOptimizer) Run(executor Executor, configJson []byte) (map[string][] for result := range resultsC { for metricName, metricFunc := range valueFunctions { + if result.Report == nil { + log.Errorf("no summaryReport found for params: %+v", result.Params) + } var metricValue = metricFunc(result.Report) bar.Set("log", fmt.Sprintf("params: %+v => %s %+v", result.Params, metricName, metricValue)) bar.Increment() From 3a37154737a1285d8caad8c931b5799db85cf697 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 4 Jul 2022 02:20:15 +0800 Subject: [PATCH 56/92] pivotshort: fix supportTakeProfit binding --- pkg/strategy/pivotshort/breaklow.go | 11 +- pkg/strategy/pivotshort/resistance.go | 9 +- pkg/strategy/pivotshort/strategy.go | 141 ++++++++++++++++---------- 3 files changed, 94 insertions(+), 67 deletions(-) diff --git a/pkg/strategy/pivotshort/breaklow.go b/pkg/strategy/pivotshort/breaklow.go index d19011cc7..ae61967c3 100644 --- a/pkg/strategy/pivotshort/breaklow.go +++ b/pkg/strategy/pivotshort/breaklow.go @@ -87,24 +87,19 @@ func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.Gener } previousLow := s.pivotLowPrices[len(s.pivotLowPrices)-1] - - // truncate the pivot low prices - if len(s.pivotLowPrices) > 10 { - s.pivotLowPrices = s.pivotLowPrices[len(s.pivotLowPrices)-10:] - } - ratio := fixedpoint.One.Add(s.Ratio) breakPrice := previousLow.Mul(ratio) openPrice := kline.Open closePrice := kline.Close - // if previous low is not break, skip + // if the previous low is not break, or the kline is not strong enough to break it, skip if closePrice.Compare(breakPrice) >= 0 { return } - // we need the price cross the break line or we do nothing + // we need the price cross the break line, or we do nothing: + // open > break price > close price if !(openPrice.Compare(breakPrice) > 0 && closePrice.Compare(breakPrice) < 0) { return } diff --git a/pkg/strategy/pivotshort/resistance.go b/pkg/strategy/pivotshort/resistance.go index 6c8aa21d6..b2726f98d 100644 --- a/pkg/strategy/pivotshort/resistance.go +++ b/pkg/strategy/pivotshort/resistance.go @@ -60,7 +60,6 @@ func (s *ResistanceShort) Bind(session *bbgo.ExchangeSession, orderExecutor *bbg session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { position := s.orderExecutor.Position() if position.IsOpened(kline.Close) { - log.Infof("position is already opened, skip placing resistance orders") return } @@ -182,10 +181,10 @@ func (s *ResistanceShort) placeResistanceOrders(ctx context.Context, resistanceP s.activeOrders.Add(createdOrders...) } -func findPossibleSupportPrices(closePrice float64, minDistance float64, lows []float64) []float64 { - return group(lower(lows, closePrice), minDistance) +func findPossibleSupportPrices(closePrice float64, groupDistance float64, lows []float64) []float64 { + return group(lower(lows, closePrice), groupDistance) } -func findPossibleResistancePrices(closePrice float64, minDistance float64, lows []float64) []float64 { - return group(higher(lows, closePrice), minDistance) +func findPossibleResistancePrices(closePrice float64, groupDistance float64, lows []float64) []float64 { + return group(higher(lows, closePrice), groupDistance) } diff --git a/pkg/strategy/pivotshort/strategy.go b/pkg/strategy/pivotshort/strategy.go index 728aa822e..60801e54c 100644 --- a/pkg/strategy/pivotshort/strategy.go +++ b/pkg/strategy/pivotshort/strategy.go @@ -33,6 +33,7 @@ type IntervalWindowSetting struct { type SupportTakeProfit struct { Symbol string types.IntervalWindow + Ratio fixedpoint.Value `json:"ratio"` pivot *indicator.Pivot @@ -40,20 +41,97 @@ type SupportTakeProfit struct { session *bbgo.ExchangeSession activeOrders *bbgo.ActiveOrderBook currentSupportPrice fixedpoint.Value + + triggeredPrices []fixedpoint.Value } func (s *SupportTakeProfit) Subscribe(session *bbgo.ExchangeSession) { + log.Infof("[supportTakeProfit] Subscribe(%s, %s)", s.Symbol, s.Interval) session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) } +func (s *SupportTakeProfit) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor) { + log.Infof("[supportTakeProfit] Bind(%s, %s)", s.Symbol, s.Interval) + + s.session = session + s.orderExecutor = orderExecutor + s.activeOrders = bbgo.NewActiveOrderBook(s.Symbol) + session.UserDataStream.OnOrderUpdate(func(order types.Order) { + if s.activeOrders.Exists(order) { + if !s.currentSupportPrice.IsZero() { + s.triggeredPrices = append(s.triggeredPrices, s.currentSupportPrice) + } + } + }) + s.activeOrders.BindStream(session.UserDataStream) + + position := orderExecutor.Position() + symbol := position.Symbol + store, _ := session.MarketDataStore(symbol) + s.pivot = &indicator.Pivot{IntervalWindow: s.IntervalWindow} + s.pivot.Bind(store) + preloadPivot(s.pivot, store) + + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { + if !s.updateSupportPrice(kline.Close) { + return + } + + if !position.IsOpened(kline.Close) { + log.Infof("position is not opened, skip updating support take profit order") + return + } + + buyPrice := s.currentSupportPrice.Mul(one.Add(s.Ratio)) + quantity := position.GetQuantity() + ctx := context.Background() + + if err := orderExecutor.GracefulCancelActiveOrderBook(ctx, s.activeOrders); err != nil { + log.WithError(err).Errorf("cancel order failed") + } + + bbgo.Notify("placing %s take profit order at price %f", s.Symbol, buyPrice.Float64()) + createdOrders, err := orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: symbol, + Type: types.OrderTypeLimitMaker, + Side: types.SideTypeBuy, + Price: buyPrice, + Quantity: quantity, + Tag: "supportTakeProfit", + }) + + if err != nil { + log.WithError(err).Errorf("can not submit orders: %+v", createdOrders) + } + + s.activeOrders.Add(createdOrders...) + })) +} + func (s *SupportTakeProfit) updateSupportPrice(closePrice fixedpoint.Value) bool { - supportPrices := findPossibleSupportPrices(closePrice.Float64(), 0.05, s.pivot.Lows) + log.Infof("[supportTakeProfit] lows: %v", s.pivot.Lows) + + groupDistance := 0.01 + minDistance := 0.05 + supportPrices := findPossibleSupportPrices(closePrice.Float64()*(1.0-minDistance), groupDistance, s.pivot.Lows) if len(supportPrices) == 0 { return false } - // nextSupportPrice are sorted in decreasing order - nextSupportPrice := fixedpoint.NewFromFloat(supportPrices[0]) + log.Infof("[supportTakeProfit] found possible support prices: %v", supportPrices) + + // nextSupportPrice are sorted in increasing order + nextSupportPrice := fixedpoint.NewFromFloat(supportPrices[len(supportPrices)-1]) + + // it's price that we have been used to take profit + for _, p := range s.triggeredPrices { + var l = p.Mul(one.Sub(fixedpoint.NewFromFloat(0.01))) + var h = p.Mul(one.Add(fixedpoint.NewFromFloat(0.01))) + if p.Compare(l) > 0 && p.Compare(h) < 0 { + return false + } + } + currentBuyPrice := s.currentSupportPrice.Mul(one.Add(s.Ratio)) if s.currentSupportPrice.IsZero() { @@ -72,52 +150,6 @@ func (s *SupportTakeProfit) updateSupportPrice(closePrice fixedpoint.Value) bool return false } -func (s *SupportTakeProfit) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor) { - s.session = session - s.orderExecutor = orderExecutor - s.activeOrders = bbgo.NewActiveOrderBook(s.Symbol) - - position := orderExecutor.Position() - symbol := position.Symbol - store, _ := session.MarketDataStore(symbol) - s.pivot = &indicator.Pivot{IntervalWindow: s.IntervalWindow} - s.pivot.Bind(store) - preloadPivot(s.pivot, store) - - session.UserDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { - if !position.IsOpened(kline.Close) { - return - } - - if !s.updateSupportPrice(kline.Close) { - return - } - - buyPrice := s.currentSupportPrice.Mul(one.Add(s.Ratio)) - quantity := position.GetQuantity() - ctx := context.Background() - - if err := orderExecutor.GracefulCancelActiveOrderBook(ctx, s.activeOrders); err != nil { - log.WithError(err).Errorf("cancel order failed") - } - - bbgo.Notify("placing %s take profit order at price %f", s.Symbol, buyPrice.Float64()) - createdOrders, err := orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ - Symbol: symbol, - Type: types.OrderTypeLimitMaker, - Price: buyPrice, - Quantity: quantity, - Tag: "supportTakeProfit", - }) - - if err != nil { - log.WithError(err).Errorf("can not submit orders: %+v", createdOrders) - } - - s.activeOrders.Add(createdOrders...) - })) -} - type Strategy struct { Environment *bbgo.Environment Symbol string `json:"symbol"` @@ -137,7 +169,7 @@ type Strategy struct { // ResistanceShort is one of the entry method ResistanceShort *ResistanceShort `json:"resistanceShort"` - SupportTakeProfit []SupportTakeProfit `json:"supportTakeProfit"` + SupportTakeProfit []*SupportTakeProfit `json:"supportTakeProfit"` ExitMethods bbgo.ExitMethodSet `json:"exits"` @@ -167,8 +199,9 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { } for i := range s.SupportTakeProfit { - dynamic.InheritStructValues(&s.SupportTakeProfit[i], s) - s.SupportTakeProfit[i].Subscribe(session) + m := s.SupportTakeProfit[i] + dynamic.InheritStructValues(m, s) + m.Subscribe(session) } if !bbgo.IsBackTesting { @@ -243,8 +276,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.BreakLow.Bind(session, s.orderExecutor) } - for _, m := range s.SupportTakeProfit { - m.Bind(session, s.orderExecutor) + for i := range s.SupportTakeProfit { + s.SupportTakeProfit[i].Bind(session, s.orderExecutor) } bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { From 9126045fa95ac91c73bb3b37ca79ea79a0ea498d Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 4 Jul 2022 02:20:30 +0800 Subject: [PATCH 57/92] pivotshort: adjust resistance ratio --- config/pivotshort.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/pivotshort.yaml b/config/pivotshort.yaml index a7512ed53..833c03e3b 100644 --- a/config/pivotshort.yaml +++ b/config/pivotshort.yaml @@ -60,7 +60,7 @@ exchangeStrategies: # higher the ratio, higher the sell price # first_layer_price = resistance_price * (1 + ratio) # second_layer_price = (resistance_price * (1 + ratio)) * (2 * layerSpread) - ratio: 1.2% + ratio: 1.5% numOfLayers: 3 layerSpread: 0.4% From 449b2d8220bc38b71eca3182f9f13a27c4563f6b Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 4 Jul 2022 02:20:50 +0800 Subject: [PATCH 58/92] backtest: fix order update emit binding --- pkg/backtest/exchange.go | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/pkg/backtest/exchange.go b/pkg/backtest/exchange.go index 2f66b0d87..2bf1160cb 100644 --- a/pkg/backtest/exchange.go +++ b/pkg/backtest/exchange.go @@ -174,8 +174,9 @@ func (e *Exchange) NewStream() types.Stream { func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) (createdOrders types.OrderSlice, err error) { if e.UserDataStream == nil { - return createdOrders, fmt.Errorf("SubmitOrders should be called after UserDataStream been initialized") + return createdOrders, fmt.Errorf("SubmitOrders() should be called after UserDataStream been initialized") } + for _, order := range orders { symbol := order.Symbol matching, ok := e.matchingBook(symbol) @@ -196,8 +197,6 @@ func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder case types.OrderStatusFilled, types.OrderStatusCanceled, types.OrderStatusRejected: e.addClosedOrder(*createdOrder) } - - e.UserDataStream.EmitOrderUpdate(*createdOrder) } } @@ -231,12 +230,10 @@ func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) erro if !ok { return fmt.Errorf("matching engine is not initialized for symbol %s", order.Symbol) } - canceledOrder, err := matching.CancelOrder(order) + _, err := matching.CancelOrder(order) if err != nil { return err } - - e.UserDataStream.EmitOrderUpdate(canceledOrder) } return nil From ecd4df86f9d3cd853c57280f0dfe1795fe380738 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 4 Jul 2022 02:21:14 +0800 Subject: [PATCH 59/92] backtest: assign user data stream to backtest.Exchange before we call EmitStart --- pkg/backtest/matching.go | 3 ++- pkg/bbgo/activeorderbook.go | 2 -- pkg/cmd/backtest.go | 16 ++++++++++------ 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/pkg/backtest/matching.go b/pkg/backtest/matching.go index 6b7b75185..b2f06d512 100644 --- a/pkg/backtest/matching.go +++ b/pkg/backtest/matching.go @@ -99,7 +99,6 @@ func (m *SimplePriceMatching) CancelOrder(o types.Order) (types.Order, error) { } m.askOrders = orders m.mu.Unlock() - } if !found { @@ -191,6 +190,8 @@ func (m *SimplePriceMatching) PlaceOrder(o types.SubmitOrder) (*types.Order, *ty order2.ExecutedQuantity = order2.Quantity order2.IsWorking = false + m.EmitOrderUpdate(order2) + // let the exchange emit the "FILLED" order update (we need the closed order) // m.EmitOrderUpdate(order2) return &order2, &trade, nil diff --git a/pkg/bbgo/activeorderbook.go b/pkg/bbgo/activeorderbook.go index e720e46fd..ba13b6293 100644 --- a/pkg/bbgo/activeorderbook.go +++ b/pkg/bbgo/activeorderbook.go @@ -131,8 +131,6 @@ func (b *ActiveOrderBook) orderUpdateHandler(order types.Order) { return } - log.Debugf("[ActiveOrderBook] received order update: %+v", order) - switch order.Status { case types.OrderStatusFilled: // make sure we have the order and we remove it diff --git a/pkg/cmd/backtest.go b/pkg/cmd/backtest.go index 1c1ac11a4..e9a567b9b 100644 --- a/pkg/cmd/backtest.go +++ b/pkg/cmd/backtest.go @@ -267,6 +267,12 @@ var BacktestCmd = &cobra.Command{ return err } + for _, session := range environ.Sessions() { + backtestEx := session.Exchange.(*backtest.Exchange) + backtestEx.UserDataStream = session.UserDataStream.(types.StandardStreamEmitter) + backtestEx.MarketDataStream = session.MarketDataStream.(types.StandardStreamEmitter) + } + trader := bbgo.NewTrader(environ) if verboseCnt == 0 { trader.DisableLogging() @@ -642,12 +648,10 @@ func confirmation(s string) bool { func toExchangeSources(sessions map[string]*bbgo.ExchangeSession, extraIntervals ...types.Interval) (exchangeSources []backtest.ExchangeDataSource, err error) { for _, session := range sessions { - exchange := session.Exchange.(*backtest.Exchange) - exchange.UserDataStream = session.UserDataStream.(types.StandardStreamEmitter) - exchange.MarketDataStream = session.MarketDataStream.(types.StandardStreamEmitter) - exchange.InitMarketData() + backtestEx := session.Exchange.(*backtest.Exchange) + backtestEx.InitMarketData() - c, err := exchange.SubscribeMarketData(extraIntervals...) + c, err := backtestEx.SubscribeMarketData(extraIntervals...) if err != nil { return exchangeSources, err } @@ -655,7 +659,7 @@ func toExchangeSources(sessions map[string]*bbgo.ExchangeSession, extraIntervals sessionCopy := session exchangeSources = append(exchangeSources, backtest.ExchangeDataSource{ C: c, - Exchange: exchange, + Exchange: backtestEx, Session: sessionCopy, }) } From a31f61736a855dc92e3d3ad4394e8ccad7fa9d7f Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 4 Jul 2022 02:27:29 +0800 Subject: [PATCH 60/92] backtest: pull out userDataStream to backtestEx.BindUserData --- pkg/backtest/exchange.go | 19 ++++++------------- pkg/cmd/backtest.go | 4 ++-- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/pkg/backtest/exchange.go b/pkg/backtest/exchange.go index 2bf1160cb..36c89d126 100644 --- a/pkg/backtest/exchange.go +++ b/pkg/backtest/exchange.go @@ -58,7 +58,7 @@ type Exchange struct { account *types.Account config *bbgo.Backtest - UserDataStream, MarketDataStream types.StandardStreamEmitter + MarketDataStream types.StandardStreamEmitter trades map[string][]types.Trade tradesMutex sync.Mutex @@ -173,10 +173,6 @@ func (e *Exchange) NewStream() types.Stream { } func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) (createdOrders types.OrderSlice, err error) { - if e.UserDataStream == nil { - return createdOrders, fmt.Errorf("SubmitOrders() should be called after UserDataStream been initialized") - } - for _, order := range orders { symbol := order.Symbol matching, ok := e.matchingBook(symbol) @@ -222,9 +218,6 @@ func (e *Exchange) QueryClosedOrders(ctx context.Context, symbol string, since, } func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) error { - if e.UserDataStream == nil { - return fmt.Errorf("CancelOrders should be called after UserDataStream been initialized") - } for _, order := range orders { matching, ok := e.matchingBook(order.Symbol) if !ok { @@ -315,16 +308,16 @@ func (e *Exchange) matchingBook(symbol string) (*SimplePriceMatching, bool) { return m, ok } -func (e *Exchange) InitMarketData() { - e.UserDataStream.OnTradeUpdate(func(trade types.Trade) { +func (e *Exchange) BindUserData(userDataStream types.StandardStreamEmitter) { + userDataStream.OnTradeUpdate(func(trade types.Trade) { e.addTrade(trade) }) e.matchingBooksMutex.Lock() for _, matching := range e.matchingBooks { - matching.OnTradeUpdate(e.UserDataStream.EmitTradeUpdate) - matching.OnOrderUpdate(e.UserDataStream.EmitOrderUpdate) - matching.OnBalanceUpdate(e.UserDataStream.EmitBalanceUpdate) + matching.OnTradeUpdate(userDataStream.EmitTradeUpdate) + matching.OnOrderUpdate(userDataStream.EmitOrderUpdate) + matching.OnBalanceUpdate(userDataStream.EmitBalanceUpdate) } e.matchingBooksMutex.Unlock() } diff --git a/pkg/cmd/backtest.go b/pkg/cmd/backtest.go index e9a567b9b..aa37a09bd 100644 --- a/pkg/cmd/backtest.go +++ b/pkg/cmd/backtest.go @@ -268,9 +268,10 @@ var BacktestCmd = &cobra.Command{ } for _, session := range environ.Sessions() { + userDataStream := session.UserDataStream.(types.StandardStreamEmitter) backtestEx := session.Exchange.(*backtest.Exchange) - backtestEx.UserDataStream = session.UserDataStream.(types.StandardStreamEmitter) backtestEx.MarketDataStream = session.MarketDataStream.(types.StandardStreamEmitter) + backtestEx.BindUserData(userDataStream) } trader := bbgo.NewTrader(environ) @@ -649,7 +650,6 @@ func confirmation(s string) bool { func toExchangeSources(sessions map[string]*bbgo.ExchangeSession, extraIntervals ...types.Interval) (exchangeSources []backtest.ExchangeDataSource, err error) { for _, session := range sessions { backtestEx := session.Exchange.(*backtest.Exchange) - backtestEx.InitMarketData() c, err := backtestEx.SubscribeMarketData(extraIntervals...) if err != nil { From 8fc17f9c0b70d64139ca9842552b51be5ca90b41 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 4 Jul 2022 02:29:18 +0800 Subject: [PATCH 61/92] backtest: move QueryOrder method --- pkg/backtest/exchange.go | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/pkg/backtest/exchange.go b/pkg/backtest/exchange.go index 36c89d126..763025594 100644 --- a/pkg/backtest/exchange.go +++ b/pkg/backtest/exchange.go @@ -72,20 +72,6 @@ type Exchange struct { markets types.MarketMap } -func (e *Exchange) QueryOrder(ctx context.Context, q types.OrderQuery) (*types.Order, error) { - book := e.matchingBooks[q.Symbol] - oid, err := strconv.ParseUint(q.OrderID, 10, 64) - if err != nil { - return nil, err - } - - order, ok := book.getOrder(oid) - if ok { - return &order, nil - } - return nil, nil -} - func NewExchange(sourceName types.ExchangeName, sourceExchange types.Exchange, srv *service.BacktestService, config *bbgo.Backtest) (*Exchange, error) { ex := sourceExchange @@ -172,6 +158,20 @@ func (e *Exchange) NewStream() types.Stream { } } +func (e *Exchange) QueryOrder(ctx context.Context, q types.OrderQuery) (*types.Order, error) { + book := e.matchingBooks[q.Symbol] + oid, err := strconv.ParseUint(q.OrderID, 10, 64) + if err != nil { + return nil, err + } + + order, ok := book.getOrder(oid) + if ok { + return &order, nil + } + return nil, nil +} + func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) (createdOrders types.OrderSlice, err error) { for _, order := range orders { symbol := order.Symbol From 82f9fc139c6ee96e1e07f729c9bea5a0c738af8f Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 4 Jul 2022 02:34:46 +0800 Subject: [PATCH 62/92] backtest: refactor exchange field, clean up startTime and endTime deps --- pkg/backtest/exchange.go | 22 +++++++--------------- pkg/cmd/backtest.go | 6 +++--- 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/pkg/backtest/exchange.go b/pkg/backtest/exchange.go index 763025594..d2c74df3a 100644 --- a/pkg/backtest/exchange.go +++ b/pkg/backtest/exchange.go @@ -50,10 +50,10 @@ var log = logrus.WithField("cmd", "backtest") var ErrUnimplemented = errors.New("unimplemented method") type Exchange struct { - sourceName types.ExchangeName - publicExchange types.Exchange - srv *service.BacktestService - startTime, endTime time.Time + sourceName types.ExchangeName + publicExchange types.Exchange + srv *service.BacktestService + startTime time.Time account *types.Account config *bbgo.Backtest @@ -80,14 +80,7 @@ func NewExchange(sourceName types.ExchangeName, sourceExchange types.Exchange, s return nil, err } - var startTime, endTime time.Time - startTime = config.StartTime.Time() - if config.EndTime != nil { - endTime = config.EndTime.Time() - } else { - endTime = time.Now() - } - + startTime := config.StartTime.Time() configAccount := config.GetAccount(sourceName.String()) account := &types.Account{ @@ -107,7 +100,6 @@ func NewExchange(sourceName types.ExchangeName, sourceExchange types.Exchange, s config: config, account: account, startTime: startTime, - endTime: endTime, closedOrders: make(map[string][]types.Order), trades: make(map[string][]types.Trade), } @@ -322,7 +314,7 @@ func (e *Exchange) BindUserData(userDataStream types.StandardStreamEmitter) { e.matchingBooksMutex.Unlock() } -func (e *Exchange) SubscribeMarketData(extraIntervals ...types.Interval) (chan types.KLine, error) { +func (e *Exchange) SubscribeMarketData(startTime, endTime time.Time, extraIntervals ...types.Interval) (chan types.KLine, error) { log.Infof("collecting backtest configurations...") loadedSymbols := map[string]struct{}{} @@ -361,7 +353,7 @@ func (e *Exchange) SubscribeMarketData(extraIntervals ...types.Interval) (chan t log.Infof("using symbols: %v and intervals: %v for back-testing", symbols, intervals) log.Infof("querying klines from database...") - klineC, errC := e.srv.QueryKLinesCh(e.startTime, e.endTime, e, symbols, intervals) + klineC, errC := e.srv.QueryKLinesCh(startTime, endTime, e, symbols, intervals) go func() { if err := <-errC; err != nil { log.WithError(err).Error("backtest data feed error") diff --git a/pkg/cmd/backtest.go b/pkg/cmd/backtest.go index aa37a09bd..18d190369 100644 --- a/pkg/cmd/backtest.go +++ b/pkg/cmd/backtest.go @@ -288,7 +288,7 @@ var BacktestCmd = &cobra.Command{ } backTestIntervals := []types.Interval{types.Interval1h, types.Interval1d} - exchangeSources, err := toExchangeSources(environ.Sessions(), backTestIntervals...) + exchangeSources, err := toExchangeSources(environ.Sessions(), startTime, endTime, backTestIntervals...) if err != nil { return err } @@ -647,11 +647,11 @@ func confirmation(s string) bool { } } -func toExchangeSources(sessions map[string]*bbgo.ExchangeSession, extraIntervals ...types.Interval) (exchangeSources []backtest.ExchangeDataSource, err error) { +func toExchangeSources(sessions map[string]*bbgo.ExchangeSession, startTime, endTime time.Time, extraIntervals ...types.Interval) (exchangeSources []backtest.ExchangeDataSource, err error) { for _, session := range sessions { backtestEx := session.Exchange.(*backtest.Exchange) - c, err := backtestEx.SubscribeMarketData(extraIntervals...) + c, err := backtestEx.SubscribeMarketData(startTime, endTime, extraIntervals...) if err != nil { return exchangeSources, err } From c258d522e6dc096015d1c3c8a01e5fa945e17fa2 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 4 Jul 2022 02:38:42 +0800 Subject: [PATCH 63/92] backtest: update backtest.Exchange currentTime --- pkg/backtest/exchange.go | 8 +++++--- pkg/backtest/matching.go | 1 + 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pkg/backtest/exchange.go b/pkg/backtest/exchange.go index d2c74df3a..d076cfba7 100644 --- a/pkg/backtest/exchange.go +++ b/pkg/backtest/exchange.go @@ -53,7 +53,7 @@ type Exchange struct { sourceName types.ExchangeName publicExchange types.Exchange srv *service.BacktestService - startTime time.Time + currentTime time.Time account *types.Account config *bbgo.Backtest @@ -99,7 +99,7 @@ func NewExchange(sourceName types.ExchangeName, sourceExchange types.Exchange, s srv: srv, config: config, account: account, - startTime: startTime, + currentTime: startTime, closedOrders: make(map[string][]types.Order), trades: make(map[string][]types.Trade), } @@ -137,7 +137,7 @@ func (e *Exchange) addMatchingBook(symbol string, market types.Market) { func (e *Exchange) _addMatchingBook(symbol string, market types.Market) { e.matchingBooks[symbol] = &SimplePriceMatching{ - CurrentTime: e.startTime, + CurrentTime: e.currentTime, Account: e.account, Market: market, closedOrders: make(map[uint64]types.Order), @@ -364,6 +364,8 @@ func (e *Exchange) SubscribeMarketData(startTime, endTime time.Time, extraInterv func (e *Exchange) ConsumeKLine(k types.KLine) { if k.Interval == types.Interval1m { + e.currentTime = k.EndTime.Time() + matching, ok := e.matchingBook(k.Symbol) if !ok { log.Errorf("matching book of %s is not initialized", k.Symbol) diff --git a/pkg/backtest/matching.go b/pkg/backtest/matching.go index b2f06d512..ad40a567a 100644 --- a/pkg/backtest/matching.go +++ b/pkg/backtest/matching.go @@ -571,6 +571,7 @@ func (m *SimplePriceMatching) getOrder(orderID uint64) (types.Order, bool) { func (m *SimplePriceMatching) processKLine(kline types.KLine) { m.CurrentTime = kline.EndTime.Time() + if m.LastPrice.IsZero() { m.LastPrice = kline.Open } else { From 9fbe2e859e820e5ad5824cae32bc6ad78f7d3d40 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 4 Jul 2022 11:58:11 +0800 Subject: [PATCH 64/92] update strategy dev doc --- doc/topics/developing-strategy.md | 153 +++++++++++++++++++++++++++++- 1 file changed, 152 insertions(+), 1 deletion(-) diff --git a/doc/topics/developing-strategy.md b/doc/topics/developing-strategy.md index c28f590a1..a61696e23 100644 --- a/doc/topics/developing-strategy.md +++ b/doc/topics/developing-strategy.md @@ -12,6 +12,89 @@ it. In general, strategies are Go struct, placed in Go package. +## Quick Start + +To add your first strategy, the fastest way is to add the built-in strategy. + +Simply edit `pkg/cmd/builtin.go` and import your strategy package there. + +When BBGO starts, the strategy will be imported as a package, and register its struct to the engine. + +You can also create a new file called `pkg/cmd/builtin_short.go` and import your strategy package. + +``` +import ( + _ "github.com/c9s/bbgo/pkg/strategy/short" +) +``` + +Create a directory for your new strategy in the BBGO source code repository: + +```shell +mkdir -p pkg/strategy/short +``` + +Open a new file at `pkg/strategy/short/strategy.go` and paste the simplest strategy code: + +``` +package short + +import ( + "context" + "fmt" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/types" +) + +const ID = "short" + +func init() { + // Register our struct type to BBGO + // Note that you don't need to field the fields. + // BBGO uses reflect to parse your type information. + bbgo.RegisterStrategy(ID, &Strategy{}) +} + +type Strategy struct { + Symbol string `json:"symbol"` + Interval types.Interval `json:"interval"` +} + +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) +} + +func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + session.MarketDataStream.OnKLineClosed(func(k types.KLine) { + fmt.Println(k) + }) + return nil +} +``` + +This is the most simple strategy with only ~30 lines code, it subscribes to the kline channel with the given symbol from the config, +And when the kline is closed, it prints the kline to the console. + +Note that, when Run() is executed, the user data stream is not connected to the exchange yet, but the history market data is already loaded, +so if you need to submit an order on start, be sure to write your order submit code inside the event closures like `OnKLineClosed` or `OnStart`. + +Now you can prepare your config file, create a file called `bbgo.yaml` with the following content: + +```yaml +exchangeStrategies: +- on: binance + short: + symbol: ETHUSDT + interval: 1m +``` + +And then, you should be able to run this strategy by running the following command: + +```shell +go run ./cmd/bbgo run +``` + ## The Strategy Struct BBGO loads the YAML config file and re-unmarshal the settings into your struct as JSON string, so you can define the @@ -68,8 +151,76 @@ Note that you don't need to fill the fields in the struct, BBGO just need to kno (BBGO use reflect to parse the fields from the given struct and allocate a new struct object from the given struct type internally) +## Market Data Stream and User Data Stream -## Built-in Strategy +When BBGO connects to the exchange, it allocates two stream objects for different purposes. + +They are: + +- MarketDataStream receives market data from the exchange, for example, KLine data (candlestick, or bars), market public trades. +- UserDataStream receives your personal trading data, for example, orders, executed trades, balance updates and other private information. + +To add your market data subscription to the `MarketDataStream`, you can register your subscription in the `Subscribe` of the strategy code, +for example: + +``` +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: "1m"}) +} +``` + +Since the back-test engine is a kline-based engine, to subscribe market trades, you need to check if you're in the back-test environment: + +``` +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { + if !bbgo.IsBackTesting { + session.Subscribe(types.MarketTradeChannel, s.Symbol, types.SubscribeOptions{}) + } +} +``` + +To receive the market data from the market data stream, you need to register the event callback: + +``` +func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { + // handle closed kline event here + }) + session.MarketDataStream.OnMarketTrade(func(trade types.Trade) { + // handle market trade event here + }) +} +``` + +In the above example, we register our event callback to the market data stream of the current exchange session, The +market data stream object here is a session-wide market data stream, so it's shared with other strategies that are also +using the same exchange session, you might receive kline with different symbol or interval. + +so it's better to add a condition to filter the kline events: + +``` +func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { + if kline.Symbol != s.Symbol || kline.Interval != s.Interval { + return + } + + // handle your kline here + }) +} +``` + +You can also use the KLineWith method to wrap your kline closure with the filter condition: + +``` +func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + session.MarketDataStream.OnKLineClosed(types.KLineWith("BTCUSDT", types.Interval1m, func(kline types.KLine) { + // handle your kline here + }) +} +``` + +Note that, when the Run() method is executed, the user data stream and market data stream are not connected yet. From 69179ab66fe81fa1928ae62d6170a01bac9b7346 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 4 Jul 2022 12:02:13 +0800 Subject: [PATCH 65/92] update doc --- README.md | 158 +++++++++--------------------- doc/topics/developing-strategy.md | 4 +- 2 files changed, 49 insertions(+), 113 deletions(-) diff --git a/README.md b/README.md index 2e896b34f..820d8cdd9 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,8 @@ You can use BBGO's trading unit and back-test unit to implement your own strateg ### Trading Unit Developers 🧑‍💻 -You can use BBGO's underlying common exchange API, currently it supports 4+ major exchanges, so you don't have to repeat the implementation. +You can use BBGO's underlying common exchange API, currently it supports 4+ major exchanges, so you don't have to repeat +the implementation. ## Features @@ -44,37 +45,38 @@ You can use BBGO's underlying common exchange API, currently it supports 4+ majo - Built-in parameter optimization tool. - Built-in Grid strategy and many other built-in strategies. - Multi-exchange session support: you can connect to more than 2 exchanges with different accounts or subaccounts. -- Indicators with interface similar to `pandas.Series`([series](https://github.com/c9s/bbgo/blob/main/doc/development/series.md))([usage](https://github.com/c9s/bbgo/blob/main/doc/development/indicator.md)): - - [Accumulation/Distribution Indicator](./pkg/indicator/ad.go) - - [Arnaud Legoux Moving Average](./pkg/indicator/alma.go) - - [Average True Range](./pkg/indicator/atr.go) - - [Bollinger Bands](./pkg/indicator/boll.go) - - [Commodity Channel Index](./pkg/indicator/cci.go) - - [Cumulative Moving Average](./pkg/indicator/cma.go) - - [Double Exponential Moving Average](./pkg/indicator/dema.go) - - [Directional Movement Index](./pkg/indicator/dmi.go) - - [Brownian Motion's Drift Factor](./pkg/indicator/drift.go) - - [Ease of Movement](./pkg/indicator/emv.go) - - [Exponentially Weighted Moving Average](./pkg/indicator/ewma.go) - - [Hull Moving Average](./pkg/indicator/hull.go) - - [Trend Line (Tool)](./pkg/indicator/line.go) - - [Moving Average Convergence Divergence Indicator](./pkg/indicator/macd.go) - - [On-Balance Volume](./pkg/indicator/obv.go) - - [Pivot](./pkg/indicator/pivot.go) - - [Running Moving Average](./pkg/indicator/rma.go) - - [Relative Strength Index](./pkg/indicator/rsi.go) - - [Simple Moving Average](./pkg/indicator/sma.go) - - [Ehler's Super Smoother Filter](./pkg/indicator/ssf.go) - - [Stochastic Oscillator](./pkg/indicator/stoch.go) - - [SuperTrend](./pkg/indicator/supertrend.go) - - [Triple Exponential Moving Average](./pkg/indicator/tema.go) - - [Tillson T3 Moving Average](./pkg/indicator/till.go) - - [Triangular Moving Average](./pkg/indicator/tma.go) - - [Variable Index Dynamic Average](./pkg/indicator/vidya.go) - - [Volatility Indicator](./pkg/indicator/volatility.go) - - [Volume Weighted Average Price](./pkg/indicator/vwap.go) - - [Zero Lag Exponential Moving Average](./pkg/indicator/zlema.go) - - And more... +- Indicators with interface similar + to `pandas.Series`([series](https://github.com/c9s/bbgo/blob/main/doc/development/series.md))([usage](https://github.com/c9s/bbgo/blob/main/doc/development/indicator.md)): + - [Accumulation/Distribution Indicator](./pkg/indicator/ad.go) + - [Arnaud Legoux Moving Average](./pkg/indicator/alma.go) + - [Average True Range](./pkg/indicator/atr.go) + - [Bollinger Bands](./pkg/indicator/boll.go) + - [Commodity Channel Index](./pkg/indicator/cci.go) + - [Cumulative Moving Average](./pkg/indicator/cma.go) + - [Double Exponential Moving Average](./pkg/indicator/dema.go) + - [Directional Movement Index](./pkg/indicator/dmi.go) + - [Brownian Motion's Drift Factor](./pkg/indicator/drift.go) + - [Ease of Movement](./pkg/indicator/emv.go) + - [Exponentially Weighted Moving Average](./pkg/indicator/ewma.go) + - [Hull Moving Average](./pkg/indicator/hull.go) + - [Trend Line (Tool)](./pkg/indicator/line.go) + - [Moving Average Convergence Divergence Indicator](./pkg/indicator/macd.go) + - [On-Balance Volume](./pkg/indicator/obv.go) + - [Pivot](./pkg/indicator/pivot.go) + - [Running Moving Average](./pkg/indicator/rma.go) + - [Relative Strength Index](./pkg/indicator/rsi.go) + - [Simple Moving Average](./pkg/indicator/sma.go) + - [Ehler's Super Smoother Filter](./pkg/indicator/ssf.go) + - [Stochastic Oscillator](./pkg/indicator/stoch.go) + - [SuperTrend](./pkg/indicator/supertrend.go) + - [Triple Exponential Moving Average](./pkg/indicator/tema.go) + - [Tillson T3 Moving Average](./pkg/indicator/till.go) + - [Triangular Moving Average](./pkg/indicator/tma.go) + - [Variable Index Dynamic Average](./pkg/indicator/vidya.go) + - [Volatility Indicator](./pkg/indicator/volatility.go) + - [Volume Weighted Average Price](./pkg/indicator/vwap.go) + - [Zero Lag Exponential Moving Average](./pkg/indicator/zlema.go) + - And more... - HeikinAshi OHLC / Normal OHLC (check [this config](https://github.com/c9s/bbgo/blob/main/config/skeleton.yaml#L5)) - React-powered Web Dashboard. - Docker image ready. @@ -115,7 +117,8 @@ Get your exchange API key and secret after you register the accounts (you can ch - OKEx: - Kucoin: -This project is maintained and supported by a small group of team. If you would like to support this project, please register on the exchanges using the provided links with referral codes above. +This project is maintained and supported by a small group of team. If you would like to support this project, please +register on the exchanges using the provided links with referral codes above. ## Installation @@ -145,8 +148,8 @@ bash <(curl -s https://raw.githubusercontent.com/c9s/bbgo/main/scripts/download. Or refer to the [Release Page](https://github.com/c9s/bbgo/releases) and download manually. -Since v2, we've added new float point implementation from dnum to support decimals with higher precision. -To download & setup, please refer to [Dnum Installation](doc/topics/dnum-binary.md) +Since v2, we've added new float point implementation from dnum to support decimals with higher precision. To download & +setup, please refer to [Dnum Installation](doc/topics/dnum-binary.md) ### One-click Linode StackScript @@ -241,8 +244,8 @@ bbgo pnl --exchange binance --asset BTC --since "2019-01-01" ### Testnet (Paper Trading) -Currently only supports binance testnet. -To run bbgo in testnet, apply new API keys from [Binance Test Network](https://testnet.binance.vision), and set the following env before you start bbgo: +Currently only supports binance testnet. To run bbgo in testnet, apply new API keys +from [Binance Test Network](https://testnet.binance.vision), and set the following env before you start bbgo: ```bash export PAPER_TRADE=1 @@ -344,7 +347,8 @@ Check out the strategy directory [strategy](pkg/strategy) for all built-in strat indicator [bollgrid](pkg/strategy/bollgrid) - `grid` strategy implements the fixed price band grid strategy [grid](pkg/strategy/grid). See [document](./doc/strategy/grid.md). -- `supertrend` strategy uses Supertrend indicator as trend, and DEMA indicator as noise filter [supertrend](pkg/strategy/supertrend). See +- `supertrend` strategy uses Supertrend indicator as trend, and DEMA indicator as noise + filter [supertrend](pkg/strategy/supertrend). See [document](./doc/strategy/supertrend.md). - `support` strategy uses K-lines with high volume as support [support](pkg/strategy/support). See [document](./doc/strategy/support.md). @@ -365,78 +369,9 @@ bbgo run --config config/buyandhold.yaml See [Back-testing](./doc/topics/back-testing.md) -## Adding New Built-in Strategy +## Adding Strategy -Fork and clone this repository, Create a directory under `pkg/strategy/newstrategy`, write your strategy -at `pkg/strategy/newstrategy/strategy.go`. - -Define a strategy struct: - -```go -package newstrategy - -import ( - "github.com/c9s/bbgo/pkg/fixedpoint" -) - -type Strategy struct { - Symbol string `json:"symbol"` - Param1 int `json:"param1"` - Param2 int `json:"param2"` - Param3 fixedpoint.Value `json:"param3"` -} -``` - -Register your strategy: - -```go -package newstrategy - -const ID = "newstrategy" - -const stateKey = "state-v1" - -var log = logrus.WithField("strategy", ID) - -func init() { - bbgo.RegisterStrategy(ID, &Strategy{}) -} -``` - -Implement the strategy methods: - -```go -package newstrategy - -func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { - session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: "2m"}) -} - -func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { - // .... - return nil -} -``` - -Edit `pkg/cmd/builtin.go`, and import the package, like this: - -```go -package cmd - -// import built-in strategies -import ( - _ "github.com/c9s/bbgo/pkg/strategy/bollgrid" - _ "github.com/c9s/bbgo/pkg/strategy/buyandhold" - _ "github.com/c9s/bbgo/pkg/strategy/flashcrash" - _ "github.com/c9s/bbgo/pkg/strategy/grid" - _ "github.com/c9s/bbgo/pkg/strategy/pricealert" - _ "github.com/c9s/bbgo/pkg/strategy/support" - _ "github.com/c9s/bbgo/pkg/strategy/swing" - _ "github.com/c9s/bbgo/pkg/strategy/trailingstop" - _ "github.com/c9s/bbgo/pkg/strategy/xmaker" - _ "github.com/c9s/bbgo/pkg/strategy/xpuremaker" -) -``` +See [Developing Strategy](./doc/topics/developing-strategy.md) ## Write your own private strategy @@ -635,8 +570,9 @@ What's Position? ## Looking For A New Strategy? -You can write an article about BBGO in any topic, in 750-1500 words for exchange, and I can implement the strategy for you (depends on the complexity and efforts). -If you're interested in, DM me in telegram or twitter , we can discuss. +You can write an article about BBGO in any topic, in 750-1500 words for exchange, and I can implement the strategy for +you (depends on the complexity and efforts). If you're interested in, DM me in telegram or +twitter , we can discuss. ## Contributing diff --git a/doc/topics/developing-strategy.md b/doc/topics/developing-strategy.md index a61696e23..fba15ad62 100644 --- a/doc/topics/developing-strategy.md +++ b/doc/topics/developing-strategy.md @@ -10,11 +10,11 @@ For built-in strategies, they are placed in `pkg/strategy` of the BBGO source re For external strategies, you can create a private repository as an isolated go package and place your strategy inside it. -In general, strategies are Go struct, placed in Go package. +In general, strategies are Go struct, defined in the Go package. ## Quick Start -To add your first strategy, the fastest way is to add the built-in strategy. +To add your first strategy, the fastest way is to add it as a built-in strategy. Simply edit `pkg/cmd/builtin.go` and import your strategy package there. From 435175b7f1ba88ffd6169afdf1cefad1e548f8eb Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 4 Jul 2022 12:13:24 +0800 Subject: [PATCH 66/92] doc: add more details to the exchange session --- doc/topics/developing-strategy.md | 91 ++++++++++++++++++++++++++----- 1 file changed, 77 insertions(+), 14 deletions(-) diff --git a/doc/topics/developing-strategy.md b/doc/topics/developing-strategy.md index fba15ad62..a5e500c8f 100644 --- a/doc/topics/developing-strategy.md +++ b/doc/topics/developing-strategy.md @@ -73,11 +73,12 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } ``` -This is the most simple strategy with only ~30 lines code, it subscribes to the kline channel with the given symbol from the config, -And when the kline is closed, it prints the kline to the console. +This is the most simple strategy with only ~30 lines code, it subscribes to the kline channel with the given symbol from +the config, And when the kline is closed, it prints the kline to the console. -Note that, when Run() is executed, the user data stream is not connected to the exchange yet, but the history market data is already loaded, -so if you need to submit an order on start, be sure to write your order submit code inside the event closures like `OnKLineClosed` or `OnStart`. +Note that, when Run() is executed, the user data stream is not connected to the exchange yet, but the history market +data is already loaded, so if you need to submit an order on start, be sure to write your order submit code inside the +event closures like `OnKLineClosed` or `OnStart`. Now you can prepare your config file, create a file called `bbgo.yaml` with the following content: @@ -130,8 +131,8 @@ func (s *Strategy) Run(ctx context.Context, session *bbgo.ExchangeSession) error } ``` -Now you have the Go struct and the Go package, but BBGO does not know your strategy, -so you need to register your strategy. +Now you have the Go struct and the Go package, but BBGO does not know your strategy, so you need to register your +strategy. Define an ID const in your package: @@ -149,7 +150,66 @@ func init() { Note that you don't need to fill the fields in the struct, BBGO just need to know the type of struct. -(BBGO use reflect to parse the fields from the given struct and allocate a new struct object from the given struct type internally) +(BBGO use reflect to parse the fields from the given struct and allocate a new struct object from the given struct type +internally) + +## Exchange Session + +The `*bbgo.ExchangeSession` represents a connectivity to a crypto exchange, it's also a hub that connects to everything +you need, for example, standard indicators, account information, balance information, market data stream, user data +stream, exchange APIs, and so on. + +By default, BBGO checks the environment variables that you defined to detect which exchange session to be created. + +For example, environment variables like `BINANCE_API_KEY`, `BINANCE_API_SECRET` will be transformed into an exchange +session that connects to Binance. + +You can not only connect to multiple different crypt exchanges, but also create multiple sessions to the same crypto +exchange with few different options. + +To do that, add the following section to your `bbgo.yaml` config file: + +```yaml +--- +sessions: + binance: + exchange: binance + envVarPrefix: binance + binance_cross_margin: + exchange: binance + envVarPrefix: binance + margin: true + binance_margin_ethusdt: + exchange: binance + envVarPrefix: binance + margin: true + isolatedMargin: true + isolatedMarginSymbol: ETHUSDT + okex1: + exchange: okex + envVarPrefix: okex + okex2: + exchange: okex + envVarPrefix: okex +``` + +You can specify which exchange session you want to mount for each strategy in the config file, it's quiet simple: + +```yaml +exchangeStrategies: + +- on: binance_margin_ethusdt + short: + symbol: ETHUSDT + +- on: binance_margin + foo: + symbol: ETHUSDT + +- on: binance + bar: + symbol: ETHUSDT +``` ## Market Data Stream and User Data Stream @@ -157,11 +217,13 @@ When BBGO connects to the exchange, it allocates two stream objects for differen They are: -- MarketDataStream receives market data from the exchange, for example, KLine data (candlestick, or bars), market public trades. -- UserDataStream receives your personal trading data, for example, orders, executed trades, balance updates and other private information. +- MarketDataStream receives market data from the exchange, for example, KLine data (candlestick, or bars), market public + trades. +- UserDataStream receives your personal trading data, for example, orders, executed trades, balance updates and other + private information. -To add your market data subscription to the `MarketDataStream`, you can register your subscription in the `Subscribe` of the strategy code, -for example: +To add your market data subscription to the `MarketDataStream`, you can register your subscription in the `Subscribe` of +the strategy code, for example: ``` func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { @@ -169,13 +231,14 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { } ``` -Since the back-test engine is a kline-based engine, to subscribe market trades, you need to check if you're in the back-test environment: +Since the back-test engine is a kline-based engine, to subscribe market trades, you need to check if you're in the +back-test environment: ``` func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { if !bbgo.IsBackTesting { - session.Subscribe(types.MarketTradeChannel, s.Symbol, types.SubscribeOptions{}) - } + session.Subscribe(types.MarketTradeChannel, s.Symbol, types.SubscribeOptions{}) + } } ``` From 51a589ba7ba1ee9285984d17db066ec70791211d Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 4 Jul 2022 12:14:19 +0800 Subject: [PATCH 67/92] doc: add markdown syntax highlight --- doc/topics/developing-strategy.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/doc/topics/developing-strategy.md b/doc/topics/developing-strategy.md index a5e500c8f..46791a3e5 100644 --- a/doc/topics/developing-strategy.md +++ b/doc/topics/developing-strategy.md @@ -36,7 +36,7 @@ mkdir -p pkg/strategy/short Open a new file at `pkg/strategy/short/strategy.go` and paste the simplest strategy code: -``` +```go package short import ( @@ -112,7 +112,7 @@ externalStrategies: You can write the following struct to load the symbol setting: -``` +```go package short type Strategy struct { @@ -123,7 +123,7 @@ type Strategy struct { To use the Symbol setting, you can get the value from the Run method of the strategy: -``` +```go func (s *Strategy) Run(ctx context.Context, session *bbgo.ExchangeSession) error { // you need to import the "log" package log.Println("%s", s.Symbol) @@ -136,13 +136,13 @@ strategy. Define an ID const in your package: -``` +```go const ID = "short" ``` Then call bbgo.RegisterStrategy with the ID you just defined and a struct reference: -``` +```go func init() { bbgo.RegisterStrategy(ID, &Strategy{}) } @@ -225,7 +225,7 @@ They are: To add your market data subscription to the `MarketDataStream`, you can register your subscription in the `Subscribe` of the strategy code, for example: -``` +```go func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: "1m"}) } @@ -234,7 +234,7 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { Since the back-test engine is a kline-based engine, to subscribe market trades, you need to check if you're in the back-test environment: -``` +```go func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { if !bbgo.IsBackTesting { session.Subscribe(types.MarketTradeChannel, s.Symbol, types.SubscribeOptions{}) @@ -244,7 +244,7 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { To receive the market data from the market data stream, you need to register the event callback: -``` +```go func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { // handle closed kline event here @@ -261,7 +261,7 @@ using the same exchange session, you might receive kline with different symbol o so it's better to add a condition to filter the kline events: -``` +```go func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { if kline.Symbol != s.Symbol || kline.Interval != s.Interval { @@ -275,7 +275,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se You can also use the KLineWith method to wrap your kline closure with the filter condition: -``` +```go func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { session.MarketDataStream.OnKLineClosed(types.KLineWith("BTCUSDT", types.Interval1m, func(kline types.KLine) { // handle your kline here From 7f48d834f54b9df85bcf485f3cbfc9121ee2101d Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 4 Jul 2022 12:15:47 +0800 Subject: [PATCH 68/92] doc: fix indentation --- doc/topics/developing-strategy.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/doc/topics/developing-strategy.md b/doc/topics/developing-strategy.md index 46791a3e5..ba32ee8d4 100644 --- a/doc/topics/developing-strategy.md +++ b/doc/topics/developing-strategy.md @@ -263,13 +263,12 @@ so it's better to add a condition to filter the kline events: ```go func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { - session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { + session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { if kline.Symbol != s.Symbol || kline.Interval != s.Interval { - return + return } - - // handle your kline here - }) + // handle your kline here + }) } ``` From f239eb16c1185657911a2ba2038d885eb75afa72 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 4 Jul 2022 12:16:22 +0800 Subject: [PATCH 69/92] doc: fix --- doc/topics/developing-strategy.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/topics/developing-strategy.md b/doc/topics/developing-strategy.md index ba32ee8d4..087f6a1c4 100644 --- a/doc/topics/developing-strategy.md +++ b/doc/topics/developing-strategy.md @@ -257,9 +257,9 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se In the above example, we register our event callback to the market data stream of the current exchange session, The market data stream object here is a session-wide market data stream, so it's shared with other strategies that are also -using the same exchange session, you might receive kline with different symbol or interval. +using the same exchange session, so you might receive kline with different symbol or interval. -so it's better to add a condition to filter the kline events: +It's better to add a condition to filter the kline events: ```go func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { From d62a13a62fefd93f6585a588c76b9c7fa06c8e60 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 4 Jul 2022 13:18:25 +0800 Subject: [PATCH 70/92] doc: add Submitting Orders section --- doc/topics/developing-strategy.md | 53 +++++++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/doc/topics/developing-strategy.md b/doc/topics/developing-strategy.md index 087f6a1c4..d6d9b64fe 100644 --- a/doc/topics/developing-strategy.md +++ b/doc/topics/developing-strategy.md @@ -284,8 +284,57 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se Note that, when the Run() method is executed, the user data stream and market data stream are not connected yet. - - +## Submitting Orders + +To place an order, you can call `SubmitOrders` exchange API: + +```go +createdOrders, err := session.Exchange.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: "BTCUSDT", + Type: types.OrderTypeLimit, + Price: fixedpoint.NewFromFloat(18000.0), + Quantity: fixedpoint.NewFromFloat(1.0), +}) +if err != nil { + log.WithError(err).Errorf("can not submit orders") +} + +log.Infof("createdOrders: %+v", createdOrders) +``` + +There are some pre-defined order types you can use: + +- `types.OrderTypeLimit` +- `types.OrderTypeMarket` +- `types.OrderTypeStopMarket` +- `types.OrderTypeStopLimit` +- `types.OrderTypeLimitMaker` - forces the order to be a maker. + +Although it's crypto market, the above order types are actually derived from the stock market: + +A limit order is an order to buy or sell a stock with a restriction on the maximum price to be paid or the minimum price +to be received (the "limit price"). If the order is filled, it will only be at the specified limit price or better. +However, there is no assurance of execution. A limit order may be appropriate when you think you can buy at a price +lower than--or sell at a price higher than -- the current quote. + +A market order is an order to buy or sell a stock at the market's current best available price. A market order typically +ensures an execution, but it does not guarantee a specified price. Market orders are optimal when the primary goal is to +execute the trade immediately. A market order is generally appropriate when you think a stock is priced right, when you +are sure you want a fill on your order, or when you want an immediate execution. + +A stop order is an order to buy or sell a stock at the market price once the stock has traded at or through a specified +price (the "stop price"). If the stock reaches the stop price, the order becomes a market order and is filled at the +next available market price. + +## UserDataStream + + + + + +## Handling Trades and Profit + +## Persistence From 858339b761b2fb1ce8853c7a25c1893e745e5b13 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 4 Jul 2022 13:38:09 +0800 Subject: [PATCH 71/92] doc: add more details to UserDataStream --- doc/topics/developing-strategy.md | 55 +++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/doc/topics/developing-strategy.md b/doc/topics/developing-strategy.md index d6d9b64fe..332dba5d0 100644 --- a/doc/topics/developing-strategy.md +++ b/doc/topics/developing-strategy.md @@ -328,8 +328,63 @@ next available market price. ## UserDataStream +When you submit an order to the exchange, you might want to know when the order is filled or not, user data stream is +the real time notification let you receive the order update event. +To get the order update from the user data stream: +```go +session.UserDataStream.OnOrderUpdate(func(order types.Order) { + if order.Status == types.OrderStatusFilled { + log.Infof("your order is filled: %+v", order) + } +}) +``` + +However, order update only contains the status of the order, if you need to get the details of the trade execution, +you need the trade update event: + +```go +session.UserDataStream.OnTrade(func(trade types.Trade) { + log.Infof("trade price %f, fee %f %s", trade.Price.Float64(), trade.Fee.Float64(), trade.FeeCurrency) +}) +``` + +To monitor your balance change, you can use the balance update event callback: + +```go +session.UserDataStream.OnBalanceUpdate(func(balances types.BalanceMap) { + log.Infof("balance update: %+v", balances) +}) +``` + +Note that, as we mentioned above, the user data stream is a session-wide stream, that means you might receive the order update event for other strategies. + +To prevent that, you need to manage your active order for your strategy: + +```go +activeBook := bbgo.NewActiveOrderBook("BTCUSDT") +activeBook.Bind(session.UserDataStream) +``` + +Then, when you create some orders, you can register your order to the active order book, so that it can manage the order +update: + +```go +createdOrders, err := session.Exchange.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: "BTCUSDT", + Type: types.OrderTypeLimit, + Price: fixedpoint.NewFromFloat(18000.0), + Quantity: fixedpoint.NewFromFloat(1.0), +}) +if err != nil { + log.WithError(err).Errorf("can not submit orders") +} + +activeBook.Add(createdOrders...) +``` + +## Notification ## Handling Trades and Profit From fc8df941d08f6cbe76364ff28526c183a69e70da Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 4 Jul 2022 20:06:50 +0800 Subject: [PATCH 72/92] doc: add notification api usage --- doc/topics/developing-strategy.md | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/doc/topics/developing-strategy.md b/doc/topics/developing-strategy.md index 332dba5d0..77870ea1a 100644 --- a/doc/topics/developing-strategy.md +++ b/doc/topics/developing-strategy.md @@ -341,8 +341,13 @@ session.UserDataStream.OnOrderUpdate(func(order types.Order) { }) ``` -However, order update only contains the status of the order, if you need to get the details of the trade execution, -you need the trade update event: +However, order update only contains status, price, quantity of the order, if you're submitting market order, you won't know +the actual price of the order execution. + +One order can be filled by different size trades from the market, by collecting the trades, you can calculate the +average price of the order execution and the total trading fee that you used for the order. + +If you need to get the details of the trade execution. you need the trade update event: ```go session.UserDataStream.OnTrade(func(trade types.Trade) { @@ -386,6 +391,17 @@ activeBook.Add(createdOrders...) ## Notification +You can use the notification API to send notification to Telegram or Slack: + +```go +bbgo.Notify(message) +bbgo.Notify(message, objs...) +bbgo.Notify(format, arg1, arg2, arg3, objs...) +bbgo.Notify(object, object2, object3) +``` + +Note that, if you're using the third format, simple arguments (float, bool, string... etc) will be used for calling the +fmt.Sprintf, and the extra arguments will be rendered as attachments. ## Handling Trades and Profit From 941e1bfb0ceba6fcdb5fe37d53995571772216bc Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 4 Jul 2022 20:08:06 +0800 Subject: [PATCH 73/92] doc: add notify api example --- doc/topics/developing-strategy.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/doc/topics/developing-strategy.md b/doc/topics/developing-strategy.md index 77870ea1a..de727474e 100644 --- a/doc/topics/developing-strategy.md +++ b/doc/topics/developing-strategy.md @@ -403,6 +403,14 @@ bbgo.Notify(object, object2, object3) Note that, if you're using the third format, simple arguments (float, bool, string... etc) will be used for calling the fmt.Sprintf, and the extra arguments will be rendered as attachments. +For example: + +```go +bbgo.Notify("%s found support price: %f", "BTCUSDT", 19000.0, kline) +``` + +The above call will render the first format string with the given float number 19000, and then attach the kline object as the attachment. + ## Handling Trades and Profit ## Persistence From bfd64813f85fdf75baeb695fa3c93f8d251b7dcc Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 5 Jul 2022 10:58:12 +0800 Subject: [PATCH 74/92] doc: update trade management and graceful shutdown --- doc/topics/developing-strategy.md | 42 +++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/doc/topics/developing-strategy.md b/doc/topics/developing-strategy.md index de727474e..b59807be0 100644 --- a/doc/topics/developing-strategy.md +++ b/doc/topics/developing-strategy.md @@ -328,6 +328,13 @@ next available market price. ## UserDataStream +UserDataStream is an authenticated connection to the crypto exchange. You can receive the following data type from the +user data stream: + +- OrderUpdate +- TradeUpdate +- BalanceUpdate + When you submit an order to the exchange, you might want to know when the order is filled or not, user data stream is the real time notification let you receive the order update event. @@ -413,6 +420,41 @@ The above call will render the first format string with the given float number 1 ## Handling Trades and Profit +In order to manage the trades and orders for each strategy, BBGO designed an order executor API that helps you collect +the related trades and orders from the strategy, so trades from other strategies won't bother your logics. + +To do that, you can use the *bbgo.GeneralOrderExecutor: + +```go +var profitStats = types.NewProfitStats(s.Market) +var position = types.NewPositionFromMarket(s.Market) +var tradeStats = &types.TradeStats{} +orderExecutor := bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, position) + +// bind the trade events to update the profit stats +orderExecutor.BindProfitStats(profitStats) + +// bind the trade events to update the trade stats +orderExecutor.BindTradeStats(tradeStats) +orderExecutor.Bind() +``` + +## Graceful Shutdown + +When BBGO shuts down, you might want to clean up your open orders for your strategy, to do that, you can use the +OnShutdown API to register your handler. + +```go +bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + defer wg.Done() + + _, _ = fmt.Fprintln(os.Stderr, s.TradeStats.String()) + if err := s.orderExecutor.GracefulCancel(ctx) ; err != nil { + log.WithError(err).Error("graceful cancel order error") + } +}) +``` + ## Persistence From 193703a9a0a11490b52e537a9442728eeca4addb Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 5 Jul 2022 11:14:50 +0800 Subject: [PATCH 75/92] all: use tradeStats constructor --- pkg/bbgo/order_executor_general.go | 3 ++- pkg/strategy/pivotshort/strategy.go | 2 +- pkg/strategy/rsmaker/strategy.go | 2 +- pkg/strategy/support/strategy.go | 2 +- pkg/types/trade_stats.go | 15 ++++++++++++++- 5 files changed, 19 insertions(+), 5 deletions(-) diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index cabed33f1..afc63cdb7 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -53,7 +53,8 @@ func (e *GeneralOrderExecutor) BindTradeStats(tradeStats *types.TradeStats) { if profit == nil { return } - tradeStats.Add(profit.Profit) + + tradeStats.Add(profit) }) } diff --git a/pkg/strategy/pivotshort/strategy.go b/pkg/strategy/pivotshort/strategy.go index 60801e54c..3e011a75a 100644 --- a/pkg/strategy/pivotshort/strategy.go +++ b/pkg/strategy/pivotshort/strategy.go @@ -235,7 +235,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } if s.TradeStats == nil { - s.TradeStats = &types.TradeStats{} + s.TradeStats = types.NewTradeStats(s.Symbol) } // StrategyController diff --git a/pkg/strategy/rsmaker/strategy.go b/pkg/strategy/rsmaker/strategy.go index 0704f8ae8..1eada96c2 100644 --- a/pkg/strategy/rsmaker/strategy.go +++ b/pkg/strategy/rsmaker/strategy.go @@ -413,7 +413,7 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. } if s.TradeStats == nil { - s.TradeStats = &types.TradeStats{} + s.TradeStats = types.NewTradeStats(s.Symbol) } // initial required information diff --git a/pkg/strategy/support/strategy.go b/pkg/strategy/support/strategy.go index d9a37b135..126bdadfe 100644 --- a/pkg/strategy/support/strategy.go +++ b/pkg/strategy/support/strategy.go @@ -334,7 +334,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // trade stats if s.TradeStats == nil { - s.TradeStats = &types.TradeStats{} + s.TradeStats = types.NewTradeStats(s.Symbol) } s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) diff --git a/pkg/types/trade_stats.go b/pkg/types/trade_stats.go index ed459e00a..ce0281d8c 100644 --- a/pkg/types/trade_stats.go +++ b/pkg/types/trade_stats.go @@ -9,6 +9,7 @@ import ( // TODO: Add more stats from the reference: // See https://www.metatrader5.com/en/terminal/help/algotrading/testing_report type TradeStats struct { + Symbol string `json:"symbol"` WinningRatio fixedpoint.Value `json:"winningRatio" yaml:"winningRatio"` NumOfLossTrade int `json:"numOfLossTrade" yaml:"numOfLossTrade"` NumOfProfitTrade int `json:"numOfProfitTrade" yaml:"numOfProfitTrade"` @@ -22,7 +23,19 @@ type TradeStats struct { TotalNetProfit fixedpoint.Value `json:"totalNetProfit" yaml:"totalNetProfit"` } -func (s *TradeStats) Add(pnl fixedpoint.Value) { +func NewTradeStats(symbol string) *TradeStats { + return &TradeStats{Symbol: symbol} +} + +func (s *TradeStats) Add(profit *Profit) { + if profit.Symbol != s.Symbol { + return + } + + s.add(profit.Profit) +} + +func (s *TradeStats) add(pnl fixedpoint.Value) { if pnl.Sign() > 0 { s.NumOfProfitTrade++ s.Profits = append(s.Profits, pnl) From 13fdea0978f50167ddf9909a91edf2a06c973018 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 5 Jul 2022 11:49:48 +0800 Subject: [PATCH 76/92] doc: add persistence section --- doc/topics/developing-strategy.md | 42 +++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/doc/topics/developing-strategy.md b/doc/topics/developing-strategy.md index b59807be0..f8bd30817 100644 --- a/doc/topics/developing-strategy.md +++ b/doc/topics/developing-strategy.md @@ -197,7 +197,7 @@ You can specify which exchange session you want to mount for each strategy in th ```yaml exchangeStrategies: - + - on: binance_margin_ethusdt short: symbol: ETHUSDT @@ -335,7 +335,7 @@ user data stream: - TradeUpdate - BalanceUpdate -When you submit an order to the exchange, you might want to know when the order is filled or not, user data stream is +When you submit an order to the exchange, you might want to know when the order is filled or not, user data stream is the real time notification let you receive the order update event. To get the order update from the user data stream: @@ -457,9 +457,47 @@ bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { ## Persistence +When you need to adjust the parameters and restart BBGO process, everything in the memory will be reset after the +restart, how can we keep these data? +Although BBGO is written in Golang, BBGO provides a useful dynamic system to help you persist your data. +If you have some state needs to preserve before shutting down, you can simply add the `persistence` struct tag to the field, +and BBGO will automatically save and restore your state. For example, +```go +type Strategy struct { + Position *types.Position `persistence:"position"` + ProfitStats *types.ProfitStats `persistence:"profit_stats"` + TradeStats *types.TradeStats `persistence:"trade_stats"` +} +``` +And remember to add the `persistence` section in your bbgo.yaml config: +```yaml +persistence: + redis: + host: 127.0.0.1 + port: 6379 + db: 0 +``` + +In the Run method of your strategy, you need to check if these fields are nil, and you need to initialize them: + +```go + if s.Position == nil { + s.Position = types.NewPositionFromMarket(s.Market) + } + + if s.ProfitStats == nil { + s.ProfitStats = types.NewProfitStats(s.Market) + } + + if s.TradeStats == nil { + s.TradeStats = types.NewTradeStats(s.Symbol) + } +``` + +That's it. Hit Ctrl-C and you should see BBGO saving your strategy states. From 8ac21fa16e089089cd94af453bf37510858548bc Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 5 Jul 2022 12:14:53 +0800 Subject: [PATCH 77/92] fix LowerShadowTakeProfit kline filter condition --- pkg/bbgo/exit_lower_shadow_take_profit.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/pkg/bbgo/exit_lower_shadow_take_profit.go b/pkg/bbgo/exit_lower_shadow_take_profit.go index ba598a69e..e12fd5936 100644 --- a/pkg/bbgo/exit_lower_shadow_take_profit.go +++ b/pkg/bbgo/exit_lower_shadow_take_profit.go @@ -32,11 +32,7 @@ func (s *LowerShadowTakeProfit) Bind(session *ExchangeSession, orderExecutor *Ge position := orderExecutor.Position() - session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { - if kline.Symbol != position.Symbol || kline.Interval != types.Interval1m { - return - } - + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { closePrice := kline.Close if position.IsClosed() || position.IsDust(closePrice) { return @@ -66,5 +62,5 @@ func (s *LowerShadowTakeProfit) Bind(session *ExchangeSession, orderExecutor *Ge _ = orderExecutor.ClosePosition(context.Background(), fixedpoint.One) return } - }) + })) } From b643b8ed0defc5fc17dfa4d6d496f990907b8d7a Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 5 Jul 2022 12:15:31 +0800 Subject: [PATCH 78/92] fix LowerShadowTakeProfit kline filter condition --- pkg/bbgo/exit_cumulated_volume_take_profit.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/pkg/bbgo/exit_cumulated_volume_take_profit.go b/pkg/bbgo/exit_cumulated_volume_take_profit.go index 864e949ab..8ba7ed65f 100644 --- a/pkg/bbgo/exit_cumulated_volume_take_profit.go +++ b/pkg/bbgo/exit_cumulated_volume_take_profit.go @@ -36,11 +36,7 @@ func (s *CumulatedVolumeTakeProfit) Bind(session *ExchangeSession, orderExecutor store, _ := session.MarketDataStore(position.Symbol) - session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { - if kline.Symbol != position.Symbol || kline.Interval != s.Interval { - return - } - + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { closePrice := kline.Close if position.IsClosed() || position.IsDust(closePrice) { return @@ -79,5 +75,5 @@ func (s *CumulatedVolumeTakeProfit) Bind(session *ExchangeSession, orderExecutor _ = orderExecutor.ClosePosition(context.Background(), fixedpoint.One, "cumulatedVolumeTakeProfit") return } - }) + })) } From 4de5b0bc9b2bcdb85d17ec05a7041de636cae409 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 5 Jul 2022 16:10:55 +0800 Subject: [PATCH 79/92] add TrailingStop2 --- pkg/bbgo/exit_trailing_stop.go | 69 ++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 pkg/bbgo/exit_trailing_stop.go diff --git a/pkg/bbgo/exit_trailing_stop.go b/pkg/bbgo/exit_trailing_stop.go new file mode 100644 index 000000000..769d7cda8 --- /dev/null +++ b/pkg/bbgo/exit_trailing_stop.go @@ -0,0 +1,69 @@ +package bbgo + +import ( + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +type TrailingStop2 struct { + Symbol string + + // CallbackRate is the callback rate from the previous high price + CallbackRate fixedpoint.Value `json:"callbackRate,omitempty"` + + // ClosePosition is a percentage of the position to be closed + ClosePosition fixedpoint.Value `json:"closePosition,omitempty"` + + // MinProfit is the percentage of the minimum profit ratio. + // Stop order will be activated only when the price reaches above this threshold. + MinProfit fixedpoint.Value `json:"minProfit,omitempty"` + + // Interval is the time resolution to update the stop order + // KLine per Interval will be used for updating the stop order + Interval types.Interval `json:"interval,omitempty"` + + // private fields + session *ExchangeSession + orderExecutor *GeneralOrderExecutor +} + +func (s *TrailingStop2) Subscribe(session *ExchangeSession) { + // use 1m kline to handle roi stop + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: types.Interval1m}) +} + +func (s *TrailingStop2) Bind(session *ExchangeSession, orderExecutor *GeneralOrderExecutor) { + s.session = session + s.orderExecutor = orderExecutor + + position := orderExecutor.Position() + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, func(kline types.KLine) { + s.checkStopPrice(kline.Close, position) + })) + + if !IsBackTesting { + session.MarketDataStream.OnMarketTrade(func(trade types.Trade) { + if trade.Symbol != position.Symbol { + return + } + + s.checkStopPrice(trade.Price, position) + }) + } +} + +func (s *TrailingStop2) checkStopPrice(closePrice fixedpoint.Value, position *types.Position) { + if position.IsClosed() || position.IsDust(closePrice) { + return + } + + /* + roi := position.ROI(closePrice) + if roi.Compare(s.CallbackRate.Neg()) < 0 { + // stop loss + Notify("[TrailingStop2] %s stop loss triggered by ROI %s/%s, price: %f", position.Symbol, roi.Percentage(), s.Percentage.Neg().Percentage(), closePrice.Float64()) + _ = s.orderExecutor.ClosePosition(context.Background(), fixedpoint.One, "TrailingStop2") + return + } + */ +} From f329af2c6b6e2977ddea1225dd3ee547c70b5c6f Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 6 Jul 2022 01:31:12 +0800 Subject: [PATCH 80/92] generate mocks for the exchange interface Signed-off-by: c9s --- go.mod | 3 + go.sum | 7 + pkg/types/exchange.go | 1 + pkg/types/mocks/mock_exchange.go | 745 +++++++++++++++++++++++++++++++ 4 files changed, 756 insertions(+) create mode 100644 pkg/types/mocks/mock_exchange.go diff --git a/go.mod b/go.mod index d1ce9dbf6..34bd47ce8 100644 --- a/go.mod +++ b/go.mod @@ -75,6 +75,7 @@ require ( github.com/go-test/deep v1.0.6 // indirect github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/golang-sql/sqlexp v0.1.0 // indirect + github.com/golang/mock v1.6.0 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect @@ -116,11 +117,13 @@ require ( go.opentelemetry.io/otel/trace v0.19.0 // indirect go.uber.org/atomic v1.9.0 // indirect golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect + golang.org/x/mod v0.5.1 // indirect golang.org/x/net v0.0.0-20220403103023-749bd193bc2b // indirect golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c // indirect golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect golang.org/x/text v0.3.7 // indirect golang.org/x/tools v0.1.9 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect google.golang.org/genproto v0.0.0-20220405205423-9d709892a2bf // indirect gopkg.in/ini.v1 v1.62.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 7d562cc24..96f942c4d 100644 --- a/go.sum +++ b/go.sum @@ -194,7 +194,10 @@ github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFU github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -523,6 +526,7 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= @@ -599,6 +603,8 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -780,6 +786,7 @@ golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.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= diff --git a/pkg/types/exchange.go b/pkg/types/exchange.go index 5bff2689c..39ced8d87 100644 --- a/pkg/types/exchange.go +++ b/pkg/types/exchange.go @@ -74,6 +74,7 @@ func ValidExchangeName(a string) (ExchangeName, error) { return "", fmt.Errorf("invalid exchange name: %s", a) } +//go:generate mockgen -destination=mocks/mock_exchange.go -package=mocks -source=exchange.go Exchange type Exchange interface { Name() ExchangeName diff --git a/pkg/types/mocks/mock_exchange.go b/pkg/types/mocks/mock_exchange.go new file mode 100644 index 000000000..5c0410af7 --- /dev/null +++ b/pkg/types/mocks/mock_exchange.go @@ -0,0 +1,745 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: exchange.go + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + time "time" + + fixedpoint "github.com/c9s/bbgo/pkg/fixedpoint" + types "github.com/c9s/bbgo/pkg/types" + gomock "github.com/golang/mock/gomock" +) + +// MockExchange is a mock of Exchange interface. +type MockExchange struct { + ctrl *gomock.Controller + recorder *MockExchangeMockRecorder +} + +// MockExchangeMockRecorder is the mock recorder for MockExchange. +type MockExchangeMockRecorder struct { + mock *MockExchange +} + +// NewMockExchange creates a new mock instance. +func NewMockExchange(ctrl *gomock.Controller) *MockExchange { + mock := &MockExchange{ctrl: ctrl} + mock.recorder = &MockExchangeMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockExchange) EXPECT() *MockExchangeMockRecorder { + return m.recorder +} + +// CancelOrders mocks base method. +func (m *MockExchange) CancelOrders(ctx context.Context, orders ...types.Order) error { + m.ctrl.T.Helper() + varargs := []interface{}{ctx} + for _, a := range orders { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "CancelOrders", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// CancelOrders indicates an expected call of CancelOrders. +func (mr *MockExchangeMockRecorder) CancelOrders(ctx interface{}, orders ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx}, orders...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CancelOrders", reflect.TypeOf((*MockExchange)(nil).CancelOrders), varargs...) +} + +// Name mocks base method. +func (m *MockExchange) Name() types.ExchangeName { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Name") + ret0, _ := ret[0].(types.ExchangeName) + return ret0 +} + +// Name indicates an expected call of Name. +func (mr *MockExchangeMockRecorder) Name() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Name", reflect.TypeOf((*MockExchange)(nil).Name)) +} + +// NewStream mocks base method. +func (m *MockExchange) NewStream() types.Stream { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewStream") + ret0, _ := ret[0].(types.Stream) + return ret0 +} + +// NewStream indicates an expected call of NewStream. +func (mr *MockExchangeMockRecorder) NewStream() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewStream", reflect.TypeOf((*MockExchange)(nil).NewStream)) +} + +// PlatformFeeCurrency mocks base method. +func (m *MockExchange) PlatformFeeCurrency() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PlatformFeeCurrency") + ret0, _ := ret[0].(string) + return ret0 +} + +// PlatformFeeCurrency indicates an expected call of PlatformFeeCurrency. +func (mr *MockExchangeMockRecorder) PlatformFeeCurrency() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PlatformFeeCurrency", reflect.TypeOf((*MockExchange)(nil).PlatformFeeCurrency)) +} + +// QueryAccount mocks base method. +func (m *MockExchange) QueryAccount(ctx context.Context) (*types.Account, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryAccount", ctx) + ret0, _ := ret[0].(*types.Account) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryAccount indicates an expected call of QueryAccount. +func (mr *MockExchangeMockRecorder) QueryAccount(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryAccount", reflect.TypeOf((*MockExchange)(nil).QueryAccount), ctx) +} + +// QueryAccountBalances mocks base method. +func (m *MockExchange) QueryAccountBalances(ctx context.Context) (types.BalanceMap, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryAccountBalances", ctx) + ret0, _ := ret[0].(types.BalanceMap) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryAccountBalances indicates an expected call of QueryAccountBalances. +func (mr *MockExchangeMockRecorder) QueryAccountBalances(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryAccountBalances", reflect.TypeOf((*MockExchange)(nil).QueryAccountBalances), ctx) +} + +// QueryKLines mocks base method. +func (m *MockExchange) QueryKLines(ctx context.Context, symbol string, interval types.Interval, options types.KLineQueryOptions) ([]types.KLine, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryKLines", ctx, symbol, interval, options) + ret0, _ := ret[0].([]types.KLine) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryKLines indicates an expected call of QueryKLines. +func (mr *MockExchangeMockRecorder) QueryKLines(ctx, symbol, interval, options interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryKLines", reflect.TypeOf((*MockExchange)(nil).QueryKLines), ctx, symbol, interval, options) +} + +// QueryMarkets mocks base method. +func (m *MockExchange) QueryMarkets(ctx context.Context) (types.MarketMap, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryMarkets", ctx) + ret0, _ := ret[0].(types.MarketMap) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryMarkets indicates an expected call of QueryMarkets. +func (mr *MockExchangeMockRecorder) QueryMarkets(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryMarkets", reflect.TypeOf((*MockExchange)(nil).QueryMarkets), ctx) +} + +// QueryOpenOrders mocks base method. +func (m *MockExchange) QueryOpenOrders(ctx context.Context, symbol string) ([]types.Order, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryOpenOrders", ctx, symbol) + ret0, _ := ret[0].([]types.Order) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryOpenOrders indicates an expected call of QueryOpenOrders. +func (mr *MockExchangeMockRecorder) QueryOpenOrders(ctx, symbol interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryOpenOrders", reflect.TypeOf((*MockExchange)(nil).QueryOpenOrders), ctx, symbol) +} + +// QueryTicker mocks base method. +func (m *MockExchange) QueryTicker(ctx context.Context, symbol string) (*types.Ticker, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryTicker", ctx, symbol) + ret0, _ := ret[0].(*types.Ticker) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryTicker indicates an expected call of QueryTicker. +func (mr *MockExchangeMockRecorder) QueryTicker(ctx, symbol interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryTicker", reflect.TypeOf((*MockExchange)(nil).QueryTicker), ctx, symbol) +} + +// QueryTickers mocks base method. +func (m *MockExchange) QueryTickers(ctx context.Context, symbol ...string) (map[string]types.Ticker, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx} + for _, a := range symbol { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "QueryTickers", varargs...) + ret0, _ := ret[0].(map[string]types.Ticker) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryTickers indicates an expected call of QueryTickers. +func (mr *MockExchangeMockRecorder) QueryTickers(ctx interface{}, symbol ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx}, symbol...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryTickers", reflect.TypeOf((*MockExchange)(nil).QueryTickers), varargs...) +} + +// SubmitOrders mocks base method. +func (m *MockExchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) (types.OrderSlice, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx} + for _, a := range orders { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "SubmitOrders", varargs...) + ret0, _ := ret[0].(types.OrderSlice) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SubmitOrders indicates an expected call of SubmitOrders. +func (mr *MockExchangeMockRecorder) SubmitOrders(ctx interface{}, orders ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx}, orders...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubmitOrders", reflect.TypeOf((*MockExchange)(nil).SubmitOrders), varargs...) +} + +// MockExchangeOrderQueryService is a mock of ExchangeOrderQueryService interface. +type MockExchangeOrderQueryService struct { + ctrl *gomock.Controller + recorder *MockExchangeOrderQueryServiceMockRecorder +} + +// MockExchangeOrderQueryServiceMockRecorder is the mock recorder for MockExchangeOrderQueryService. +type MockExchangeOrderQueryServiceMockRecorder struct { + mock *MockExchangeOrderQueryService +} + +// NewMockExchangeOrderQueryService creates a new mock instance. +func NewMockExchangeOrderQueryService(ctrl *gomock.Controller) *MockExchangeOrderQueryService { + mock := &MockExchangeOrderQueryService{ctrl: ctrl} + mock.recorder = &MockExchangeOrderQueryServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockExchangeOrderQueryService) EXPECT() *MockExchangeOrderQueryServiceMockRecorder { + return m.recorder +} + +// QueryOrder mocks base method. +func (m *MockExchangeOrderQueryService) QueryOrder(ctx context.Context, q types.OrderQuery) (*types.Order, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryOrder", ctx, q) + ret0, _ := ret[0].(*types.Order) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryOrder indicates an expected call of QueryOrder. +func (mr *MockExchangeOrderQueryServiceMockRecorder) QueryOrder(ctx, q interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryOrder", reflect.TypeOf((*MockExchangeOrderQueryService)(nil).QueryOrder), ctx, q) +} + +// MockExchangeTradeService is a mock of ExchangeTradeService interface. +type MockExchangeTradeService struct { + ctrl *gomock.Controller + recorder *MockExchangeTradeServiceMockRecorder +} + +// MockExchangeTradeServiceMockRecorder is the mock recorder for MockExchangeTradeService. +type MockExchangeTradeServiceMockRecorder struct { + mock *MockExchangeTradeService +} + +// NewMockExchangeTradeService creates a new mock instance. +func NewMockExchangeTradeService(ctrl *gomock.Controller) *MockExchangeTradeService { + mock := &MockExchangeTradeService{ctrl: ctrl} + mock.recorder = &MockExchangeTradeServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockExchangeTradeService) EXPECT() *MockExchangeTradeServiceMockRecorder { + return m.recorder +} + +// CancelOrders mocks base method. +func (m *MockExchangeTradeService) CancelOrders(ctx context.Context, orders ...types.Order) error { + m.ctrl.T.Helper() + varargs := []interface{}{ctx} + for _, a := range orders { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "CancelOrders", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// CancelOrders indicates an expected call of CancelOrders. +func (mr *MockExchangeTradeServiceMockRecorder) CancelOrders(ctx interface{}, orders ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx}, orders...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CancelOrders", reflect.TypeOf((*MockExchangeTradeService)(nil).CancelOrders), varargs...) +} + +// QueryAccount mocks base method. +func (m *MockExchangeTradeService) QueryAccount(ctx context.Context) (*types.Account, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryAccount", ctx) + ret0, _ := ret[0].(*types.Account) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryAccount indicates an expected call of QueryAccount. +func (mr *MockExchangeTradeServiceMockRecorder) QueryAccount(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryAccount", reflect.TypeOf((*MockExchangeTradeService)(nil).QueryAccount), ctx) +} + +// QueryAccountBalances mocks base method. +func (m *MockExchangeTradeService) QueryAccountBalances(ctx context.Context) (types.BalanceMap, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryAccountBalances", ctx) + ret0, _ := ret[0].(types.BalanceMap) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryAccountBalances indicates an expected call of QueryAccountBalances. +func (mr *MockExchangeTradeServiceMockRecorder) QueryAccountBalances(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryAccountBalances", reflect.TypeOf((*MockExchangeTradeService)(nil).QueryAccountBalances), ctx) +} + +// QueryOpenOrders mocks base method. +func (m *MockExchangeTradeService) QueryOpenOrders(ctx context.Context, symbol string) ([]types.Order, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryOpenOrders", ctx, symbol) + ret0, _ := ret[0].([]types.Order) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryOpenOrders indicates an expected call of QueryOpenOrders. +func (mr *MockExchangeTradeServiceMockRecorder) QueryOpenOrders(ctx, symbol interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryOpenOrders", reflect.TypeOf((*MockExchangeTradeService)(nil).QueryOpenOrders), ctx, symbol) +} + +// SubmitOrders mocks base method. +func (m *MockExchangeTradeService) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) (types.OrderSlice, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx} + for _, a := range orders { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "SubmitOrders", varargs...) + ret0, _ := ret[0].(types.OrderSlice) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SubmitOrders indicates an expected call of SubmitOrders. +func (mr *MockExchangeTradeServiceMockRecorder) SubmitOrders(ctx interface{}, orders ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx}, orders...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubmitOrders", reflect.TypeOf((*MockExchangeTradeService)(nil).SubmitOrders), varargs...) +} + +// MockExchangeDefaultFeeRates is a mock of ExchangeDefaultFeeRates interface. +type MockExchangeDefaultFeeRates struct { + ctrl *gomock.Controller + recorder *MockExchangeDefaultFeeRatesMockRecorder +} + +// MockExchangeDefaultFeeRatesMockRecorder is the mock recorder for MockExchangeDefaultFeeRates. +type MockExchangeDefaultFeeRatesMockRecorder struct { + mock *MockExchangeDefaultFeeRates +} + +// NewMockExchangeDefaultFeeRates creates a new mock instance. +func NewMockExchangeDefaultFeeRates(ctrl *gomock.Controller) *MockExchangeDefaultFeeRates { + mock := &MockExchangeDefaultFeeRates{ctrl: ctrl} + mock.recorder = &MockExchangeDefaultFeeRatesMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockExchangeDefaultFeeRates) EXPECT() *MockExchangeDefaultFeeRatesMockRecorder { + return m.recorder +} + +// DefaultFeeRates mocks base method. +func (m *MockExchangeDefaultFeeRates) DefaultFeeRates() types.ExchangeFee { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DefaultFeeRates") + ret0, _ := ret[0].(types.ExchangeFee) + return ret0 +} + +// DefaultFeeRates indicates an expected call of DefaultFeeRates. +func (mr *MockExchangeDefaultFeeRatesMockRecorder) DefaultFeeRates() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DefaultFeeRates", reflect.TypeOf((*MockExchangeDefaultFeeRates)(nil).DefaultFeeRates)) +} + +// MockExchangeTradeHistoryService is a mock of ExchangeTradeHistoryService interface. +type MockExchangeTradeHistoryService struct { + ctrl *gomock.Controller + recorder *MockExchangeTradeHistoryServiceMockRecorder +} + +// MockExchangeTradeHistoryServiceMockRecorder is the mock recorder for MockExchangeTradeHistoryService. +type MockExchangeTradeHistoryServiceMockRecorder struct { + mock *MockExchangeTradeHistoryService +} + +// NewMockExchangeTradeHistoryService creates a new mock instance. +func NewMockExchangeTradeHistoryService(ctrl *gomock.Controller) *MockExchangeTradeHistoryService { + mock := &MockExchangeTradeHistoryService{ctrl: ctrl} + mock.recorder = &MockExchangeTradeHistoryServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockExchangeTradeHistoryService) EXPECT() *MockExchangeTradeHistoryServiceMockRecorder { + return m.recorder +} + +// QueryClosedOrders mocks base method. +func (m *MockExchangeTradeHistoryService) QueryClosedOrders(ctx context.Context, symbol string, since, until time.Time, lastOrderID uint64) ([]types.Order, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryClosedOrders", ctx, symbol, since, until, lastOrderID) + ret0, _ := ret[0].([]types.Order) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryClosedOrders indicates an expected call of QueryClosedOrders. +func (mr *MockExchangeTradeHistoryServiceMockRecorder) QueryClosedOrders(ctx, symbol, since, until, lastOrderID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryClosedOrders", reflect.TypeOf((*MockExchangeTradeHistoryService)(nil).QueryClosedOrders), ctx, symbol, since, until, lastOrderID) +} + +// QueryTrades mocks base method. +func (m *MockExchangeTradeHistoryService) QueryTrades(ctx context.Context, symbol string, options *types.TradeQueryOptions) ([]types.Trade, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryTrades", ctx, symbol, options) + ret0, _ := ret[0].([]types.Trade) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryTrades indicates an expected call of QueryTrades. +func (mr *MockExchangeTradeHistoryServiceMockRecorder) QueryTrades(ctx, symbol, options interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryTrades", reflect.TypeOf((*MockExchangeTradeHistoryService)(nil).QueryTrades), ctx, symbol, options) +} + +// MockExchangeMarketDataService is a mock of ExchangeMarketDataService interface. +type MockExchangeMarketDataService struct { + ctrl *gomock.Controller + recorder *MockExchangeMarketDataServiceMockRecorder +} + +// MockExchangeMarketDataServiceMockRecorder is the mock recorder for MockExchangeMarketDataService. +type MockExchangeMarketDataServiceMockRecorder struct { + mock *MockExchangeMarketDataService +} + +// NewMockExchangeMarketDataService creates a new mock instance. +func NewMockExchangeMarketDataService(ctrl *gomock.Controller) *MockExchangeMarketDataService { + mock := &MockExchangeMarketDataService{ctrl: ctrl} + mock.recorder = &MockExchangeMarketDataServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockExchangeMarketDataService) EXPECT() *MockExchangeMarketDataServiceMockRecorder { + return m.recorder +} + +// NewStream mocks base method. +func (m *MockExchangeMarketDataService) NewStream() types.Stream { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewStream") + ret0, _ := ret[0].(types.Stream) + return ret0 +} + +// NewStream indicates an expected call of NewStream. +func (mr *MockExchangeMarketDataServiceMockRecorder) NewStream() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewStream", reflect.TypeOf((*MockExchangeMarketDataService)(nil).NewStream)) +} + +// QueryKLines mocks base method. +func (m *MockExchangeMarketDataService) QueryKLines(ctx context.Context, symbol string, interval types.Interval, options types.KLineQueryOptions) ([]types.KLine, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryKLines", ctx, symbol, interval, options) + ret0, _ := ret[0].([]types.KLine) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryKLines indicates an expected call of QueryKLines. +func (mr *MockExchangeMarketDataServiceMockRecorder) QueryKLines(ctx, symbol, interval, options interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryKLines", reflect.TypeOf((*MockExchangeMarketDataService)(nil).QueryKLines), ctx, symbol, interval, options) +} + +// QueryMarkets mocks base method. +func (m *MockExchangeMarketDataService) QueryMarkets(ctx context.Context) (types.MarketMap, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryMarkets", ctx) + ret0, _ := ret[0].(types.MarketMap) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryMarkets indicates an expected call of QueryMarkets. +func (mr *MockExchangeMarketDataServiceMockRecorder) QueryMarkets(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryMarkets", reflect.TypeOf((*MockExchangeMarketDataService)(nil).QueryMarkets), ctx) +} + +// QueryTicker mocks base method. +func (m *MockExchangeMarketDataService) QueryTicker(ctx context.Context, symbol string) (*types.Ticker, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryTicker", ctx, symbol) + ret0, _ := ret[0].(*types.Ticker) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryTicker indicates an expected call of QueryTicker. +func (mr *MockExchangeMarketDataServiceMockRecorder) QueryTicker(ctx, symbol interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryTicker", reflect.TypeOf((*MockExchangeMarketDataService)(nil).QueryTicker), ctx, symbol) +} + +// QueryTickers mocks base method. +func (m *MockExchangeMarketDataService) QueryTickers(ctx context.Context, symbol ...string) (map[string]types.Ticker, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx} + for _, a := range symbol { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "QueryTickers", varargs...) + ret0, _ := ret[0].(map[string]types.Ticker) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryTickers indicates an expected call of QueryTickers. +func (mr *MockExchangeMarketDataServiceMockRecorder) QueryTickers(ctx interface{}, symbol ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx}, symbol...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryTickers", reflect.TypeOf((*MockExchangeMarketDataService)(nil).QueryTickers), varargs...) +} + +// MockCustomIntervalProvider is a mock of CustomIntervalProvider interface. +type MockCustomIntervalProvider struct { + ctrl *gomock.Controller + recorder *MockCustomIntervalProviderMockRecorder +} + +// MockCustomIntervalProviderMockRecorder is the mock recorder for MockCustomIntervalProvider. +type MockCustomIntervalProviderMockRecorder struct { + mock *MockCustomIntervalProvider +} + +// NewMockCustomIntervalProvider creates a new mock instance. +func NewMockCustomIntervalProvider(ctrl *gomock.Controller) *MockCustomIntervalProvider { + mock := &MockCustomIntervalProvider{ctrl: ctrl} + mock.recorder = &MockCustomIntervalProviderMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockCustomIntervalProvider) EXPECT() *MockCustomIntervalProviderMockRecorder { + return m.recorder +} + +// IsSupportedInterval mocks base method. +func (m *MockCustomIntervalProvider) IsSupportedInterval(interval types.Interval) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsSupportedInterval", interval) + ret0, _ := ret[0].(bool) + return ret0 +} + +// IsSupportedInterval indicates an expected call of IsSupportedInterval. +func (mr *MockCustomIntervalProviderMockRecorder) IsSupportedInterval(interval interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsSupportedInterval", reflect.TypeOf((*MockCustomIntervalProvider)(nil).IsSupportedInterval), interval) +} + +// SupportedInterval mocks base method. +func (m *MockCustomIntervalProvider) SupportedInterval() map[types.Interval]int { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SupportedInterval") + ret0, _ := ret[0].(map[types.Interval]int) + return ret0 +} + +// SupportedInterval indicates an expected call of SupportedInterval. +func (mr *MockCustomIntervalProviderMockRecorder) SupportedInterval() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SupportedInterval", reflect.TypeOf((*MockCustomIntervalProvider)(nil).SupportedInterval)) +} + +// MockExchangeTransferService is a mock of ExchangeTransferService interface. +type MockExchangeTransferService struct { + ctrl *gomock.Controller + recorder *MockExchangeTransferServiceMockRecorder +} + +// MockExchangeTransferServiceMockRecorder is the mock recorder for MockExchangeTransferService. +type MockExchangeTransferServiceMockRecorder struct { + mock *MockExchangeTransferService +} + +// NewMockExchangeTransferService creates a new mock instance. +func NewMockExchangeTransferService(ctrl *gomock.Controller) *MockExchangeTransferService { + mock := &MockExchangeTransferService{ctrl: ctrl} + mock.recorder = &MockExchangeTransferServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockExchangeTransferService) EXPECT() *MockExchangeTransferServiceMockRecorder { + return m.recorder +} + +// QueryDepositHistory mocks base method. +func (m *MockExchangeTransferService) QueryDepositHistory(ctx context.Context, asset string, since, until time.Time) ([]types.Deposit, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryDepositHistory", ctx, asset, since, until) + ret0, _ := ret[0].([]types.Deposit) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryDepositHistory indicates an expected call of QueryDepositHistory. +func (mr *MockExchangeTransferServiceMockRecorder) QueryDepositHistory(ctx, asset, since, until interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryDepositHistory", reflect.TypeOf((*MockExchangeTransferService)(nil).QueryDepositHistory), ctx, asset, since, until) +} + +// QueryWithdrawHistory mocks base method. +func (m *MockExchangeTransferService) QueryWithdrawHistory(ctx context.Context, asset string, since, until time.Time) ([]types.Withdraw, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryWithdrawHistory", ctx, asset, since, until) + ret0, _ := ret[0].([]types.Withdraw) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryWithdrawHistory indicates an expected call of QueryWithdrawHistory. +func (mr *MockExchangeTransferServiceMockRecorder) QueryWithdrawHistory(ctx, asset, since, until interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryWithdrawHistory", reflect.TypeOf((*MockExchangeTransferService)(nil).QueryWithdrawHistory), ctx, asset, since, until) +} + +// MockExchangeWithdrawalService is a mock of ExchangeWithdrawalService interface. +type MockExchangeWithdrawalService struct { + ctrl *gomock.Controller + recorder *MockExchangeWithdrawalServiceMockRecorder +} + +// MockExchangeWithdrawalServiceMockRecorder is the mock recorder for MockExchangeWithdrawalService. +type MockExchangeWithdrawalServiceMockRecorder struct { + mock *MockExchangeWithdrawalService +} + +// NewMockExchangeWithdrawalService creates a new mock instance. +func NewMockExchangeWithdrawalService(ctrl *gomock.Controller) *MockExchangeWithdrawalService { + mock := &MockExchangeWithdrawalService{ctrl: ctrl} + mock.recorder = &MockExchangeWithdrawalServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockExchangeWithdrawalService) EXPECT() *MockExchangeWithdrawalServiceMockRecorder { + return m.recorder +} + +// Withdraw mocks base method. +func (m *MockExchangeWithdrawalService) Withdraw(ctx context.Context, asset string, amount fixedpoint.Value, address string, options *types.WithdrawalOptions) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Withdraw", ctx, asset, amount, address, options) + ret0, _ := ret[0].(error) + return ret0 +} + +// Withdraw indicates an expected call of Withdraw. +func (mr *MockExchangeWithdrawalServiceMockRecorder) Withdraw(ctx, asset, amount, address, options interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Withdraw", reflect.TypeOf((*MockExchangeWithdrawalService)(nil).Withdraw), ctx, asset, amount, address, options) +} + +// MockExchangeRewardService is a mock of ExchangeRewardService interface. +type MockExchangeRewardService struct { + ctrl *gomock.Controller + recorder *MockExchangeRewardServiceMockRecorder +} + +// MockExchangeRewardServiceMockRecorder is the mock recorder for MockExchangeRewardService. +type MockExchangeRewardServiceMockRecorder struct { + mock *MockExchangeRewardService +} + +// NewMockExchangeRewardService creates a new mock instance. +func NewMockExchangeRewardService(ctrl *gomock.Controller) *MockExchangeRewardService { + mock := &MockExchangeRewardService{ctrl: ctrl} + mock.recorder = &MockExchangeRewardServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockExchangeRewardService) EXPECT() *MockExchangeRewardServiceMockRecorder { + return m.recorder +} + +// QueryRewards mocks base method. +func (m *MockExchangeRewardService) QueryRewards(ctx context.Context, startTime time.Time) ([]types.Reward, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryRewards", ctx, startTime) + ret0, _ := ret[0].([]types.Reward) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryRewards indicates an expected call of QueryRewards. +func (mr *MockExchangeRewardServiceMockRecorder) QueryRewards(ctx, startTime interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryRewards", reflect.TypeOf((*MockExchangeRewardService)(nil).QueryRewards), ctx, startTime) +} From d140012fd5af99d470d99f3cb8334b66d8dd07b2 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 6 Jul 2022 01:32:05 +0800 Subject: [PATCH 81/92] fix mockgen command Signed-off-by: c9s --- pkg/types/exchange.go | 2 +- pkg/types/mocks/mock_exchange.go | 598 +++---------------------------- 2 files changed, 41 insertions(+), 559 deletions(-) diff --git a/pkg/types/exchange.go b/pkg/types/exchange.go index 39ced8d87..e026decb4 100644 --- a/pkg/types/exchange.go +++ b/pkg/types/exchange.go @@ -74,7 +74,7 @@ func ValidExchangeName(a string) (ExchangeName, error) { return "", fmt.Errorf("invalid exchange name: %s", a) } -//go:generate mockgen -destination=mocks/mock_exchange.go -package=mocks -source=exchange.go Exchange +//go:generate mockgen -destination=mocks/mock_exchange.go -package=mocks . Exchange type Exchange interface { Name() ExchangeName diff --git a/pkg/types/mocks/mock_exchange.go b/pkg/types/mocks/mock_exchange.go index 5c0410af7..731ab94de 100644 --- a/pkg/types/mocks/mock_exchange.go +++ b/pkg/types/mocks/mock_exchange.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: exchange.go +// Source: github.com/c9s/bbgo/pkg/types (interfaces: Exchange) // Package mocks is a generated GoMock package. package mocks @@ -7,9 +7,7 @@ package mocks import ( context "context" reflect "reflect" - time "time" - fixedpoint "github.com/c9s/bbgo/pkg/fixedpoint" types "github.com/c9s/bbgo/pkg/types" gomock "github.com/golang/mock/gomock" ) @@ -38,10 +36,10 @@ func (m *MockExchange) EXPECT() *MockExchangeMockRecorder { } // CancelOrders mocks base method. -func (m *MockExchange) CancelOrders(ctx context.Context, orders ...types.Order) error { +func (m *MockExchange) CancelOrders(arg0 context.Context, arg1 ...types.Order) error { m.ctrl.T.Helper() - varargs := []interface{}{ctx} - for _, a := range orders { + varargs := []interface{}{arg0} + for _, a := range arg1 { varargs = append(varargs, a) } ret := m.ctrl.Call(m, "CancelOrders", varargs...) @@ -50,9 +48,9 @@ func (m *MockExchange) CancelOrders(ctx context.Context, orders ...types.Order) } // CancelOrders indicates an expected call of CancelOrders. -func (mr *MockExchangeMockRecorder) CancelOrders(ctx interface{}, orders ...interface{}) *gomock.Call { +func (mr *MockExchangeMockRecorder) CancelOrders(arg0 interface{}, arg1 ...interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{ctx}, orders...) + varargs := append([]interface{}{arg0}, arg1...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CancelOrders", reflect.TypeOf((*MockExchange)(nil).CancelOrders), varargs...) } @@ -99,100 +97,100 @@ func (mr *MockExchangeMockRecorder) PlatformFeeCurrency() *gomock.Call { } // QueryAccount mocks base method. -func (m *MockExchange) QueryAccount(ctx context.Context) (*types.Account, error) { +func (m *MockExchange) QueryAccount(arg0 context.Context) (*types.Account, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "QueryAccount", ctx) + ret := m.ctrl.Call(m, "QueryAccount", arg0) ret0, _ := ret[0].(*types.Account) ret1, _ := ret[1].(error) return ret0, ret1 } // QueryAccount indicates an expected call of QueryAccount. -func (mr *MockExchangeMockRecorder) QueryAccount(ctx interface{}) *gomock.Call { +func (mr *MockExchangeMockRecorder) QueryAccount(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryAccount", reflect.TypeOf((*MockExchange)(nil).QueryAccount), ctx) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryAccount", reflect.TypeOf((*MockExchange)(nil).QueryAccount), arg0) } // QueryAccountBalances mocks base method. -func (m *MockExchange) QueryAccountBalances(ctx context.Context) (types.BalanceMap, error) { +func (m *MockExchange) QueryAccountBalances(arg0 context.Context) (types.BalanceMap, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "QueryAccountBalances", ctx) + ret := m.ctrl.Call(m, "QueryAccountBalances", arg0) ret0, _ := ret[0].(types.BalanceMap) ret1, _ := ret[1].(error) return ret0, ret1 } // QueryAccountBalances indicates an expected call of QueryAccountBalances. -func (mr *MockExchangeMockRecorder) QueryAccountBalances(ctx interface{}) *gomock.Call { +func (mr *MockExchangeMockRecorder) QueryAccountBalances(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryAccountBalances", reflect.TypeOf((*MockExchange)(nil).QueryAccountBalances), ctx) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryAccountBalances", reflect.TypeOf((*MockExchange)(nil).QueryAccountBalances), arg0) } // QueryKLines mocks base method. -func (m *MockExchange) QueryKLines(ctx context.Context, symbol string, interval types.Interval, options types.KLineQueryOptions) ([]types.KLine, error) { +func (m *MockExchange) QueryKLines(arg0 context.Context, arg1 string, arg2 types.Interval, arg3 types.KLineQueryOptions) ([]types.KLine, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "QueryKLines", ctx, symbol, interval, options) + ret := m.ctrl.Call(m, "QueryKLines", arg0, arg1, arg2, arg3) ret0, _ := ret[0].([]types.KLine) ret1, _ := ret[1].(error) return ret0, ret1 } // QueryKLines indicates an expected call of QueryKLines. -func (mr *MockExchangeMockRecorder) QueryKLines(ctx, symbol, interval, options interface{}) *gomock.Call { +func (mr *MockExchangeMockRecorder) QueryKLines(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryKLines", reflect.TypeOf((*MockExchange)(nil).QueryKLines), ctx, symbol, interval, options) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryKLines", reflect.TypeOf((*MockExchange)(nil).QueryKLines), arg0, arg1, arg2, arg3) } // QueryMarkets mocks base method. -func (m *MockExchange) QueryMarkets(ctx context.Context) (types.MarketMap, error) { +func (m *MockExchange) QueryMarkets(arg0 context.Context) (types.MarketMap, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "QueryMarkets", ctx) + ret := m.ctrl.Call(m, "QueryMarkets", arg0) ret0, _ := ret[0].(types.MarketMap) ret1, _ := ret[1].(error) return ret0, ret1 } // QueryMarkets indicates an expected call of QueryMarkets. -func (mr *MockExchangeMockRecorder) QueryMarkets(ctx interface{}) *gomock.Call { +func (mr *MockExchangeMockRecorder) QueryMarkets(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryMarkets", reflect.TypeOf((*MockExchange)(nil).QueryMarkets), ctx) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryMarkets", reflect.TypeOf((*MockExchange)(nil).QueryMarkets), arg0) } // QueryOpenOrders mocks base method. -func (m *MockExchange) QueryOpenOrders(ctx context.Context, symbol string) ([]types.Order, error) { +func (m *MockExchange) QueryOpenOrders(arg0 context.Context, arg1 string) ([]types.Order, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "QueryOpenOrders", ctx, symbol) + ret := m.ctrl.Call(m, "QueryOpenOrders", arg0, arg1) ret0, _ := ret[0].([]types.Order) ret1, _ := ret[1].(error) return ret0, ret1 } // QueryOpenOrders indicates an expected call of QueryOpenOrders. -func (mr *MockExchangeMockRecorder) QueryOpenOrders(ctx, symbol interface{}) *gomock.Call { +func (mr *MockExchangeMockRecorder) QueryOpenOrders(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryOpenOrders", reflect.TypeOf((*MockExchange)(nil).QueryOpenOrders), ctx, symbol) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryOpenOrders", reflect.TypeOf((*MockExchange)(nil).QueryOpenOrders), arg0, arg1) } // QueryTicker mocks base method. -func (m *MockExchange) QueryTicker(ctx context.Context, symbol string) (*types.Ticker, error) { +func (m *MockExchange) QueryTicker(arg0 context.Context, arg1 string) (*types.Ticker, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "QueryTicker", ctx, symbol) + ret := m.ctrl.Call(m, "QueryTicker", arg0, arg1) ret0, _ := ret[0].(*types.Ticker) ret1, _ := ret[1].(error) return ret0, ret1 } // QueryTicker indicates an expected call of QueryTicker. -func (mr *MockExchangeMockRecorder) QueryTicker(ctx, symbol interface{}) *gomock.Call { +func (mr *MockExchangeMockRecorder) QueryTicker(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryTicker", reflect.TypeOf((*MockExchange)(nil).QueryTicker), ctx, symbol) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryTicker", reflect.TypeOf((*MockExchange)(nil).QueryTicker), arg0, arg1) } // QueryTickers mocks base method. -func (m *MockExchange) QueryTickers(ctx context.Context, symbol ...string) (map[string]types.Ticker, error) { +func (m *MockExchange) QueryTickers(arg0 context.Context, arg1 ...string) (map[string]types.Ticker, error) { m.ctrl.T.Helper() - varargs := []interface{}{ctx} - for _, a := range symbol { + varargs := []interface{}{arg0} + for _, a := range arg1 { varargs = append(varargs, a) } ret := m.ctrl.Call(m, "QueryTickers", varargs...) @@ -202,17 +200,17 @@ func (m *MockExchange) QueryTickers(ctx context.Context, symbol ...string) (map[ } // QueryTickers indicates an expected call of QueryTickers. -func (mr *MockExchangeMockRecorder) QueryTickers(ctx interface{}, symbol ...interface{}) *gomock.Call { +func (mr *MockExchangeMockRecorder) QueryTickers(arg0 interface{}, arg1 ...interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{ctx}, symbol...) + varargs := append([]interface{}{arg0}, arg1...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryTickers", reflect.TypeOf((*MockExchange)(nil).QueryTickers), varargs...) } // SubmitOrders mocks base method. -func (m *MockExchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) (types.OrderSlice, error) { +func (m *MockExchange) SubmitOrders(arg0 context.Context, arg1 ...types.SubmitOrder) (types.OrderSlice, error) { m.ctrl.T.Helper() - varargs := []interface{}{ctx} - for _, a := range orders { + varargs := []interface{}{arg0} + for _, a := range arg1 { varargs = append(varargs, a) } ret := m.ctrl.Call(m, "SubmitOrders", varargs...) @@ -222,524 +220,8 @@ func (m *MockExchange) SubmitOrders(ctx context.Context, orders ...types.SubmitO } // SubmitOrders indicates an expected call of SubmitOrders. -func (mr *MockExchangeMockRecorder) SubmitOrders(ctx interface{}, orders ...interface{}) *gomock.Call { +func (mr *MockExchangeMockRecorder) SubmitOrders(arg0 interface{}, arg1 ...interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{ctx}, orders...) + varargs := append([]interface{}{arg0}, arg1...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubmitOrders", reflect.TypeOf((*MockExchange)(nil).SubmitOrders), varargs...) } - -// MockExchangeOrderQueryService is a mock of ExchangeOrderQueryService interface. -type MockExchangeOrderQueryService struct { - ctrl *gomock.Controller - recorder *MockExchangeOrderQueryServiceMockRecorder -} - -// MockExchangeOrderQueryServiceMockRecorder is the mock recorder for MockExchangeOrderQueryService. -type MockExchangeOrderQueryServiceMockRecorder struct { - mock *MockExchangeOrderQueryService -} - -// NewMockExchangeOrderQueryService creates a new mock instance. -func NewMockExchangeOrderQueryService(ctrl *gomock.Controller) *MockExchangeOrderQueryService { - mock := &MockExchangeOrderQueryService{ctrl: ctrl} - mock.recorder = &MockExchangeOrderQueryServiceMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockExchangeOrderQueryService) EXPECT() *MockExchangeOrderQueryServiceMockRecorder { - return m.recorder -} - -// QueryOrder mocks base method. -func (m *MockExchangeOrderQueryService) QueryOrder(ctx context.Context, q types.OrderQuery) (*types.Order, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "QueryOrder", ctx, q) - ret0, _ := ret[0].(*types.Order) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// QueryOrder indicates an expected call of QueryOrder. -func (mr *MockExchangeOrderQueryServiceMockRecorder) QueryOrder(ctx, q interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryOrder", reflect.TypeOf((*MockExchangeOrderQueryService)(nil).QueryOrder), ctx, q) -} - -// MockExchangeTradeService is a mock of ExchangeTradeService interface. -type MockExchangeTradeService struct { - ctrl *gomock.Controller - recorder *MockExchangeTradeServiceMockRecorder -} - -// MockExchangeTradeServiceMockRecorder is the mock recorder for MockExchangeTradeService. -type MockExchangeTradeServiceMockRecorder struct { - mock *MockExchangeTradeService -} - -// NewMockExchangeTradeService creates a new mock instance. -func NewMockExchangeTradeService(ctrl *gomock.Controller) *MockExchangeTradeService { - mock := &MockExchangeTradeService{ctrl: ctrl} - mock.recorder = &MockExchangeTradeServiceMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockExchangeTradeService) EXPECT() *MockExchangeTradeServiceMockRecorder { - return m.recorder -} - -// CancelOrders mocks base method. -func (m *MockExchangeTradeService) CancelOrders(ctx context.Context, orders ...types.Order) error { - m.ctrl.T.Helper() - varargs := []interface{}{ctx} - for _, a := range orders { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "CancelOrders", varargs...) - ret0, _ := ret[0].(error) - return ret0 -} - -// CancelOrders indicates an expected call of CancelOrders. -func (mr *MockExchangeTradeServiceMockRecorder) CancelOrders(ctx interface{}, orders ...interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{ctx}, orders...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CancelOrders", reflect.TypeOf((*MockExchangeTradeService)(nil).CancelOrders), varargs...) -} - -// QueryAccount mocks base method. -func (m *MockExchangeTradeService) QueryAccount(ctx context.Context) (*types.Account, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "QueryAccount", ctx) - ret0, _ := ret[0].(*types.Account) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// QueryAccount indicates an expected call of QueryAccount. -func (mr *MockExchangeTradeServiceMockRecorder) QueryAccount(ctx interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryAccount", reflect.TypeOf((*MockExchangeTradeService)(nil).QueryAccount), ctx) -} - -// QueryAccountBalances mocks base method. -func (m *MockExchangeTradeService) QueryAccountBalances(ctx context.Context) (types.BalanceMap, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "QueryAccountBalances", ctx) - ret0, _ := ret[0].(types.BalanceMap) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// QueryAccountBalances indicates an expected call of QueryAccountBalances. -func (mr *MockExchangeTradeServiceMockRecorder) QueryAccountBalances(ctx interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryAccountBalances", reflect.TypeOf((*MockExchangeTradeService)(nil).QueryAccountBalances), ctx) -} - -// QueryOpenOrders mocks base method. -func (m *MockExchangeTradeService) QueryOpenOrders(ctx context.Context, symbol string) ([]types.Order, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "QueryOpenOrders", ctx, symbol) - ret0, _ := ret[0].([]types.Order) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// QueryOpenOrders indicates an expected call of QueryOpenOrders. -func (mr *MockExchangeTradeServiceMockRecorder) QueryOpenOrders(ctx, symbol interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryOpenOrders", reflect.TypeOf((*MockExchangeTradeService)(nil).QueryOpenOrders), ctx, symbol) -} - -// SubmitOrders mocks base method. -func (m *MockExchangeTradeService) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) (types.OrderSlice, error) { - m.ctrl.T.Helper() - varargs := []interface{}{ctx} - for _, a := range orders { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "SubmitOrders", varargs...) - ret0, _ := ret[0].(types.OrderSlice) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// SubmitOrders indicates an expected call of SubmitOrders. -func (mr *MockExchangeTradeServiceMockRecorder) SubmitOrders(ctx interface{}, orders ...interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{ctx}, orders...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubmitOrders", reflect.TypeOf((*MockExchangeTradeService)(nil).SubmitOrders), varargs...) -} - -// MockExchangeDefaultFeeRates is a mock of ExchangeDefaultFeeRates interface. -type MockExchangeDefaultFeeRates struct { - ctrl *gomock.Controller - recorder *MockExchangeDefaultFeeRatesMockRecorder -} - -// MockExchangeDefaultFeeRatesMockRecorder is the mock recorder for MockExchangeDefaultFeeRates. -type MockExchangeDefaultFeeRatesMockRecorder struct { - mock *MockExchangeDefaultFeeRates -} - -// NewMockExchangeDefaultFeeRates creates a new mock instance. -func NewMockExchangeDefaultFeeRates(ctrl *gomock.Controller) *MockExchangeDefaultFeeRates { - mock := &MockExchangeDefaultFeeRates{ctrl: ctrl} - mock.recorder = &MockExchangeDefaultFeeRatesMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockExchangeDefaultFeeRates) EXPECT() *MockExchangeDefaultFeeRatesMockRecorder { - return m.recorder -} - -// DefaultFeeRates mocks base method. -func (m *MockExchangeDefaultFeeRates) DefaultFeeRates() types.ExchangeFee { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DefaultFeeRates") - ret0, _ := ret[0].(types.ExchangeFee) - return ret0 -} - -// DefaultFeeRates indicates an expected call of DefaultFeeRates. -func (mr *MockExchangeDefaultFeeRatesMockRecorder) DefaultFeeRates() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DefaultFeeRates", reflect.TypeOf((*MockExchangeDefaultFeeRates)(nil).DefaultFeeRates)) -} - -// MockExchangeTradeHistoryService is a mock of ExchangeTradeHistoryService interface. -type MockExchangeTradeHistoryService struct { - ctrl *gomock.Controller - recorder *MockExchangeTradeHistoryServiceMockRecorder -} - -// MockExchangeTradeHistoryServiceMockRecorder is the mock recorder for MockExchangeTradeHistoryService. -type MockExchangeTradeHistoryServiceMockRecorder struct { - mock *MockExchangeTradeHistoryService -} - -// NewMockExchangeTradeHistoryService creates a new mock instance. -func NewMockExchangeTradeHistoryService(ctrl *gomock.Controller) *MockExchangeTradeHistoryService { - mock := &MockExchangeTradeHistoryService{ctrl: ctrl} - mock.recorder = &MockExchangeTradeHistoryServiceMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockExchangeTradeHistoryService) EXPECT() *MockExchangeTradeHistoryServiceMockRecorder { - return m.recorder -} - -// QueryClosedOrders mocks base method. -func (m *MockExchangeTradeHistoryService) QueryClosedOrders(ctx context.Context, symbol string, since, until time.Time, lastOrderID uint64) ([]types.Order, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "QueryClosedOrders", ctx, symbol, since, until, lastOrderID) - ret0, _ := ret[0].([]types.Order) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// QueryClosedOrders indicates an expected call of QueryClosedOrders. -func (mr *MockExchangeTradeHistoryServiceMockRecorder) QueryClosedOrders(ctx, symbol, since, until, lastOrderID interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryClosedOrders", reflect.TypeOf((*MockExchangeTradeHistoryService)(nil).QueryClosedOrders), ctx, symbol, since, until, lastOrderID) -} - -// QueryTrades mocks base method. -func (m *MockExchangeTradeHistoryService) QueryTrades(ctx context.Context, symbol string, options *types.TradeQueryOptions) ([]types.Trade, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "QueryTrades", ctx, symbol, options) - ret0, _ := ret[0].([]types.Trade) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// QueryTrades indicates an expected call of QueryTrades. -func (mr *MockExchangeTradeHistoryServiceMockRecorder) QueryTrades(ctx, symbol, options interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryTrades", reflect.TypeOf((*MockExchangeTradeHistoryService)(nil).QueryTrades), ctx, symbol, options) -} - -// MockExchangeMarketDataService is a mock of ExchangeMarketDataService interface. -type MockExchangeMarketDataService struct { - ctrl *gomock.Controller - recorder *MockExchangeMarketDataServiceMockRecorder -} - -// MockExchangeMarketDataServiceMockRecorder is the mock recorder for MockExchangeMarketDataService. -type MockExchangeMarketDataServiceMockRecorder struct { - mock *MockExchangeMarketDataService -} - -// NewMockExchangeMarketDataService creates a new mock instance. -func NewMockExchangeMarketDataService(ctrl *gomock.Controller) *MockExchangeMarketDataService { - mock := &MockExchangeMarketDataService{ctrl: ctrl} - mock.recorder = &MockExchangeMarketDataServiceMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockExchangeMarketDataService) EXPECT() *MockExchangeMarketDataServiceMockRecorder { - return m.recorder -} - -// NewStream mocks base method. -func (m *MockExchangeMarketDataService) NewStream() types.Stream { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "NewStream") - ret0, _ := ret[0].(types.Stream) - return ret0 -} - -// NewStream indicates an expected call of NewStream. -func (mr *MockExchangeMarketDataServiceMockRecorder) NewStream() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewStream", reflect.TypeOf((*MockExchangeMarketDataService)(nil).NewStream)) -} - -// QueryKLines mocks base method. -func (m *MockExchangeMarketDataService) QueryKLines(ctx context.Context, symbol string, interval types.Interval, options types.KLineQueryOptions) ([]types.KLine, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "QueryKLines", ctx, symbol, interval, options) - ret0, _ := ret[0].([]types.KLine) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// QueryKLines indicates an expected call of QueryKLines. -func (mr *MockExchangeMarketDataServiceMockRecorder) QueryKLines(ctx, symbol, interval, options interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryKLines", reflect.TypeOf((*MockExchangeMarketDataService)(nil).QueryKLines), ctx, symbol, interval, options) -} - -// QueryMarkets mocks base method. -func (m *MockExchangeMarketDataService) QueryMarkets(ctx context.Context) (types.MarketMap, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "QueryMarkets", ctx) - ret0, _ := ret[0].(types.MarketMap) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// QueryMarkets indicates an expected call of QueryMarkets. -func (mr *MockExchangeMarketDataServiceMockRecorder) QueryMarkets(ctx interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryMarkets", reflect.TypeOf((*MockExchangeMarketDataService)(nil).QueryMarkets), ctx) -} - -// QueryTicker mocks base method. -func (m *MockExchangeMarketDataService) QueryTicker(ctx context.Context, symbol string) (*types.Ticker, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "QueryTicker", ctx, symbol) - ret0, _ := ret[0].(*types.Ticker) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// QueryTicker indicates an expected call of QueryTicker. -func (mr *MockExchangeMarketDataServiceMockRecorder) QueryTicker(ctx, symbol interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryTicker", reflect.TypeOf((*MockExchangeMarketDataService)(nil).QueryTicker), ctx, symbol) -} - -// QueryTickers mocks base method. -func (m *MockExchangeMarketDataService) QueryTickers(ctx context.Context, symbol ...string) (map[string]types.Ticker, error) { - m.ctrl.T.Helper() - varargs := []interface{}{ctx} - for _, a := range symbol { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "QueryTickers", varargs...) - ret0, _ := ret[0].(map[string]types.Ticker) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// QueryTickers indicates an expected call of QueryTickers. -func (mr *MockExchangeMarketDataServiceMockRecorder) QueryTickers(ctx interface{}, symbol ...interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{ctx}, symbol...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryTickers", reflect.TypeOf((*MockExchangeMarketDataService)(nil).QueryTickers), varargs...) -} - -// MockCustomIntervalProvider is a mock of CustomIntervalProvider interface. -type MockCustomIntervalProvider struct { - ctrl *gomock.Controller - recorder *MockCustomIntervalProviderMockRecorder -} - -// MockCustomIntervalProviderMockRecorder is the mock recorder for MockCustomIntervalProvider. -type MockCustomIntervalProviderMockRecorder struct { - mock *MockCustomIntervalProvider -} - -// NewMockCustomIntervalProvider creates a new mock instance. -func NewMockCustomIntervalProvider(ctrl *gomock.Controller) *MockCustomIntervalProvider { - mock := &MockCustomIntervalProvider{ctrl: ctrl} - mock.recorder = &MockCustomIntervalProviderMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockCustomIntervalProvider) EXPECT() *MockCustomIntervalProviderMockRecorder { - return m.recorder -} - -// IsSupportedInterval mocks base method. -func (m *MockCustomIntervalProvider) IsSupportedInterval(interval types.Interval) bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "IsSupportedInterval", interval) - ret0, _ := ret[0].(bool) - return ret0 -} - -// IsSupportedInterval indicates an expected call of IsSupportedInterval. -func (mr *MockCustomIntervalProviderMockRecorder) IsSupportedInterval(interval interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsSupportedInterval", reflect.TypeOf((*MockCustomIntervalProvider)(nil).IsSupportedInterval), interval) -} - -// SupportedInterval mocks base method. -func (m *MockCustomIntervalProvider) SupportedInterval() map[types.Interval]int { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SupportedInterval") - ret0, _ := ret[0].(map[types.Interval]int) - return ret0 -} - -// SupportedInterval indicates an expected call of SupportedInterval. -func (mr *MockCustomIntervalProviderMockRecorder) SupportedInterval() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SupportedInterval", reflect.TypeOf((*MockCustomIntervalProvider)(nil).SupportedInterval)) -} - -// MockExchangeTransferService is a mock of ExchangeTransferService interface. -type MockExchangeTransferService struct { - ctrl *gomock.Controller - recorder *MockExchangeTransferServiceMockRecorder -} - -// MockExchangeTransferServiceMockRecorder is the mock recorder for MockExchangeTransferService. -type MockExchangeTransferServiceMockRecorder struct { - mock *MockExchangeTransferService -} - -// NewMockExchangeTransferService creates a new mock instance. -func NewMockExchangeTransferService(ctrl *gomock.Controller) *MockExchangeTransferService { - mock := &MockExchangeTransferService{ctrl: ctrl} - mock.recorder = &MockExchangeTransferServiceMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockExchangeTransferService) EXPECT() *MockExchangeTransferServiceMockRecorder { - return m.recorder -} - -// QueryDepositHistory mocks base method. -func (m *MockExchangeTransferService) QueryDepositHistory(ctx context.Context, asset string, since, until time.Time) ([]types.Deposit, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "QueryDepositHistory", ctx, asset, since, until) - ret0, _ := ret[0].([]types.Deposit) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// QueryDepositHistory indicates an expected call of QueryDepositHistory. -func (mr *MockExchangeTransferServiceMockRecorder) QueryDepositHistory(ctx, asset, since, until interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryDepositHistory", reflect.TypeOf((*MockExchangeTransferService)(nil).QueryDepositHistory), ctx, asset, since, until) -} - -// QueryWithdrawHistory mocks base method. -func (m *MockExchangeTransferService) QueryWithdrawHistory(ctx context.Context, asset string, since, until time.Time) ([]types.Withdraw, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "QueryWithdrawHistory", ctx, asset, since, until) - ret0, _ := ret[0].([]types.Withdraw) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// QueryWithdrawHistory indicates an expected call of QueryWithdrawHistory. -func (mr *MockExchangeTransferServiceMockRecorder) QueryWithdrawHistory(ctx, asset, since, until interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryWithdrawHistory", reflect.TypeOf((*MockExchangeTransferService)(nil).QueryWithdrawHistory), ctx, asset, since, until) -} - -// MockExchangeWithdrawalService is a mock of ExchangeWithdrawalService interface. -type MockExchangeWithdrawalService struct { - ctrl *gomock.Controller - recorder *MockExchangeWithdrawalServiceMockRecorder -} - -// MockExchangeWithdrawalServiceMockRecorder is the mock recorder for MockExchangeWithdrawalService. -type MockExchangeWithdrawalServiceMockRecorder struct { - mock *MockExchangeWithdrawalService -} - -// NewMockExchangeWithdrawalService creates a new mock instance. -func NewMockExchangeWithdrawalService(ctrl *gomock.Controller) *MockExchangeWithdrawalService { - mock := &MockExchangeWithdrawalService{ctrl: ctrl} - mock.recorder = &MockExchangeWithdrawalServiceMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockExchangeWithdrawalService) EXPECT() *MockExchangeWithdrawalServiceMockRecorder { - return m.recorder -} - -// Withdraw mocks base method. -func (m *MockExchangeWithdrawalService) Withdraw(ctx context.Context, asset string, amount fixedpoint.Value, address string, options *types.WithdrawalOptions) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Withdraw", ctx, asset, amount, address, options) - ret0, _ := ret[0].(error) - return ret0 -} - -// Withdraw indicates an expected call of Withdraw. -func (mr *MockExchangeWithdrawalServiceMockRecorder) Withdraw(ctx, asset, amount, address, options interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Withdraw", reflect.TypeOf((*MockExchangeWithdrawalService)(nil).Withdraw), ctx, asset, amount, address, options) -} - -// MockExchangeRewardService is a mock of ExchangeRewardService interface. -type MockExchangeRewardService struct { - ctrl *gomock.Controller - recorder *MockExchangeRewardServiceMockRecorder -} - -// MockExchangeRewardServiceMockRecorder is the mock recorder for MockExchangeRewardService. -type MockExchangeRewardServiceMockRecorder struct { - mock *MockExchangeRewardService -} - -// NewMockExchangeRewardService creates a new mock instance. -func NewMockExchangeRewardService(ctrl *gomock.Controller) *MockExchangeRewardService { - mock := &MockExchangeRewardService{ctrl: ctrl} - mock.recorder = &MockExchangeRewardServiceMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockExchangeRewardService) EXPECT() *MockExchangeRewardServiceMockRecorder { - return m.recorder -} - -// QueryRewards mocks base method. -func (m *MockExchangeRewardService) QueryRewards(ctx context.Context, startTime time.Time) ([]types.Reward, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "QueryRewards", ctx, startTime) - ret0, _ := ret[0].([]types.Reward) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// QueryRewards indicates an expected call of QueryRewards. -func (mr *MockExchangeRewardServiceMockRecorder) QueryRewards(ctx, startTime interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryRewards", reflect.TypeOf((*MockExchangeRewardService)(nil).QueryRewards), ctx, startTime) -} From 2bc12c0522e27ead0b213342b9c47de3c6c6ccb5 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 6 Jul 2022 03:04:01 +0800 Subject: [PATCH 82/92] add trailing stop and it's test cases with gomock Signed-off-by: c9s --- pkg/bbgo/exit_trailing_stop.go | 104 ++++++++++++++++++++++++---- pkg/bbgo/exit_trailing_stop_test.go | 102 +++++++++++++++++++++++++++ pkg/bbgo/session.go | 21 +----- pkg/strategy/pivotshort/strategy.go | 3 - 4 files changed, 194 insertions(+), 36 deletions(-) create mode 100644 pkg/bbgo/exit_trailing_stop_test.go diff --git a/pkg/bbgo/exit_trailing_stop.go b/pkg/bbgo/exit_trailing_stop.go index 769d7cda8..fc953037b 100644 --- a/pkg/bbgo/exit_trailing_stop.go +++ b/pkg/bbgo/exit_trailing_stop.go @@ -1,6 +1,9 @@ package bbgo import ( + "context" + "fmt" + "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) @@ -11,6 +14,8 @@ type TrailingStop2 struct { // CallbackRate is the callback rate from the previous high price CallbackRate fixedpoint.Value `json:"callbackRate,omitempty"` + ActivationRatio fixedpoint.Value `json:"activationRatio,omitempty"` + // ClosePosition is a percentage of the position to be closed ClosePosition fixedpoint.Value `json:"closePosition,omitempty"` @@ -22,6 +27,13 @@ type TrailingStop2 struct { // KLine per Interval will be used for updating the stop order Interval types.Interval `json:"interval,omitempty"` + Side types.SideType `json:"side,omitempty"` + + latestHigh fixedpoint.Value + + // activated: when the price reaches the min profit price, we set the activated to true to enable trailing stop + activated bool + // private fields session *ExchangeSession orderExecutor *GeneralOrderExecutor @@ -29,7 +41,7 @@ type TrailingStop2 struct { func (s *TrailingStop2) Subscribe(session *ExchangeSession) { // use 1m kline to handle roi stop - session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: types.Interval1m}) + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) } func (s *TrailingStop2) Bind(session *ExchangeSession, orderExecutor *GeneralOrderExecutor) { @@ -37,7 +49,7 @@ func (s *TrailingStop2) Bind(session *ExchangeSession, orderExecutor *GeneralOrd s.orderExecutor = orderExecutor position := orderExecutor.Position() - session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, func(kline types.KLine) { + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { s.checkStopPrice(kline.Close, position) })) @@ -52,18 +64,82 @@ func (s *TrailingStop2) Bind(session *ExchangeSession, orderExecutor *GeneralOrd } } -func (s *TrailingStop2) checkStopPrice(closePrice fixedpoint.Value, position *types.Position) { - if position.IsClosed() || position.IsDust(closePrice) { - return +func (s *TrailingStop2) getRatio(price fixedpoint.Value, position *types.Position) (fixedpoint.Value, error) { + switch s.Side { + case types.SideTypeBuy: + // for short position + return position.AverageCost.Sub(price).Div(price), nil + case types.SideTypeSell: + return price.Sub(position.AverageCost).Div(position.AverageCost), nil } - /* - roi := position.ROI(closePrice) - if roi.Compare(s.CallbackRate.Neg()) < 0 { - // stop loss - Notify("[TrailingStop2] %s stop loss triggered by ROI %s/%s, price: %f", position.Symbol, roi.Percentage(), s.Percentage.Neg().Percentage(), closePrice.Float64()) - _ = s.orderExecutor.ClosePosition(context.Background(), fixedpoint.One, "TrailingStop2") - return - } - */ + return fixedpoint.Zero, fmt.Errorf("unexpected side type: %v", s.Side) +} + +func (s *TrailingStop2) checkStopPrice(price fixedpoint.Value, position *types.Position) error { + if position.IsClosed() || position.IsDust(price) { + return nil + } + + if !s.MinProfit.IsZero() { + // check if we have the minimal profit + roi := position.ROI(price) + if roi.Compare(s.MinProfit) >= 0 { + Notify("[trailingStop] activated: ROI %f > minimal profit ratio %f", roi.Float64(), s.MinProfit.Float64()) + s.activated = true + } + } else if !s.ActivationRatio.IsZero() { + ratio, err := s.getRatio(price, position) + if err != nil { + return err + } + + if ratio.Compare(s.ActivationRatio) >= 0 { + s.activated = true + } + } + + // update the latest high for the sell order, or the latest low for the buy order + if s.latestHigh.IsZero() { + s.latestHigh = price + } else { + switch s.Side { + case types.SideTypeBuy: + s.latestHigh = fixedpoint.Min(price, s.latestHigh) + case types.SideTypeSell: + s.latestHigh = fixedpoint.Max(price, s.latestHigh) + } + } + + if !s.activated { + return nil + } + + switch s.Side { + case types.SideTypeBuy: + s.latestHigh = fixedpoint.Min(price, s.latestHigh) + + change := price.Sub(s.latestHigh).Div(s.latestHigh) + if change.Compare(s.CallbackRate) >= 0 { + // submit order + return s.triggerStop(price) + } + + case types.SideTypeSell: + s.latestHigh = fixedpoint.Max(price, s.latestHigh) + + change := price.Sub(s.latestHigh).Div(s.latestHigh) + if change.Compare(s.CallbackRate) >= 0 { + // submit order + return s.triggerStop(price) + } + } + + return nil +} + +func (s *TrailingStop2) triggerStop(price fixedpoint.Value) error { + Notify("[TrailingStop] %s stop loss triggered. price: %f callback rate: %f", s.Symbol, price.Float64(), s.CallbackRate.Float64()) + ctx := context.Background() + return s.orderExecutor.ClosePosition(ctx, fixedpoint.One, "trailingStop") } diff --git a/pkg/bbgo/exit_trailing_stop_test.go b/pkg/bbgo/exit_trailing_stop_test.go new file mode 100644 index 000000000..d1285a1a3 --- /dev/null +++ b/pkg/bbgo/exit_trailing_stop_test.go @@ -0,0 +1,102 @@ +package bbgo + +import ( + "testing" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/types/mocks" +) + +// getTestMarket returns the BTCUSDT market information +// for tests, we always use BTCUSDT +func getTestMarket() types.Market { + market := types.Market{ + Symbol: "BTCUSDT", + PricePrecision: 8, + VolumePrecision: 8, + QuoteCurrency: "USDT", + BaseCurrency: "BTC", + MinNotional: fixedpoint.MustNewFromString("0.001"), + MinAmount: fixedpoint.MustNewFromString("10.0"), + MinQuantity: fixedpoint.MustNewFromString("0.001"), + } + return market +} + +func TestTrailingStop(t *testing.T) { + market := getTestMarket() + + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockEx := mocks.NewMockExchange(mockCtrl) + mockEx.EXPECT().NewStream().Return(&types.StandardStream{}).Times(2) + mockEx.EXPECT().SubmitOrders(gomock.Any(), types.SubmitOrder{ + Symbol: "BTCUSDT", + Side: types.SideTypeBuy, + Type: types.OrderTypeMarket, + Market: market, + Quantity: fixedpoint.NewFromFloat(1.0), + Tag: "trailingStop", + }) + + session := NewExchangeSession("test", mockEx) + assert.NotNil(t, session) + + session.markets[market.Symbol] = market + + position := types.NewPositionFromMarket(market) + position.AverageCost = fixedpoint.NewFromFloat(20000.0) + position.Base = fixedpoint.NewFromFloat(-1.0) + + orderExecutor := NewGeneralOrderExecutor(session, "BTCUSDT", "test", "test-01", position) + + activationRatio := fixedpoint.NewFromFloat(0.01) + callbackRatio := fixedpoint.NewFromFloat(0.01) + stop := &TrailingStop2{ + Symbol: "BTCUSDT", + Interval: types.Interval1m, + Side: types.SideTypeBuy, + CallbackRate: callbackRatio, + ActivationRatio: activationRatio, + } + stop.Bind(session, orderExecutor) + + // the same price + currentPrice := fixedpoint.NewFromFloat(20000.0) + err := stop.checkStopPrice(currentPrice, position) + if assert.NoError(t, err) { + assert.False(t, stop.activated) + } + + // 20000 - 1% = 19800 + currentPrice = currentPrice.Mul(one.Sub(activationRatio)) + err = stop.checkStopPrice(currentPrice, position) + if assert.NoError(t, err) { + assert.True(t, stop.activated) + assert.Equal(t, fixedpoint.NewFromFloat(19800.0), currentPrice) + assert.Equal(t, fixedpoint.NewFromFloat(19800.0), stop.latestHigh) + } + + // 19800 - 1% = 19602 + currentPrice = currentPrice.Mul(one.Sub(callbackRatio)) + err = stop.checkStopPrice(currentPrice, position) + if assert.NoError(t, err) { + assert.Equal(t, fixedpoint.NewFromFloat(19602.0), currentPrice) + assert.Equal(t, fixedpoint.NewFromFloat(19602.0), stop.latestHigh) + assert.True(t, stop.activated) + } + + // 19602 + 1% = 19798.02 + currentPrice = currentPrice.Mul(one.Add(callbackRatio)) + err = stop.checkStopPrice(currentPrice, position) + if assert.NoError(t, err) { + assert.Equal(t, fixedpoint.NewFromFloat(19798.02), currentPrice) + assert.Equal(t, fixedpoint.NewFromFloat(19602.0), stop.latestHigh) + assert.True(t, stop.activated) + } +} diff --git a/pkg/bbgo/session.go b/pkg/bbgo/session.go index c6796c8f1..87bea4fe1 100644 --- a/pkg/bbgo/session.go +++ b/pkg/bbgo/session.go @@ -160,10 +160,6 @@ func (set *StandardIndicatorSet) VOLATILITY(iw types.IntervalWindow) *indicator. // ExchangeSession presents the exchange connection Session // It also maintains and collects the data returned from the stream. type ExchangeSession struct { - // exchange Session based notification system - // we make it as a value field so that we can configure it separately - Notifiability `json:"-" yaml:"-"` - // --------------------------- // Session config fields // --------------------------- @@ -253,12 +249,6 @@ func NewExchangeSession(name string, exchange types.Exchange) *ExchangeSession { marketDataStream.SetPublicOnly() session := &ExchangeSession{ - Notifiability: Notifiability{ - SymbolChannelRouter: NewPatternChannelRouter(nil), - SessionChannelRouter: NewPatternChannelRouter(nil), - ObjectChannelRouter: NewObjectChannelRouter(), - }, - Name: name, Exchange: exchange, UserDataStream: userDataStream, @@ -282,8 +272,7 @@ func NewExchangeSession(name string, exchange types.Exchange) *ExchangeSession { session.OrderExecutor = &ExchangeOrderExecutor{ // copy the notification system so that we can route - Notifiability: session.Notifiability, - Session: session, + Session: session, } return session @@ -805,11 +794,6 @@ func (session *ExchangeSession) InitExchange(name string, ex types.Exchange) err } session.Name = name - session.Notifiability = Notifiability{ - SymbolChannelRouter: NewPatternChannelRouter(nil), - SessionChannelRouter: NewPatternChannelRouter(nil), - ObjectChannelRouter: NewObjectChannelRouter(), - } session.Exchange = ex session.UserDataStream = ex.NewStream() session.MarketDataStream = ex.NewStream() @@ -830,8 +814,7 @@ func (session *ExchangeSession) InitExchange(name string, ex types.Exchange) err session.orderStores = make(map[string]*OrderStore) session.OrderExecutor = &ExchangeOrderExecutor{ // copy the notification system so that we can route - Notifiability: session.Notifiability, - Session: session, + Session: session, } session.usedSymbols = make(map[string]struct{}) diff --git a/pkg/strategy/pivotshort/strategy.go b/pkg/strategy/pivotshort/strategy.go index 3e011a75a..1d9acf8b6 100644 --- a/pkg/strategy/pivotshort/strategy.go +++ b/pkg/strategy/pivotshort/strategy.go @@ -46,13 +46,10 @@ type SupportTakeProfit struct { } func (s *SupportTakeProfit) Subscribe(session *bbgo.ExchangeSession) { - log.Infof("[supportTakeProfit] Subscribe(%s, %s)", s.Symbol, s.Interval) session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) } func (s *SupportTakeProfit) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor) { - log.Infof("[supportTakeProfit] Bind(%s, %s)", s.Symbol, s.Interval) - s.session = session s.orderExecutor = orderExecutor s.activeOrders = bbgo.NewActiveOrderBook(s.Symbol) From 03481000cc779dc1da4eb18a2b0bedeba7c5b1f8 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 6 Jul 2022 03:09:57 +0800 Subject: [PATCH 83/92] reset activated flag when stop order is submitted Signed-off-by: c9s --- pkg/bbgo/exit_trailing_stop.go | 6 ++++++ pkg/bbgo/exit_trailing_stop_test.go | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/pkg/bbgo/exit_trailing_stop.go b/pkg/bbgo/exit_trailing_stop.go index fc953037b..2162512e9 100644 --- a/pkg/bbgo/exit_trailing_stop.go +++ b/pkg/bbgo/exit_trailing_stop.go @@ -47,6 +47,7 @@ func (s *TrailingStop2) Subscribe(session *ExchangeSession) { func (s *TrailingStop2) Bind(session *ExchangeSession, orderExecutor *GeneralOrderExecutor) { s.session = session s.orderExecutor = orderExecutor + s.latestHigh = fixedpoint.Zero position := orderExecutor.Position() session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { @@ -139,6 +140,11 @@ func (s *TrailingStop2) checkStopPrice(price fixedpoint.Value, position *types.P } func (s *TrailingStop2) triggerStop(price fixedpoint.Value) error { + // reset activated flag + defer func() { + s.activated = false + s.latestHigh = fixedpoint.Zero + }() Notify("[TrailingStop] %s stop loss triggered. price: %f callback rate: %f", s.Symbol, price.Float64(), s.CallbackRate.Float64()) ctx := context.Background() return s.orderExecutor.ClosePosition(ctx, fixedpoint.One, "trailingStop") diff --git a/pkg/bbgo/exit_trailing_stop_test.go b/pkg/bbgo/exit_trailing_stop_test.go index d1285a1a3..ddf53c566 100644 --- a/pkg/bbgo/exit_trailing_stop_test.go +++ b/pkg/bbgo/exit_trailing_stop_test.go @@ -96,7 +96,7 @@ func TestTrailingStop(t *testing.T) { err = stop.checkStopPrice(currentPrice, position) if assert.NoError(t, err) { assert.Equal(t, fixedpoint.NewFromFloat(19798.02), currentPrice) - assert.Equal(t, fixedpoint.NewFromFloat(19602.0), stop.latestHigh) - assert.True(t, stop.activated) + assert.Equal(t, fixedpoint.Zero, stop.latestHigh) + assert.False(t, stop.activated) } } From b49f12300cafd05d37b7edd3c30063c494ecbde3 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 6 Jul 2022 10:54:53 +0800 Subject: [PATCH 84/92] add long position test for trailing stop Signed-off-by: c9s --- pkg/bbgo/exit_trailing_stop.go | 12 +++- pkg/bbgo/exit_trailing_stop_test.go | 88 +++++++++++++++++++++++++++-- 2 files changed, 93 insertions(+), 7 deletions(-) diff --git a/pkg/bbgo/exit_trailing_stop.go b/pkg/bbgo/exit_trailing_stop.go index 2162512e9..993c25ce0 100644 --- a/pkg/bbgo/exit_trailing_stop.go +++ b/pkg/bbgo/exit_trailing_stop.go @@ -4,6 +4,8 @@ import ( "context" "fmt" + log "github.com/sirupsen/logrus" + "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) @@ -51,7 +53,9 @@ func (s *TrailingStop2) Bind(session *ExchangeSession, orderExecutor *GeneralOrd position := orderExecutor.Position() session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { - s.checkStopPrice(kline.Close, position) + if err := s.checkStopPrice(kline.Close, position); err != nil { + log.WithError(err).Errorf("error") + } })) if !IsBackTesting { @@ -60,7 +64,9 @@ func (s *TrailingStop2) Bind(session *ExchangeSession, orderExecutor *GeneralOrd return } - s.checkStopPrice(trade.Price, position) + if err := s.checkStopPrice(trade.Price, position); err != nil { + log.WithError(err).Errorf("error") + } }) } } @@ -129,7 +135,7 @@ func (s *TrailingStop2) checkStopPrice(price fixedpoint.Value, position *types.P case types.SideTypeSell: s.latestHigh = fixedpoint.Max(price, s.latestHigh) - change := price.Sub(s.latestHigh).Div(s.latestHigh) + change := s.latestHigh.Sub(price).Div(price) if change.Compare(s.CallbackRate) >= 0 { // submit order return s.triggerStop(price) diff --git a/pkg/bbgo/exit_trailing_stop_test.go b/pkg/bbgo/exit_trailing_stop_test.go index ddf53c566..385d89363 100644 --- a/pkg/bbgo/exit_trailing_stop_test.go +++ b/pkg/bbgo/exit_trailing_stop_test.go @@ -27,7 +27,7 @@ func getTestMarket() types.Market { return market } -func TestTrailingStop(t *testing.T) { +func TestTrailingStop_ShortPosition(t *testing.T) { market := getTestMarket() mockCtrl := gomock.NewController(t) @@ -75,27 +75,107 @@ func TestTrailingStop(t *testing.T) { // 20000 - 1% = 19800 currentPrice = currentPrice.Mul(one.Sub(activationRatio)) + assert.Equal(t, fixedpoint.NewFromFloat(19800.0), currentPrice) + err = stop.checkStopPrice(currentPrice, position) if assert.NoError(t, err) { assert.True(t, stop.activated) - assert.Equal(t, fixedpoint.NewFromFloat(19800.0), currentPrice) assert.Equal(t, fixedpoint.NewFromFloat(19800.0), stop.latestHigh) } // 19800 - 1% = 19602 currentPrice = currentPrice.Mul(one.Sub(callbackRatio)) + assert.Equal(t, fixedpoint.NewFromFloat(19602.0), currentPrice) + err = stop.checkStopPrice(currentPrice, position) if assert.NoError(t, err) { - assert.Equal(t, fixedpoint.NewFromFloat(19602.0), currentPrice) assert.Equal(t, fixedpoint.NewFromFloat(19602.0), stop.latestHigh) assert.True(t, stop.activated) } // 19602 + 1% = 19798.02 currentPrice = currentPrice.Mul(one.Add(callbackRatio)) + assert.Equal(t, fixedpoint.NewFromFloat(19798.02), currentPrice) + + err = stop.checkStopPrice(currentPrice, position) + if assert.NoError(t, err) { + assert.Equal(t, fixedpoint.Zero, stop.latestHigh) + assert.False(t, stop.activated) + } +} + +func TestTrailingStop_LongPosition(t *testing.T) { + market := getTestMarket() + + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockEx := mocks.NewMockExchange(mockCtrl) + mockEx.EXPECT().NewStream().Return(&types.StandardStream{}).Times(2) + mockEx.EXPECT().SubmitOrders(gomock.Any(), types.SubmitOrder{ + Symbol: "BTCUSDT", + Side: types.SideTypeSell, + Type: types.OrderTypeMarket, + Market: market, + Quantity: fixedpoint.NewFromFloat(1.0), + Tag: "trailingStop", + }) + + session := NewExchangeSession("test", mockEx) + assert.NotNil(t, session) + + session.markets[market.Symbol] = market + + position := types.NewPositionFromMarket(market) + position.AverageCost = fixedpoint.NewFromFloat(20000.0) + position.Base = fixedpoint.NewFromFloat(1.0) + + orderExecutor := NewGeneralOrderExecutor(session, "BTCUSDT", "test", "test-01", position) + + activationRatio := fixedpoint.NewFromFloat(0.01) + callbackRatio := fixedpoint.NewFromFloat(0.01) + stop := &TrailingStop2{ + Symbol: "BTCUSDT", + Interval: types.Interval1m, + Side: types.SideTypeSell, + CallbackRate: callbackRatio, + ActivationRatio: activationRatio, + } + stop.Bind(session, orderExecutor) + + // the same price + currentPrice := fixedpoint.NewFromFloat(20000.0) + err := stop.checkStopPrice(currentPrice, position) + if assert.NoError(t, err) { + assert.False(t, stop.activated) + } + + // 20000 + 1% = 20200 + currentPrice = currentPrice.Mul(one.Add(activationRatio)) + assert.Equal(t, fixedpoint.NewFromFloat(20200.0), currentPrice) + + err = stop.checkStopPrice(currentPrice, position) + if assert.NoError(t, err) { + assert.True(t, stop.activated) + assert.Equal(t, fixedpoint.NewFromFloat(20200.0), stop.latestHigh) + } + + // 20200 + 1% = 20402 + currentPrice = currentPrice.Mul(one.Add(callbackRatio)) + assert.Equal(t, fixedpoint.NewFromFloat(20402.0), currentPrice) + + err = stop.checkStopPrice(currentPrice, position) + if assert.NoError(t, err) { + assert.Equal(t, fixedpoint.NewFromFloat(20402.0), stop.latestHigh) + assert.True(t, stop.activated) + } + + // 20402 - 1% + currentPrice = currentPrice.Mul(one.Sub(callbackRatio)) + assert.Equal(t, fixedpoint.NewFromFloat(20197.98), currentPrice) + err = stop.checkStopPrice(currentPrice, position) if assert.NoError(t, err) { - assert.Equal(t, fixedpoint.NewFromFloat(19798.02), currentPrice) assert.Equal(t, fixedpoint.Zero, stop.latestHigh) assert.False(t, stop.activated) } From 3d9db2786db34b07ceb13c2782fe9345f634af6d Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 6 Jul 2022 10:56:10 +0800 Subject: [PATCH 85/92] add trailing stop to the exit method --- pkg/bbgo/exit.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/bbgo/exit.go b/pkg/bbgo/exit.go index 2701fdb94..91c0d1b2f 100644 --- a/pkg/bbgo/exit.go +++ b/pkg/bbgo/exit.go @@ -26,6 +26,7 @@ type ExitMethod struct { RoiTakeProfit *RoiTakeProfit `json:"roiTakeProfit"` LowerShadowTakeProfit *LowerShadowTakeProfit `json:"lowerShadowTakeProfit"` CumulatedVolumeTakeProfit *CumulatedVolumeTakeProfit `json:"cumulatedVolumeTakeProfit"` + TrailingStop *TrailingStop2 `json:"trailingStop"` } // Inherit is used for inheriting properties from the given strategy struct From a3f88bb3a9efd187f60cf18daa034e11ee03fe4c Mon Sep 17 00:00:00 2001 From: Yo-An Lin Date: Wed, 6 Jul 2022 14:03:44 +0800 Subject: [PATCH 86/92] Update README.md --- doc/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/README.md b/doc/README.md index ade7d4089..869a0a2c5 100644 --- a/doc/README.md +++ b/doc/README.md @@ -26,6 +26,7 @@ * [Support](strategy/support.md) - Support strategy that buys on high volume support ### Development +* [Developing Strategy](topics/developing-strategy.md) - developing strategy * [Adding New Exchange](development/adding-new-exchange.md) - Check lists for adding new exchanges * [KuCoin Command-line Test Tool](development/kucoin-cli.md) - Kucoin command-line tools * [SQL Migration](development/migration.md) - Adding new SQL migration scripts From 0440b1ab92e499639b020431d0b974ffbd3dd36b Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 6 Jul 2022 17:40:50 +0800 Subject: [PATCH 87/92] config/pivotshort.yaml: change default date to 2022-01-01 --- config/pivotshort.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/pivotshort.yaml b/config/pivotshort.yaml index 833c03e3b..ad69887e4 100644 --- a/config/pivotshort.yaml +++ b/config/pivotshort.yaml @@ -103,7 +103,7 @@ exchangeStrategies: backtest: sessions: - binance - startTime: "2022-04-01" + startTime: "2022-01-01" endTime: "2022-06-18" symbols: - ETHUSDT From d86338d6e6999a051000bf508b483314d07dd268 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 6 Jul 2022 17:51:35 +0800 Subject: [PATCH 88/92] update supertrend default back-test date range --- config/supertrend.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/supertrend.yaml b/config/supertrend.yaml index db880bce2..0bef21bd2 100644 --- a/config/supertrend.yaml +++ b/config/supertrend.yaml @@ -18,8 +18,8 @@ backtest: # for testing max draw down (MDD) at 03-12 # see here for more details # https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp - startTime: "2022-04-01" - endTime: "2022-04-30" + startTime: "2022-01-01" + endTime: "2022-06-18" symbols: - BTCUSDT accounts: From b3e04a68daa35c03fa0e204b1fef7688fdb883af Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 6 Jul 2022 21:50:38 +0800 Subject: [PATCH 89/92] bbgo: fix trailing stop binding --- pkg/bbgo/exit.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/bbgo/exit.go b/pkg/bbgo/exit.go index 91c0d1b2f..a88e3f44d 100644 --- a/pkg/bbgo/exit.go +++ b/pkg/bbgo/exit.go @@ -75,4 +75,8 @@ func (m *ExitMethod) Bind(session *ExchangeSession, orderExecutor *GeneralOrderE if m.CumulatedVolumeTakeProfit != nil { m.CumulatedVolumeTakeProfit.Bind(session, orderExecutor) } + + if m.TrailingStop != nil { + m.TrailingStop.Bind(session, orderExecutor) + } } From 825022715d5069bae8ad507b7b7dbcacdf0248c1 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 6 Jul 2022 21:58:26 +0800 Subject: [PATCH 90/92] dynamic: add IterateFields --- pkg/dynamic/iterate.go | 38 +++++++++++++++++++++++++++++++++++++ pkg/dynamic/iterate_test.go | 25 ++++++++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 pkg/dynamic/iterate_test.go diff --git a/pkg/dynamic/iterate.go b/pkg/dynamic/iterate.go index 12d6e2842..47be50891 100644 --- a/pkg/dynamic/iterate.go +++ b/pkg/dynamic/iterate.go @@ -10,6 +10,44 @@ type StructFieldIterator func(tag string, ft reflect.StructField, fv reflect.Val var ErrCanNotIterateNilPointer = errors.New("can not iterate struct on a nil pointer") +func IterateFields(obj interface{}, cb func(ft reflect.StructField, fv reflect.Value) error) error { + sv := reflect.ValueOf(obj) + st := reflect.TypeOf(obj) + + if st.Kind() != reflect.Ptr { + return fmt.Errorf("f should be a pointer of a struct, %s given", st) + } + + // for pointer, check if it's nil + if sv.IsNil() { + return ErrCanNotIterateNilPointer + } + + // solve the reference + st = st.Elem() + sv = sv.Elem() + + if st.Kind() != reflect.Struct { + return fmt.Errorf("f should be a struct, %s given", st) + } + + for i := 0; i < sv.NumField(); i++ { + fv := sv.Field(i) + ft := st.Field(i) + + // skip unexported fields + if !st.Field(i).IsExported() { + continue + } + + if err := cb(ft, fv) ; err != nil { + return err + } + } + + return nil +} + func IterateFieldsByTag(obj interface{}, tagName string, cb StructFieldIterator) error { sv := reflect.ValueOf(obj) st := reflect.TypeOf(obj) diff --git a/pkg/dynamic/iterate_test.go b/pkg/dynamic/iterate_test.go new file mode 100644 index 000000000..61cdc66cc --- /dev/null +++ b/pkg/dynamic/iterate_test.go @@ -0,0 +1,25 @@ +package dynamic + +import ( + "os" + "reflect" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIterateFields(t *testing.T) { + var a = struct { + A int + B float64 + C *os.File + }{} + + cnt := 0 + err := IterateFields(&a, func(ft reflect.StructField, fv reflect.Value) error { + cnt++ + return nil + }) + assert.NoError(t, err) + assert.Equal(t, 3, cnt) +} From 81e05a3f2c9831eafacfd0cab3fb6a66ca6a9d4f Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 6 Jul 2022 22:01:35 +0800 Subject: [PATCH 91/92] add more struct field tests --- pkg/dynamic/iterate.go | 4 ++++ pkg/dynamic/iterate_test.go | 41 +++++++++++++++++++++++++++---------- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/pkg/dynamic/iterate.go b/pkg/dynamic/iterate.go index 47be50891..765616392 100644 --- a/pkg/dynamic/iterate.go +++ b/pkg/dynamic/iterate.go @@ -11,6 +11,10 @@ type StructFieldIterator func(tag string, ft reflect.StructField, fv reflect.Val var ErrCanNotIterateNilPointer = errors.New("can not iterate struct on a nil pointer") func IterateFields(obj interface{}, cb func(ft reflect.StructField, fv reflect.Value) error) error { + if obj == nil { + return errors.New("can not iterate field, given object is nil") + } + sv := reflect.ValueOf(obj) st := reflect.TypeOf(obj) diff --git a/pkg/dynamic/iterate_test.go b/pkg/dynamic/iterate_test.go index 61cdc66cc..b15b74bf8 100644 --- a/pkg/dynamic/iterate_test.go +++ b/pkg/dynamic/iterate_test.go @@ -9,17 +9,36 @@ import ( ) func TestIterateFields(t *testing.T) { - var a = struct { - A int - B float64 - C *os.File - }{} - cnt := 0 - err := IterateFields(&a, func(ft reflect.StructField, fv reflect.Value) error { - cnt++ - return nil + t.Run("basic", func(t *testing.T) { + var a = struct { + A int + B float64 + C *os.File + }{} + + cnt := 0 + err := IterateFields(&a, func(ft reflect.StructField, fv reflect.Value) error { + cnt++ + return nil + }) + assert.NoError(t, err) + assert.Equal(t, 3, cnt) }) - assert.NoError(t, err) - assert.Equal(t, 3, cnt) + + t.Run("non-ptr", func(t *testing.T) { + err := IterateFields(struct{}{}, func(ft reflect.StructField, fv reflect.Value) error { + return nil + }) + assert.Error(t, err) + }) + + t.Run("nil", func(t *testing.T) { + err := IterateFields(nil, func(ft reflect.StructField, fv reflect.Value) error { + return nil + }) + assert.Error(t, err) + }) + + } From 7b7d0690c745caa798b395df63e121b341a133e6 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 7 Jul 2022 02:11:52 +0800 Subject: [PATCH 92/92] optimizer: support --tsv option and render tsv output --- pkg/cmd/optimize.go | 106 ++++++++++++++++++++++++++++++++++++++++++ pkg/optimizer/grid.go | 44 +++++++++++++----- 2 files changed, 138 insertions(+), 12 deletions(-) diff --git a/pkg/cmd/optimize.go b/pkg/cmd/optimize.go index 27ed89f55..32bb2faab 100644 --- a/pkg/cmd/optimize.go +++ b/pkg/cmd/optimize.go @@ -4,12 +4,16 @@ import ( "context" "encoding/json" "fmt" + "io" "io/ioutil" "os" + "strconv" "github.com/spf13/cobra" "gopkg.in/yaml.v3" + "github.com/c9s/bbgo/pkg/data/tsv" + "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/optimizer" ) @@ -17,6 +21,7 @@ func init() { optimizeCmd.Flags().String("optimizer-config", "optimizer.yaml", "config file") optimizeCmd.Flags().String("output", "output", "backtest report output directory") optimizeCmd.Flags().Bool("json", false, "print optimizer metrics in json format") + optimizeCmd.Flags().Bool("tsv", false, "print optimizer metrics in csv format") RootCmd.AddCommand(optimizeCmd) } @@ -43,6 +48,11 @@ var optimizeCmd = &cobra.Command{ return err } + printTsvFormat, err := cmd.Flags().GetBool("tsv") + if err != nil { + return err + } + outputDirectory, err := cmd.Flags().GetString("output") if err != nil { return err @@ -104,6 +114,10 @@ var optimizeCmd = &cobra.Command{ // print metrics JSON to stdout fmt.Println(string(out)) + } else if printTsvFormat { + if err := formatMetricsTsv(metrics, os.Stdout); err != nil { + return err + } } else { for n, values := range metrics { if len(values) == 0 { @@ -120,3 +134,95 @@ var optimizeCmd = &cobra.Command{ return nil }, } + +func transformMetricsToRows(metrics map[string][]optimizer.Metric) (headers []string, rows [][]interface{}) { + var metricsKeys []string + for k := range metrics { + metricsKeys = append(metricsKeys, k) + } + + var numEntries int + var paramLabels []string + for _, ms := range metrics { + for _, m := range ms { + paramLabels = m.Labels + break + } + + numEntries = len(ms) + break + } + + headers = append(paramLabels, metricsKeys...) + rows = make([][]interface{}, numEntries) + + var metricsRows = make([][]interface{}, numEntries) + + // build params into the rows + for i, m := range metrics[metricsKeys[0]] { + rows[i] = m.Params + } + + for _, metricKey := range metricsKeys { + for i, ms := range metrics[metricKey] { + if len(metricsRows[i]) == 0 { + metricsRows[i] = make([]interface{}, 0, len(metricsKeys)) + } + metricsRows[i] = append(metricsRows[i], ms.Value) + } + } + + // merge rows + for i := range rows { + rows[i] = append(rows[i], metricsRows[i]...) + } + + return headers, rows +} + +func formatMetricsTsv(metrics map[string][]optimizer.Metric, writer io.WriteCloser) error { + headers, rows := transformMetricsToRows(metrics) + w := tsv.NewWriter(writer) + if err := w.Write(headers); err != nil { + return err + } + + for _, row := range rows { + var cells []string + for _, o := range row { + cell, err := castCellValue(o) + if err != nil { + return err + } + cells = append(cells, cell) + } + + if err := w.Write(cells); err != nil { + return err + } + } + return w.Close() +} + +func castCellValue(a interface{}) (string, error) { + switch tv := a.(type) { + case fixedpoint.Value: + return tv.String(), nil + case float64: + return strconv.FormatFloat(tv, 'f', -1, 64), nil + case int64: + return strconv.FormatInt(tv, 10), nil + case int32: + return strconv.FormatInt(int64(tv), 10), nil + case int: + return strconv.Itoa(tv), nil + case bool: + return strconv.FormatBool(tv), nil + case string: + return tv, nil + case []byte: + return string(tv), nil + default: + return "", fmt.Errorf("unsupported object type: %T value: %v", tv, tv) + } +} diff --git a/pkg/optimizer/grid.go b/pkg/optimizer/grid.go index 952e6e06a..0b76ca886 100644 --- a/pkg/optimizer/grid.go +++ b/pkg/optimizer/grid.go @@ -17,16 +17,31 @@ import ( type MetricValueFunc func(summaryReport *backtest.SummaryReport) fixedpoint.Value var TotalProfitMetricValueFunc = func(summaryReport *backtest.SummaryReport) fixedpoint.Value { - if summaryReport == nil { - return fixedpoint.Zero - } return summaryReport.TotalProfit } +var TotalVolume = func(summaryReport *backtest.SummaryReport) fixedpoint.Value { + if len(summaryReport.SymbolReports) == 0 { + return fixedpoint.Zero + } + + buyVolume := summaryReport.SymbolReports[0].PnL.BuyVolume + sellVolume := summaryReport.SymbolReports[0].PnL.SellVolume + return buyVolume.Add(sellVolume) +} + type Metric struct { - Labels []string `json:"labels,omitempty"` - Params []interface{} `json:"params,omitempty"` - Value fixedpoint.Value `json:"value,omitempty"` + // Labels is the labels of the given parameters + Labels []string `json:"labels,omitempty"` + + // Params is the parameters used to output the metrics result + Params []interface{} `json:"params,omitempty"` + + // Key is the metric name + Key string `json:"key"` + + // Value is the metric value of the metric + Value fixedpoint.Value `json:"value,omitempty"` } func copyParams(params []interface{}) []interface{} { @@ -172,6 +187,7 @@ func (o *GridOptimizer) Run(executor Executor, configJson []byte) (map[string][] var valueFunctions = map[string]MetricValueFunc{ "totalProfit": TotalProfitMetricValueFunc, + "totalVolume": TotalVolume, } var metrics = map[string][]Metric{} @@ -220,16 +236,20 @@ func (o *GridOptimizer) Run(executor Executor, configJson []byte) (map[string][] close(taskC) // this will shut down the executor for result := range resultsC { - for metricName, metricFunc := range valueFunctions { - if result.Report == nil { - log.Errorf("no summaryReport found for params: %+v", result.Params) - } + if result.Report == nil { + log.Errorf("no summaryReport found for params: %+v", result.Params) + continue + } + + for metricKey, metricFunc := range valueFunctions { var metricValue = metricFunc(result.Report) - bar.Set("log", fmt.Sprintf("params: %+v => %s %+v", result.Params, metricName, metricValue)) + bar.Set("log", fmt.Sprintf("params: %+v => %s %+v", result.Params, metricKey, metricValue)) bar.Increment() - metrics[metricName] = append(metrics[metricName], Metric{ + + metrics[metricKey] = append(metrics[metricKey], Metric{ Params: result.Params, Labels: result.Labels, + Key: metricKey, Value: metricValue, }) }