2020-10-26 13:45:02 +00:00
|
|
|
package bbgo
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
2020-11-09 08:34:35 +00:00
|
|
|
"fmt"
|
2020-10-26 13:45:02 +00:00
|
|
|
"io/ioutil"
|
|
|
|
"reflect"
|
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-11-06 18:57:50 +00:00
|
|
|
|
|
|
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
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 {
|
|
|
|
AverageCostBySymbols StringSlice `json:"averageCostBySymbols" yaml:"averageCostBySymbols"`
|
|
|
|
Of StringSlice `json:"of" yaml:"of"`
|
|
|
|
When StringSlice `json:"when" yaml:"when"`
|
|
|
|
}
|
|
|
|
|
2020-11-17 00:19:22 +00:00
|
|
|
// ExchangeStrategyMount wraps the SingleExchangeStrategy with the Session name for mounting
|
2020-10-26 13:45:02 +00:00
|
|
|
type ExchangeStrategyMount struct {
|
2020-11-17 00:19:22 +00:00
|
|
|
// Mounts contains the Session name to mount
|
2020-10-26 13:46:38 +00:00
|
|
|
Mounts []string
|
2020-10-26 13:45:02 +00:00
|
|
|
|
|
|
|
// Strategy is the strategy we loaded from config
|
|
|
|
Strategy SingleExchangeStrategy
|
|
|
|
}
|
|
|
|
|
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"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type NotificationRouting struct {
|
2020-11-06 18:57:50 +00:00
|
|
|
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"`
|
2020-11-06 18:57:50 +00:00
|
|
|
PnL string `json:"pnL,omitempty" yaml:"pnL,omitempty"`
|
2020-10-27 00:48:47 +00:00
|
|
|
}
|
|
|
|
|
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"`
|
|
|
|
|
|
|
|
SymbolChannels map[string]string `json:"symbolChannels,omitempty" yaml:"symbolChannels,omitempty"`
|
|
|
|
SessionChannels map[string]string `json:"sessionChannels,omitempty" yaml:"sessionChannels,omitempty"`
|
|
|
|
|
|
|
|
Routing *NotificationRouting `json:"routing,omitempty" yaml:"routing,omitempty"`
|
|
|
|
}
|
|
|
|
|
2020-10-26 13:45:02 +00:00
|
|
|
type Session struct {
|
|
|
|
ExchangeName string `json:"exchange" yaml:"exchange"`
|
|
|
|
EnvVarPrefix string `json:"envVarPrefix" yaml:"envVarPrefix"`
|
|
|
|
}
|
|
|
|
|
2020-11-06 18:57:50 +00:00
|
|
|
type Backtest struct {
|
2020-11-10 11:06:20 +00:00
|
|
|
StartTime string `json:"startTime" yaml:"startTime"`
|
|
|
|
EndTime string `json:"endTime" yaml:"endTime"`
|
|
|
|
|
|
|
|
Account BacktestAccount `json:"account" yaml:"account"`
|
|
|
|
Symbols []string `json:"symbols" yaml:"symbols"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (t Backtest) ParseEndTime() (time.Time, error) {
|
|
|
|
if len(t.EndTime) == 0 {
|
|
|
|
return time.Time{}, errors.New("backtest.endTime must be defined")
|
|
|
|
}
|
|
|
|
|
|
|
|
return time.Parse("2006-01-02", t.EndTime)
|
2020-11-06 18:57:50 +00:00
|
|
|
}
|
|
|
|
|
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 time.Parse("2006-01-02", t.StartTime)
|
|
|
|
}
|
|
|
|
|
2020-11-06 18:57:50 +00:00
|
|
|
type BacktestAccount struct {
|
|
|
|
MakerCommission int `json:"makerCommission"`
|
|
|
|
TakerCommission int `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 RedisPersistenceConfig struct {
|
|
|
|
Host string `json:"host" env:"REDIS_HOST"`
|
|
|
|
Port string `json:"port" env:"REDIS_PORT"`
|
|
|
|
Password string `json:"password" env:"REDIS_PASSWORD"`
|
|
|
|
DB int `json:"db" env:"REDIS_DB"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type JsonPersistenceConfig struct {
|
|
|
|
Directory string `json:"directory"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type PersistenceConfig struct {
|
|
|
|
Redis *RedisPersistenceConfig `json:"redis,omitempty" yaml:"redis,omitempty"`
|
|
|
|
Json *JsonPersistenceConfig `json:"json,omitempty" yaml:"json,omitempty"`
|
|
|
|
}
|
|
|
|
|
2020-10-26 13:45:02 +00:00
|
|
|
type Config struct {
|
|
|
|
Imports []string `json:"imports" yaml:"imports"`
|
|
|
|
|
2020-11-06 18:57:50 +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"`
|
|
|
|
|
2020-10-26 13:45:02 +00:00
|
|
|
Sessions map[string]Session `json:"sessions,omitempty" yaml:"sessions,omitempty"`
|
|
|
|
|
|
|
|
RiskControls *RiskControls `json:"riskControls,omitempty" yaml:"riskControls,omitempty"`
|
|
|
|
|
|
|
|
ExchangeStrategies []ExchangeStrategyMount
|
|
|
|
CrossExchangeStrategies []CrossExchangeStrategy
|
|
|
|
|
|
|
|
PnLReporters []PnLReporterConfig `json:"reportPnL,omitempty" yaml:"reportPnL,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
return &config, nil
|
|
|
|
}
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
2020-12-29 08:00:03 +00:00
|
|
|
|
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
|
2020-11-17 00:19:22 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
func loadExchangeStrategies(config *Config, stash Stash) (err error) {
|
|
|
|
exchangeStrategiesConf, ok := stash["exchangeStrategies"]
|
|
|
|
if !ok {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
|
|
|
if values, ok := val.([]string); ok {
|
|
|
|
mounts = append(mounts, values...)
|
|
|
|
} else if str, ok := val.(string); ok {
|
|
|
|
mounts = append(mounts, str)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for id, conf := range configStash {
|
|
|
|
// look up the real struct type
|
|
|
|
if st, ok := LoadedExchangeStrategies[id]; ok {
|
|
|
|
val, err := reUnmarshal(conf, st)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
config.ExchangeStrategies = append(config.ExchangeStrategies, ExchangeStrategyMount{
|
|
|
|
Mounts: mounts,
|
|
|
|
Strategy: val.(SingleExchangeStrategy),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|