bbgo_origin/doc/topics/developing-strategy.md

566 lines
17 KiB
Markdown
Raw Permalink Normal View History

# Developing Strategy
There are two types of strategies in BBGO:
1. built-in strategy: like grid, bollmaker, pricealert strategies, which are included in the pre-compiled binary.
2. external strategy: custom or private strategies that you don't want to expose to public.
For built-in strategies, they are placed in `pkg/strategy` of the BBGO source repository.
For external strategies, you can create a private repository as an isolated go package and place your strategy inside
it.
2022-07-04 04:02:13 +00:00
In general, strategies are Go struct, defined in the Go package.
2022-07-04 03:58:11 +00:00
## Quick Start
2022-07-04 04:02:13 +00:00
To add your first strategy, the fastest way is to add it as a built-in strategy.
2022-07-04 03:58:11 +00:00
2022-08-01 17:07:48 +00:00
Simply edit `pkg/cmd/strategy/builtin.go` and import your strategy package there.
2022-07-04 03:58:11 +00:00
When BBGO starts, the strategy will be imported as a package, and register its struct to the engine.
2022-08-01 17:07:48 +00:00
You can also create a new file called `pkg/cmd/strategy/short.go` and import your strategy package.
2022-07-04 03:58:11 +00:00
```
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:
2022-07-04 04:14:19 +00:00
```go
2022-07-04 03:58:11 +00:00
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.
2022-07-04 03:58:11 +00:00
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`.
2022-07-04 03:58:11 +00:00
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
json tag to get the settings from the YAML config.
For example, if you're writing a strategy in a package called `short`, to load the following config:
```yaml
externalStrategies:
- on: binance
short:
symbol: BTCUSDT
```
You can write the following struct to load the symbol setting:
2022-07-04 04:14:19 +00:00
```go
package short
type Strategy struct {
Symbol string `json:"symbol"`
}
```
To use the Symbol setting, you can get the value from the Run method of the strategy:
2022-07-04 04:14:19 +00:00
```go
func (s *Strategy) Run(ctx context.Context, session *bbgo.ExchangeSession) error {
2022-06-03 16:14:13 +00:00
// you need to import the "log" package
log.Println("%s", s.Symbol)
return nil
}
```
Now you have the Go struct and the Go package, but BBGO does not know your strategy, so you need to register your
strategy.
2022-06-03 16:14:13 +00:00
Define an ID const in your package:
2022-07-04 04:14:19 +00:00
```go
2022-06-03 16:14:13 +00:00
const ID = "short"
```
Then call bbgo.RegisterStrategy with the ID you just defined and a struct reference:
2022-07-04 04:14:19 +00:00
```go
2022-06-03 16:14:13 +00:00
func init() {
bbgo.RegisterStrategy(ID, &Strategy{})
}
```
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)
## 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:
2022-07-05 03:49:48 +00:00
- on: binance_margin_ethusdt
short:
symbol: ETHUSDT
- on: binance_margin
foo:
symbol: ETHUSDT
- on: binance
bar:
symbol: ETHUSDT
```
2022-07-04 03:58:11 +00:00
## Market Data Stream and User Data Stream
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.
2022-07-04 03:58:11 +00:00
To add your market data subscription to the `MarketDataStream`, you can register your subscription in the `Subscribe` of
the strategy code, for example:
2022-07-04 03:58:11 +00:00
2022-07-04 04:14:19 +00:00
```go
2022-07-04 03:58:11 +00:00
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:
2022-07-04 03:58:11 +00:00
2022-07-04 04:14:19 +00:00
```go
2022-07-04 03:58:11 +00:00
func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
if !bbgo.IsBackTesting {
session.Subscribe(types.MarketTradeChannel, s.Symbol, types.SubscribeOptions{})
}
2022-07-04 03:58:11 +00:00
}
```
To receive the market data from the market data stream, you need to register the event callback:
2022-07-04 04:14:19 +00:00
```go
2022-07-04 03:58:11 +00:00
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
2022-07-04 04:16:22 +00:00
using the same exchange session, so you might receive kline with different symbol or interval.
2022-07-04 03:58:11 +00:00
2022-07-04 04:16:22 +00:00
It's better to add a condition to filter the kline events:
2022-07-04 03:58:11 +00:00
2022-07-04 04:14:19 +00:00
```go
2022-07-04 03:58:11 +00:00
func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
2022-07-04 04:15:47 +00:00
session.MarketDataStream.OnKLineClosed(func(kline types.KLine) {
2022-07-04 03:58:11 +00:00
if kline.Symbol != s.Symbol || kline.Interval != s.Interval {
2022-07-04 04:15:47 +00:00
return
2022-07-04 03:58:11 +00:00
}
2022-07-04 04:15:47 +00:00
// handle your kline here
})
2022-07-04 03:58:11 +00:00
}
```
You can also use the KLineWith method to wrap your kline closure with the filter condition:
2022-07-04 04:14:19 +00:00
```go
2022-07-04 03:58:11 +00:00
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
})
}
```
2022-07-04 03:58:11 +00:00
Note that, when the Run() method is executed, the user data stream and market data stream are not connected yet.
2022-07-04 05:18:25 +00:00
## Submitting Orders
2022-07-04 05:18:25 +00:00
To place an order, you can call `SubmitOrders` exchange API:
2022-07-04 05:18:25 +00:00
```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
UserDataStream is an authenticated connection to the crypto exchange. You can receive the following data type from the
user data stream:
- OrderUpdate
- TradeUpdate
- BalanceUpdate
2022-07-05 03:49:48 +00:00
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.
2022-07-04 05:18:25 +00:00
To get the order update from the user data stream:
2022-07-04 05:18:25 +00:00
```go
session.UserDataStream.OnOrderUpdate(func(order types.Order) {
if order.Status == types.OrderStatusFilled {
log.Infof("your order is filled: %+v", order)
}
})
```
2022-07-04 12:06:50 +00:00
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) {
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
2022-07-04 05:18:25 +00:00
2022-07-04 12:06:50 +00:00
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.
2022-07-04 05:18:25 +00:00
2022-07-04 12:08:06 +00:00
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.
2022-07-04 05:18:25 +00:00
## 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")
}
})
```
2022-07-04 05:18:25 +00:00
## Persistence
2022-07-05 03:49:48 +00:00
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:
2022-07-05 03:49:48 +00:00
```yaml
persistence:
redis:
host: 127.0.0.1
port: 6379
db: 0
```
2022-07-05 03:49:48 +00:00
In the Run method of your strategy, you need to check if these fields are nil, and you need to initialize them:
2022-07-05 03:49:48 +00:00
```go
if s.Position == nil {
s.Position = types.NewPositionFromMarket(s.Market)
}
if s.ProfitStats == nil {
s.ProfitStats = types.NewProfitStats(s.Market)
}
2022-07-05 03:49:48 +00:00
if s.TradeStats == nil {
s.TradeStats = types.NewTradeStats(s.Symbol)
}
```
2022-07-05 03:49:48 +00:00
That's it. Hit Ctrl-C and you should see BBGO saving your strategy states.
2022-07-06 18:23:55 +00:00
## Exit Method Set
To integrate the built-in exit methods into your strategy, simply add a field with type bbgo.ExitMethodSet:
```go
type Strategy struct {
ExitMethods bbgo.ExitMethodSet `json:"exits"`
}
func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
s.ExitMethods.SetAndSubscribe(session, s)
}
func (s *Strategy) Run() {
s.ExitMethods.Bind(session, s.orderExecutor)
}
```
And then you can use the following config structure to configure your exit settings like this:
```yaml
- on: binance
pivotshort:
exits:
# (0) roiStopLoss is the stop loss percentage of the position ROI (currently the price change)
- roiStopLoss:
percentage: 0.8%
# (1) roiTakeProfit is used to force taking profit by percentage of the position ROI (currently the price change)
# force to take the profit ROI exceeded the percentage.
- roiTakeProfit:
percentage: 35%
# (2) protective stop loss -- short term
- protectiveStopLoss:
activationRatio: 0.6%
stopLossRatio: 0.1%
placeStopOrder: false
# (3) protective stop loss -- long term
- protectiveStopLoss:
activationRatio: 5%
stopLossRatio: 1%
placeStopOrder: false
# (4) lowerShadowTakeProfit is used to taking profit when the (lower shadow height / low price) > lowerShadowRatio
# 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:
interval: 5m
window: 2
minQuoteVolume: 200_000_000
```