bbgo_origin/pkg/bbgo/config.go

507 lines
13 KiB
Go
Raw Normal View History

2020-10-26 13:45:02 +00:00
package bbgo
import (
2021-02-03 01:58:31 +00:00
"bytes"
2020-10-26 13:45:02 +00:00
"encoding/json"
2020-11-09 08:34:35 +00:00
"fmt"
2020-10-26 13:45:02 +00:00
"io/ioutil"
"reflect"
2021-01-21 04:06:03 +00:00
"runtime"
2020-11-06 19:18:05 +00:00
"time"
2020-10-26 13:45:02 +00:00
"github.com/pkg/errors"
"gopkg.in/yaml.v3"
2020-12-31 09:07:12 +00:00
"github.com/c9s/bbgo/pkg/datatype"
"github.com/c9s/bbgo/pkg/fixedpoint"
2021-02-20 17:01:39 +00:00
"github.com/c9s/bbgo/pkg/service"
2020-11-06 19:18:05 +00:00
"github.com/c9s/bbgo/pkg/types"
2020-10-26 13:45:02 +00:00
)
type PnLReporterConfig struct {
2020-12-31 09:07:12 +00:00
AverageCostBySymbols datatype.StringSlice `json:"averageCostBySymbols" yaml:"averageCostBySymbols"`
Of datatype.StringSlice `json:"of" yaml:"of"`
When datatype.StringSlice `json:"when" yaml:"when"`
2020-10-26 13:45:02 +00:00
}
2021-02-02 18:26:41 +00:00
// ExchangeStrategyMount wraps the SingleExchangeStrategy with the ExchangeSession name for mounting
2020-10-26 13:45:02 +00:00
type ExchangeStrategyMount struct {
2021-02-02 18:26:41 +00:00
// Mounts contains the ExchangeSession name to mount
Mounts []string `json:"mounts"`
2020-10-26 13:45:02 +00:00
// Strategy is the strategy we loaded from config
Strategy SingleExchangeStrategy `json:"strategy"`
}
func (m *ExchangeStrategyMount) Map() (map[string]interface{}, error) {
strategyID := m.Strategy.ID()
var params map[string]interface{}
out, err := json.Marshal(m.Strategy)
if err != nil {
return nil, err
}
if err := json.Unmarshal(out, &params); err != nil {
return nil, err
}
return map[string]interface{}{
"on": m.Mounts,
strategyID: params,
}, nil
2020-10-26 13:45:02 +00:00
}
2020-10-27 00:48:47 +00:00
type SlackNotification struct {
DefaultChannel string `json:"defaultChannel,omitempty" yaml:"defaultChannel,omitempty"`
ErrorChannel string `json:"errorChannel,omitempty" yaml:"errorChannel,omitempty"`
}
2021-02-21 08:52:47 +00:00
type SlackNotificationRouting struct {
Trade string `json:"trade,omitempty" yaml:"trade,omitempty"`
Order string `json:"order,omitempty" yaml:"order,omitempty"`
2020-10-27 00:48:47 +00:00
SubmitOrder string `json:"submitOrder,omitempty" yaml:"submitOrder,omitempty"`
PnL string `json:"pnL,omitempty" yaml:"pnL,omitempty"`
2020-10-27 00:48:47 +00:00
}
2021-10-15 08:10:09 +00:00
type TelegramNotification struct {
Broadcast bool `json:"broadcast" yaml:"broadcast"`
}
2020-10-30 21:21:17 +00:00
type NotificationConfig struct {
2020-10-27 00:48:47 +00:00
Slack *SlackNotification `json:"slack,omitempty" yaml:"slack,omitempty"`
2021-10-15 08:10:09 +00:00
Telegram *TelegramNotification `json:"telegram,omitempty" yaml:"telegram,omitempty"`
2020-10-27 00:48:47 +00:00
SymbolChannels map[string]string `json:"symbolChannels,omitempty" yaml:"symbolChannels,omitempty"`
SessionChannels map[string]string `json:"sessionChannels,omitempty" yaml:"sessionChannels,omitempty"`
2021-02-21 08:52:47 +00:00
Routing *SlackNotificationRouting `json:"routing,omitempty" yaml:"routing,omitempty"`
2020-10-27 00:48:47 +00:00
}
2020-10-26 13:45:02 +00:00
type Session struct {
Name string `json:"name,omitempty" yaml:"name,omitempty"`
ExchangeName string `json:"exchange" yaml:"exchange"`
EnvVarPrefix string `json:"envVarPrefix" yaml:"envVarPrefix"`
2021-02-02 18:26:41 +00:00
Key string `json:"key,omitempty" yaml:"key,omitempty"`
Secret string `json:"secret,omitempty" yaml:"secret,omitempty"`
2021-01-09 03:27:14 +00:00
PublicOnly bool `json:"publicOnly,omitempty" yaml:"publicOnly"`
Margin bool `json:"margin,omitempty" yaml:"margin,omitempty"`
2021-01-09 03:27:14 +00:00
IsolatedMargin bool `json:"isolatedMargin,omitempty" yaml:"isolatedMargin,omitempty"`
2021-01-09 03:12:48 +00:00
IsolatedMarginSymbol string `json:"isolatedMarginSymbol,omitempty" yaml:"isolatedMarginSymbol,omitempty"`
2020-10-26 13:45:02 +00:00
}
var supportedTimeFormats = []string{
time.RFC3339,
time.RFC822,
"2006-01-02T15:04:05",
"2006-01-02",
}
type Backtest struct {
2020-11-10 11:06:20 +00:00
StartTime string `json:"startTime" yaml:"startTime"`
EndTime string `json:"endTime" yaml:"endTime"`
// RecordTrades is an option, if set to true, back-testing should record the trades into database
RecordTrades bool `json:"recordTrades,omitempty" yaml:"recordTrades,omitempty"`
Account BacktestAccount `json:"account" yaml:"account"`
Symbols []string `json:"symbols" yaml:"symbols"`
2020-11-10 11:06:20 +00:00
}
func parseTimeWithFormats(strTime string, formats []string) (time.Time, error) {
for _, format := range formats {
tt, err := time.Parse(format, strTime)
if err == nil {
return tt, nil
}
}
return time.Time{}, fmt.Errorf("failed to parse time %s, valid formats are %+v", strTime, formats)
}
2020-11-10 11:06:20 +00:00
func (t Backtest) ParseEndTime() (time.Time, error) {
if len(t.EndTime) == 0 {
return time.Time{}, errors.New("backtest.endTime must be defined")
}
return parseTimeWithFormats(t.EndTime, supportedTimeFormats)
}
2020-11-06 19:18:05 +00:00
func (t Backtest) ParseStartTime() (time.Time, error) {
if len(t.StartTime) == 0 {
return time.Time{}, errors.New("backtest.startTime must be defined")
}
return parseTimeWithFormats(t.StartTime, supportedTimeFormats)
2020-11-06 19:18:05 +00:00
}
type BacktestAccount struct {
2021-12-05 04:23:27 +00:00
// TODO: MakerFeeRate should replace the commission fields
MakerFeeRate fixedpoint.Value `json:"makerFeeRate"`
TakerFeeRate fixedpoint.Value `json:"takerFeeRate"`
2021-03-19 00:49:24 +00:00
MakerCommission fixedpoint.Value `json:"makerCommission"`
TakerCommission fixedpoint.Value `json:"takerCommission"`
BuyerCommission int `json:"buyerCommission"`
SellerCommission int `json:"sellerCommission"`
Balances BacktestAccountBalanceMap `json:"balances" yaml:"balances"`
}
type BacktestAccountBalanceMap map[string]fixedpoint.Value
2020-11-06 19:18:05 +00:00
func (m BacktestAccountBalanceMap) BalanceMap() types.BalanceMap {
balances := make(types.BalanceMap)
for currency, value := range m {
balances[currency] = types.Balance{
Currency: currency,
2020-11-10 06:19:33 +00:00
Available: value,
Locked: 0,
2020-11-06 19:18:05 +00:00
}
}
return balances
}
2020-12-06 10:58:05 +00:00
type PersistenceConfig struct {
2021-02-20 17:01:39 +00:00
Redis *service.RedisPersistenceConfig `json:"redis,omitempty" yaml:"redis,omitempty"`
Json *service.JsonPersistenceConfig `json:"json,omitempty" yaml:"json,omitempty"`
2020-12-06 10:58:05 +00:00
}
2021-01-21 04:06:03 +00:00
type BuildTargetConfig struct {
Name string `json:"name" yaml:"name"`
Arch string `json:"arch" yaml:"arch"`
OS string `json:"os" yaml:"os"`
2021-02-03 01:34:53 +00:00
LDFlags datatype.StringSlice `json:"ldflags,omitempty" yaml:"ldflags,omitempty"`
GCFlags datatype.StringSlice `json:"gcflags,omitempty" yaml:"gcflags,omitempty"`
Imports []string `json:"imports,omitempty" yaml:"imports,omitempty"`
2021-01-21 04:06:03 +00:00
}
type BuildConfig struct {
2021-02-03 01:34:53 +00:00
BuildDir string `json:"buildDir,omitempty" yaml:"buildDir,omitempty"`
Imports []string `json:"imports,omitempty" yaml:"imports,omitempty"`
Targets []BuildTargetConfig `json:"targets,omitempty" yaml:"targets,omitempty"`
2021-01-21 04:06:03 +00:00
}
func GetNativeBuildTargetConfig() BuildTargetConfig {
return BuildTargetConfig{
Name: "bbgow",
Arch: runtime.GOARCH,
2021-01-22 17:03:56 +00:00
OS: runtime.GOOS,
2021-01-21 04:06:03 +00:00
}
}
2020-10-26 13:45:02 +00:00
type Config struct {
2021-02-02 18:26:41 +00:00
Build *BuildConfig `json:"build,omitempty" yaml:"build,omitempty"`
2021-01-21 04:06:03 +00:00
// Imports is deprecated
// Deprecated: use BuildConfig instead
2021-02-02 18:26:41 +00:00
Imports []string `json:"imports,omitempty" yaml:"imports,omitempty"`
2020-10-26 13:45:02 +00:00
Backtest *Backtest `json:"backtest,omitempty" yaml:"backtest,omitempty"`
2020-10-30 21:21:17 +00:00
Notifications *NotificationConfig `json:"notifications,omitempty" yaml:"notifications,omitempty"`
2020-10-27 00:48:47 +00:00
2020-12-06 10:58:05 +00:00
Persistence *PersistenceConfig `json:"persistence,omitempty" yaml:"persistence,omitempty"`
2021-02-02 09:26:35 +00:00
Sessions map[string]*ExchangeSession `json:"sessions,omitempty" yaml:"sessions,omitempty"`
2020-10-26 13:45:02 +00:00
RiskControls *RiskControls `json:"riskControls,omitempty" yaml:"riskControls,omitempty"`
2021-02-03 01:08:05 +00:00
ExchangeStrategies []ExchangeStrategyMount `json:"-" yaml:"-"`
CrossExchangeStrategies []CrossExchangeStrategy `json:"-" yaml:"-"`
2020-10-26 13:45:02 +00:00
PnLReporters []PnLReporterConfig `json:"reportPnL,omitempty" yaml:"reportPnL,omitempty"`
}
2021-02-03 01:08:05 +00:00
func (c *Config) Map() (map[string]interface{}, error) {
text, err := json.Marshal(c)
if err != nil {
return nil, err
}
var data map[string]interface{}
err = json.Unmarshal(text, &data)
if err != nil {
return nil, err
}
// convert strategy config back to the DSL format
2021-02-03 01:34:53 +00:00
var exchangeStrategies []map[string]interface{}
for _, m := range c.ExchangeStrategies {
params, err := m.Map()
2021-02-03 01:34:53 +00:00
if err != nil {
return nil, err
}
exchangeStrategies = append(exchangeStrategies, params)
2021-02-03 01:08:05 +00:00
}
2021-02-03 01:58:31 +00:00
if len(exchangeStrategies) > 0 {
data["exchangeStrategies"] = exchangeStrategies
}
var crossExchangeStrategies []map[string]interface{}
for _, st := range c.CrossExchangeStrategies {
strategyID := st.ID()
var params Stash
out, err := json.Marshal(st)
if err != nil {
return nil, err
}
if err := json.Unmarshal(out, &params); err != nil {
return nil, err
}
crossExchangeStrategies = append(crossExchangeStrategies, map[string]interface{}{
strategyID: params,
})
}
if len(crossExchangeStrategies) > 0 {
data["crossExchangeStrategies"] = crossExchangeStrategies
}
2021-02-03 01:08:05 +00:00
return data, err
}
2021-02-03 01:58:31 +00:00
func (c *Config) YAML() ([]byte, error) {
m, err := c.Map()
if err != nil {
return nil, err
}
var buf bytes.Buffer
var enc = yaml.NewEncoder(&buf)
enc.SetIndent(2)
err = enc.Encode(m)
return buf.Bytes(), err
}
2020-10-26 13:45:02 +00:00
type Stash map[string]interface{}
func loadStash(config []byte) (Stash, error) {
stash := make(Stash)
if err := yaml.Unmarshal(config, stash); err != nil {
return nil, err
}
return stash, nil
}
2020-12-06 03:40:22 +00:00
func LoadBuildConfig(configFile string) (*Config, error) {
2020-11-15 05:23:26 +00:00
var config Config
content, err := ioutil.ReadFile(configFile)
if err != nil {
return nil, err
}
if err := yaml.Unmarshal(content, &config); err != nil {
return nil, err
}
2021-01-21 04:06:03 +00:00
// for backward compatible
if config.Build == nil {
if len(config.Imports) > 0 {
config.Build = &BuildConfig{
BuildDir: "build",
Imports: config.Imports,
Targets: []BuildTargetConfig{
{Name: "bbgow-amd64-darwin", Arch: "amd64", OS: "darwin"},
{Name: "bbgow-amd64-linux", Arch: "amd64", OS: "linux"},
},
}
2021-01-21 04:06:03 +00:00
}
}
2020-11-15 05:23:26 +00:00
return &config, nil
}
2021-01-22 16:50:23 +00:00
// Load parses the config
2020-12-29 08:00:03 +00:00
func Load(configFile string, loadStrategies bool) (*Config, error) {
2020-10-26 13:45:02 +00:00
var config Config
content, err := ioutil.ReadFile(configFile)
if err != nil {
return nil, err
}
if err := yaml.Unmarshal(content, &config); err != nil {
return nil, err
}
2021-01-22 17:03:56 +00:00
// for backward compatible
if config.Build == nil {
2021-01-22 17:06:56 +00:00
config.Build = &BuildConfig{
BuildDir: "build",
Imports: config.Imports,
Targets: []BuildTargetConfig{
{Name: "bbgow-amd64-darwin", Arch: "amd64", OS: "darwin"},
{Name: "bbgow-amd64-linux", Arch: "amd64", OS: "linux"},
},
2021-01-22 17:03:56 +00:00
}
}
2020-10-26 13:45:02 +00:00
stash, err := loadStash(content)
if err != nil {
return nil, err
}
2020-12-29 08:00:03 +00:00
if loadStrategies {
if err := loadExchangeStrategies(&config, stash); err != nil {
return nil, err
}
2020-10-26 13:45:02 +00:00
2020-12-29 08:00:03 +00:00
if err := loadCrossExchangeStrategies(&config, stash); err != nil {
return nil, err
}
2020-10-26 13:45:02 +00:00
}
return &config, nil
}
func loadCrossExchangeStrategies(config *Config, stash Stash) (err error) {
exchangeStrategiesConf, ok := stash["crossExchangeStrategies"]
if !ok {
return nil
}
if len(LoadedCrossExchangeStrategies) == 0 {
return errors.New("no cross exchange strategy is registered")
}
configList, ok := exchangeStrategiesConf.([]interface{})
if !ok {
return errors.New("expecting list in crossExchangeStrategies")
}
for _, entry := range configList {
configStash, ok := entry.(Stash)
if !ok {
2020-11-09 08:34:35 +00:00
return fmt.Errorf("strategy config should be a map, given: %T %+v", entry, entry)
2020-10-26 13:45:02 +00:00
}
for id, conf := range configStash {
// look up the real struct type
if st, ok := LoadedCrossExchangeStrategies[id]; ok {
2020-10-26 13:45:02 +00:00
val, err := reUnmarshal(conf, st)
if err != nil {
return err
}
config.CrossExchangeStrategies = append(config.CrossExchangeStrategies, val.(CrossExchangeStrategy))
}
}
}
return nil
}
2021-02-02 18:26:41 +00:00
func NewStrategyFromMap(id string, conf interface{}) (SingleExchangeStrategy, error) {
if st, ok := LoadedExchangeStrategies[id]; ok {
val, err := reUnmarshal(conf, st)
if err != nil {
return nil, err
}
return val.(SingleExchangeStrategy), nil
}
return nil, fmt.Errorf("strategy %s not found", id)
}
2020-10-26 13:45:02 +00:00
func loadExchangeStrategies(config *Config, stash Stash) (err error) {
exchangeStrategiesConf, ok := stash["exchangeStrategies"]
if !ok {
exchangeStrategiesConf, ok = stash["strategies"]
if !ok {
return nil
}
2020-10-26 13:45:02 +00:00
}
if len(LoadedExchangeStrategies) == 0 {
return errors.New("no exchange strategy is registered")
}
configList, ok := exchangeStrategiesConf.([]interface{})
if !ok {
return errors.New("expecting list in exchangeStrategies")
}
for _, entry := range configList {
configStash, ok := entry.(Stash)
if !ok {
2020-11-09 08:34:35 +00:00
return fmt.Errorf("strategy config should be a map, given: %T %+v", entry, entry)
2020-10-26 13:45:02 +00:00
}
var mounts []string
if val, ok := configStash["on"]; ok {
2021-02-03 01:58:31 +00:00
switch tv := val.(type) {
case []string:
mounts = append(mounts, tv...)
case string:
mounts = append(mounts, tv)
case []interface{}:
for _, f := range tv {
s, ok := f.(string)
if !ok {
return fmt.Errorf("%+v (%T) is not a string", f, f)
}
mounts = append(mounts, s)
}
default:
return fmt.Errorf("unexpected mount type: %T value: %+v", val, val)
2020-10-26 13:45:02 +00:00
}
}
for id, conf := range configStash {
2021-02-02 18:26:41 +00:00
2020-10-26 13:45:02 +00:00
// look up the real struct type
2021-02-02 18:26:41 +00:00
if _, ok := LoadedExchangeStrategies[id]; ok {
st, err := NewStrategyFromMap(id, conf)
2020-10-26 13:45:02 +00:00
if err != nil {
return err
}
config.ExchangeStrategies = append(config.ExchangeStrategies, ExchangeStrategyMount{
Mounts: mounts,
2021-02-02 18:26:41 +00:00
Strategy: st,
2020-10-26 13:45:02 +00:00
})
}
}
}
return nil
}
func reUnmarshal(conf interface{}, tpe interface{}) (interface{}, error) {
// get the type "*Strategy"
rt := reflect.TypeOf(tpe)
// allocate new object from the given type
val := reflect.New(rt)
// now we have &(*Strategy) -> **Strategy
valRef := val.Interface()
plain, err := json.Marshal(conf)
if err != nil {
return nil, err
}
if err := json.Unmarshal(plain, valRef); err != nil {
return nil, errors.Wrapf(err, "json parsing error, given payload: %s", plain)
}
return val.Elem().Interface(), nil
}