mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-26 00:35:15 +00:00
Merge pull request #584 from c9s/add-nav-columns
feature: record nav values into db
This commit is contained in:
commit
8cf9218dce
23
.github/workflows/go.yml
vendored
23
.github/workflows/go.yml
vendored
|
@ -15,8 +15,13 @@ jobs:
|
|||
matrix:
|
||||
redis-version:
|
||||
- 6.2
|
||||
env:
|
||||
MYSQL_DATABASE: bbgo
|
||||
MYSQL_USER: "root"
|
||||
MYSQL_PASSWORD: "root"
|
||||
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: actions/cache@v2
|
||||
|
@ -28,7 +33,12 @@ jobs:
|
|||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
|
||||
- name: Setup redis
|
||||
- name: Set up MySQL
|
||||
run: |
|
||||
sudo /etc/init.d/mysql start
|
||||
mysql -e 'CREATE DATABASE ${{ env.MYSQL_DATABASE }};' -u${{ env.MYSQL_USER }} -p${{ env.MYSQL_PASSWORD }}
|
||||
|
||||
- name: Set up redis
|
||||
uses: shogo82148/actions-setup-redis@v1
|
||||
with:
|
||||
redis-version: ${{ matrix.redis-version }}
|
||||
|
@ -39,6 +49,17 @@ jobs:
|
|||
with:
|
||||
go-version: 1.17
|
||||
|
||||
- name: Install Migration Tool
|
||||
run: go install github.com/c9s/rockhopper/cmd/rockhopper@v1.2.1
|
||||
|
||||
- name: Test Migration SQL Files For MySQL
|
||||
run: |
|
||||
rockhopper --config rockhopper_mysql.yaml up
|
||||
|
||||
- name: Test Migration SQL Files For SQLite
|
||||
run: |
|
||||
rockhopper --config rockhopper_sqlite.yaml up
|
||||
|
||||
- name: Build
|
||||
run: go build -v ./cmd/bbgo
|
||||
|
||||
|
|
47
config/xnav.yaml
Normal file
47
config/xnav.yaml
Normal file
|
@ -0,0 +1,47 @@
|
|||
---
|
||||
notifications:
|
||||
slack:
|
||||
defaultChannel: "dev-bbgo"
|
||||
errorChannel: "bbgo-error"
|
||||
|
||||
# if you want to route channel by symbol
|
||||
symbolChannels:
|
||||
"^BTC": "btc"
|
||||
"^ETH": "eth"
|
||||
|
||||
# if you want to route channel by exchange session
|
||||
sessionChannels:
|
||||
max: "bbgo-max"
|
||||
binance: "bbgo-binance"
|
||||
|
||||
# routing rules
|
||||
routing:
|
||||
trade: "$symbol"
|
||||
order: "$slient"
|
||||
submitOrder: "$slient"
|
||||
pnL: "bbgo-pnl"
|
||||
|
||||
sessions:
|
||||
max:
|
||||
exchange: max
|
||||
envVarPrefix: max
|
||||
|
||||
binance:
|
||||
exchange: binance
|
||||
envVarPrefix: binance
|
||||
|
||||
persistence:
|
||||
json:
|
||||
directory: var/data
|
||||
redis:
|
||||
host: 127.0.0.1
|
||||
port: 6379
|
||||
db: 0
|
||||
|
||||
crossExchangeStrategies:
|
||||
|
||||
- xnav:
|
||||
interval: 1h
|
||||
reportOnStart: true
|
||||
ignoreDusts: true
|
||||
|
|
@ -16,7 +16,7 @@ CREATE TABLE nav_history_details
|
|||
-- +end
|
||||
-- +begin
|
||||
CREATE INDEX idx_nav_history_details
|
||||
on nav_history_details(time, currency, exchange);
|
||||
on nav_history_details (time, currency, exchange);
|
||||
-- +end
|
||||
|
||||
-- +down
|
||||
|
|
27
migrations/mysql/20220503144849_add_margin_info_to_nav.sql
Normal file
27
migrations/mysql/20220503144849_add_margin_info_to_nav.sql
Normal file
|
@ -0,0 +1,27 @@
|
|||
-- +up
|
||||
-- +begin
|
||||
ALTER TABLE `nav_history_details`
|
||||
ADD COLUMN `session` VARCHAR(30) NOT NULL,
|
||||
ADD COLUMN `is_margin` BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
ADD COLUMN `is_isolated` BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
ADD COLUMN `isolated_symbol` VARCHAR(30) NOT NULL DEFAULT '',
|
||||
ADD COLUMN `net_asset` DECIMAL(32, 8) UNSIGNED DEFAULT 0.00000000 NOT NULL,
|
||||
ADD COLUMN `borrowed` DECIMAL(32, 8) UNSIGNED DEFAULT 0.00000000 NOT NULL,
|
||||
ADD COLUMN `price_in_usd` DECIMAL(32, 8) UNSIGNED DEFAULT 0.00000000 NOT NULL
|
||||
;
|
||||
-- +end
|
||||
|
||||
|
||||
-- +down
|
||||
|
||||
-- +begin
|
||||
ALTER TABLE `nav_history_details`
|
||||
DROP COLUMN `session`,
|
||||
DROP COLUMN `net_asset`,
|
||||
DROP COLUMN `borrowed`,
|
||||
DROP COLUMN `price_in_usd`,
|
||||
DROP COLUMN `is_margin`,
|
||||
DROP COLUMN `is_isolated`,
|
||||
DROP COLUMN `isolated_symbol`
|
||||
;
|
||||
-- +end
|
12
migrations/sqlite3/20220503144849_add_margin_info_to_nav.sql
Normal file
12
migrations/sqlite3/20220503144849_add_margin_info_to_nav.sql
Normal file
|
@ -0,0 +1,12 @@
|
|||
-- +up
|
||||
ALTER TABLE `nav_history_details` ADD COLUMN `session` VARCHAR(50) NOT NULL;
|
||||
ALTER TABLE `nav_history_details` ADD COLUMN `borrowed` DECIMAL UNSIGNED DEFAULT 0.00000000 NOT NULL;
|
||||
ALTER TABLE `nav_history_details` ADD COLUMN `net_asset` DECIMAL UNSIGNED DEFAULT 0.00000000 NOT NULL;
|
||||
ALTER TABLE `nav_history_details` ADD COLUMN `price_in_usd` DECIMAL UNSIGNED DEFAULT 0.00000000 NOT NULL;
|
||||
ALTER TABLE `nav_history_details` ADD COLUMN `is_margin` BOOL DEFAULT FALSE NOT NULL;
|
||||
ALTER TABLE `nav_history_details` ADD COLUMN `is_isolated` BOOL DEFAULT FALSE NOT NULL;
|
||||
ALTER TABLE `nav_history_details` ADD COLUMN `isolated_symbol` VARCHAR(30) DEFAULT '' NOT NULL;
|
||||
|
||||
-- +down
|
||||
-- we can not rollback alter table change in sqlite
|
||||
SELECT 1;
|
|
@ -649,6 +649,29 @@ func (environ *Environment) Sync(ctx context.Context, userConfig ...*Config) err
|
|||
return nil
|
||||
}
|
||||
|
||||
func (environ *Environment) RecordAsset(t time.Time, session *ExchangeSession, assets types.AssetMap) {
|
||||
// skip for back-test
|
||||
if environ.BacktestService != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if environ.DatabaseService == nil || environ.AccountService == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := environ.AccountService.InsertAsset(
|
||||
t,
|
||||
session.Name,
|
||||
session.ExchangeName,
|
||||
session.SubAccount,
|
||||
session.Margin,
|
||||
session.IsolatedMargin,
|
||||
session.IsolatedMarginSymbol,
|
||||
assets); err != nil {
|
||||
log.WithError(err).Errorf("can not insert asset record")
|
||||
}
|
||||
}
|
||||
|
||||
func (environ *Environment) RecordPosition(position *types.Position, trade types.Trade, profit *types.Profit) {
|
||||
// skip for back-test
|
||||
if environ.BacktestService != nil {
|
||||
|
@ -679,17 +702,6 @@ func (environ *Environment) RecordPosition(position *types.Position, trade types
|
|||
log.WithError(err).Errorf("can not insert position record")
|
||||
}
|
||||
}
|
||||
|
||||
// if:
|
||||
// 1) we are not using sync
|
||||
// 2) and not sync-ing trades from the user data stream
|
||||
if environ.TradeService != nil && (environ.syncConfig == nil ||
|
||||
(environ.syncConfig.UserDataStream == nil) ||
|
||||
(environ.syncConfig.UserDataStream != nil && !environ.syncConfig.UserDataStream.Trades)) {
|
||||
if err := environ.TradeService.Insert(trade); err != nil {
|
||||
log.WithError(err).Errorf("can not insert trade record: %+v", trade)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (environ *Environment) RecordProfit(profit types.Profit) {
|
||||
|
|
|
@ -7,6 +7,8 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/slack-go/slack"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/cache"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
@ -646,21 +648,18 @@ func (session *ExchangeSession) FormatOrder(order types.SubmitOrder) (types.Subm
|
|||
return order, nil
|
||||
}
|
||||
|
||||
func (session *ExchangeSession) UpdatePrices(ctx context.Context) (err error) {
|
||||
func (session *ExchangeSession) UpdatePrices(ctx context.Context, currencies []string, fiat string) (err error) {
|
||||
if session.lastPriceUpdatedAt.After(time.Now().Add(-time.Hour)) {
|
||||
return nil
|
||||
}
|
||||
|
||||
balances := session.GetAccount().Balances()
|
||||
|
||||
var symbols []string
|
||||
for _, b := range balances {
|
||||
symbols = append(symbols, b.Currency+"USDT")
|
||||
symbols = append(symbols, "USDT"+b.Currency)
|
||||
for _, c := range currencies {
|
||||
symbols = append(symbols, c+fiat) // BTC/USDT
|
||||
symbols = append(symbols, fiat+c) // USDT/TWD
|
||||
}
|
||||
|
||||
tickers, err := session.Exchange.QueryTickers(ctx, symbols...)
|
||||
|
||||
if err != nil || len(tickers) == 0 {
|
||||
return err
|
||||
}
|
||||
|
@ -668,12 +667,7 @@ func (session *ExchangeSession) UpdatePrices(ctx context.Context) (err error) {
|
|||
var lastTime time.Time
|
||||
for k, v := range tickers {
|
||||
// for {Crypto}/USDT markets
|
||||
if strings.HasSuffix(k, "USDT") {
|
||||
session.lastPrices[k] = v.Last
|
||||
} else if strings.HasPrefix(k, "USDT") {
|
||||
session.lastPrices[k] = fixedpoint.One.Div(v.Last)
|
||||
}
|
||||
|
||||
if v.Time.After(lastTime) {
|
||||
lastTime = v.Time
|
||||
}
|
||||
|
@ -929,3 +923,16 @@ func (session *ExchangeSession) bindConnectionStatusNotification(stream types.St
|
|||
session.Notifiability.Notify("session %s %s stream connected", session.Name, streamName)
|
||||
})
|
||||
}
|
||||
|
||||
func (session *ExchangeSession) SlackAttachment() slack.Attachment {
|
||||
var fields []slack.AttachmentField
|
||||
var footerIcon = types.ExchangeFooterIcon(session.ExchangeName)
|
||||
return slack.Attachment{
|
||||
// Pretext: "",
|
||||
// Text: text,
|
||||
Title: session.Name,
|
||||
Fields: fields,
|
||||
FooterIcon: footerIcon,
|
||||
Footer: util.Render("update time {{ . }}", time.Now().Format(time.RFC822)),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -242,7 +242,7 @@ func (s *Slack) listen(ctx context.Context) {
|
|||
innerEvent := eventsAPIEvent.InnerEvent
|
||||
switch ev := innerEvent.Data.(type) {
|
||||
case *slackevents.MessageEvent:
|
||||
log.Infof("message event: text=%+v", ev.Text)
|
||||
log.Debugf("message event: text=%+v", ev.Text)
|
||||
|
||||
if len(ev.BotID) > 0 {
|
||||
log.Debug("skip bot message")
|
||||
|
|
|
@ -19,7 +19,7 @@ func upAddNavHistoryDetails(ctx context.Context, tx rockhopper.SQLExecutor) (err
|
|||
return err
|
||||
}
|
||||
|
||||
_, err = tx.ExecContext(ctx, "CREATE INDEX idx_nav_history_details\n on nav_history_details(time, currency, exchange);")
|
||||
_, err = tx.ExecContext(ctx, "CREATE INDEX idx_nav_history_details\n on nav_history_details (time, currency, exchange);")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
package mysql
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/c9s/rockhopper"
|
||||
)
|
||||
|
||||
func init() {
|
||||
AddMigration(upAddMarginInfoToNav, downAddMarginInfoToNav)
|
||||
|
||||
}
|
||||
|
||||
func upAddMarginInfoToNav(ctx context.Context, tx rockhopper.SQLExecutor) (err error) {
|
||||
// This code is executed when the migration is applied.
|
||||
|
||||
_, err = tx.ExecContext(ctx, "ALTER TABLE `nav_history_details`\n ADD COLUMN `session` VARCHAR(30) NOT NULL,\n ADD COLUMN `is_margin` BOOLEAN NOT NULL DEFAULT FALSE,\n ADD COLUMN `is_isolated` BOOLEAN NOT NULL DEFAULT FALSE,\n ADD COLUMN `isolated_symbol` VARCHAR(30) NOT NULL DEFAULT '',\n ADD COLUMN `net_asset` DECIMAL(32, 8) UNSIGNED DEFAULT 0.00000000 NOT NULL,\n ADD COLUMN `borrowed` DECIMAL(32, 8) UNSIGNED DEFAULT 0.00000000 NOT NULL,\n ADD COLUMN `price_in_usd` DECIMAL(32, 8) UNSIGNED DEFAULT 0.00000000 NOT NULL\n;")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func downAddMarginInfoToNav(ctx context.Context, tx rockhopper.SQLExecutor) (err error) {
|
||||
// This code is executed when the migration is rolled back.
|
||||
|
||||
_, err = tx.ExecContext(ctx, "ALTER TABLE `nav_history_details`\n DROP COLUMN `session`,\n DROP COLUMN `net_asset`,\n DROP COLUMN `borrowed`,\n DROP COLUMN `price_in_usd`,\n DROP COLUMN `is_margin`,\n DROP COLUMN `is_isolated`,\n DROP COLUMN `isolated_symbol`\n;")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
package sqlite3
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/c9s/rockhopper"
|
||||
)
|
||||
|
||||
func init() {
|
||||
AddMigration(upAddMarginInfoToNav, downAddMarginInfoToNav)
|
||||
|
||||
}
|
||||
|
||||
func upAddMarginInfoToNav(ctx context.Context, tx rockhopper.SQLExecutor) (err error) {
|
||||
// This code is executed when the migration is applied.
|
||||
|
||||
_, err = tx.ExecContext(ctx, "ALTER TABLE `nav_history_details` ADD COLUMN `session` VARCHAR(50) NOT NULL;")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.ExecContext(ctx, "ALTER TABLE `nav_history_details` ADD COLUMN `borrowed` DECIMAL UNSIGNED DEFAULT 0.00000000 NOT NULL;")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.ExecContext(ctx, "ALTER TABLE `nav_history_details` ADD COLUMN `net_asset` DECIMAL UNSIGNED DEFAULT 0.00000000 NOT NULL;")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.ExecContext(ctx, "ALTER TABLE `nav_history_details` ADD COLUMN `price_in_usd` DECIMAL UNSIGNED DEFAULT 0.00000000 NOT NULL;")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.ExecContext(ctx, "ALTER TABLE `nav_history_details` ADD COLUMN `is_margin` BOOL DEFAULT FALSE NOT NULL;")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.ExecContext(ctx, "ALTER TABLE `nav_history_details` ADD COLUMN `is_isolated` BOOL DEFAULT FALSE NOT NULL;")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.ExecContext(ctx, "ALTER TABLE `nav_history_details` ADD COLUMN `isolated_symbol` VARCHAR(30) DEFAULT '' NOT NULL;")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func downAddMarginInfoToNav(ctx context.Context, tx rockhopper.SQLExecutor) (err error) {
|
||||
// This code is executed when the migration is rolled back.
|
||||
|
||||
_, err = tx.ExecContext(ctx, "SELECT 1;")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
|
@ -442,7 +442,7 @@ func genFakeAssets() types.AssetMap {
|
|||
"DOTUSDT": fixedpoint.NewFromFloat(20.0),
|
||||
"SANDUSDT": fixedpoint.NewFromFloat(0.13),
|
||||
"MAXUSDT": fixedpoint.NewFromFloat(0.122),
|
||||
})
|
||||
}, time.Now())
|
||||
for currency, asset := range assets {
|
||||
totalAssets[currency] = asset
|
||||
}
|
||||
|
@ -460,13 +460,13 @@ func (s *Server) listAssets(c *gin.Context) {
|
|||
for _, session := range s.Environ.Sessions() {
|
||||
balances := session.GetAccount().Balances()
|
||||
|
||||
if err := session.UpdatePrices(c); err != nil {
|
||||
if err := session.UpdatePrices(c, balances.Currencies(), "USDT"); err != nil {
|
||||
logrus.WithError(err).Error("price update failed")
|
||||
c.Status(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
assets := balances.Assets(session.LastPrices())
|
||||
assets := balances.Assets(session.LastPrices(), time.Now())
|
||||
|
||||
for currency, asset := range assets {
|
||||
totalAssets[currency] = asset
|
||||
|
|
|
@ -15,20 +15,48 @@ func NewAccountService(db *sqlx.DB) *AccountService {
|
|||
return &AccountService{DB: db}
|
||||
}
|
||||
|
||||
func (s *AccountService) InsertAsset(time time.Time, name types.ExchangeName, account string, assets types.AssetMap) error {
|
||||
|
||||
// TODO: should pass bbgo.ExchangeSession to this function, but that might cause cyclic import
|
||||
func (s *AccountService) InsertAsset(time time.Time, session string, name types.ExchangeName, account string, isMargin bool, isIsolatedMargin bool, isolatedMarginSymbol string, assets types.AssetMap) error {
|
||||
if s.DB == nil {
|
||||
//skip db insert when no db connection setting.
|
||||
// skip db insert when no db connection setting.
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
for _, v := range assets {
|
||||
_, _err := s.DB.Exec(`
|
||||
insert into nav_history_details ( exchange, subaccount, time, currency, balance_in_usd, balance_in_btc,
|
||||
balance,available,locked)
|
||||
values (?,?,?,?,?,?,?,?,?);
|
||||
`, name, account, time, v.Currency, v.InUSD, v.InBTC, v.Total, v.Available, v.Locked)
|
||||
INSERT INTO nav_history_details (
|
||||
session,
|
||||
exchange,
|
||||
subaccount,
|
||||
time,
|
||||
currency,
|
||||
balance_in_usd,
|
||||
balance_in_btc,
|
||||
balance,
|
||||
available,
|
||||
locked,
|
||||
borrowed,
|
||||
net_asset,
|
||||
price_in_usd,
|
||||
is_margin, is_isolated, isolated_symbol)
|
||||
values (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);`,
|
||||
session,
|
||||
name,
|
||||
account,
|
||||
time,
|
||||
v.Currency,
|
||||
v.InUSD,
|
||||
v.InBTC,
|
||||
v.Total,
|
||||
v.Available,
|
||||
v.Locked,
|
||||
v.Borrowed,
|
||||
v.NetAsset,
|
||||
v.PriceInUSD,
|
||||
isMargin,
|
||||
isIsolatedMargin,
|
||||
isolatedMarginSymbol)
|
||||
|
||||
err = multierr.Append(err, _err) // successful request
|
||||
|
||||
|
|
41
pkg/service/account_test.go
Normal file
41
pkg/service/account_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 TestAccountService(t *testing.T) {
|
||||
db, err := prepareDB(t)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
defer db.Close()
|
||||
|
||||
xdb := sqlx.NewDb(db.DB, "sqlite3")
|
||||
service := &AccountService{DB: xdb}
|
||||
|
||||
t1 := time.Now()
|
||||
err = service.InsertAsset(t1, "binance", types.ExchangeBinance, "main", false, false, "", types.AssetMap{
|
||||
"BTC": types.Asset{
|
||||
Currency: "BTC",
|
||||
Total: fixedpoint.MustNewFromString("1.0"),
|
||||
InUSD: fixedpoint.MustNewFromString("10.0"),
|
||||
InBTC: fixedpoint.MustNewFromString("0.0001"),
|
||||
Time: t1,
|
||||
Locked: fixedpoint.MustNewFromString("0"),
|
||||
Available: fixedpoint.MustNewFromString("1.0"),
|
||||
Borrowed: fixedpoint.MustNewFromString("0"),
|
||||
NetAsset: fixedpoint.MustNewFromString("1"),
|
||||
PriceInUSD: fixedpoint.MustNewFromString("44870"),
|
||||
},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
}
|
|
@ -8,7 +8,7 @@ import (
|
|||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/slack-go/slack"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/bbgo"
|
||||
|
@ -21,6 +21,8 @@ const ID = "xnav"
|
|||
|
||||
const stateKey = "state-v1"
|
||||
|
||||
var log = logrus.WithField("strategy", ID)
|
||||
|
||||
func init() {
|
||||
bbgo.RegisterStrategy(ID, &Strategy{})
|
||||
}
|
||||
|
@ -59,8 +61,9 @@ type Strategy struct {
|
|||
Notifiability *bbgo.Notifiability
|
||||
*bbgo.Graceful
|
||||
*bbgo.Persistence
|
||||
*bbgo.Environment
|
||||
|
||||
Interval types.Duration `json:"interval"`
|
||||
Interval types.Interval `json:"interval"`
|
||||
ReportOnStart bool `json:"reportOnStart"`
|
||||
IgnoreDusts bool `json:"ignoreDusts"`
|
||||
state *State
|
||||
|
@ -77,44 +80,41 @@ func (s *Strategy) CrossSubscribe(sessions map[string]*bbgo.ExchangeSession) {}
|
|||
func (s *Strategy) recordNetAssetValue(ctx context.Context, sessions map[string]*bbgo.ExchangeSession) {
|
||||
totalAssets := types.AssetMap{}
|
||||
totalBalances := types.BalanceMap{}
|
||||
totalBorrowed := map[string]fixedpoint.Value{}
|
||||
lastPrices := map[string]fixedpoint.Value{}
|
||||
for _, session := range sessions {
|
||||
if err := session.UpdateAccount(ctx) ; err != nil {
|
||||
allPrices := map[string]fixedpoint.Value{}
|
||||
sessionBalances := map[string]types.BalanceMap{}
|
||||
priceTime := time.Now()
|
||||
|
||||
// iterate the sessions and record them
|
||||
for sessionName, session := range sessions {
|
||||
// update the account balances and the margin information
|
||||
if err := session.UpdateAccount(ctx); err != nil {
|
||||
log.WithError(err).Errorf("can not update account")
|
||||
return
|
||||
}
|
||||
|
||||
account := session.GetAccount()
|
||||
balances := account.Balances()
|
||||
if err := session.UpdatePrices(ctx); err != nil {
|
||||
if err := session.UpdatePrices(ctx, balances.Currencies(), "USDT"); err != nil {
|
||||
log.WithError(err).Error("price update failed")
|
||||
return
|
||||
}
|
||||
|
||||
for _, b := range balances {
|
||||
if tb, ok := totalBalances[b.Currency]; ok {
|
||||
tb.Available = tb.Available.Add(b.Available)
|
||||
tb.Locked = tb.Locked.Add(b.Locked)
|
||||
totalBalances[b.Currency] = tb
|
||||
|
||||
if b.Borrowed.Sign() > 0 {
|
||||
totalBorrowed[b.Currency] = totalBorrowed[b.Currency].Add(b.Borrowed)
|
||||
}
|
||||
} else {
|
||||
totalBalances[b.Currency] = b
|
||||
totalBorrowed[b.Currency] = b.Borrowed
|
||||
}
|
||||
}
|
||||
sessionBalances[sessionName] = balances
|
||||
totalBalances = totalBalances.Add(balances)
|
||||
|
||||
prices := session.LastPrices()
|
||||
assets := balances.Assets(prices, priceTime)
|
||||
|
||||
// merge prices
|
||||
for m, p := range prices {
|
||||
lastPrices[m] = p
|
||||
}
|
||||
allPrices[m] = p
|
||||
}
|
||||
|
||||
assets := totalBalances.Assets(lastPrices)
|
||||
for currency, asset := range assets {
|
||||
s.Environment.RecordAsset(priceTime, session, assets)
|
||||
}
|
||||
|
||||
allAssets := totalBalances.Assets(allPrices, priceTime)
|
||||
for currency, asset := range allAssets {
|
||||
// calculated if it's dust only when InUSD (usd value) is defined.
|
||||
if s.IgnoreDusts && !asset.InUSD.IsZero() && asset.InUSD.Compare(Ten) < 0 {
|
||||
continue
|
||||
|
@ -123,6 +123,8 @@ func (s *Strategy) recordNetAssetValue(ctx context.Context, sessions map[string]
|
|||
totalAssets[currency] = asset
|
||||
}
|
||||
|
||||
s.Environment.RecordAsset(priceTime, &bbgo.ExchangeSession{Name: "ALL"}, totalAssets)
|
||||
|
||||
s.Notifiability.Notify(totalAssets)
|
||||
|
||||
if s.state != nil {
|
||||
|
@ -171,8 +173,8 @@ func (s *Strategy) LoadState() error {
|
|||
}
|
||||
|
||||
func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, sessions map[string]*bbgo.ExchangeSession) error {
|
||||
if s.Interval == 0 {
|
||||
return errors.New("interval can not be zero")
|
||||
if s.Interval == "" {
|
||||
return errors.New("interval can not be empty")
|
||||
}
|
||||
|
||||
if err := s.LoadState(); err != nil {
|
||||
|
@ -189,6 +191,15 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se
|
|||
s.recordNetAssetValue(ctx, sessions)
|
||||
}
|
||||
|
||||
if s.Environment.BacktestService != nil {
|
||||
log.Warnf("xnav does not support backtesting")
|
||||
}
|
||||
|
||||
// TODO: if interval is supported, we can use kline as the ticker
|
||||
if _, ok := types.SupportedIntervals[s.Interval] ; ok {
|
||||
|
||||
}
|
||||
|
||||
go func() {
|
||||
ticker := time.NewTicker(util.MillisecondsJitter(s.Interval.Duration(), 1000))
|
||||
defer ticker.Stop()
|
||||
|
|
|
@ -56,11 +56,14 @@ func (b Balance) String() (o string) {
|
|||
type Asset struct {
|
||||
Currency string `json:"currency" db:"currency"`
|
||||
Total fixedpoint.Value `json:"total" db:"total"`
|
||||
InUSD fixedpoint.Value `json:"inUSD" db:"inUSD"`
|
||||
InBTC fixedpoint.Value `json:"inBTC" db:"inBTC"`
|
||||
InUSD fixedpoint.Value `json:"inUSD" db:"in_usd"`
|
||||
InBTC fixedpoint.Value `json:"inBTC" db:"in_btc"`
|
||||
Time time.Time `json:"time" db:"time"`
|
||||
Locked fixedpoint.Value `json:"lock" db:"lock" `
|
||||
Available fixedpoint.Value `json:"available" db:"available"`
|
||||
Borrowed fixedpoint.Value `json:"borrowed" db:"borrowed"`
|
||||
NetAsset fixedpoint.Value `json:"netAsset" db:"net_asset"`
|
||||
PriceInUSD fixedpoint.Value `json:"priceInUSD" db:"price_in_usd"`
|
||||
}
|
||||
|
||||
type AssetMap map[string]Asset
|
||||
|
@ -161,6 +164,27 @@ type MarginAssetMap map[string]MarginUserAsset
|
|||
type FuturesAssetMap map[string]FuturesUserAsset
|
||||
type FuturesPositionMap map[string]FuturesPosition
|
||||
|
||||
func (m BalanceMap) Currencies() (currencies []string) {
|
||||
for _, b := range m {
|
||||
currencies = append(currencies, b.Currency)
|
||||
}
|
||||
return currencies
|
||||
}
|
||||
|
||||
func (m BalanceMap) Add(bm BalanceMap) BalanceMap {
|
||||
var total = BalanceMap{}
|
||||
for _, b := range bm {
|
||||
tb := total[b.Currency]
|
||||
tb.Available = tb.Available.Add(b.Available)
|
||||
tb.Locked = tb.Locked.Add(b.Locked)
|
||||
tb.Borrowed = tb.Borrowed.Add(b.Borrowed)
|
||||
tb.NetAsset = tb.NetAsset.Add(b.NetAsset)
|
||||
tb.Interest = tb.Interest.Add(b.Interest)
|
||||
total[b.Currency] = tb
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
func (m BalanceMap) String() string {
|
||||
var ss []string
|
||||
for _, b := range m {
|
||||
|
@ -178,41 +202,49 @@ func (m BalanceMap) Copy() (d BalanceMap) {
|
|||
return d
|
||||
}
|
||||
|
||||
func (m BalanceMap) Assets(prices map[string]fixedpoint.Value) AssetMap {
|
||||
// Assets converts balances into assets with the given prices
|
||||
func (m BalanceMap) Assets(prices map[string]fixedpoint.Value, priceTime time.Time) AssetMap {
|
||||
assets := make(AssetMap)
|
||||
|
||||
now := time.Now()
|
||||
btcusdt, hasBtcPrice := prices["BTCUSDT"]
|
||||
for currency, b := range m {
|
||||
if b.Locked.IsZero() && b.Available.IsZero() {
|
||||
if b.Locked.IsZero() && b.Available.IsZero() && b.Borrowed.IsZero() {
|
||||
continue
|
||||
}
|
||||
|
||||
asset := Asset{
|
||||
Currency: currency,
|
||||
Total: b.Available.Add(b.Locked),
|
||||
Time: now,
|
||||
Time: priceTime,
|
||||
Locked: b.Locked,
|
||||
Available: b.Available,
|
||||
Borrowed: b.Borrowed,
|
||||
NetAsset: b.NetAsset,
|
||||
}
|
||||
|
||||
btcusdt, hasBtcPrice := prices["BTCUSDT"]
|
||||
|
||||
usdMarkets := []string{currency + "USDT", currency + "USDC", currency + "USD", "USDT" + currency}
|
||||
|
||||
for _, market := range usdMarkets {
|
||||
if val, ok := prices[market]; ok {
|
||||
usdPrice, ok := prices[market]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// this includes USDT, USD, USDC and so on
|
||||
if strings.HasPrefix(market, "USD") {
|
||||
asset.InUSD = asset.Total.Div(val)
|
||||
if !asset.Total.IsZero() {
|
||||
asset.InUSD = asset.Total.Div(usdPrice)
|
||||
}
|
||||
asset.PriceInUSD = usdPrice
|
||||
} else {
|
||||
asset.InUSD = asset.Total.Mul(val)
|
||||
if !asset.Total.IsZero() {
|
||||
asset.InUSD = asset.Total.Mul(usdPrice)
|
||||
}
|
||||
asset.PriceInUSD = fixedpoint.One.Div(usdPrice)
|
||||
}
|
||||
|
||||
if hasBtcPrice {
|
||||
if hasBtcPrice && !asset.InUSD.IsZero() {
|
||||
asset.InBTC = asset.InUSD.Div(btcusdt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assets[currency] = asset
|
||||
}
|
||||
|
@ -280,6 +312,8 @@ type Account struct {
|
|||
balances BalanceMap
|
||||
}
|
||||
|
||||
|
||||
|
||||
type FuturesAccountInfo struct {
|
||||
// Futures fields
|
||||
Assets FuturesAssetMap `json:"assets"`
|
||||
|
|
|
@ -8,6 +8,19 @@ import (
|
|||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
)
|
||||
|
||||
func TestBalanceMap_Add(t *testing.T) {
|
||||
var bm = BalanceMap{}
|
||||
var bm2 = bm.Add(BalanceMap{
|
||||
"BTC": Balance{
|
||||
Currency: "BTC",
|
||||
Available: fixedpoint.MustNewFromString("10.0"),
|
||||
Locked: fixedpoint.MustNewFromString("0"),
|
||||
NetAsset: fixedpoint.MustNewFromString("10.0"),
|
||||
},
|
||||
})
|
||||
assert.Len(t, bm2, 1)
|
||||
}
|
||||
|
||||
func TestAccountLockAndUnlock(t *testing.T) {
|
||||
a := NewAccount()
|
||||
a.AddBalance("USDT", fixedpoint.NewFromInt(1000))
|
||||
|
|
20
pkg/types/exchange_icon.go
Normal file
20
pkg/types/exchange_icon.go
Normal file
|
@ -0,0 +1,20 @@
|
|||
package types
|
||||
|
||||
func ExchangeFooterIcon(exName ExchangeName) string {
|
||||
footerIcon := ""
|
||||
|
||||
switch exName {
|
||||
case ExchangeBinance:
|
||||
footerIcon = "https://bin.bnbstatic.com/static/images/common/favicon.ico"
|
||||
case ExchangeMax:
|
||||
footerIcon = "https://max.maicoin.com/favicon-16x16.png"
|
||||
case ExchangeFTX:
|
||||
footerIcon = "https://ftx.com/favicon.ico?v=2"
|
||||
case ExchangeOKEx:
|
||||
footerIcon = "https://static.okex.com/cdn/assets/imgs/MjAxODg/D91A7323087D31A588E0D2A379DD7747.png"
|
||||
case ExchangeKucoin:
|
||||
footerIcon = "https://assets.staticimg.com/cms/media/7AV75b9jzr9S8H3eNuOuoqj8PwdUjaDQGKGczGqTS.png"
|
||||
}
|
||||
|
||||
return footerIcon
|
||||
}
|
|
@ -300,7 +300,7 @@ func (o Order) SlackAttachment() slack.Attachment {
|
|||
Short: true,
|
||||
})
|
||||
|
||||
footerIcon := exchangeFooterIcon(o.Exchange)
|
||||
footerIcon := ExchangeFooterIcon(o.Exchange)
|
||||
|
||||
return slack.Attachment{
|
||||
Color: SideToColorName(o.Side),
|
||||
|
|
|
@ -151,25 +151,6 @@ func (trade Trade) PlainText() string {
|
|||
|
||||
var slackTradeTextTemplate = ":handshake: Trade {{ .Symbol }} {{ .Side }} {{ .Quantity }} @ {{ .Price }}"
|
||||
|
||||
func exchangeFooterIcon(exName ExchangeName) string {
|
||||
footerIcon := ""
|
||||
|
||||
switch exName {
|
||||
case ExchangeBinance:
|
||||
footerIcon = "https://bin.bnbstatic.com/static/images/common/favicon.ico"
|
||||
case ExchangeMax:
|
||||
footerIcon = "https://max.maicoin.com/favicon-16x16.png"
|
||||
case ExchangeFTX:
|
||||
footerIcon = "https://ftx.com/favicon.ico?v=2"
|
||||
case ExchangeOKEx:
|
||||
footerIcon = "https://static.okex.com/cdn/assets/imgs/MjAxODg/D91A7323087D31A588E0D2A379DD7747.png"
|
||||
case ExchangeKucoin:
|
||||
footerIcon = "https://assets.staticimg.com/cms/media/7AV75b9jzr9S8H3eNuOuoqj8PwdUjaDQGKGczGqTS.png"
|
||||
}
|
||||
|
||||
return footerIcon
|
||||
}
|
||||
|
||||
func (trade Trade) SlackAttachment() slack.Attachment {
|
||||
var color = "#DC143C"
|
||||
|
||||
|
@ -179,7 +160,7 @@ func (trade Trade) SlackAttachment() slack.Attachment {
|
|||
|
||||
liquidity := trade.Liquidity()
|
||||
text := util.Render(slackTradeTextTemplate, trade)
|
||||
footerIcon := exchangeFooterIcon(trade.Exchange)
|
||||
footerIcon := ExchangeFooterIcon(trade.Exchange)
|
||||
|
||||
return slack.Attachment{
|
||||
Text: text,
|
||||
|
|
2
pkg/util/exchange_icon.go
Normal file
2
pkg/util/exchange_icon.go
Normal file
|
@ -0,0 +1,2 @@
|
|||
package util
|
||||
|
|
@ -8,7 +8,7 @@ dialect: mysql
|
|||
# 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"
|
||||
dsn: "root:root@tcp(localhost:3306)/bbgo?parseTime=true"
|
||||
|
||||
# tcp connection to mysql without password
|
||||
# dsn: "root@tcp(localhost:3306)/bbgo_dev?parseTime=true"
|
||||
|
|
Loading…
Reference in New Issue
Block a user