From e93e060984488effeee0ff879631e716f67bf308 Mon Sep 17 00:00:00 2001 From: rs-assistant Date: Mon, 13 Jul 2026 12:09:38 +0800 Subject: [PATCH] fix: supertrend Subscribe panics on disabled linearRegression config setupIndicators() nils LinearRegression when window == 0 or the interval is empty, but Subscribe() dereferenced s.LinearRegression.Interval unconditionally: a config with linearRegression: window: 0 panics with a nil pointer (and an interval-less block panics inside session.Subscribe on the empty kline interval). Guard both panic paths; degenerate configs that set an interval with window == 0 keep their historical (unused) subscription so existing backtest replay inputs are unchanged. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_018hiceFQWejhHyHnLayt1dE --- pkg/strategy/supertrend/strategy.go | 9 +++- pkg/strategy/supertrend/strategy_test.go | 66 ++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 pkg/strategy/supertrend/strategy_test.go diff --git a/pkg/strategy/supertrend/strategy.go b/pkg/strategy/supertrend/strategy.go index bc14bdd1c4..da9eb76000 100644 --- a/pkg/strategy/supertrend/strategy.go +++ b/pkg/strategy/supertrend/strategy.go @@ -129,7 +129,14 @@ func (s *Strategy) Validate() error { func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) - session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.LinearRegression.Interval}) + // LinearRegression is optional: setupIndicators() nils it for window==0 or + // empty-interval configs, and session.Subscribe panics on an empty kline + // interval — guard both panic paths here. Degenerate configs (window==0 + // with an interval set) keep their historical unused subscription so + // existing backtest replay inputs are unchanged. + if s.LinearRegression != nil && s.LinearRegression.Interval != "" { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.LinearRegression.Interval}) + } s.ExitMethods.SetAndSubscribe(session, s) diff --git a/pkg/strategy/supertrend/strategy_test.go b/pkg/strategy/supertrend/strategy_test.go new file mode 100644 index 0000000000..3b170cf546 --- /dev/null +++ b/pkg/strategy/supertrend/strategy_test.go @@ -0,0 +1,66 @@ +package supertrend + +import ( + "testing" + + "go.uber.org/mock/gomock" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/types/mocks" +) + +func newTestSession(t *testing.T) *bbgo.ExchangeSession { + t.Helper() + + mockCtrl := gomock.NewController(t) + t.Cleanup(mockCtrl.Finish) + + mockEx := mocks.NewMockExchange(mockCtrl) + mockEx.EXPECT().NewStream().Return(&types.StandardStream{}).AnyTimes() + mockEx.EXPECT().Name().Return(types.ExchangeName("backtest")).AnyTimes() + + return bbgo.NewExchangeSession("test", mockEx) +} + +// A linearRegression block with window: 0 (or no interval) is nil-ed by +// setupIndicators, but Subscribe used to dereference it unconditionally and +// session.Subscribe panics on an empty kline interval. +func TestStrategySubscribe_LinearRegressionNilSafe(t *testing.T) { + cases := []struct { + name string + linReg *LinReg + subs int + }{ + {name: "nil block", linReg: nil, subs: 1}, + { + name: "window zero without interval", + linReg: &LinReg{IntervalWindow: types.IntervalWindow{Window: 0}}, + subs: 1, + }, + { + name: "valid block", + linReg: &LinReg{ + IntervalWindow: types.IntervalWindow{Window: 20, Interval: types.Interval4h}, + }, + subs: 2, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + session := newTestSession(t) + s := &Strategy{ + Symbol: "BTCUSDT", + IntervalWindow: types.IntervalWindow{Interval: types.Interval1h, Window: 5}, + LinearRegression: tc.linReg, + } + + s.Subscribe(session) + + if got := len(session.Subscriptions); got != tc.subs { + t.Fatalf("subscriptions = %d, want %d", got, tc.subs) + } + }) + } +}