mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-25 16:25:16 +00:00
Merge pull request #466 from c9s/feature/strategy-profit
feature: add strategy profit records
This commit is contained in:
commit
35ef21ab1c
|
@ -8,19 +8,22 @@
|
||||||
|
|
||||||
2. Create migration files
|
2. Create migration files
|
||||||
|
|
||||||
```sh
|
|
||||||
rockhopper --config rockhopper_sqlite.yaml create --type sql add_pnl_column
|
|
||||||
rockhopper --config rockhopper_mysql.yaml create --type sql add_pnl_column
|
|
||||||
```
|
|
||||||
|
|
||||||
or you can use the util script:
|
You can use the util script to generate the migration files:
|
||||||
|
|
||||||
```
|
```
|
||||||
bash utils/generate-new-migration.sh add_pnl_column
|
bash utils/generate-new-migration.sh add_pnl_column
|
||||||
```
|
```
|
||||||
|
|
||||||
Be sure to edit both sqlite3 and mysql migration files. ( [Sample](migrations/mysql/20210531234123_add_kline_taker_buy_columns.sql) )
|
Or, you can generate the migration files separately:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
rockhopper --config rockhopper_sqlite.yaml create --type sql add_pnl_column
|
||||||
|
rockhopper --config rockhopper_mysql.yaml create --type sql add_pnl_column
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Be sure to edit both sqlite3 and mysql migration files. ( [Sample](migrations/mysql/20210531234123_add_kline_taker_buy_columns.sql) )
|
||||||
|
|
||||||
To test the drivers, you have to update the rockhopper_mysql.yaml file to connect your database,
|
To test the drivers, you have to update the rockhopper_mysql.yaml file to connect your database,
|
||||||
then do:
|
then do:
|
||||||
|
@ -39,9 +42,9 @@ make migrations
|
||||||
or
|
or
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
rockhopper compile --config rockhopper_mysql.yaml --output pkg/migrations/mysql
|
rockhopper compile --config rockhopper_mysql.yaml --output pkg/migrations/mysql
|
||||||
rockhopper compile --config rockhopper_sqlite.yaml --output pkg/migrations/sqlite3
|
rockhopper compile --config rockhopper_sqlite.yaml --output pkg/migrations/sqlite3
|
||||||
git add -v pkg/migrations && git commit -m "compile and update migration package" pkg/migrations || true
|
git add -v pkg/migrations && git commit -m "compile and update migration package" pkg/migrations || true
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
71
migrations/mysql/20220304153317_add_profit_table.sql
Normal file
71
migrations/mysql/20220304153317_add_profit_table.sql
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
-- +up
|
||||||
|
CREATE TABLE `profits`
|
||||||
|
(
|
||||||
|
`gid` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
|
||||||
|
`strategy` VARCHAR(32) NOT NULL,
|
||||||
|
`strategy_instance_id` VARCHAR(64) NOT NULL,
|
||||||
|
|
||||||
|
`symbol` VARCHAR(8) NOT NULL,
|
||||||
|
|
||||||
|
-- average_cost is the position average cost
|
||||||
|
`average_cost` DECIMAL(16, 8) UNSIGNED NOT NULL,
|
||||||
|
|
||||||
|
-- profit is the pnl (profit and loss)
|
||||||
|
`profit` DECIMAL(16, 8) NOT NULL,
|
||||||
|
|
||||||
|
-- net_profit is the pnl (profit and loss)
|
||||||
|
`net_profit` DECIMAL(16, 8) NOT NULL,
|
||||||
|
|
||||||
|
-- profit_margin is the pnl (profit and loss)
|
||||||
|
`profit_margin` DECIMAL(16, 8) NOT NULL,
|
||||||
|
|
||||||
|
-- net_profit_margin is the pnl (profit and loss)
|
||||||
|
`net_profit_margin` DECIMAL(16, 8) NOT NULL,
|
||||||
|
|
||||||
|
`quote_currency` VARCHAR(10) NOT NULL,
|
||||||
|
|
||||||
|
`base_currency` VARCHAR(10) NOT NULL,
|
||||||
|
|
||||||
|
-- -------------------------------------------------------
|
||||||
|
-- embedded trade data --
|
||||||
|
-- -------------------------------------------------------
|
||||||
|
`exchange` VARCHAR(24) NOT NULL DEFAULT '',
|
||||||
|
|
||||||
|
`is_futures` BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
|
||||||
|
`is_margin` BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
|
||||||
|
`is_isolated` BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
|
||||||
|
`trade_id` BIGINT UNSIGNED NOT NULL,
|
||||||
|
|
||||||
|
-- side is the side of the trade that makes profit
|
||||||
|
`side` VARCHAR(4) NOT NULL DEFAULT '',
|
||||||
|
|
||||||
|
`is_buyer` BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
|
||||||
|
`is_maker` BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
|
||||||
|
-- price is the price of the trade that makes profit
|
||||||
|
`price` DECIMAL(16, 8) UNSIGNED NOT NULL,
|
||||||
|
|
||||||
|
-- quantity is the quantity of the trade that makes profit
|
||||||
|
`quantity` DECIMAL(16, 8) UNSIGNED NOT NULL,
|
||||||
|
|
||||||
|
-- quote_quantity is the quote quantity of the trade that makes profit
|
||||||
|
`quote_quantity` DECIMAL(16, 8) UNSIGNED NOT NULL,
|
||||||
|
|
||||||
|
`traded_at` DATETIME(3) NOT NULL,
|
||||||
|
|
||||||
|
-- fee
|
||||||
|
`fee_in_usd` DECIMAL(16, 8) UNSIGNED,
|
||||||
|
`fee` DECIMAL(16, 8) UNSIGNED NOT NULL,
|
||||||
|
`fee_currency` VARCHAR(10) NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (`gid`),
|
||||||
|
UNIQUE KEY `trade_id` (`trade_id`)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- +down
|
||||||
|
DROP TABLE IF EXISTS `profits`;
|
68
migrations/sqlite3/20220304153309_add_profit_table.sql
Normal file
68
migrations/sqlite3/20220304153309_add_profit_table.sql
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
-- +up
|
||||||
|
CREATE TABLE `profits`
|
||||||
|
(
|
||||||
|
`gid` INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
|
||||||
|
`strategy` VARCHAR(32) NOT NULL,
|
||||||
|
`strategy_instance_id` VARCHAR(64) NOT NULL,
|
||||||
|
|
||||||
|
`symbol` VARCHAR(8) NOT NULL,
|
||||||
|
|
||||||
|
-- average_cost is the position average cost
|
||||||
|
`average_cost` DECIMAL(16, 8) NOT NULL,
|
||||||
|
|
||||||
|
-- profit is the pnl (profit and loss)
|
||||||
|
`profit` DECIMAL(16, 8) NOT NULL,
|
||||||
|
|
||||||
|
-- net_profit is the pnl (profit and loss)
|
||||||
|
`net_profit` DECIMAL(16, 8) NOT NULL,
|
||||||
|
|
||||||
|
-- profit_margin is the pnl (profit and loss)
|
||||||
|
`profit_margin` DECIMAL(16, 8) NOT NULL,
|
||||||
|
|
||||||
|
-- net_profit_margin is the pnl (profit and loss)
|
||||||
|
`net_profit_margin` DECIMAL(16, 8) NOT NULL,
|
||||||
|
|
||||||
|
`quote_currency` VARCHAR(10) NOT NULL,
|
||||||
|
|
||||||
|
`base_currency` VARCHAR(10) NOT NULL,
|
||||||
|
|
||||||
|
-- -------------------------------------------------------
|
||||||
|
-- embedded trade data --
|
||||||
|
-- -------------------------------------------------------
|
||||||
|
`exchange` VARCHAR(24) NOT NULL DEFAULT '',
|
||||||
|
|
||||||
|
`is_futures` BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
|
||||||
|
`is_margin` BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
|
||||||
|
`is_isolated` BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
|
||||||
|
`trade_id` BIGINT NOT NULL,
|
||||||
|
|
||||||
|
-- side is the side of the trade that makes profit
|
||||||
|
`side` VARCHAR(4) NOT NULL DEFAULT '',
|
||||||
|
|
||||||
|
`is_buyer` BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
|
||||||
|
`is_maker` BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
|
||||||
|
-- price is the price of the trade that makes profit
|
||||||
|
`price` DECIMAL(16, 8) NOT NULL,
|
||||||
|
|
||||||
|
-- quantity is the quantity of the trade that makes profit
|
||||||
|
`quantity` DECIMAL(16, 8) NOT NULL,
|
||||||
|
|
||||||
|
-- trade_amount is the quote quantity of the trade that makes profit
|
||||||
|
`quote_quantity` DECIMAL(16, 8) NOT NULL,
|
||||||
|
|
||||||
|
`traded_at` DATETIME(3) NOT NULL,
|
||||||
|
|
||||||
|
-- fee
|
||||||
|
`fee_in_usd` DECIMAL(16, 8),
|
||||||
|
`fee` DECIMAL(16, 8) NOT NULL,
|
||||||
|
`fee_currency` VARCHAR(10) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- +down
|
||||||
|
DROP TABLE IF EXISTS `profits`;
|
|
@ -76,6 +76,7 @@ type Environment struct {
|
||||||
DatabaseService *service.DatabaseService
|
DatabaseService *service.DatabaseService
|
||||||
OrderService *service.OrderService
|
OrderService *service.OrderService
|
||||||
TradeService *service.TradeService
|
TradeService *service.TradeService
|
||||||
|
ProfitService *service.ProfitService
|
||||||
BacktestService *service.BacktestService
|
BacktestService *service.BacktestService
|
||||||
RewardService *service.RewardService
|
RewardService *service.RewardService
|
||||||
SyncService *service.SyncService
|
SyncService *service.SyncService
|
||||||
|
@ -170,6 +171,7 @@ func (environ *Environment) ConfigureDatabaseDriver(ctx context.Context, driver
|
||||||
environ.TradeService = &service.TradeService{DB: db}
|
environ.TradeService = &service.TradeService{DB: db}
|
||||||
environ.RewardService = &service.RewardService{DB: db}
|
environ.RewardService = &service.RewardService{DB: db}
|
||||||
environ.AccountService = &service.AccountService{DB: db}
|
environ.AccountService = &service.AccountService{DB: db}
|
||||||
|
environ.ProfitService = &service.ProfitService{DB: db}
|
||||||
|
|
||||||
environ.SyncService = &service.SyncService{
|
environ.SyncService = &service.SyncService{
|
||||||
TradeService: environ.TradeService,
|
TradeService: environ.TradeService,
|
||||||
|
@ -569,6 +571,19 @@ func (environ *Environment) Sync(ctx context.Context, userConfig ...*Config) err
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (environ *Environment) RecordProfit(profit types.Profit) {
|
||||||
|
if environ.DatabaseService == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if environ.ProfitService == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := environ.ProfitService.Insert(profit) ; err != nil {
|
||||||
|
log.WithError(err).Errorf("can not insert profit record: %+v", profit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (environ *Environment) SyncSession(ctx context.Context, session *ExchangeSession, defaultSymbols ...string) error {
|
func (environ *Environment) SyncSession(ctx context.Context, session *ExchangeSession, defaultSymbols ...string) error {
|
||||||
if environ.SyncService == nil {
|
if environ.SyncService == nil {
|
||||||
return nil
|
return nil
|
||||||
|
@ -584,7 +599,7 @@ func (environ *Environment) SyncSession(ctx context.Context, session *ExchangeSe
|
||||||
}
|
}
|
||||||
|
|
||||||
func (environ *Environment) syncSession(ctx context.Context, session *ExchangeSession, defaultSymbols ...string) error {
|
func (environ *Environment) syncSession(ctx context.Context, session *ExchangeSession, defaultSymbols ...string) error {
|
||||||
symbols, err := getSessionSymbols(session, defaultSymbols...)
|
symbols, err := session.getSessionSymbols(defaultSymbols...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -594,17 +609,6 @@ func (environ *Environment) syncSession(ctx context.Context, session *ExchangeSe
|
||||||
return environ.SyncService.SyncSessionSymbols(ctx, session.Exchange, environ.syncStartTime, symbols...)
|
return environ.SyncService.SyncSessionSymbols(ctx, session.Exchange, environ.syncStartTime, symbols...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSessionSymbols(session *ExchangeSession, defaultSymbols ...string) ([]string, error) {
|
|
||||||
if session.IsolatedMargin {
|
|
||||||
return []string{session.IsolatedMarginSymbol}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(defaultSymbols) > 0 {
|
|
||||||
return defaultSymbols, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return session.FindPossibleSymbols()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (environ *Environment) ConfigureNotificationSystem(userConfig *Config) error {
|
func (environ *Environment) ConfigureNotificationSystem(userConfig *Config) error {
|
||||||
environ.Notifiability = Notifiability{
|
environ.Notifiability = Notifiability{
|
||||||
|
@ -654,6 +658,10 @@ func (environ *Environment) ConfigureNotificationSystem(userConfig *Config) erro
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getAuthStoreID returns the authentication store id
|
||||||
|
// if telegram bot token is defined, the bot id will be used.
|
||||||
|
// if not, env var $USER will be used.
|
||||||
|
// if both are not defined, a default "default" will be used.
|
||||||
func getAuthStoreID() string {
|
func getAuthStoreID() string {
|
||||||
telegramBotToken := viper.GetString("telegram-bot-token")
|
telegramBotToken := viper.GetString("telegram-bot-token")
|
||||||
if len(telegramBotToken) > 0 {
|
if len(telegramBotToken) > 0 {
|
||||||
|
@ -924,3 +932,15 @@ And then enter your token
|
||||||
|
|
||||||
`, token)
|
`, token)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (session *ExchangeSession) getSessionSymbols(defaultSymbols ...string) ([]string, error) {
|
||||||
|
if session.IsolatedMargin {
|
||||||
|
return []string{session.IsolatedMarginSymbol}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(defaultSymbols) > 0 {
|
||||||
|
return defaultSymbols, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return session.FindPossibleSymbols()
|
||||||
|
}
|
||||||
|
|
|
@ -3,6 +3,8 @@ package bbgo
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func isSymbolBasedStrategy(rs reflect.Value) (string, bool) {
|
func isSymbolBasedStrategy(rs reflect.Value) (string, bool) {
|
||||||
|
@ -53,3 +55,79 @@ func injectField(rs reflect.Value, fieldName string, obj interface{}, pointerOnl
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseStructAndInject parses the struct fields and injects the objects into the corresponding fields by its type.
|
||||||
|
// if the given object is a reference of an object, the type of the target field MUST BE a pointer field.
|
||||||
|
// if the given object is a struct value, the type of the target field CAN BE a pointer field or a struct value field.
|
||||||
|
func parseStructAndInject(f interface{}, objects ...interface{}) error {
|
||||||
|
sv := reflect.ValueOf(f)
|
||||||
|
st := reflect.TypeOf(f)
|
||||||
|
|
||||||
|
if st.Kind() != reflect.Ptr {
|
||||||
|
return fmt.Errorf("f needs to be a pointer of a struct, %s given", st)
|
||||||
|
}
|
||||||
|
|
||||||
|
// solve the reference
|
||||||
|
st = st.Elem()
|
||||||
|
sv = sv.Elem()
|
||||||
|
|
||||||
|
if st.Kind() != reflect.Struct {
|
||||||
|
return fmt.Errorf("f needs to be a struct, %s given", st)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < sv.NumField(); i++ {
|
||||||
|
fv := sv.Field(i)
|
||||||
|
ft := fv.Type()
|
||||||
|
|
||||||
|
// skip unexported fields
|
||||||
|
if !st.Field(i).IsExported() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch k := fv.Kind(); k {
|
||||||
|
|
||||||
|
case reflect.Ptr, reflect.Struct:
|
||||||
|
for oi := 0; oi < len(objects); oi++ {
|
||||||
|
obj := objects[oi]
|
||||||
|
if obj == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ot := reflect.TypeOf(obj)
|
||||||
|
if ft.AssignableTo(ot) {
|
||||||
|
if !fv.CanSet() {
|
||||||
|
return fmt.Errorf("field %v of %s can not be set to %s, make sure it is an exported field", fv, sv.Type(), ot)
|
||||||
|
}
|
||||||
|
|
||||||
|
if k == reflect.Ptr && ot.Kind() == reflect.Struct {
|
||||||
|
fv.Set(reflect.ValueOf(obj).Addr())
|
||||||
|
} else {
|
||||||
|
fv.Set(reflect.ValueOf(obj))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case reflect.Interface:
|
||||||
|
for oi := 0; oi < len(objects); oi++ {
|
||||||
|
obj := objects[oi]
|
||||||
|
if obj == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
objT := reflect.TypeOf(obj)
|
||||||
|
logrus.Debugln(
|
||||||
|
ft.PkgPath(),
|
||||||
|
ft.Name(),
|
||||||
|
objT, "implements", ft, "=", objT.Implements(ft),
|
||||||
|
)
|
||||||
|
|
||||||
|
if objT.Implements(ft) {
|
||||||
|
fv.Set(reflect.ValueOf(obj))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -3,10 +3,12 @@ package bbgo
|
||||||
import (
|
import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"github.com/c9s/bbgo/pkg/service"
|
"github.com/c9s/bbgo/pkg/service"
|
||||||
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_injectField(t *testing.T) {
|
func Test_injectField(t *testing.T) {
|
||||||
|
@ -28,3 +30,76 @@ func Test_injectField(t *testing.T) {
|
||||||
err := injectField(rv, "TradeService", ts, true)
|
err := injectField(rv, "TradeService", ts, true)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_parseStructAndInject(t *testing.T) {
|
||||||
|
t.Run("skip nil", func(t *testing.T) {
|
||||||
|
ss := struct {
|
||||||
|
a int
|
||||||
|
Env *Environment
|
||||||
|
}{
|
||||||
|
a: 1,
|
||||||
|
Env: nil,
|
||||||
|
}
|
||||||
|
err := parseStructAndInject(&ss, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Nil(t, ss.Env)
|
||||||
|
})
|
||||||
|
t.Run("pointer", func(t *testing.T) {
|
||||||
|
ss := struct {
|
||||||
|
a int
|
||||||
|
Env *Environment
|
||||||
|
}{
|
||||||
|
a: 1,
|
||||||
|
Env: nil,
|
||||||
|
}
|
||||||
|
err := parseStructAndInject(&ss, &Environment{})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, ss.Env)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("composition", func(t *testing.T) {
|
||||||
|
type TT struct {
|
||||||
|
*service.TradeService
|
||||||
|
}
|
||||||
|
ss := TT{}
|
||||||
|
err := parseStructAndInject(&ss, &service.TradeService{})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, ss.TradeService)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("struct", func(t *testing.T) {
|
||||||
|
ss := struct {
|
||||||
|
a int
|
||||||
|
Env Environment
|
||||||
|
}{
|
||||||
|
a: 1,
|
||||||
|
}
|
||||||
|
err := parseStructAndInject(&ss, Environment{
|
||||||
|
startTime: time.Now(),
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotEqual(t, time.Time{}, ss.Env.startTime)
|
||||||
|
})
|
||||||
|
t.Run("interface/any", func(t *testing.T) {
|
||||||
|
ss := struct {
|
||||||
|
Any interface{} // anything
|
||||||
|
}{
|
||||||
|
Any: nil,
|
||||||
|
}
|
||||||
|
err := parseStructAndInject(&ss, &Environment{
|
||||||
|
startTime: time.Now(),
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, ss.Any)
|
||||||
|
})
|
||||||
|
t.Run("interface/stringer", func(t *testing.T) {
|
||||||
|
ss := struct {
|
||||||
|
Stringer types.Stringer // stringer interface
|
||||||
|
}{
|
||||||
|
Stringer: nil,
|
||||||
|
}
|
||||||
|
err := parseStructAndInject(&ss, &types.Trade{})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, ss.Stringer)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -1,343 +1,2 @@
|
||||||
package bbgo
|
package bbgo
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/slack-go/slack"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
|
||||||
"github.com/c9s/bbgo/pkg/types"
|
|
||||||
"github.com/c9s/bbgo/pkg/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Profit struct stores the PnL information
|
|
||||||
type Profit struct {
|
|
||||||
Symbol string `json:"symbol"`
|
|
||||||
|
|
||||||
// Profit is the profit of this trade made. negative profit means loss.
|
|
||||||
Profit fixedpoint.Value `json:"profit" db:"profit"`
|
|
||||||
|
|
||||||
// NetProfit is (profit - trading fee)
|
|
||||||
NetProfit fixedpoint.Value `json:"netProfit" db:"net_profit"`
|
|
||||||
AverageCost fixedpoint.Value `json:"averageCost" db:"average_ost"`
|
|
||||||
|
|
||||||
TradeAmount fixedpoint.Value `json:"tradeAmount" db:"trade_amount"`
|
|
||||||
|
|
||||||
// ProfitMargin is a percentage of the profit and the capital amount
|
|
||||||
ProfitMargin fixedpoint.Value `json:"profitMargin" db:"profit_margin"`
|
|
||||||
|
|
||||||
// NetProfitMargin is a percentage of the net profit and the capital amount
|
|
||||||
NetProfitMargin fixedpoint.Value `json:"netProfitMargin" db:"net_profit_margin"`
|
|
||||||
|
|
||||||
QuoteCurrency string `json:"quoteCurrency" db:"quote_currency"`
|
|
||||||
BaseCurrency string `json:"baseCurrency" db:"base_currency"`
|
|
||||||
|
|
||||||
// FeeInUSD is the summed fee of this profit,
|
|
||||||
// you will need to convert the trade fee into USD since the fee currencies can be different.
|
|
||||||
FeeInUSD fixedpoint.Value `json:"feeInUSD" db:"fee_in_usd"`
|
|
||||||
Time time.Time `json:"time" db:"time"`
|
|
||||||
Strategy string `json:"strategy" db:"strategy"`
|
|
||||||
StrategyInstanceID string `json:"strategyInstanceID" db:"strategy_instance_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Profit) SlackAttachment() slack.Attachment {
|
|
||||||
var color = pnlColor(p.Profit)
|
|
||||||
var title = fmt.Sprintf("%s PnL ", p.Symbol)
|
|
||||||
title += pnlEmojiMargin(p.Profit, p.ProfitMargin, defaultPnlLevelResolution) + " "
|
|
||||||
title += pnlSignString(p.Profit) + " " + p.QuoteCurrency
|
|
||||||
|
|
||||||
var fields []slack.AttachmentField
|
|
||||||
|
|
||||||
if !p.NetProfit.IsZero() {
|
|
||||||
fields = append(fields, slack.AttachmentField{
|
|
||||||
Title: "Net Profit",
|
|
||||||
Value: pnlSignString(p.NetProfit) + " " + p.QuoteCurrency,
|
|
||||||
Short: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if !p.ProfitMargin.IsZero() {
|
|
||||||
fields = append(fields, slack.AttachmentField{
|
|
||||||
Title: "Profit Margin",
|
|
||||||
Value: p.ProfitMargin.Percentage(),
|
|
||||||
Short: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if !p.NetProfitMargin.IsZero() {
|
|
||||||
fields = append(fields, slack.AttachmentField{
|
|
||||||
Title: "Net Profit Margin",
|
|
||||||
Value: p.NetProfitMargin.Percentage(),
|
|
||||||
Short: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if !p.TradeAmount.IsZero() {
|
|
||||||
fields = append(fields, slack.AttachmentField{
|
|
||||||
Title: "Trade Amount",
|
|
||||||
Value: p.TradeAmount.String() + " " + p.QuoteCurrency,
|
|
||||||
Short: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if !p.FeeInUSD.IsZero() {
|
|
||||||
fields = append(fields, slack.AttachmentField{
|
|
||||||
Title: "Fee In USD",
|
|
||||||
Value: p.FeeInUSD.String() + " USD",
|
|
||||||
Short: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(p.Strategy) != 0 {
|
|
||||||
fields = append(fields, slack.AttachmentField{
|
|
||||||
Title: "Strategy",
|
|
||||||
Value: p.Strategy,
|
|
||||||
Short: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return slack.Attachment{
|
|
||||||
Color: color,
|
|
||||||
Title: title,
|
|
||||||
Fields: fields,
|
|
||||||
// Footer: "",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Profit) PlainText() string {
|
|
||||||
var emoji string
|
|
||||||
if !p.ProfitMargin.IsZero() {
|
|
||||||
emoji = pnlEmojiMargin(p.Profit, p.ProfitMargin, defaultPnlLevelResolution)
|
|
||||||
} else {
|
|
||||||
emoji = pnlEmojiSimple(p.Profit)
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("%s trade profit %s %s %s (%s), net profit =~ %s %s (%s)",
|
|
||||||
p.Symbol,
|
|
||||||
emoji,
|
|
||||||
p.Profit.String(), p.QuoteCurrency,
|
|
||||||
p.ProfitMargin.Percentage(),
|
|
||||||
p.NetProfit.String(), p.QuoteCurrency,
|
|
||||||
p.NetProfitMargin.Percentage(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var lossEmoji = "🔥"
|
|
||||||
var profitEmoji = "💰"
|
|
||||||
var defaultPnlLevelResolution = fixedpoint.NewFromFloat(0.001)
|
|
||||||
|
|
||||||
func pnlColor(pnl fixedpoint.Value) string {
|
|
||||||
if pnl.Sign() > 0 {
|
|
||||||
return types.GreenColor
|
|
||||||
}
|
|
||||||
return types.RedColor
|
|
||||||
}
|
|
||||||
|
|
||||||
func pnlSignString(pnl fixedpoint.Value) string {
|
|
||||||
if pnl.Sign() > 0 {
|
|
||||||
return "+" + pnl.String()
|
|
||||||
}
|
|
||||||
return pnl.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func pnlEmojiSimple(pnl fixedpoint.Value) string {
|
|
||||||
if pnl.Sign() < 0 {
|
|
||||||
return lossEmoji
|
|
||||||
}
|
|
||||||
|
|
||||||
if pnl.IsZero() {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return profitEmoji
|
|
||||||
}
|
|
||||||
|
|
||||||
func pnlEmojiMargin(pnl, margin, resolution fixedpoint.Value) (out string) {
|
|
||||||
if margin.IsZero() {
|
|
||||||
return pnlEmojiSimple(pnl)
|
|
||||||
}
|
|
||||||
|
|
||||||
if pnl.Sign() < 0 {
|
|
||||||
out = lossEmoji
|
|
||||||
level := (margin.Neg()).Div(resolution).Int()
|
|
||||||
for i := 1; i < level; i++ {
|
|
||||||
out += lossEmoji
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
if pnl.IsZero() {
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
out = profitEmoji
|
|
||||||
level := margin.Div(resolution).Int()
|
|
||||||
for i := 1; i < level; i++ {
|
|
||||||
out += profitEmoji
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProfitStats struct {
|
|
||||||
Symbol string `json:"symbol"`
|
|
||||||
QuoteCurrency string `json:"quoteCurrency"`
|
|
||||||
BaseCurrency string `json:"baseCurrency"`
|
|
||||||
|
|
||||||
AccumulatedPnL fixedpoint.Value `json:"accumulatedPnL,omitempty"`
|
|
||||||
AccumulatedNetProfit fixedpoint.Value `json:"accumulatedNetProfit,omitempty"`
|
|
||||||
AccumulatedProfit fixedpoint.Value `json:"accumulatedProfit,omitempty"`
|
|
||||||
AccumulatedLoss fixedpoint.Value `json:"accumulatedLoss,omitempty"`
|
|
||||||
AccumulatedVolume fixedpoint.Value `json:"accumulatedVolume,omitempty"`
|
|
||||||
AccumulatedSince int64 `json:"accumulatedSince,omitempty"`
|
|
||||||
|
|
||||||
TodayPnL fixedpoint.Value `json:"todayPnL,omitempty"`
|
|
||||||
TodayNetProfit fixedpoint.Value `json:"todayNetProfit,omitempty"`
|
|
||||||
TodayProfit fixedpoint.Value `json:"todayProfit,omitempty"`
|
|
||||||
TodayLoss fixedpoint.Value `json:"todayLoss,omitempty"`
|
|
||||||
TodaySince int64 `json:"todaySince,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProfitStats) Init(market types.Market) {
|
|
||||||
s.Symbol = market.Symbol
|
|
||||||
s.BaseCurrency = market.BaseCurrency
|
|
||||||
s.QuoteCurrency = market.QuoteCurrency
|
|
||||||
if s.AccumulatedSince == 0 {
|
|
||||||
s.AccumulatedSince = time.Now().Unix()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProfitStats) AddProfit(profit Profit) {
|
|
||||||
s.AccumulatedPnL = s.AccumulatedPnL.Add(profit.Profit)
|
|
||||||
s.AccumulatedNetProfit = s.AccumulatedNetProfit.Add(profit.NetProfit)
|
|
||||||
s.TodayPnL = s.TodayPnL.Add(profit.Profit)
|
|
||||||
s.TodayNetProfit = s.TodayNetProfit.Add(profit.NetProfit)
|
|
||||||
|
|
||||||
if profit.Profit.Sign() < 0 {
|
|
||||||
s.AccumulatedLoss = s.AccumulatedLoss.Add(profit.Profit)
|
|
||||||
s.TodayLoss = s.TodayLoss.Add(profit.Profit)
|
|
||||||
} else if profit.Profit.Sign() > 0 {
|
|
||||||
s.AccumulatedProfit = s.AccumulatedLoss.Add(profit.Profit)
|
|
||||||
s.TodayProfit = s.TodayProfit.Add(profit.Profit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProfitStats) AddTrade(trade types.Trade) {
|
|
||||||
if s.IsOver24Hours() {
|
|
||||||
s.ResetToday()
|
|
||||||
}
|
|
||||||
|
|
||||||
s.AccumulatedVolume = s.AccumulatedVolume.Add(trade.Quantity)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProfitStats) IsOver24Hours() bool {
|
|
||||||
return time.Since(time.Unix(s.TodaySince, 0)) > 24*time.Hour
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProfitStats) ResetToday() {
|
|
||||||
s.TodayPnL = fixedpoint.Zero
|
|
||||||
s.TodayNetProfit = fixedpoint.Zero
|
|
||||||
s.TodayProfit = fixedpoint.Zero
|
|
||||||
s.TodayLoss = fixedpoint.Zero
|
|
||||||
|
|
||||||
var beginningOfTheDay = util.BeginningOfTheDay(time.Now().Local())
|
|
||||||
s.TodaySince = beginningOfTheDay.Unix()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProfitStats) PlainText() string {
|
|
||||||
since := time.Unix(s.AccumulatedSince, 0).Local()
|
|
||||||
return fmt.Sprintf("%s Profit Today\n"+
|
|
||||||
"Profit %s %s\n"+
|
|
||||||
"Net profit %s %s\n"+
|
|
||||||
"Trade Loss %s %s\n"+
|
|
||||||
"Summary:\n"+
|
|
||||||
"Accumulated Profit %s %s\n"+
|
|
||||||
"Accumulated Net Profit %s %s\n"+
|
|
||||||
"Accumulated Trade Loss %s %s\n"+
|
|
||||||
"Since %s",
|
|
||||||
s.Symbol,
|
|
||||||
s.TodayPnL.String(), s.QuoteCurrency,
|
|
||||||
s.TodayNetProfit.String(), s.QuoteCurrency,
|
|
||||||
s.TodayLoss.String(), s.QuoteCurrency,
|
|
||||||
s.AccumulatedPnL.String(), s.QuoteCurrency,
|
|
||||||
s.AccumulatedNetProfit.String(), s.QuoteCurrency,
|
|
||||||
s.AccumulatedLoss.String(), s.QuoteCurrency,
|
|
||||||
since.Format(time.RFC822),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProfitStats) SlackAttachment() slack.Attachment {
|
|
||||||
var color = pnlColor(s.AccumulatedPnL)
|
|
||||||
var title = fmt.Sprintf("%s Accumulated PnL %s %s", s.Symbol, pnlSignString(s.AccumulatedPnL), s.QuoteCurrency)
|
|
||||||
|
|
||||||
since := time.Unix(s.AccumulatedSince, 0).Local()
|
|
||||||
title += " Since " + since.Format(time.RFC822)
|
|
||||||
|
|
||||||
var fields []slack.AttachmentField
|
|
||||||
|
|
||||||
if !s.TodayPnL.IsZero() {
|
|
||||||
fields = append(fields, slack.AttachmentField{
|
|
||||||
Title: "P&L Today",
|
|
||||||
Value: pnlSignString(s.TodayPnL) + " " + s.QuoteCurrency,
|
|
||||||
Short: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if !s.TodayProfit.IsZero() {
|
|
||||||
fields = append(fields, slack.AttachmentField{
|
|
||||||
Title: "Profit Today",
|
|
||||||
Value: pnlSignString(s.TodayProfit) + " " + s.QuoteCurrency,
|
|
||||||
Short: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if !s.TodayNetProfit.IsZero() {
|
|
||||||
fields = append(fields, slack.AttachmentField{
|
|
||||||
Title: "Net Profit Today",
|
|
||||||
Value: pnlSignString(s.TodayNetProfit) + " " + s.QuoteCurrency,
|
|
||||||
Short: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if !s.TodayLoss.IsZero() {
|
|
||||||
fields = append(fields, slack.AttachmentField{
|
|
||||||
Title: "Loss Today",
|
|
||||||
Value: pnlSignString(s.TodayLoss) + " " + s.QuoteCurrency,
|
|
||||||
Short: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if !s.AccumulatedPnL.IsZero() {
|
|
||||||
fields = append(fields, slack.AttachmentField{
|
|
||||||
Title: "Accumulated P&L",
|
|
||||||
Value: pnlSignString(s.AccumulatedPnL) + " " + s.QuoteCurrency,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if !s.AccumulatedProfit.IsZero() {
|
|
||||||
fields = append(fields, slack.AttachmentField{
|
|
||||||
Title: "Accumulated Profit",
|
|
||||||
Value: pnlSignString(s.AccumulatedProfit) + " " + s.QuoteCurrency,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if !s.AccumulatedNetProfit.IsZero() {
|
|
||||||
fields = append(fields, slack.AttachmentField{
|
|
||||||
Title: "Accumulated Net Profit",
|
|
||||||
Value: pnlSignString(s.AccumulatedNetProfit) + " " + s.QuoteCurrency,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if !s.AccumulatedLoss.IsZero() {
|
|
||||||
fields = append(fields, slack.AttachmentField{
|
|
||||||
Title: "Accumulated Loss",
|
|
||||||
Value: pnlSignString(s.AccumulatedLoss) + " " + s.QuoteCurrency,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return slack.Attachment{
|
|
||||||
Color: color,
|
|
||||||
Title: title,
|
|
||||||
Fields: fields,
|
|
||||||
// Footer: "",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -219,7 +219,7 @@ func (trader *Trader) RunSingleExchangeStrategy(ctx context.Context, strategy Si
|
||||||
return errors.New("strategy object is not a struct")
|
return errors.New("strategy object is not a struct")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := trader.injectCommonServices(rs); err != nil {
|
if err := trader.injectCommonServices(strategy) ; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -229,36 +229,30 @@ func (trader *Trader) RunSingleExchangeStrategy(ctx context.Context, strategy Si
|
||||||
|
|
||||||
if symbol, ok := isSymbolBasedStrategy(rs); ok {
|
if symbol, ok := isSymbolBasedStrategy(rs); ok {
|
||||||
log.Infof("found symbol based strategy from %s", rs.Type())
|
log.Infof("found symbol based strategy from %s", rs.Type())
|
||||||
if _, ok := hasField(rs, "Market"); ok {
|
|
||||||
if market, ok := session.Market(symbol); ok {
|
market, ok := session.Market(symbol)
|
||||||
// let's make the market object passed by pointer
|
if !ok {
|
||||||
if err := injectField(rs, "Market", &market, false); err != nil {
|
return fmt.Errorf("market of symbol %s not found", symbol)
|
||||||
return errors.Wrapf(err, "failed to inject Market on %T", strategy)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// StandardIndicatorSet
|
indicatorSet, ok := session.StandardIndicatorSet(symbol)
|
||||||
if _, ok := hasField(rs, "StandardIndicatorSet"); ok {
|
if !ok {
|
||||||
indicatorSet, ok := session.StandardIndicatorSet(symbol)
|
return fmt.Errorf("standardIndicatorSet of symbol %s not found", symbol)
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("standardIndicatorSet of symbol %s not found", symbol)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := injectField(rs, "StandardIndicatorSet", indicatorSet, true); err != nil {
|
|
||||||
return errors.Wrapf(err, "failed to inject StandardIndicatorSet on %T", strategy)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := hasField(rs, "MarketDataStore"); ok {
|
store, ok := session.MarketDataStore(symbol)
|
||||||
store, ok := session.MarketDataStore(symbol)
|
if !ok {
|
||||||
if !ok {
|
return fmt.Errorf("marketDataStore of symbol %s not found", symbol)
|
||||||
return fmt.Errorf("marketDataStore of symbol %s not found", symbol)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if err := injectField(rs, "MarketDataStore", store, true); err != nil {
|
if err := parseStructAndInject(strategy,
|
||||||
return errors.Wrapf(err, "failed to inject MarketDataStore on %T", strategy)
|
market,
|
||||||
}
|
indicatorSet,
|
||||||
|
store,
|
||||||
|
session,
|
||||||
|
session.OrderExecutor,
|
||||||
|
); err != nil {
|
||||||
|
return errors.Wrapf(err, "failed to inject object into %T", strategy)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -339,12 +333,11 @@ func (trader *Trader) Run(ctx context.Context) error {
|
||||||
|
|
||||||
// get the struct element from the struct pointer
|
// get the struct element from the struct pointer
|
||||||
rs = rs.Elem()
|
rs = rs.Elem()
|
||||||
|
|
||||||
if rs.Kind() != reflect.Struct {
|
if rs.Kind() != reflect.Struct {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := trader.injectCommonServices(rs); err != nil {
|
if err := trader.injectCommonServices(strategy) ; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -356,59 +349,33 @@ func (trader *Trader) Run(ctx context.Context) error {
|
||||||
return trader.environment.Connect(ctx)
|
return trader.environment.Connect(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (trader *Trader) injectCommonServices(rs reflect.Value) error {
|
var defaultPersistenceSelector = &PersistenceSelector{
|
||||||
if err := injectField(rs, "Graceful", &trader.Graceful, true); err != nil {
|
StoreID: "default",
|
||||||
return errors.Wrap(err, "failed to inject Graceful")
|
Type: "memory",
|
||||||
}
|
|
||||||
|
|
||||||
if err := injectField(rs, "Logger", &trader.logger, false); err != nil {
|
|
||||||
return errors.Wrap(err, "failed to inject Logger")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := injectField(rs, "Notifiability", &trader.environment.Notifiability, false); err != nil {
|
|
||||||
return errors.Wrap(err, "failed to inject Notifiability")
|
|
||||||
}
|
|
||||||
|
|
||||||
if trader.environment.TradeService != nil {
|
|
||||||
if err := injectField(rs, "TradeService", trader.environment.TradeService, true); err != nil {
|
|
||||||
return errors.Wrap(err, "failed to inject TradeService")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if trader.environment.AccountService != nil {
|
|
||||||
if err := injectField(rs, "AccountService", trader.environment.AccountService, true); err != nil {
|
|
||||||
return errors.Wrap(err, "failed to inject AccountService")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if field, ok := hasField(rs, "Persistence"); ok {
|
|
||||||
if trader.environment.PersistenceServiceFacade == nil {
|
|
||||||
log.Warnf("strategy has Persistence field but persistence service is not defined")
|
|
||||||
} else {
|
|
||||||
if field.IsNil() {
|
|
||||||
field.Set(reflect.ValueOf(&Persistence{
|
|
||||||
PersistenceSelector: &PersistenceSelector{
|
|
||||||
StoreID: "default",
|
|
||||||
Type: "memory",
|
|
||||||
},
|
|
||||||
Facade: trader.environment.PersistenceServiceFacade,
|
|
||||||
}))
|
|
||||||
} else {
|
|
||||||
elem := field.Elem()
|
|
||||||
if elem.Kind() != reflect.Struct {
|
|
||||||
return fmt.Errorf("field Persistence is not a struct element")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := injectField(elem, "Facade", trader.environment.PersistenceServiceFacade, true); err != nil {
|
|
||||||
return errors.Wrap(err, "failed to inject Persistence")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (trader *Trader) injectCommonServices(s interface{}) error {
|
||||||
|
persistenceFacade := trader.environment.PersistenceServiceFacade
|
||||||
|
persistence := &Persistence{
|
||||||
|
PersistenceSelector: defaultPersistenceSelector,
|
||||||
|
Facade: persistenceFacade,
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseStructAndInject(s,
|
||||||
|
&trader.Graceful,
|
||||||
|
&trader.logger,
|
||||||
|
&trader.environment.Notifiability,
|
||||||
|
trader.environment.TradeService,
|
||||||
|
trader.environment.OrderService,
|
||||||
|
trader.environment.DatabaseService,
|
||||||
|
trader.environment.AccountService,
|
||||||
|
trader.environment,
|
||||||
|
persistence,
|
||||||
|
persistenceFacade,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ReportPnL configure and set the PnLReporter with the given notifier
|
// ReportPnL configure and set the PnLReporter with the given notifier
|
||||||
func (trader *Trader) ReportPnL() *PnLReporterManager {
|
func (trader *Trader) ReportPnL() *PnLReporterManager {
|
||||||
return NewPnLReporter(&trader.environment.Notifiability)
|
return NewPnLReporter(&trader.environment.Notifiability)
|
||||||
|
|
2
pkg/bbgo/trader_test.go
Normal file
2
pkg/bbgo/trader_test.go
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
package bbgo
|
||||||
|
|
|
@ -102,7 +102,6 @@ type ExecutionReportEvent struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *ExecutionReportEvent) Order() (*types.Order, error) {
|
func (e *ExecutionReportEvent) Order() (*types.Order, error) {
|
||||||
|
|
||||||
switch e.CurrentExecutionType {
|
switch e.CurrentExecutionType {
|
||||||
case "NEW", "CANCELED", "REJECTED", "EXPIRED":
|
case "NEW", "CANCELED", "REJECTED", "EXPIRED":
|
||||||
case "REPLACED":
|
case "REPLACED":
|
||||||
|
@ -113,7 +112,6 @@ func (e *ExecutionReportEvent) Order() (*types.Order, error) {
|
||||||
|
|
||||||
orderCreationTime := time.Unix(0, e.OrderCreationTime*int64(time.Millisecond))
|
orderCreationTime := time.Unix(0, e.OrderCreationTime*int64(time.Millisecond))
|
||||||
return &types.Order{
|
return &types.Order{
|
||||||
Exchange: types.ExchangeBinance,
|
|
||||||
SubmitOrder: types.SubmitOrder{
|
SubmitOrder: types.SubmitOrder{
|
||||||
Symbol: e.Symbol,
|
Symbol: e.Symbol,
|
||||||
ClientOrderID: e.ClientOrderID,
|
ClientOrderID: e.ClientOrderID,
|
||||||
|
@ -123,10 +121,13 @@ func (e *ExecutionReportEvent) Order() (*types.Order, error) {
|
||||||
Price: e.OrderPrice,
|
Price: e.OrderPrice,
|
||||||
TimeInForce: types.TimeInForce(e.TimeInForce),
|
TimeInForce: types.TimeInForce(e.TimeInForce),
|
||||||
},
|
},
|
||||||
|
Exchange: types.ExchangeBinance,
|
||||||
|
IsWorking: e.IsOnBook,
|
||||||
OrderID: uint64(e.OrderID),
|
OrderID: uint64(e.OrderID),
|
||||||
Status: toGlobalOrderStatus(binance.OrderStatusType(e.CurrentOrderStatus)),
|
Status: toGlobalOrderStatus(binance.OrderStatusType(e.CurrentOrderStatus)),
|
||||||
ExecutedQuantity: e.CumulativeFilledQuantity,
|
ExecutedQuantity: e.CumulativeFilledQuantity,
|
||||||
CreationTime: types.Time(orderCreationTime),
|
CreationTime: types.Time(orderCreationTime),
|
||||||
|
UpdateTime: types.Time(orderCreationTime),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -316,6 +316,11 @@ func convertWebSocketOrderUpdate(u max.OrderUpdate) (*types.Order, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
timeInForce := types.TimeInForceGTC
|
||||||
|
if u.OrderType == max.OrderTypeIOCLimit {
|
||||||
|
timeInForce = types.TimeInForceIOC
|
||||||
|
}
|
||||||
|
|
||||||
return &types.Order{
|
return &types.Order{
|
||||||
SubmitOrder: types.SubmitOrder{
|
SubmitOrder: types.SubmitOrder{
|
||||||
ClientOrderID: u.ClientOID,
|
ClientOrderID: u.ClientOID,
|
||||||
|
@ -325,7 +330,7 @@ func convertWebSocketOrderUpdate(u max.OrderUpdate) (*types.Order, error) {
|
||||||
Quantity: fixedpoint.MustNewFromString(u.Volume),
|
Quantity: fixedpoint.MustNewFromString(u.Volume),
|
||||||
Price: fixedpoint.MustNewFromString(u.Price),
|
Price: fixedpoint.MustNewFromString(u.Price),
|
||||||
StopPrice: fixedpoint.MustNewFromString(u.StopPrice),
|
StopPrice: fixedpoint.MustNewFromString(u.StopPrice),
|
||||||
TimeInForce: "GTC", // MAX only supports GTC
|
TimeInForce: timeInForce, // MAX only supports GTC
|
||||||
GroupID: u.GroupID,
|
GroupID: u.GroupID,
|
||||||
},
|
},
|
||||||
Exchange: types.ExchangeMax,
|
Exchange: types.ExchangeMax,
|
||||||
|
@ -333,5 +338,6 @@ func convertWebSocketOrderUpdate(u max.OrderUpdate) (*types.Order, error) {
|
||||||
Status: toGlobalOrderStatus(u.State, executedVolume, remainingVolume),
|
Status: toGlobalOrderStatus(u.State, executedVolume, remainingVolume),
|
||||||
ExecutedQuantity: executedVolume,
|
ExecutedQuantity: executedVolume,
|
||||||
CreationTime: types.Time(time.Unix(0, u.CreatedAtMs*int64(time.Millisecond))),
|
CreationTime: types.Time(time.Unix(0, u.CreatedAtMs*int64(time.Millisecond))),
|
||||||
|
UpdateTime: types.Time(time.Unix(0, u.CreatedAtMs*int64(time.Millisecond))),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
34
pkg/migrations/mysql/20220304153317_add_profit_table.go
Normal file
34
pkg/migrations/mysql/20220304153317_add_profit_table.go
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/c9s/rockhopper"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
AddMigration(upAddProfitTable, downAddProfitTable)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func upAddProfitTable(ctx context.Context, tx rockhopper.SQLExecutor) (err error) {
|
||||||
|
// This code is executed when the migration is applied.
|
||||||
|
|
||||||
|
_, err = tx.ExecContext(ctx, "CREATE TABLE `profits`\n(\n `gid` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,\n `strategy` VARCHAR(32) NOT NULL,\n `strategy_instance_id` VARCHAR(64) NOT NULL,\n `symbol` VARCHAR(8) NOT NULL,\n -- average_cost is the position average cost\n `average_cost` DECIMAL(16, 8) UNSIGNED NOT NULL,\n -- profit is the pnl (profit and loss)\n `profit` DECIMAL(16, 8) NOT NULL,\n -- net_profit is the pnl (profit and loss)\n `net_profit` DECIMAL(16, 8) NOT NULL,\n -- profit_margin is the pnl (profit and loss)\n `profit_margin` DECIMAL(16, 8) NOT NULL,\n -- net_profit_margin is the pnl (profit and loss)\n `net_profit_margin` DECIMAL(16, 8) NOT NULL,\n `quote_currency` VARCHAR(10) NOT NULL,\n `base_currency` VARCHAR(10) NOT NULL,\n -- -------------------------------------------------------\n -- embedded trade data --\n -- -------------------------------------------------------\n `exchange` VARCHAR(24) NOT NULL DEFAULT '',\n `is_futures` BOOLEAN NOT NULL DEFAULT FALSE,\n `is_margin` BOOLEAN NOT NULL DEFAULT FALSE,\n `is_isolated` BOOLEAN NOT NULL DEFAULT FALSE,\n `trade_id` BIGINT UNSIGNED NOT NULL,\n -- side is the side of the trade that makes profit\n `side` VARCHAR(4) NOT NULL DEFAULT '',\n `is_buyer` BOOLEAN NOT NULL DEFAULT FALSE,\n `is_maker` BOOLEAN NOT NULL DEFAULT FALSE,\n -- price is the price of the trade that makes profit\n `price` DECIMAL(16, 8) UNSIGNED NOT NULL,\n -- quantity is the quantity of the trade that makes profit\n `quantity` DECIMAL(16, 8) UNSIGNED NOT NULL,\n -- quote_quantity is the quote quantity of the trade that makes profit\n `quote_quantity` DECIMAL(16, 8) UNSIGNED NOT NULL,\n `traded_at` DATETIME(3) NOT NULL,\n -- fee\n `fee_in_usd` DECIMAL(16, 8) UNSIGNED,\n `fee` DECIMAL(16, 8) UNSIGNED NOT NULL,\n `fee_currency` VARCHAR(10) NOT NULL,\n PRIMARY KEY (`gid`),\n UNIQUE KEY `trade_id` (`trade_id`)\n);")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func downAddProfitTable(ctx context.Context, tx rockhopper.SQLExecutor) (err error) {
|
||||||
|
// This code is executed when the migration is rolled back.
|
||||||
|
|
||||||
|
_, err = tx.ExecContext(ctx, "DROP TABLE IF EXISTS `profits`;")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
34
pkg/migrations/sqlite3/20220304153309_add_profit_table.go
Normal file
34
pkg/migrations/sqlite3/20220304153309_add_profit_table.go
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
package sqlite3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/c9s/rockhopper"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
AddMigration(upAddProfitTable, downAddProfitTable)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func upAddProfitTable(ctx context.Context, tx rockhopper.SQLExecutor) (err error) {
|
||||||
|
// This code is executed when the migration is applied.
|
||||||
|
|
||||||
|
_, err = tx.ExecContext(ctx, "CREATE TABLE `profits`\n(\n `gid` INTEGER PRIMARY KEY AUTOINCREMENT,\n `strategy` VARCHAR(32) NOT NULL,\n `strategy_instance_id` VARCHAR(64) NOT NULL,\n `symbol` VARCHAR(8) NOT NULL,\n -- average_cost is the position average cost\n `average_cost` DECIMAL(16, 8) NOT NULL,\n -- profit is the pnl (profit and loss)\n `profit` DECIMAL(16, 8) NOT NULL,\n -- net_profit is the pnl (profit and loss)\n `net_profit` DECIMAL(16, 8) NOT NULL,\n -- profit_margin is the pnl (profit and loss)\n `profit_margin` DECIMAL(16, 8) NOT NULL,\n -- net_profit_margin is the pnl (profit and loss)\n `net_profit_margin` DECIMAL(16, 8) NOT NULL,\n `quote_currency` VARCHAR(10) NOT NULL,\n `base_currency` VARCHAR(10) NOT NULL,\n -- -------------------------------------------------------\n -- embedded trade data --\n -- -------------------------------------------------------\n `exchange` VARCHAR(24) NOT NULL DEFAULT '',\n `is_futures` BOOLEAN NOT NULL DEFAULT FALSE,\n `is_margin` BOOLEAN NOT NULL DEFAULT FALSE,\n `is_isolated` BOOLEAN NOT NULL DEFAULT FALSE,\n `trade_id` BIGINT NOT NULL,\n -- side is the side of the trade that makes profit\n `side` VARCHAR(4) NOT NULL DEFAULT '',\n `is_buyer` BOOLEAN NOT NULL DEFAULT FALSE,\n `is_maker` BOOLEAN NOT NULL DEFAULT FALSE,\n -- price is the price of the trade that makes profit\n `price` DECIMAL(16, 8) NOT NULL,\n -- quantity is the quantity of the trade that makes profit\n `quantity` DECIMAL(16, 8) NOT NULL,\n -- trade_amount is the quote quantity of the trade that makes profit\n `quote_quantity` DECIMAL(16, 8) NOT NULL,\n `traded_at` DATETIME(3) NOT NULL,\n -- fee\n `fee_in_usd` DECIMAL(16, 8),\n `fee` DECIMAL(16, 8) NOT NULL,\n `fee_currency` VARCHAR(10) NOT NULL\n);")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func downAddProfitTable(ctx context.Context, tx rockhopper.SQLExecutor) (err error) {
|
||||||
|
// This code is executed when the migration is rolled back.
|
||||||
|
|
||||||
|
_, err = tx.ExecContext(ctx, "DROP TABLE IF EXISTS `profits`;")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
110
pkg/service/profit.go
Normal file
110
pkg/service/profit.go
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProfitService struct {
|
||||||
|
DB *sqlx.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProfitService(db *sqlx.DB) *ProfitService {
|
||||||
|
return &ProfitService{db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProfitService) Load(ctx context.Context, id int64) (*types.Trade, error) {
|
||||||
|
var trade types.Trade
|
||||||
|
|
||||||
|
rows, err := s.DB.NamedQuery("SELECT * FROM trades WHERE id = :id", map[string]interface{}{
|
||||||
|
"id": id,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
if rows.Next() {
|
||||||
|
err = rows.StructScan(&trade)
|
||||||
|
return &trade, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.Wrapf(ErrTradeNotFound, "trade id:%d not found", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProfitService) scanRows(rows *sqlx.Rows) (profits []types.Profit, err error) {
|
||||||
|
for rows.Next() {
|
||||||
|
var profit types.Profit
|
||||||
|
if err := rows.StructScan(&profit); err != nil {
|
||||||
|
return profits, err
|
||||||
|
}
|
||||||
|
|
||||||
|
profits = append(profits, profit)
|
||||||
|
}
|
||||||
|
|
||||||
|
return profits, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProfitService) Insert(profit types.Profit) error {
|
||||||
|
_, err := s.DB.NamedExec(`
|
||||||
|
INSERT INTO profits (
|
||||||
|
strategy,
|
||||||
|
strategy_instance_id,
|
||||||
|
symbol,
|
||||||
|
quote_currency,
|
||||||
|
base_currency,
|
||||||
|
average_cost,
|
||||||
|
profit,
|
||||||
|
net_profit,
|
||||||
|
profit_margin,
|
||||||
|
net_profit_margin,
|
||||||
|
trade_id,
|
||||||
|
price,
|
||||||
|
quantity,
|
||||||
|
quote_quantity,
|
||||||
|
side,
|
||||||
|
is_buyer,
|
||||||
|
is_maker,
|
||||||
|
fee,
|
||||||
|
fee_currency,
|
||||||
|
fee_in_usd,
|
||||||
|
traded_at,
|
||||||
|
exchange,
|
||||||
|
is_margin,
|
||||||
|
is_futures,
|
||||||
|
is_isolated
|
||||||
|
) VALUES (
|
||||||
|
:strategy,
|
||||||
|
:strategy_instance_id,
|
||||||
|
:symbol,
|
||||||
|
:quote_currency,
|
||||||
|
:base_currency,
|
||||||
|
:average_cost,
|
||||||
|
:profit,
|
||||||
|
:net_profit,
|
||||||
|
:profit_margin,
|
||||||
|
:net_profit_margin,
|
||||||
|
:trade_id,
|
||||||
|
:price,
|
||||||
|
:quantity,
|
||||||
|
:quote_quantity,
|
||||||
|
:side,
|
||||||
|
:is_buyer,
|
||||||
|
:is_maker,
|
||||||
|
:fee,
|
||||||
|
:fee_currency,
|
||||||
|
:fee_in_usd,
|
||||||
|
:traded_at,
|
||||||
|
:exchange,
|
||||||
|
:is_margin,
|
||||||
|
:is_futures,
|
||||||
|
:is_isolated
|
||||||
|
)`,
|
||||||
|
profit)
|
||||||
|
return err
|
||||||
|
}
|
41
pkg/service/profit_test.go
Normal file
41
pkg/service/profit_test.go
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestProfitService(t *testing.T) {
|
||||||
|
db, err := prepareDB(t)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
xdb := sqlx.NewDb(db.DB, "sqlite3")
|
||||||
|
service := &ProfitService{DB: xdb}
|
||||||
|
|
||||||
|
err = service.Insert(types.Profit{
|
||||||
|
Symbol: "BTCUSDT",
|
||||||
|
BaseCurrency: "BTC",
|
||||||
|
QuoteCurrency: "USDT",
|
||||||
|
AverageCost: fixedpoint.NewFromFloat(44000),
|
||||||
|
Profit: fixedpoint.NewFromFloat(1.01),
|
||||||
|
NetProfit: fixedpoint.NewFromFloat(0.98),
|
||||||
|
TradeID: 99,
|
||||||
|
Side: types.SideTypeSell,
|
||||||
|
Price: fixedpoint.NewFromFloat(44300),
|
||||||
|
Quantity: fixedpoint.NewFromFloat(0.001),
|
||||||
|
QuoteQuantity: fixedpoint.NewFromFloat(44.0),
|
||||||
|
Exchange: types.ExchangeMax,
|
||||||
|
TradedAt: time.Now(),
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
|
@ -3,12 +3,13 @@ package service
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"github.com/c9s/bbgo/pkg/types"
|
|
||||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_tradeService(t *testing.T) {
|
func Test_tradeService(t *testing.T) {
|
||||||
|
@ -34,6 +35,7 @@ func Test_tradeService(t *testing.T) {
|
||||||
Symbol: "BTCUSDT",
|
Symbol: "BTCUSDT",
|
||||||
Side: "BUY",
|
Side: "BUY",
|
||||||
IsBuyer: true,
|
IsBuyer: true,
|
||||||
|
Time: types.Time(time.Now()),
|
||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
|
|
@ -38,8 +38,8 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
type State struct {
|
type State struct {
|
||||||
Position *types.Position `json:"position,omitempty"`
|
Position *types.Position `json:"position,omitempty"`
|
||||||
ProfitStats bbgo.ProfitStats `json:"profitStats,omitempty"`
|
ProfitStats types.ProfitStats `json:"profitStats,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type BollingerSetting struct {
|
type BollingerSetting struct {
|
||||||
|
@ -52,7 +52,9 @@ type Strategy struct {
|
||||||
*bbgo.Notifiability
|
*bbgo.Notifiability
|
||||||
*bbgo.Persistence
|
*bbgo.Persistence
|
||||||
|
|
||||||
|
Environment *bbgo.Environment
|
||||||
StandardIndicatorSet *bbgo.StandardIndicatorSet
|
StandardIndicatorSet *bbgo.StandardIndicatorSet
|
||||||
|
Market types.Market
|
||||||
|
|
||||||
// Symbol is the market symbol you want to trade
|
// Symbol is the market symbol you want to trade
|
||||||
Symbol string `json:"symbol"`
|
Symbol string `json:"symbol"`
|
||||||
|
@ -141,8 +143,7 @@ type Strategy struct {
|
||||||
bbgo.SmartStops
|
bbgo.SmartStops
|
||||||
|
|
||||||
session *bbgo.ExchangeSession
|
session *bbgo.ExchangeSession
|
||||||
book *types.StreamOrderBook
|
book *types.StreamOrderBook
|
||||||
market types.Market
|
|
||||||
|
|
||||||
state *State
|
state *State
|
||||||
|
|
||||||
|
@ -214,8 +215,8 @@ func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Valu
|
||||||
side = types.SideTypeSell
|
side = types.SideTypeSell
|
||||||
}
|
}
|
||||||
|
|
||||||
if quantity.Compare(s.market.MinQuantity) < 0 {
|
if quantity.Compare(s.Market.MinQuantity) < 0 {
|
||||||
return fmt.Errorf("order quantity %v is too small, less than %v", quantity, s.market.MinQuantity)
|
return fmt.Errorf("order quantity %v is too small, less than %v", quantity, s.Market.MinQuantity)
|
||||||
}
|
}
|
||||||
|
|
||||||
submitOrder := types.SubmitOrder{
|
submitOrder := types.SubmitOrder{
|
||||||
|
@ -223,7 +224,7 @@ func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Valu
|
||||||
Side: side,
|
Side: side,
|
||||||
Type: types.OrderTypeMarket,
|
Type: types.OrderTypeMarket,
|
||||||
Quantity: quantity,
|
Quantity: quantity,
|
||||||
Market: s.market,
|
Market: s.Market,
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Notify("Submitting %s %s order to close position by %v", s.Symbol, side.String(), percentage, submitOrder)
|
s.Notify("Submitting %s %s order to close position by %v", s.Symbol, side.String(), percentage, submitOrder)
|
||||||
|
@ -264,13 +265,13 @@ func (s *Strategy) LoadState() error {
|
||||||
|
|
||||||
// if position is nil, we need to allocate a new position for calculation
|
// if position is nil, we need to allocate a new position for calculation
|
||||||
if s.state.Position == nil {
|
if s.state.Position == nil {
|
||||||
s.state.Position = types.NewPositionFromMarket(s.market)
|
s.state.Position = types.NewPositionFromMarket(s.Market)
|
||||||
}
|
}
|
||||||
|
|
||||||
// init profit states
|
// init profit states
|
||||||
s.state.ProfitStats.Symbol = s.market.Symbol
|
s.state.ProfitStats.Symbol = s.Market.Symbol
|
||||||
s.state.ProfitStats.BaseCurrency = s.market.BaseCurrency
|
s.state.ProfitStats.BaseCurrency = s.Market.BaseCurrency
|
||||||
s.state.ProfitStats.QuoteCurrency = s.market.QuoteCurrency
|
s.state.ProfitStats.QuoteCurrency = s.Market.QuoteCurrency
|
||||||
if s.state.ProfitStats.AccumulatedSince == 0 {
|
if s.state.ProfitStats.AccumulatedSince == 0 {
|
||||||
s.state.ProfitStats.AccumulatedSince = time.Now().Unix()
|
s.state.ProfitStats.AccumulatedSince = time.Now().Unix()
|
||||||
}
|
}
|
||||||
|
@ -323,7 +324,7 @@ func (s *Strategy) placeOrders(ctx context.Context, orderExecutor bbgo.OrderExec
|
||||||
Type: types.OrderTypeLimitMaker,
|
Type: types.OrderTypeLimitMaker,
|
||||||
Quantity: sellQuantity,
|
Quantity: sellQuantity,
|
||||||
Price: askPrice,
|
Price: askPrice,
|
||||||
Market: s.market,
|
Market: s.Market,
|
||||||
GroupID: s.groupID,
|
GroupID: s.groupID,
|
||||||
}
|
}
|
||||||
buyOrder := types.SubmitOrder{
|
buyOrder := types.SubmitOrder{
|
||||||
|
@ -332,14 +333,14 @@ func (s *Strategy) placeOrders(ctx context.Context, orderExecutor bbgo.OrderExec
|
||||||
Type: types.OrderTypeLimitMaker,
|
Type: types.OrderTypeLimitMaker,
|
||||||
Quantity: buyQuantity,
|
Quantity: buyQuantity,
|
||||||
Price: bidPrice,
|
Price: bidPrice,
|
||||||
Market: s.market,
|
Market: s.Market,
|
||||||
GroupID: s.groupID,
|
GroupID: s.groupID,
|
||||||
}
|
}
|
||||||
|
|
||||||
var submitOrders []types.SubmitOrder
|
var submitOrders []types.SubmitOrder
|
||||||
|
|
||||||
baseBalance, hasBaseBalance := balances[s.market.BaseCurrency]
|
baseBalance, hasBaseBalance := balances[s.Market.BaseCurrency]
|
||||||
quoteBalance, hasQuoteBalance := balances[s.market.QuoteCurrency]
|
quoteBalance, hasQuoteBalance := balances[s.Market.QuoteCurrency]
|
||||||
|
|
||||||
downBand := s.defaultBoll.LastDownBand()
|
downBand := s.defaultBoll.LastDownBand()
|
||||||
upBand := s.defaultBoll.LastUpBand()
|
upBand := s.defaultBoll.LastUpBand()
|
||||||
|
@ -421,12 +422,12 @@ func (s *Strategy) placeOrders(ctx context.Context, orderExecutor bbgo.OrderExec
|
||||||
|
|
||||||
case UpTrend:
|
case UpTrend:
|
||||||
skew := s.UptrendSkew
|
skew := s.UptrendSkew
|
||||||
buyOrder.Quantity = fixedpoint.Max(s.market.MinQuantity, sellOrder.Quantity.Mul(skew))
|
buyOrder.Quantity = fixedpoint.Max(s.Market.MinQuantity, sellOrder.Quantity.Mul(skew))
|
||||||
|
|
||||||
case DownTrend:
|
case DownTrend:
|
||||||
skew := s.DowntrendSkew
|
skew := s.DowntrendSkew
|
||||||
ratio := fixedpoint.One.Div(skew)
|
ratio := fixedpoint.One.Div(skew)
|
||||||
sellOrder.Quantity = fixedpoint.Max(s.market.MinQuantity, buyOrder.Quantity.Mul(ratio))
|
sellOrder.Quantity = fixedpoint.Max(s.Market.MinQuantity, buyOrder.Quantity.Mul(ratio))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -506,12 +507,12 @@ func (s *Strategy) detectPriceTrend(inc *indicator.BOLL, price float64) PriceTre
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Strategy) adjustOrderQuantity(submitOrder types.SubmitOrder) types.SubmitOrder {
|
func (s *Strategy) adjustOrderQuantity(submitOrder types.SubmitOrder) types.SubmitOrder {
|
||||||
if submitOrder.Quantity.Mul(submitOrder.Price).Compare(s.market.MinNotional) < 0 {
|
if submitOrder.Quantity.Mul(submitOrder.Price).Compare(s.Market.MinNotional) < 0 {
|
||||||
submitOrder.Quantity = bbgo.AdjustFloatQuantityByMinAmount(submitOrder.Quantity, submitOrder.Price, s.market.MinNotional.Mul(notionModifier))
|
submitOrder.Quantity = bbgo.AdjustFloatQuantityByMinAmount(submitOrder.Quantity, submitOrder.Price, s.Market.MinNotional.Mul(notionModifier))
|
||||||
}
|
}
|
||||||
|
|
||||||
if submitOrder.Quantity.Compare(s.market.MinQuantity) < 0 {
|
if submitOrder.Quantity.Compare(s.Market.MinQuantity) < 0 {
|
||||||
submitOrder.Quantity = fixedpoint.Max(submitOrder.Quantity, s.market.MinQuantity)
|
submitOrder.Quantity = fixedpoint.Max(submitOrder.Quantity, s.Market.MinQuantity)
|
||||||
}
|
}
|
||||||
|
|
||||||
return submitOrder
|
return submitOrder
|
||||||
|
@ -540,13 +541,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
||||||
|
|
||||||
// initial required information
|
// initial required information
|
||||||
s.session = session
|
s.session = session
|
||||||
|
|
||||||
market, ok := session.Market(s.Symbol)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("market %s not found", s.Symbol)
|
|
||||||
}
|
|
||||||
s.market = market
|
|
||||||
|
|
||||||
s.neutralBoll = s.StandardIndicatorSet.BOLL(s.NeutralBollinger.IntervalWindow, s.NeutralBollinger.BandWidth)
|
s.neutralBoll = s.StandardIndicatorSet.BOLL(s.NeutralBollinger.IntervalWindow, s.NeutralBollinger.BandWidth)
|
||||||
s.defaultBoll = s.StandardIndicatorSet.BOLL(s.DefaultBollinger.IntervalWindow, s.DefaultBollinger.BandWidth)
|
s.defaultBoll = s.StandardIndicatorSet.BOLL(s.DefaultBollinger.IntervalWindow, s.DefaultBollinger.BandWidth)
|
||||||
|
|
||||||
|
@ -571,17 +565,11 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
||||||
s.tradeCollector = bbgo.NewTradeCollector(s.Symbol, s.state.Position, s.orderStore)
|
s.tradeCollector = bbgo.NewTradeCollector(s.Symbol, s.state.Position, s.orderStore)
|
||||||
s.tradeCollector.OnProfit(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) {
|
s.tradeCollector.OnProfit(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) {
|
||||||
log.Infof("generated profit: %v", profit)
|
log.Infof("generated profit: %v", profit)
|
||||||
p := bbgo.Profit{
|
p := s.state.Position.NewProfit(trade, profit, netProfit)
|
||||||
Symbol: s.Symbol,
|
p.Strategy = ID
|
||||||
Profit: profit,
|
p.StrategyInstanceID = instanceID
|
||||||
NetProfit: netProfit,
|
s.Environment.RecordProfit(p)
|
||||||
TradeAmount: trade.QuoteQuantity,
|
|
||||||
ProfitMargin: profit.Div(trade.QuoteQuantity),
|
|
||||||
NetProfitMargin: netProfit.Div(trade.QuoteQuantity),
|
|
||||||
QuoteCurrency: s.state.Position.QuoteCurrency,
|
|
||||||
BaseCurrency: s.state.Position.BaseCurrency,
|
|
||||||
Time: trade.Time.Time(),
|
|
||||||
}
|
|
||||||
s.state.ProfitStats.AddProfit(p)
|
s.state.ProfitStats.AddProfit(p)
|
||||||
s.Notify(&p)
|
s.Notify(&p)
|
||||||
s.Notify(&s.state.ProfitStats)
|
s.Notify(&s.state.ProfitStats)
|
||||||
|
|
|
@ -41,7 +41,7 @@ type State struct {
|
||||||
// [source Order ID] -> arbitrage order
|
// [source Order ID] -> arbitrage order
|
||||||
ArbitrageOrders map[uint64]types.Order `json:"arbitrageOrders"`
|
ArbitrageOrders map[uint64]types.Order `json:"arbitrageOrders"`
|
||||||
|
|
||||||
ProfitStats bbgo.ProfitStats `json:"profitStats,omitempty"`
|
ProfitStats types.ProfitStats `json:"profitStats,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Strategy struct {
|
type Strategy struct {
|
||||||
|
|
|
@ -16,7 +16,7 @@ func init() {
|
||||||
|
|
||||||
type Strategy struct {
|
type Strategy struct {
|
||||||
// The notification system will be injected into the strategy automatically.
|
// The notification system will be injected into the strategy automatically.
|
||||||
bbgo.Notifiability
|
*bbgo.Notifiability
|
||||||
|
|
||||||
// These fields will be filled from the config file (it translates YAML to JSON)
|
// These fields will be filled from the config file (it translates YAML to JSON)
|
||||||
Symbol string `json:"symbol"`
|
Symbol string `json:"symbol"`
|
||||||
|
|
|
@ -3,7 +3,6 @@ package xmaker
|
||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/c9s/bbgo/pkg/bbgo"
|
|
||||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
"github.com/c9s/bbgo/pkg/types"
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
)
|
)
|
||||||
|
@ -15,7 +14,7 @@ type State struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProfitStats struct {
|
type ProfitStats struct {
|
||||||
bbgo.ProfitStats
|
types.ProfitStats
|
||||||
lock sync.Mutex
|
lock sync.Mutex
|
||||||
|
|
||||||
MakerExchange types.ExchangeName `json:"makerExchange"`
|
MakerExchange types.ExchangeName `json:"makerExchange"`
|
||||||
|
|
|
@ -763,16 +763,16 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
s.tradeCollector.OnProfit(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) {
|
s.tradeCollector.OnProfit(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) {
|
||||||
p := bbgo.Profit{
|
p := types.Profit{
|
||||||
Symbol: s.Symbol,
|
Symbol: s.Symbol,
|
||||||
Profit: profit,
|
Profit: profit,
|
||||||
NetProfit: netProfit,
|
NetProfit: netProfit,
|
||||||
TradeAmount: trade.QuoteQuantity,
|
QuoteQuantity: trade.QuoteQuantity,
|
||||||
ProfitMargin: profit.Div(trade.QuoteQuantity),
|
ProfitMargin: profit.Div(trade.QuoteQuantity),
|
||||||
NetProfitMargin: netProfit.Div(trade.QuoteQuantity),
|
NetProfitMargin: netProfit.Div(trade.QuoteQuantity),
|
||||||
QuoteCurrency: s.state.Position.QuoteCurrency,
|
QuoteCurrency: s.state.Position.QuoteCurrency,
|
||||||
BaseCurrency: s.state.Position.BaseCurrency,
|
BaseCurrency: s.state.Position.BaseCurrency,
|
||||||
Time: trade.Time.Time(),
|
TradedAt: trade.Time.Time(),
|
||||||
}
|
}
|
||||||
s.state.ProfitStats.AddProfit(p)
|
s.state.ProfitStats.AddProfit(p)
|
||||||
s.Notify(&p)
|
s.Notify(&p)
|
||||||
|
|
|
@ -53,6 +53,38 @@ type Position struct {
|
||||||
sync.Mutex
|
sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewProfit generates the profit object from the current position
|
||||||
|
func (p *Position) NewProfit(trade Trade, profit, netProfit fixedpoint.Value) Profit {
|
||||||
|
return Profit{
|
||||||
|
Symbol: p.Symbol,
|
||||||
|
QuoteCurrency: p.QuoteCurrency,
|
||||||
|
BaseCurrency: p.BaseCurrency,
|
||||||
|
AverageCost: p.AverageCost,
|
||||||
|
// profit related fields
|
||||||
|
Profit: profit,
|
||||||
|
NetProfit: netProfit,
|
||||||
|
ProfitMargin: profit.Div(trade.QuoteQuantity),
|
||||||
|
NetProfitMargin: netProfit.Div(trade.QuoteQuantity),
|
||||||
|
// trade related fields
|
||||||
|
TradeID: trade.ID,
|
||||||
|
Side: trade.Side,
|
||||||
|
IsBuyer: trade.IsBuyer,
|
||||||
|
IsMaker: trade.IsMaker,
|
||||||
|
Price: trade.Price,
|
||||||
|
Quantity: trade.Quantity,
|
||||||
|
QuoteQuantity: trade.QuoteQuantity,
|
||||||
|
// FeeInUSD: 0,
|
||||||
|
Fee: trade.Fee,
|
||||||
|
FeeCurrency: trade.FeeCurrency,
|
||||||
|
|
||||||
|
Exchange: trade.Exchange,
|
||||||
|
IsMargin: trade.IsMargin,
|
||||||
|
IsFutures: trade.IsFutures,
|
||||||
|
IsIsolated: trade.IsIsolated,
|
||||||
|
TradedAt: trade.Time.Time(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Position) NewClosePositionOrder(percentage fixedpoint.Value) *SubmitOrder {
|
func (p *Position) NewClosePositionOrder(percentage fixedpoint.Value) *SubmitOrder {
|
||||||
base := p.GetBase()
|
base := p.GetBase()
|
||||||
quantity := base.Mul(percentage)
|
quantity := base.Mul(percentage)
|
||||||
|
|
364
pkg/types/profit.go
Normal file
364
pkg/types/profit.go
Normal file
|
@ -0,0 +1,364 @@
|
||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/slack-go/slack"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
|
"github.com/c9s/bbgo/pkg/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Profit struct stores the PnL information
|
||||||
|
type Profit struct {
|
||||||
|
// --- position related fields
|
||||||
|
// -------------------------------------------
|
||||||
|
// Symbol is the symbol of the position
|
||||||
|
Symbol string `json:"symbol"`
|
||||||
|
QuoteCurrency string `json:"quoteCurrency" db:"quote_currency"`
|
||||||
|
BaseCurrency string `json:"baseCurrency" db:"base_currency"`
|
||||||
|
AverageCost fixedpoint.Value `json:"averageCost" db:"average_cost"`
|
||||||
|
|
||||||
|
// profit related fields
|
||||||
|
// -------------------------------------------
|
||||||
|
// Profit is the profit of this trade made. negative profit means loss.
|
||||||
|
Profit fixedpoint.Value `json:"profit" db:"profit"`
|
||||||
|
|
||||||
|
// NetProfit is (profit - trading fee)
|
||||||
|
NetProfit fixedpoint.Value `json:"netProfit" db:"net_profit"`
|
||||||
|
|
||||||
|
// ProfitMargin is a percentage of the profit and the capital amount
|
||||||
|
ProfitMargin fixedpoint.Value `json:"profitMargin" db:"profit_margin"`
|
||||||
|
|
||||||
|
// NetProfitMargin is a percentage of the net profit and the capital amount
|
||||||
|
NetProfitMargin fixedpoint.Value `json:"netProfitMargin" db:"net_profit_margin"`
|
||||||
|
|
||||||
|
// trade related fields
|
||||||
|
// --------------------------------------------
|
||||||
|
// TradeID is the exchange trade id of that trade
|
||||||
|
TradeID uint64 `json:"tradeID" db:"trade_id"`
|
||||||
|
Side SideType `json:"side" db:"side"`
|
||||||
|
IsBuyer bool `json:"isBuyer" db:"is_buyer"`
|
||||||
|
IsMaker bool `json:"isMaker" db:"is_maker"`
|
||||||
|
Price fixedpoint.Value `json:"price" db:"price"`
|
||||||
|
Quantity fixedpoint.Value `json:"quantity" db:"quantity"`
|
||||||
|
QuoteQuantity fixedpoint.Value `json:"quoteQuantity" db:"quote_quantity"`
|
||||||
|
|
||||||
|
// FeeInUSD is the summed fee of this profit,
|
||||||
|
// you will need to convert the trade fee into USD since the fee currencies can be different.
|
||||||
|
FeeInUSD fixedpoint.Value `json:"feeInUSD" db:"fee_in_usd"`
|
||||||
|
Fee fixedpoint.Value `json:"fee" db:"fee"`
|
||||||
|
FeeCurrency string `json:"feeCurrency" db:"fee_currency"`
|
||||||
|
Exchange ExchangeName `json:"exchange" db:"exchange"`
|
||||||
|
IsMargin bool `json:"isMargin" db:"is_margin"`
|
||||||
|
IsFutures bool `json:"isFutures" db:"is_futures"`
|
||||||
|
IsIsolated bool `json:"isIsolated" db:"is_isolated"`
|
||||||
|
TradedAt time.Time `json:"tradedAt" db:"traded_at"`
|
||||||
|
|
||||||
|
// strategy related fields
|
||||||
|
Strategy string `json:"strategy" db:"strategy"`
|
||||||
|
StrategyInstanceID string `json:"strategyInstanceID" db:"strategy_instance_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Profit) SlackAttachment() slack.Attachment {
|
||||||
|
var color = pnlColor(p.Profit)
|
||||||
|
var title = fmt.Sprintf("%s PnL ", p.Symbol)
|
||||||
|
title += pnlEmojiMargin(p.Profit, p.ProfitMargin, defaultPnlLevelResolution) + " "
|
||||||
|
title += pnlSignString(p.Profit) + " " + p.QuoteCurrency
|
||||||
|
|
||||||
|
var fields []slack.AttachmentField
|
||||||
|
|
||||||
|
if !p.NetProfit.IsZero() {
|
||||||
|
fields = append(fields, slack.AttachmentField{
|
||||||
|
Title: "Net Profit",
|
||||||
|
Value: pnlSignString(p.NetProfit) + " " + p.QuoteCurrency,
|
||||||
|
Short: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if !p.ProfitMargin.IsZero() {
|
||||||
|
fields = append(fields, slack.AttachmentField{
|
||||||
|
Title: "Profit Margin",
|
||||||
|
Value: p.ProfitMargin.Percentage(),
|
||||||
|
Short: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if !p.NetProfitMargin.IsZero() {
|
||||||
|
fields = append(fields, slack.AttachmentField{
|
||||||
|
Title: "Net Profit Margin",
|
||||||
|
Value: p.NetProfitMargin.Percentage(),
|
||||||
|
Short: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if !p.QuoteQuantity.IsZero() {
|
||||||
|
fields = append(fields, slack.AttachmentField{
|
||||||
|
Title: "Trade Amount",
|
||||||
|
Value: p.QuoteQuantity.String() + " " + p.QuoteCurrency,
|
||||||
|
Short: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if !p.FeeInUSD.IsZero() {
|
||||||
|
fields = append(fields, slack.AttachmentField{
|
||||||
|
Title: "Fee In USD",
|
||||||
|
Value: p.FeeInUSD.String() + " USD",
|
||||||
|
Short: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(p.Strategy) != 0 {
|
||||||
|
fields = append(fields, slack.AttachmentField{
|
||||||
|
Title: "Strategy",
|
||||||
|
Value: p.Strategy,
|
||||||
|
Short: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return slack.Attachment{
|
||||||
|
Color: color,
|
||||||
|
Title: title,
|
||||||
|
Fields: fields,
|
||||||
|
// Footer: "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Profit) PlainText() string {
|
||||||
|
var emoji string
|
||||||
|
if !p.ProfitMargin.IsZero() {
|
||||||
|
emoji = pnlEmojiMargin(p.Profit, p.ProfitMargin, defaultPnlLevelResolution)
|
||||||
|
} else {
|
||||||
|
emoji = pnlEmojiSimple(p.Profit)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s trade profit %s %s %s (%s), net profit =~ %s %s (%s)",
|
||||||
|
p.Symbol,
|
||||||
|
emoji,
|
||||||
|
p.Profit.String(), p.QuoteCurrency,
|
||||||
|
p.ProfitMargin.Percentage(),
|
||||||
|
p.NetProfit.String(), p.QuoteCurrency,
|
||||||
|
p.NetProfitMargin.Percentage(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var lossEmoji = "🔥"
|
||||||
|
var profitEmoji = "💰"
|
||||||
|
var defaultPnlLevelResolution = fixedpoint.NewFromFloat(0.001)
|
||||||
|
|
||||||
|
func pnlColor(pnl fixedpoint.Value) string {
|
||||||
|
if pnl.Sign() > 0 {
|
||||||
|
return GreenColor
|
||||||
|
}
|
||||||
|
return RedColor
|
||||||
|
}
|
||||||
|
|
||||||
|
func pnlSignString(pnl fixedpoint.Value) string {
|
||||||
|
if pnl.Sign() > 0 {
|
||||||
|
return "+" + pnl.String()
|
||||||
|
}
|
||||||
|
return pnl.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func pnlEmojiSimple(pnl fixedpoint.Value) string {
|
||||||
|
if pnl.Sign() < 0 {
|
||||||
|
return lossEmoji
|
||||||
|
}
|
||||||
|
|
||||||
|
if pnl.IsZero() {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return profitEmoji
|
||||||
|
}
|
||||||
|
|
||||||
|
func pnlEmojiMargin(pnl, margin, resolution fixedpoint.Value) (out string) {
|
||||||
|
if margin.IsZero() {
|
||||||
|
return pnlEmojiSimple(pnl)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pnl.Sign() < 0 {
|
||||||
|
out = lossEmoji
|
||||||
|
level := (margin.Neg()).Div(resolution).Int()
|
||||||
|
for i := 1; i < level; i++ {
|
||||||
|
out += lossEmoji
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
if pnl.IsZero() {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
out = profitEmoji
|
||||||
|
level := margin.Div(resolution).Int()
|
||||||
|
for i := 1; i < level; i++ {
|
||||||
|
out += profitEmoji
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProfitStats struct {
|
||||||
|
Symbol string `json:"symbol"`
|
||||||
|
QuoteCurrency string `json:"quoteCurrency"`
|
||||||
|
BaseCurrency string `json:"baseCurrency"`
|
||||||
|
|
||||||
|
AccumulatedPnL fixedpoint.Value `json:"accumulatedPnL,omitempty"`
|
||||||
|
AccumulatedNetProfit fixedpoint.Value `json:"accumulatedNetProfit,omitempty"`
|
||||||
|
AccumulatedProfit fixedpoint.Value `json:"accumulatedProfit,omitempty"`
|
||||||
|
AccumulatedLoss fixedpoint.Value `json:"accumulatedLoss,omitempty"`
|
||||||
|
AccumulatedVolume fixedpoint.Value `json:"accumulatedVolume,omitempty"`
|
||||||
|
AccumulatedSince int64 `json:"accumulatedSince,omitempty"`
|
||||||
|
|
||||||
|
TodayPnL fixedpoint.Value `json:"todayPnL,omitempty"`
|
||||||
|
TodayNetProfit fixedpoint.Value `json:"todayNetProfit,omitempty"`
|
||||||
|
TodayProfit fixedpoint.Value `json:"todayProfit,omitempty"`
|
||||||
|
TodayLoss fixedpoint.Value `json:"todayLoss,omitempty"`
|
||||||
|
TodaySince int64 `json:"todaySince,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProfitStats) Init(market Market) {
|
||||||
|
s.Symbol = market.Symbol
|
||||||
|
s.BaseCurrency = market.BaseCurrency
|
||||||
|
s.QuoteCurrency = market.QuoteCurrency
|
||||||
|
if s.AccumulatedSince == 0 {
|
||||||
|
s.AccumulatedSince = time.Now().Unix()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProfitStats) AddProfit(profit Profit) {
|
||||||
|
s.AccumulatedPnL = s.AccumulatedPnL.Add(profit.Profit)
|
||||||
|
s.AccumulatedNetProfit = s.AccumulatedNetProfit.Add(profit.NetProfit)
|
||||||
|
s.TodayPnL = s.TodayPnL.Add(profit.Profit)
|
||||||
|
s.TodayNetProfit = s.TodayNetProfit.Add(profit.NetProfit)
|
||||||
|
|
||||||
|
if profit.Profit.Sign() < 0 {
|
||||||
|
s.AccumulatedLoss = s.AccumulatedLoss.Add(profit.Profit)
|
||||||
|
s.TodayLoss = s.TodayLoss.Add(profit.Profit)
|
||||||
|
} else if profit.Profit.Sign() > 0 {
|
||||||
|
s.AccumulatedProfit = s.AccumulatedLoss.Add(profit.Profit)
|
||||||
|
s.TodayProfit = s.TodayProfit.Add(profit.Profit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProfitStats) AddTrade(trade Trade) {
|
||||||
|
if s.IsOver24Hours() {
|
||||||
|
s.ResetToday()
|
||||||
|
}
|
||||||
|
|
||||||
|
s.AccumulatedVolume = s.AccumulatedVolume.Add(trade.Quantity)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProfitStats) IsOver24Hours() bool {
|
||||||
|
return time.Since(time.Unix(s.TodaySince, 0)) > 24*time.Hour
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProfitStats) ResetToday() {
|
||||||
|
s.TodayPnL = fixedpoint.Zero
|
||||||
|
s.TodayNetProfit = fixedpoint.Zero
|
||||||
|
s.TodayProfit = fixedpoint.Zero
|
||||||
|
s.TodayLoss = fixedpoint.Zero
|
||||||
|
|
||||||
|
var beginningOfTheDay = util.BeginningOfTheDay(time.Now().Local())
|
||||||
|
s.TodaySince = beginningOfTheDay.Unix()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProfitStats) PlainText() string {
|
||||||
|
since := time.Unix(s.AccumulatedSince, 0).Local()
|
||||||
|
return fmt.Sprintf("%s Profit Today\n"+
|
||||||
|
"Profit %s %s\n"+
|
||||||
|
"Net profit %s %s\n"+
|
||||||
|
"Trade Loss %s %s\n"+
|
||||||
|
"Summary:\n"+
|
||||||
|
"Accumulated Profit %s %s\n"+
|
||||||
|
"Accumulated Net Profit %s %s\n"+
|
||||||
|
"Accumulated Trade Loss %s %s\n"+
|
||||||
|
"Since %s",
|
||||||
|
s.Symbol,
|
||||||
|
s.TodayPnL.String(), s.QuoteCurrency,
|
||||||
|
s.TodayNetProfit.String(), s.QuoteCurrency,
|
||||||
|
s.TodayLoss.String(), s.QuoteCurrency,
|
||||||
|
s.AccumulatedPnL.String(), s.QuoteCurrency,
|
||||||
|
s.AccumulatedNetProfit.String(), s.QuoteCurrency,
|
||||||
|
s.AccumulatedLoss.String(), s.QuoteCurrency,
|
||||||
|
since.Format(time.RFC822),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProfitStats) SlackAttachment() slack.Attachment {
|
||||||
|
var color = pnlColor(s.AccumulatedPnL)
|
||||||
|
var title = fmt.Sprintf("%s Accumulated PnL %s %s", s.Symbol, pnlSignString(s.AccumulatedPnL), s.QuoteCurrency)
|
||||||
|
|
||||||
|
since := time.Unix(s.AccumulatedSince, 0).Local()
|
||||||
|
title += " Since " + since.Format(time.RFC822)
|
||||||
|
|
||||||
|
var fields []slack.AttachmentField
|
||||||
|
|
||||||
|
if !s.TodayPnL.IsZero() {
|
||||||
|
fields = append(fields, slack.AttachmentField{
|
||||||
|
Title: "P&L Today",
|
||||||
|
Value: pnlSignString(s.TodayPnL) + " " + s.QuoteCurrency,
|
||||||
|
Short: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.TodayProfit.IsZero() {
|
||||||
|
fields = append(fields, slack.AttachmentField{
|
||||||
|
Title: "Profit Today",
|
||||||
|
Value: pnlSignString(s.TodayProfit) + " " + s.QuoteCurrency,
|
||||||
|
Short: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.TodayNetProfit.IsZero() {
|
||||||
|
fields = append(fields, slack.AttachmentField{
|
||||||
|
Title: "Net Profit Today",
|
||||||
|
Value: pnlSignString(s.TodayNetProfit) + " " + s.QuoteCurrency,
|
||||||
|
Short: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.TodayLoss.IsZero() {
|
||||||
|
fields = append(fields, slack.AttachmentField{
|
||||||
|
Title: "Loss Today",
|
||||||
|
Value: pnlSignString(s.TodayLoss) + " " + s.QuoteCurrency,
|
||||||
|
Short: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.AccumulatedPnL.IsZero() {
|
||||||
|
fields = append(fields, slack.AttachmentField{
|
||||||
|
Title: "Accumulated P&L",
|
||||||
|
Value: pnlSignString(s.AccumulatedPnL) + " " + s.QuoteCurrency,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.AccumulatedProfit.IsZero() {
|
||||||
|
fields = append(fields, slack.AttachmentField{
|
||||||
|
Title: "Accumulated Profit",
|
||||||
|
Value: pnlSignString(s.AccumulatedProfit) + " " + s.QuoteCurrency,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.AccumulatedNetProfit.IsZero() {
|
||||||
|
fields = append(fields, slack.AttachmentField{
|
||||||
|
Title: "Accumulated Net Profit",
|
||||||
|
Value: pnlSignString(s.AccumulatedNetProfit) + " " + s.QuoteCurrency,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.AccumulatedLoss.IsZero() {
|
||||||
|
fields = append(fields, slack.AttachmentField{
|
||||||
|
Title: "Accumulated Loss",
|
||||||
|
Value: pnlSignString(s.AccumulatedLoss) + " " + s.QuoteCurrency,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return slack.Attachment{
|
||||||
|
Color: color,
|
||||||
|
Title: title,
|
||||||
|
Fields: fields,
|
||||||
|
// Footer: "",
|
||||||
|
}
|
||||||
|
}
|
|
@ -176,6 +176,9 @@ func NewTimeFromUnix(sec int64, nsec int64) Time {
|
||||||
// Value implements the driver.Valuer interface
|
// Value implements the driver.Valuer interface
|
||||||
// see http://jmoiron.net/blog/built-in-interfaces/
|
// see http://jmoiron.net/blog/built-in-interfaces/
|
||||||
func (t Time) Value() (driver.Value, error) {
|
func (t Time) Value() (driver.Value, error) {
|
||||||
|
if time.Time(t) == (time.Time{}) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
return time.Time(t), nil
|
return time.Time(t), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,15 @@
|
||||||
|
# vim:filetype=yaml:
|
||||||
|
# you can copy this file to rockhopper_mysql_local.yaml to have your modification
|
||||||
---
|
---
|
||||||
driver: mysql
|
driver: mysql
|
||||||
dialect: mysql
|
dialect: mysql
|
||||||
dsn: "root@tcp(localhost:3306)/bbgo_dev?parseTime=true"
|
|
||||||
# dsn: "root@tcp(localhost:3306)/bbgo_backtest?parseTime=true"
|
# unix socket connection to mysql with password
|
||||||
|
# dsn: "root:123123@unix(/opt/local/var/run/mysql57/mysqld.sock)/bbgo_dev?parseTime=true"
|
||||||
|
|
||||||
|
# tcp connection to mysql with password
|
||||||
|
# dsn: "root:123123@tcp(localhost:3306)/bbgo_dev?parseTime=true"
|
||||||
|
|
||||||
|
# tcp connection to mysql without password
|
||||||
|
# dsn: "root@tcp(localhost:3306)/bbgo_dev?parseTime=true"
|
||||||
migrationsDir: migrations/mysql
|
migrationsDir: migrations/mysql
|
||||||
|
|
Loading…
Reference in New Issue
Block a user