You can quickly get familiar with and use our sample strategy project. Of course, you may have your own trading strategy to test. This page helps you understand how to implement a custom strategy.
Overview
To start a new strategy, there are only two steps.
First, implement a strategy function of func Demo1(pol *config.RunPolicyConfig) *strat.TradeStrat
.
Second, you only need to register this strategy in the current go package's init
function.
Let's see how to implement our custom logic in the strategy function.
TIP
It is strongly recommended that you use AI tools such as Cursor or Claude to convert trading strategies in other languages into banbot strategies. Just attach the knowledge base
Strategy Naming
The recommended naming format is folder:strategy_name
. You may have many different categories of trading strategies. For easy long-term maintenance, it is recommended to put similar strategies in the same folder. The depth is not limited.
For example:
root
|-org
| |-ma
| | |-demo.go
| | |-trend.go
| grid
| |-inv.go
|-main.go
Above there are two valid strategy groups ma
and grid
. You can also continue to create subfolders in ma
to manage strategies. Note that all strategy groups need to be registered in main.go
in the root directory:
import (
"github.com/banbox/banbot/entry"
_ "github.com/banbox/banstrats/org/ma"
_ "github.com/banbox/banstrats/grid"
)
RunPolicyConfig parameters
The parameter pol
of the strategy function is an item in the run_policy
list in the yaml configuration file.
You can get the parameters passed in from the configuration file through atrLen := pol.Param("atrLen", 9)
so that different parameters can be used for different products. You can also define a hyperparameter using atrLen := pol.Def("atrLen", 9, core.PNorm(7, 20))
, which works exactly the same as pol.Param
during backtesting, but when switching to hyperparameter tuning mode, the value of atrLen
will be randomly generated using a normal distribution with a default value of the mean and upper and lower limits of (7, 20)
. You can also replace core.PNorm
with core.PNormF
to specify a different mean and multiplier. Similarly, you can also use core.PUniform
to specify a uniform linear distribution hyperparameter.
Here are several complete code examples:
func Demo(pol *config.RunPolicyConfig) *strat.TradeStrat {
atrLen := pol.Param("atrLen", 9)
atrLen1 := pol.Def("atrLen1", 9, core.PNorm(3, 20))
atrLen2 := pol.Def("atrLen2", 9, core.PNormF(3, 20, 12, 1))
atrLen3 := pol.Def("atrLen3", 9, core.PUniform(3, 20))
return &strat.TradeStrat{
// more
}
}
Tips
Each item in the yaml configuration run_policy
corresponds to a call to the strategy function, generating a strategy with specific parameters. The same strategy can appear in multiple run_poolicy
items, that is, the same strategy function may be executed multiple times. The *strat.TradeStrat
returned by the strategy function will be applied to multiple varieties, corresponding to multiple *strat.StratJob
. Therefore, any variables related to a single variety should not be saved in the strategy function, but should be saved through *strat.StratJob.More
. The variables in the strategy function should remain unchanged. Otherwise, it will lead to unexpected states.
strat.TradeStrat Strategy Object
TradeStrat
provides a wealth of property configurations and callback functions. The most commonly used is OnBar
.
The following is the complete definition of TradeStrat
:
type TradeStrat struct {
Name string // The strategy name does not need to be set and will be automatically set to the name used during registration
Version int
WarmupNum int // candle preheating length
MinTfScore float64 // Minimum time cycle quality, default 0.8 最小时间周期质量,默认0.8
WatchBook bool // Whether to monitor the real-time depth information of the order book
DrawDownExit bool // Whether to enable retracement stop loss (i.e. trailing stop loss)
BatchInOut bool // Whether to batch execute entry/exit 是否批量执行入场/出场
BatchInfo bool // whether to perform batch processing after OninfoBar 是否对OnInfoBar后执行批量处理
StakeRate float64 // Relative basic amount billing rate 相对基础金额开单倍率
StopEnterBars int // If the limit order exceeds the given K line and still does not enter the market, it will be cancelled
EachMaxLong int // max number of long open orders for one pair
EachMaxShort int // max number of short open orders for one pair
AllowTFs []string // Allow running time period, use global configuration when not provided 允许运行的时间周期,不提供时使用全局配置
Outputs []string // The content of the text file output by the strategy, where each string is one line 策略输出的文本文件内容,每个字符串是一行
Policy *config.RunPolicyConfig
OnPairInfos func(s *StratJob) []*PairSub // The strategy requires additional data on other types or periods
OnStartUp func(s *StratJob) // Callback at startup. Called before the first execution
OnBar func(s *StratJob) // Callback function for each K line
OnInfoBar func(s *StratJob, e *ta.BarEnv, pair, tf string) // Other dependent bar data 其他依赖的bar数据
OnTrades func(s *StratJob, trades []*banexg.Trade) // Transaction by transaction data 逐笔交易数据
OnBatchJobs func(jobs []*StratJob) // All target jobs at the current time, used for bulk opening/closing of orders 当前时间所有标的job,用于批量开单/平仓
OnBatchInfos func(jobs map[string]*StratJob) // All info marked jobs at the current time, used for batch processing 当前时间所有info标的job,用于批量处理
OnCheckExit func(s *StratJob, od *ormo.InOutOrder) *ExitReq // Custom order exit logic 自定义订单退出逻辑
OnOrderChange func(s *StratJob, od *ormo.InOutOrder, chgType int) // Order update callback 订单更新回调
GetDrawDownExitRate CalcDDExitRate // Calculate the ratio of tracking profit taking, drawdown, and exit 计算跟踪止盈回撤退出的比率
PickTimeFrame PickTimeFrameFunc // Choose a suitable trading cycle for the specified currency 为指定币选择适合的交易周期
OnShutDown func(s *StratJob) // Callback when the robot stops 机器人停止时回调
}
Simple policy example
package ma
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
)
func init() {
// Register the policy in Banbot, and use ma: demo in the configuration file to reference this policy later
// `init`The function is a special function in Go that will be executed immediately when the current package is imported
strat.AddStratGroup("ma", map[string]strat.FuncMakeStrat{
"demo": Demo,
})
}
func Demo(pol *config.RunPolicyConfig) *strat.TradeStrat {
smlLen := int(pol.Def("smlLen", 5, core.PNorm(3, 10)))
bigLen := int(pol.Def("bigLen", 20, core.PNorm(10, 40)))
return &strat.TradeStrat{
WarmupNum: 100,
OnBar: func(s *strat.StratJob) {
e := s.Env
ma5 := ta.SMA(e.Close, smlLen)
ma20 := ta.SMA(e.Close, bigLen)
maCrx := ta.Cross(ma5, ma20)
if maCrx == 1 {
s.OpenOrder(&strat.EnterReq{Tag: "open"})
} else if maCrx == -1 {
s.CloseOrders(&strat.ExitReq{Tag: "exit"})
}
},
}
}
banta.BarEnv and banta.Series
banta.BarEnv
is the operating environment of a technical indicator, which stores information such as the current exchange, market, product, time period, etc. A strategy task will require at least one banta.BarEnv
. If other products or time periods are subscribed through OnPairInfos
, multiple operating environments will be required. But it should be noted that only one operating environment BarEnv
will be created for each exchange, each market, each product, and each time period.
banta.BarEnv
has several built-in original Series
, namely: Open
, High
, Low
, Close
, Volume
, which respectively save the sequence information of opening price, highest price, lowest price, closing price, and trading volume.
Series
is a special structure in banta
for storing serialized data. All technical indicator functions in banta
receive one or more Series
, and return a Series
after calculation.
You can use Series.Get(0)
to get the latest value of a series, such as e.Close.Get(0)
to get the latest closing price; You can also use Series.Range(0,5)
to get the 5 most recent values of a series, which returns a []float64
array of length 5.
When performing multiple repeated calculations at the same bar time (such as running the same batch of strategies on multiple accounts in real trading), Series
will automatically use cached results to avoid repeated calculations.
For the Bollinger Bands indicator banta.BBANDS
that returns the three parts of upper/mid/lower
, the only Series
it returns is an aggregate, and the value cannot be obtained through Get(i)
. Instead, it should be extracted into 3 series objects in the following way:
bbolCols := ta.BBANDS(e.Close, 10, 2, 2).Cols
upper, mid, lower := bbolCols[0], bbolCols[1], bbolCols[2]
Then you can execute Get(i)
for upper/mid/lower
to get the value.
Indicator operation tips
banta
is an event-driven technical indicator library. The OnBar
function will be executed once for each candle, which means that each time the technical indicator is called, only the latest value will be updated to Series
. Instead of vectorized calculation like ta-lib
, all historical series are calculated at once.
Series
saves the latest status of each indicator calculation. When a new candle is received, it will be quickly calculated from the last calculation status without repeating all the data of the series.
For example, banta.EMA
requires the previous moving average value when calculating, and the ta-lib
vector indicator library needs to calculate from scratch given the entire column of historical data, while banta
will only calculate once from the last cached value.
Custom indicators
You can implement your custom indicator logic quickly and easily, taking Highest
as an example:
func Highest(obj *Series, period int) *Series {
res := obj.To("_hh", period)
if res.Cached() {
return res
}
if obj.Len() < period {
return res.Append(math.NaN())
}
resVal := slices.Max(obj.Range(0, period))
return res.Append(resVal)
}
The above is a simple technical indicator of the highest value in a specified period. The first 4 lines of all technical indicators are almost similar. A Series
object is derived from obj
to save the results. The _hh
and period
parameters are used as hashes here.
In fact, Series
has a member attribute:
Subs map[string]map[int]*Series
It saves all Series
derived from this Series
. In essence, all Series
in the strategy are derived from the original Open/High/Low/Close/Volume
.
Since the indicator function only updates one value each time it is called, when the length of lines 6-8 is insufficient, nan is directly returned. Line 9 is the key to this indicator, which calculates the maximum value of the most recent n series and returns it.
More complex custom indicators
Your custom indicator may sometimes need to cache more intermediate state information. In this case, you can customize the structure and save it in Series.More
.
type sumState struct {
sumVal float64
addLen int
}
func Sum(obj *Series, period int) *Series {
res := obj.To("_sum", period)
if res.Cached() {
return res
}
sta, _ := res.More.(*sumState)
if sta == nil {
sta = &sumState{}
res.More = sta
}
curVal := obj.Get(0)
if math.IsNaN(curVal) {
// 输入值无效,重置,重新开始累计
curVal = 0
sta.sumVal = 0
sta.addLen = 0
} else {
if sta.addLen < period {
sta.sumVal += curVal
sta.addLen += 1
} else {
oldVal := obj.Get(period)
if math.IsNaN(oldVal) {
sta.sumVal = 0
sta.addLen = 0
} else {
sta.sumVal += curVal - oldVal
}
}
}
if sta.addLen < period {
return res.Append(math.NaN())
}
return res.Append(sta.sumVal)
}
The above is an indicator that calculates the sum of the sequences of the most recent n periods. Lines 11 to 39 can actually be replaced by the following line, but the above approach of caching the last calculation result is significantly more efficient than the following two loops.
// gonum.org/v1/gonum/floats
return res.Append(floats.Sum(obj.Range(0, period)))
Strategy Warmup
Most indicators have an unstable startup period during which they are either unavailable (NaN) or calculated incorrectly. This can cause inconsistencies because the banbot does not know how long this unstable period should be. To solve this problem, the WarmupNum
property can be set for the strategy. This should be set to the maximum number of candlesticks required for the strategy to calculate a stable indicator.
For additional symbols or periods that OnPairInfos
subscribes to, you can specify them in its return value:
OnPairInfos: func(s *strat.StratJob) []*strat.PairSub {
return []*strat.PairSub{
{"_cur_", "1h", 30},
}
},
The above additional subscription is for the 1h
period candle of the current product, and the preheating quantity is 30.
Save intermediate state
Sometimes you may need to save some intermediate states for the next calculation; or you may need to synchronize some information between different callback functions.
package ma
import (
"github.com/banbox/banbot/config"
"github.com/banbox/banbot/core"
"github.com/banbox/banbot/strat"
ta "github.com/banbox/banta"
)
type Demo2Sta struct {
bigDirt int
}
func Demo2(pol *config.RunPolicyConfig) *strat.TradeStrat {
smlLen := int(pol.Def("smlLen", 5, core.PNorm(3, 10)))
bigLen := int(pol.Def("bigLen", 20, core.PNorm(10, 40)))
return &strat.TradeStrat{
WarmupNum: 100,
OnPairInfos: func(s *strat.StratJob) []*strat.PairSub {
return []*strat.PairSub{
{"_cur_", "1h", 30},
}
},
OnStartUp: func(s *strat.StratJob) {
s.More = &Demo2Sta{}
},
OnBar: func(s *strat.StratJob) {
e := s.Env
m, _ := s.More.(*Demo2Sta)
ma5 := ta.SMA(e.Close, smlLen)
ma20 := ta.SMA(e.Close, bigLen)
maCrx := ta.Cross(ma5, ma20)
if maCrx == 1 && m.bigDirt > 0 {
s.OpenOrder(&strat.EnterReq{Tag: "open"})
} else if maCrx == -1 {
s.CloseOrders(&strat.ExitReq{Tag: "exit"})
}
},
OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) {
m, _ := s.More.(*Demo2Sta)
ma5 := ta.SMA(e.Close, smlLen)
ma20 := ta.SMA(e.Close, bigLen)
m.bigDirt = ta.Cross(ma5, ma20)
},
}
}
As shown above, this is a moving average crossover strategy combining large and small periods. When the short moving average of the large period is above the long moving average and the short moving average of the small period crosses the long moving average, the market will be entered.
First, initialize StratJob.More
in OnStartUp
. Then perform type conversion in OnBar
and OnInfoBar
and assign it to the m
variable:
m, _ := s.More.(*Demo2Sta)
Issue an entry signal
To issue an entry signal, you only need to call the StratJob.OpenOrder
method and pass a *strat.EnterReq
parameter.
The simplest case is to only assign a value to the Tag
property of EnterReq
, and use the default parameters for all others. That is, open a long order according to the default order amount and leverage multiple. There is no stop profit or stop loss.
The following are all the parameters of EnterReq
:
type EnterReq struct {
Tag string // Entry signal 入场信号
StgyName string // Strategy Name 策略名称
Short bool // Whether to short sell or not 是否做空
OrderType int // 订单类型, core.OrderType*
Limit float64 // The entry price of a limit order will be submitted as a limit order when specified 限价单入场价格,指定时订单将作为限价单提交
CostRate float64 // The opening ratio is set to 1 times by default according to the configuration. Used for calculating LegalList 开仓倍率、默认按配置1倍。用于计算LegalCost
LegalCost float64 // Spend the amount in fiat currency. Ignore CostRate when specified 花费法币金额。指定时忽略CostRate
Leverage float64 // Leverage ratio 杠杆倍数
Amount float64 // The number of admission targets is calculated by LegalList and price 入场标的数量,由LegalCost和price计算
StopLossVal float64 // The distance from the entry price to the stop loss price is used to calculate StopLoss 入场价格到止损价格的距离,用于计算StopLoss
StopLoss float64 // Stop loss trigger price, submit a stop loss order on the exchange when it is not empty 止损触发价格,不为空时在交易所提交一个止损单
StopLossLimit float64 // Stop loss limit price, does not provide the use of StopLoss 止损限制价格,不提供使用StopLoss
StopLossRate float64 // Stop loss exit ratio, 0 means all exits, needs to be between (0,1) 止损退出比例,0表示全部退出,需介于(0,1]之间
StopLossTag string // Reason for Stop Loss 止损原因
TakeProfitVal float64 // The distance from the entry price to the take profit price is used to calculate TakeProfit 入场价格到止盈价格的距离,用于计算TakeProfit
TakeProfit float64 // When the take profit trigger price is not empty, submit a take profit order on the exchange. 止盈触发价格,不为空时在交易所提交一个止盈单。
TakeProfitLimit float64 // Profit taking limit price, TakeProfit is not available for use 止盈限制价格,不提供使用TakeProfit
TakeProfitRate float64 // Take profit exit ratio, 0 indicates full exit, needs to be between (0,1) 止盈退出比率,0表示全部退出,需介于(0,1]之间
TakeProfitTag string // Reason for profit taking 止盈原因
StopBars int // If the entry limit order exceeds how many bars and is not executed, it will be cancelled 入场限价单超过多少个bar未成交则取消
}
Send an exit signal
To send an exit signal, just call the StratJob.CloseOrders
method and pass a *strat.ExitReq
parameter.
The simplest case is to only assign a value to the Tag
property of ExitReq
and use the default parameters for all other parameters. That is, close all orders with market orders, including long orders and short orders.
The following are all the parameters of ExitReq
:
type ExitReq struct {
Tag string // Exit signal 退出信号
StgyName string // Strategy Name 策略名称
EnterTag string // Only exit orders with EnterTag as the entry signal 只退出入场信号为EnterTag的订单
Dirt int // core.OdDirt* long/short/both
OrderType int // 订单类型, core.OrderType*
Limit float64 // Limit order exit price, the order will be submitted as a limit order when specified 限价单退出价格,指定时订单将作为限价单提交
ExitRate float64 // Exit rate, default is 100%, which means all orders are exited 退出比率,默认100%即所有订单全部退出
Amount float64 // The number of targets to be exited. ExitRate is invalid when specified 要退出的标的数量。指定时ExitRate无效
OrderID int64 // Only exit specified orders 只退出指定订单
UnOpenOnly bool // When True, only exit orders that have not yet entered the venue True时只退出尚未入场的订单
FilledOnly bool // Only exit orders that have already entered when True True时只退出已入场的订单
Force bool // Whether to force exit 是否强制退出
}
Stop Loss and Take Profit
In addition to setting the stop loss and take profit of an order when entering the market, you can also directly set the stop loss and take profit of a certain order at any time:
if len(s.LongOrders) > 0 {
od := s.LongOrders[0]
ma5Val := ma5.Get(0)
od.SetStopLoss(&ormo.ExitTrigger{Price: ma5Val * 0.97})
od.SetTakeProfit(&ormo.ExitTrigger{Price: ma5Val * 1.03})
}
You can also set stop loss and take profit for all orders:
ma5Val := ma5.Get(0)
s.SetAllStopLoss(core.OdDirtLong, &ormo.ExitTrigger{
Price: ma5Val * 0.97,
Limit: ma5Val * 0.975,
Rate: 0.5,
Tag: "stop half",
})
For all open long orders, a stop loss of 50% of the position is set. When the price triggers 0.97 times the 5-period moving average, half of the position is closed with a limit order stop loss of 0.975 times.
Batch task processing
Sometimes you may need to perform some calculations (such as correlation coefficients) for all products of the current strategy together, get some intermediate states to save, or open or close orders together. In this case, you can use the OnBatchJobs
or OnBatchInfos
callback function.
func calcCorrs(jobs []*strat.StratJob, isBig bool) {
// Calculate the average correlation coefficient between each variety and other varieties, and save it to More
dataArr := make([][]float64, 0, len(jobs))
for _, j := range jobs {
dataArr = append(dataArr, j.Env.Close.Range(0, 70))
}
_, arr, err := utils.CalcCorrMat(dataArr, true)
if err != nil {
log.Error("calc corr mat fail", zap.Error(err))
return
}
for i, j := range jobs {
m, _ := j.More.(*BatchSta)
if isBig {
m.bigCorr = arr[i]
} else {
m.smlCorr = arr[i]
}
}
}
type BatchSta struct {
smlCorr float64
bigCorr float64
}
func BatchDemo(pol *config.RunPolicyConfig) *strat.TradeStrat {
return &strat.TradeStrat{
WarmupNum: 100,
BatchInOut: true,
BatchInfo: true,
OnPairInfos: func(s *strat.StratJob) []*strat.PairSub {
return []*strat.PairSub{
{"_cur_", "1h", 100},
}
},
OnStartUp: func(s *strat.StratJob) {
s.More = &BatchSta{}
},
OnBar: func(s *strat.StratJob) {
m, _ := s.More.(*BatchSta)
if m.bigCorr < 0.5 && m.smlCorr < 0.5 {
// When the correlation between the large and small cycles is less than 50%, the order will be opened.
s.OpenOrder(&strat.EnterReq{Tag: "open"})
} else if m.smlCorr > 0.9 {
// The current small cycle correlation of the variety is higher than 90%, close the position
s.CloseOrders(&strat.ExitReq{Tag: "close"})
}
},
OnBatchJobs: func(jobs []*strat.StratJob) {
if jobs[0].IsWarmUp {
return
}
calcCorrs(jobs, false)
},
OnBatchInfos: func(jobs map[string]*strat.StratJob) {
jobList := utils.ValsOfMap(jobs)
if jobList[0].IsWarmUp {
return
}
calcCorrs(jobList, true)
},
}
}
Custom exit logic
You can perform custom exit logic checks on each candlestick, for each open order:
func CustomExitDemo(pol *config.RunPolicyConfig) *strat.TradeStrat {
return &strat.TradeStrat{
OnBar: func(s *strat.StratJob) {
if len(s.LongOrders) == 0 {
s.OpenOrder(&strat.EnterReq{Tag: "long"})
} else if rand.Float64() < 0.1 {
s.CloseOrders(&strat.ExitReq{Tag: "close"})
}
},
OnCheckExit: func(s *strat.StratJob, od *ormo.InOutOrder) *strat.ExitReq {
if od.ProfitRate > 0.1 {
// Exit if profit exceeds 10%
return &strat.ExitReq{Tag: "profit"}
}
return nil
},
}
}
Stop loss by retracement (trailing stop loss)
You can easily implement stop loss by retracement through DrawDownExit
and GetDrawDownExitRate
. For example, when the best historical profit of an order exceeds 10%, the order will be exited when the retracement exceeds 50%:
func DrawDown(pol *config.RunPolicyConfig) *strat.TradeStrat {
return &strat.TradeStrat{
DrawDownExit: true,
OnBar: func(s *strat.StratJob) {
if len(s.LongOrders) == 0 {
s.OpenOrder(&strat.EnterReq{Tag: "long"})
}
},
GetDrawDownExitRate: func(s *strat.StratJob, od *ormo.InOutOrder, maxChg float64) float64 {
if maxChg > 0.1 {
// After the best profit of the order exceeds 10%, withdraw 50% and exit
return 0.5
}
return 0
},
}
}