mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-10 09:11:55 +00:00
integrate reward service into the sync service
This commit is contained in:
parent
c02213c8e5
commit
5a7cf05701
|
@ -31,8 +31,12 @@ func main() {
|
||||||
} else {
|
} else {
|
||||||
req = api.RewardService.NewRewardsRequest()
|
req = api.RewardService.NewRewardsRequest()
|
||||||
}
|
}
|
||||||
|
|
||||||
// req.From(1613931192)
|
// req.From(1613931192)
|
||||||
// req.From(1613240048)
|
// req.From(1613240048)
|
||||||
|
// req.From(maxapi.TimestampSince)
|
||||||
|
// req.To(maxapi.TimestampSince + 3600 * 24)
|
||||||
|
req.Limit(100)
|
||||||
|
|
||||||
rewards, err := req.Do(ctx)
|
rewards, err := req.Do(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -20,12 +20,12 @@ CREATE TABLE `rewards`
|
||||||
|
|
||||||
`created_at` DATETIME NOT NULL,
|
`created_at` DATETIME NOT NULL,
|
||||||
|
|
||||||
`used` BOOLEAN NOT NULL DEFAULT FALSE,
|
`spent` BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
|
||||||
`note` TEXT NULL,
|
`note` TEXT NULL,
|
||||||
|
|
||||||
PRIMARY KEY (`gid`),
|
PRIMARY KEY (`gid`),
|
||||||
UNIQUE KEY `id` (`id`)
|
UNIQUE KEY `uuid` (`exchange`, `uuid`)
|
||||||
);
|
);
|
||||||
|
|
||||||
-- +down
|
-- +down
|
||||||
|
|
|
@ -20,7 +20,7 @@ CREATE TABLE `rewards`
|
||||||
|
|
||||||
`created_at` DATETIME NOT NULL,
|
`created_at` DATETIME NOT NULL,
|
||||||
|
|
||||||
`used` BOOLEAN NOT NULL DEFAULT FALSE,
|
`spent` BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
|
||||||
`note` TEXT NULL
|
`note` TEXT NULL
|
||||||
);
|
);
|
||||||
|
|
|
@ -68,7 +68,8 @@ type Environment struct {
|
||||||
DatabaseService *service.DatabaseService
|
DatabaseService *service.DatabaseService
|
||||||
OrderService *service.OrderService
|
OrderService *service.OrderService
|
||||||
TradeService *service.TradeService
|
TradeService *service.TradeService
|
||||||
TradeSync *service.SyncService
|
RewardService *service.RewardService
|
||||||
|
SyncService *service.SyncService
|
||||||
|
|
||||||
// startTime is the time of start point (which is used in the backtest)
|
// startTime is the time of start point (which is used in the backtest)
|
||||||
startTime time.Time
|
startTime time.Time
|
||||||
|
@ -157,9 +158,12 @@ func (environ *Environment) ConfigureDatabaseDriver(ctx context.Context, driver
|
||||||
db := environ.DatabaseService.DB
|
db := environ.DatabaseService.DB
|
||||||
environ.OrderService = &service.OrderService{DB: db}
|
environ.OrderService = &service.OrderService{DB: db}
|
||||||
environ.TradeService = &service.TradeService{DB: db}
|
environ.TradeService = &service.TradeService{DB: db}
|
||||||
environ.TradeSync = &service.SyncService{
|
environ.RewardService = &service.RewardService{DB: db}
|
||||||
|
|
||||||
|
environ.SyncService = &service.SyncService{
|
||||||
TradeService: environ.TradeService,
|
TradeService: environ.TradeService,
|
||||||
OrderService: environ.OrderService,
|
OrderService: environ.OrderService,
|
||||||
|
RewardService: environ.RewardService,
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -528,7 +532,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 {
|
||||||
if err := session.Init(ctx, environ) ; err != nil {
|
if err := session.Init(ctx, environ); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -539,7 +543,7 @@ func (environ *Environment) syncSession(ctx context.Context, session *ExchangeSe
|
||||||
|
|
||||||
log.Infof("syncing symbols %v from session %s", symbols, session.Name)
|
log.Infof("syncing symbols %v from session %s", symbols, session.Name)
|
||||||
|
|
||||||
return environ.TradeSync.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) {
|
func getSessionSymbols(session *ExchangeSession, defaultSymbols ...string) ([]string, error) {
|
||||||
|
|
|
@ -291,7 +291,7 @@ func (session *ExchangeSession) InitSymbol(ctx context.Context, environ *Environ
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
var trades []types.Trade
|
var trades []types.Trade
|
||||||
if environ.TradeSync != nil {
|
if environ.SyncService != nil {
|
||||||
tradingFeeCurrency := session.Exchange.PlatformFeeCurrency()
|
tradingFeeCurrency := session.Exchange.PlatformFeeCurrency()
|
||||||
if strings.HasPrefix(symbol, tradingFeeCurrency) {
|
if strings.HasPrefix(symbol, tradingFeeCurrency) {
|
||||||
trades, err = environ.TradeService.QueryForTradingFeeCurrency(session.Exchange.Name(), symbol, tradingFeeCurrency)
|
trades, err = environ.TradeService.QueryForTradingFeeCurrency(session.Exchange.Name(), symbol, tradingFeeCurrency)
|
||||||
|
|
|
@ -49,6 +49,27 @@ func toGlobalSideType(v string) types.SideType {
|
||||||
return types.SideType(v)
|
return types.SideType(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toGlobalRewards(maxRewards []max.Reward) ([]types.Reward, error) {
|
||||||
|
// convert to global reward
|
||||||
|
var rewards []types.Reward
|
||||||
|
for _, r := range maxRewards {
|
||||||
|
// ignore "accepted"
|
||||||
|
if r.State != "done" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
reward, err := r.Reward()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rewards = append(rewards, *reward)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rewards, nil
|
||||||
|
}
|
||||||
|
|
||||||
func toGlobalOrderStatus(orderStatus max.OrderState, executedVolume, remainingVolume fixedpoint.Value) types.OrderStatus {
|
func toGlobalOrderStatus(orderStatus max.OrderState, executedVolume, remainingVolume fixedpoint.Value) types.OrderStatus {
|
||||||
|
|
||||||
switch orderStatus {
|
switch orderStatus {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -69,7 +70,7 @@ func (e *Exchange) QueryTicker(ctx context.Context, symbol string) (*types.Ticke
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Exchange) QueryTickers(ctx context.Context, symbol ...string) (map[string]types.Ticker, error) {
|
func (e *Exchange) QueryTickers(ctx context.Context, symbol ...string) (map[string]types.Ticker, error) {
|
||||||
if err := marketDataLimiter.Wait(ctx) ; err != nil {
|
if err := marketDataLimiter.Wait(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,7 +175,7 @@ func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders [
|
||||||
|
|
||||||
// lastOrderID is not supported on MAX
|
// lastOrderID is not supported on MAX
|
||||||
func (e *Exchange) QueryClosedOrders(ctx context.Context, symbol string, since, until time.Time, lastOrderID uint64) (orders []types.Order, err error) {
|
func (e *Exchange) QueryClosedOrders(ctx context.Context, symbol string, since, until time.Time, lastOrderID uint64) (orders []types.Order, err error) {
|
||||||
if err := closedOrderQueryLimiter.Wait(ctx) ; err != nil {
|
if err := closedOrderQueryLimiter.Wait(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -376,7 +377,7 @@ func (e *Exchange) PlatformFeeCurrency() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Exchange) QueryAccount(ctx context.Context) (*types.Account, error) {
|
func (e *Exchange) QueryAccount(ctx context.Context) (*types.Account, error) {
|
||||||
if err := accountQueryLimiter.Wait(ctx) ; err != nil {
|
if err := accountQueryLimiter.Wait(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -521,7 +522,7 @@ func (e *Exchange) QueryDepositHistory(ctx context.Context, asset string, since,
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Exchange) QueryAccountBalances(ctx context.Context) (types.BalanceMap, error) {
|
func (e *Exchange) QueryAccountBalances(ctx context.Context) (types.BalanceMap, error) {
|
||||||
if err := accountQueryLimiter.Wait(ctx) ; err != nil {
|
if err := accountQueryLimiter.Wait(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -544,7 +545,7 @@ func (e *Exchange) QueryAccountBalances(ctx context.Context) (types.BalanceMap,
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *types.TradeQueryOptions) (trades []types.Trade, err error) {
|
func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *types.TradeQueryOptions) (trades []types.Trade, err error) {
|
||||||
if err := tradeQueryLimiter.Wait(ctx) ; err != nil {
|
if err := tradeQueryLimiter.Wait(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -583,8 +584,56 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type
|
||||||
return trades, nil
|
return trades, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *Exchange) QueryRewards(ctx context.Context, startTime time.Time) ([]types.Reward, error) {
|
||||||
|
var from = startTime
|
||||||
|
var emptyTime = time.Time{}
|
||||||
|
|
||||||
|
if from == emptyTime {
|
||||||
|
from = time.Unix(maxapi.TimestampSince, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
var now = time.Now()
|
||||||
|
for {
|
||||||
|
if from.After(now) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// scan by 30 days
|
||||||
|
// an user might get most 14 commission records by currency per day
|
||||||
|
// limit 1000 / 14 = 71 days
|
||||||
|
to := from.Add(time.Hour * 24 * 30)
|
||||||
|
req := e.client.RewardService.NewRewardsRequest()
|
||||||
|
req.From(from.Unix())
|
||||||
|
req.To(to.Unix())
|
||||||
|
req.Limit(1000)
|
||||||
|
|
||||||
|
maxRewards, err := req.Do(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(maxRewards) == 0 {
|
||||||
|
// next page
|
||||||
|
from = to
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
rewards, err := toGlobalRewards(maxRewards)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort them in the ascending order
|
||||||
|
sort.Reverse(types.RewardSliceByCreationTime{rewards})
|
||||||
|
|
||||||
|
return rewards, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("unknown error")
|
||||||
|
}
|
||||||
|
|
||||||
func (e *Exchange) QueryKLines(ctx context.Context, symbol string, interval types.Interval, options types.KLineQueryOptions) ([]types.KLine, error) {
|
func (e *Exchange) QueryKLines(ctx context.Context, symbol string, interval types.Interval, options types.KLineQueryOptions) ([]types.KLine, error) {
|
||||||
if err := marketDataLimiter.Wait(ctx) ; err != nil {
|
if err := marketDataLimiter.Wait(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -605,7 +654,6 @@ func (e *Exchange) QueryKLines(ctx context.Context, symbol string, interval type
|
||||||
return nil, errors.New("start time can not be empty")
|
return nil, errors.New("start time can not be empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
log.Infof("querying kline %s %s %+v", symbol, interval, options)
|
log.Infof("querying kline %s %s %+v", symbol, interval, options)
|
||||||
localKLines, err := e.client.PublicService.KLines(toLocalSymbol(symbol), string(interval), *options.StartTime, limit)
|
localKLines, err := e.client.PublicService.KLines(toLocalSymbol(symbol), string(interval), *options.StartTime, limit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -20,6 +20,7 @@ import (
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
|
||||||
"github.com/c9s/bbgo/pkg/util"
|
"github.com/c9s/bbgo/pkg/util"
|
||||||
"github.com/c9s/bbgo/pkg/version"
|
"github.com/c9s/bbgo/pkg/version"
|
||||||
|
@ -32,8 +33,16 @@ const (
|
||||||
UserAgent = "bbgo/" + version.Version
|
UserAgent = "bbgo/" + version.Version
|
||||||
|
|
||||||
defaultHTTPTimeout = time.Second * 30
|
defaultHTTPTimeout = time.Second * 30
|
||||||
|
|
||||||
|
TimestampSince = 1535760000
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var debugMaxRequestPayload = true
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
debugMaxRequestPayload = viper.GetBool("MAX_DEBUG_REQUEST_PAYLOAD")
|
||||||
|
}
|
||||||
|
|
||||||
var logger = log.WithField("exchange", "max")
|
var logger = log.WithField("exchange", "max")
|
||||||
|
|
||||||
var htmlTagPattern = regexp.MustCompile("<[/]?[a-zA-Z-]+.*?>")
|
var htmlTagPattern = regexp.MustCompile("<[/]?[a-zA-Z-]+.*?>")
|
||||||
|
@ -187,6 +196,10 @@ func (c *RestClient) newAuthenticatedRequest(m string, refURL string, data inter
|
||||||
p, err = json.Marshal(d)
|
p, err = json.Marshal(d)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if debugMaxRequestPayload {
|
||||||
|
log.Infof("request payload: %s", p)
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -204,6 +217,7 @@ func (c *RestClient) newAuthenticatedRequest(m string, refURL string, data inter
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
encoded := base64.StdEncoding.EncodeToString(p)
|
encoded := base64.StdEncoding.EncodeToString(p)
|
||||||
|
|
||||||
req.Header.Add("Content-Type", "application/json")
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
|
|
@ -4,8 +4,11 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/datatype"
|
||||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RewardType string
|
type RewardType string
|
||||||
|
@ -55,7 +58,34 @@ func (t *RewardType) UnmarshalJSON(o []byte) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t RewardType) RewardType() (types.RewardType, error) {
|
||||||
|
switch t {
|
||||||
|
|
||||||
|
case RewardAirdrop:
|
||||||
|
return types.RewardAirdrop, nil
|
||||||
|
|
||||||
|
case RewardCommission:
|
||||||
|
return types.RewardCommission, nil
|
||||||
|
|
||||||
|
case RewardHolding:
|
||||||
|
return types.RewardHolding, nil
|
||||||
|
|
||||||
|
case RewardMining:
|
||||||
|
return types.RewardMining, nil
|
||||||
|
|
||||||
|
case RewardTrading:
|
||||||
|
return types.RewardTrading, nil
|
||||||
|
|
||||||
|
case RewardVipRebate:
|
||||||
|
return types.RewardVipRebate, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return types.RewardType(""), fmt.Errorf("unknown reward type: %s", t)
|
||||||
|
}
|
||||||
|
|
||||||
type Reward struct {
|
type Reward struct {
|
||||||
|
// UUID here is more like SN, not the real UUID
|
||||||
UUID string `json:"uuid"`
|
UUID string `json:"uuid"`
|
||||||
Type RewardType `json:"type"`
|
Type RewardType `json:"type"`
|
||||||
Currency string `json:"currency"`
|
Currency string `json:"currency"`
|
||||||
|
@ -67,6 +97,25 @@ type Reward struct {
|
||||||
CreatedAt Timestamp `json:"created_at"`
|
CreatedAt Timestamp `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (reward Reward) Reward() (*types.Reward, error) {
|
||||||
|
rt, err := reward.Type.RewardType()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.Reward{
|
||||||
|
UUID: reward.UUID,
|
||||||
|
Exchange: types.ExchangeMax,
|
||||||
|
Type: rt,
|
||||||
|
Currency: strings.ToUpper(reward.Currency),
|
||||||
|
Quantity: reward.Amount,
|
||||||
|
State: reward.State,
|
||||||
|
Note: reward.Note,
|
||||||
|
Used: false,
|
||||||
|
CreatedAt: datatype.Time(reward.CreatedAt),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
type RewardService struct {
|
type RewardService struct {
|
||||||
client *RestClient
|
client *RestClient
|
||||||
}
|
}
|
||||||
|
@ -91,6 +140,8 @@ type RewardsRequest struct {
|
||||||
|
|
||||||
// To Unix-timestamp
|
// To Unix-timestamp
|
||||||
to *int64
|
to *int64
|
||||||
|
|
||||||
|
limit *int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RewardsRequest) Currency(currency string) *RewardsRequest {
|
func (r *RewardsRequest) Currency(currency string) *RewardsRequest {
|
||||||
|
@ -103,6 +154,11 @@ func (r *RewardsRequest) From(from int64) *RewardsRequest {
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *RewardsRequest) Limit(limit int) *RewardsRequest {
|
||||||
|
r.limit = &limit
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
func (r *RewardsRequest) To(to int64) *RewardsRequest {
|
func (r *RewardsRequest) To(to int64) *RewardsRequest {
|
||||||
r.to = &to
|
r.to = &to
|
||||||
return r
|
return r
|
||||||
|
@ -123,6 +179,10 @@ func (r *RewardsRequest) Do(ctx context.Context) (rewards []Reward, err error) {
|
||||||
payload["from"] = r.from
|
payload["from"] = r.from
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if r.limit != nil {
|
||||||
|
payload["limit"] = r.limit
|
||||||
|
}
|
||||||
|
|
||||||
refURL := "v2/rewards"
|
refURL := "v2/rewards"
|
||||||
|
|
||||||
if r.pathType != nil {
|
if r.pathType != nil {
|
||||||
|
|
|
@ -14,7 +14,7 @@ func init() {
|
||||||
func upAddRewardsTable(ctx context.Context, tx rockhopper.SQLExecutor) (err error) {
|
func upAddRewardsTable(ctx context.Context, tx rockhopper.SQLExecutor) (err error) {
|
||||||
// This code is executed when the migration is applied.
|
// This code is executed when the migration is applied.
|
||||||
|
|
||||||
_, err = tx.ExecContext(ctx, "CREATE TABLE `rewards`\n(\n `gid` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,\n -- for exchange\n `exchange` VARCHAR(24) NOT NULL DEFAULT '',\n -- reward record id\n `uuid` VARCHAR(32) NOT NULL,\n `reward_type` VARCHAR(24) NOT NULL DEFAULT '',\n -- currency symbol, BTC, MAX, USDT ... etc\n `currency` VARCHAR(5) NOT NULL,\n -- the quantity of the rewards\n `quantity` DECIMAL(16, 8) UNSIGNED NOT NULL,\n `state` VARCHAR(5) NOT NULL,\n `created_at` DATETIME NOT NULL,\n `used` BOOLEAN NOT NULL DEFAULT FALSE,\n `note` TEXT NULL,\n PRIMARY KEY (`gid`),\n UNIQUE KEY `id` (`id`)\n);")
|
_, err = tx.ExecContext(ctx, "CREATE TABLE `rewards`\n(\n `gid` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,\n -- for exchange\n `exchange` VARCHAR(24) NOT NULL DEFAULT '',\n -- reward record id\n `uuid` VARCHAR(32) NOT NULL,\n `reward_type` VARCHAR(24) NOT NULL DEFAULT '',\n -- currency symbol, BTC, MAX, USDT ... etc\n `currency` VARCHAR(5) NOT NULL,\n -- the quantity of the rewards\n `quantity` DECIMAL(16, 8) UNSIGNED NOT NULL,\n `state` VARCHAR(5) NOT NULL,\n `created_at` DATETIME NOT NULL,\n `spent` BOOLEAN NOT NULL DEFAULT FALSE,\n `note` TEXT NULL,\n PRIMARY KEY (`gid`),\n UNIQUE KEY `uuid` (`exchange`, `uuid`)\n);")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ func init() {
|
||||||
func upAddRewardsTable(ctx context.Context, tx rockhopper.SQLExecutor) (err error) {
|
func upAddRewardsTable(ctx context.Context, tx rockhopper.SQLExecutor) (err error) {
|
||||||
// This code is executed when the migration is applied.
|
// This code is executed when the migration is applied.
|
||||||
|
|
||||||
_, err = tx.ExecContext(ctx, "CREATE TABLE `rewards`\n(\n `gid` INTEGER PRIMARY KEY AUTOINCREMENT,\n -- for exchange\n `exchange` VARCHAR(24) NOT NULL DEFAULT '',\n -- reward record id\n `uuid` VARCHAR(32) NOT NULL,\n `reward_type` VARCHAR(24) NOT NULL DEFAULT '',\n -- currency symbol, BTC, MAX, USDT ... etc\n `currency` VARCHAR(5) NOT NULL,\n -- the quantity of the rewards\n `quantity` DECIMAL(16, 8) NOT NULL,\n `state` VARCHAR(5) NOT NULL,\n `created_at` DATETIME NOT NULL,\n `used` BOOLEAN NOT NULL DEFAULT FALSE,\n `note` TEXT NULL\n);")
|
_, err = tx.ExecContext(ctx, "CREATE TABLE `rewards`\n(\n `gid` INTEGER PRIMARY KEY AUTOINCREMENT,\n -- for exchange\n `exchange` VARCHAR(24) NOT NULL DEFAULT '',\n -- reward record id\n `uuid` VARCHAR(32) NOT NULL,\n `reward_type` VARCHAR(24) NOT NULL DEFAULT '',\n -- currency symbol, BTC, MAX, USDT ... etc\n `currency` VARCHAR(5) NOT NULL,\n -- the quantity of the rewards\n `quantity` DECIMAL(16, 8) NOT NULL,\n `state` VARCHAR(5) NOT NULL,\n `created_at` DATETIME NOT NULL,\n `spent` BOOLEAN NOT NULL DEFAULT FALSE,\n `note` TEXT NULL\n);")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,11 +15,24 @@ type RewardService struct {
|
||||||
func NewRewardService(db *sqlx.DB) *RewardService {
|
func NewRewardService(db *sqlx.DB) *RewardService {
|
||||||
return &RewardService{db}
|
return &RewardService{db}
|
||||||
}
|
}
|
||||||
func (s *RewardService) Query(ex types.ExchangeName, rewardType string, from time.Time) ([]types.Trade, error) {
|
|
||||||
rows, err := s.DB.NamedQuery(`SELECT * FROM trades WHERE exchange = :exchange AND (symbol = :symbol OR fee_currency = :fee_currency) ORDER BY traded_at ASC`, map[string]interface{}{
|
func (s *RewardService) QueryLast(ex types.ExchangeName, limit int) ([]types.Reward, error) {
|
||||||
|
rows, err := s.DB.NamedQuery(`SELECT * FROM rewards WHERE exchange = :exchange ORDER BY created_at DESC LIMIT :limit`, map[string]interface{}{
|
||||||
"exchange": ex,
|
"exchange": ex,
|
||||||
"reward_type": rewardType,
|
"limit": limit,
|
||||||
"from": from.Unix(),
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer rows.Close()
|
||||||
|
return s.scanRows(rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *RewardService) QueryUnspent(ex types.ExchangeName, from time.Time) ([]types.Reward, error) {
|
||||||
|
rows, err := s.DB.NamedQuery(`SELECT * FROM rewards WHERE exchange = :exchange AND spent IS FALSE ORDER BY created_at ASC`, map[string]interface{}{
|
||||||
|
"exchange": ex,
|
||||||
|
"from": from,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -30,24 +43,23 @@ func (s *RewardService) Query(ex types.ExchangeName, rewardType string, from tim
|
||||||
return s.scanRows(rows)
|
return s.scanRows(rows)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *RewardService) scanRows(rows *sqlx.Rows) (trades []types.Trade, err error) {
|
func (s *RewardService) scanRows(rows *sqlx.Rows) (rewards []types.Reward, err error) {
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var trade types.Trade
|
var reward types.Reward
|
||||||
if err := rows.StructScan(&trade); err != nil {
|
if err := rows.StructScan(&reward); err != nil {
|
||||||
return trades, err
|
return rewards, err
|
||||||
}
|
}
|
||||||
|
|
||||||
trades = append(trades, trade)
|
rewards = append(rewards, reward)
|
||||||
}
|
}
|
||||||
|
|
||||||
return trades, rows.Err()
|
return rewards, rows.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *RewardService) Insert(trade types.Trade) error {
|
func (s *RewardService) Insert(reward types.Reward) error {
|
||||||
_, err := s.DB.NamedExec(`
|
_, err := s.DB.NamedExec(`
|
||||||
INSERT INTO trades (id, exchange, order_id, symbol, price, quantity, quote_quantity, side, is_buyer, is_maker, fee, fee_currency, traded_at, is_margin, is_isolated)
|
INSERT INTO rewards (exchange, uuid, reward_type, quantity, state, created_at)
|
||||||
VALUES (:id, :exchange, :order_id, :symbol, :price, :quantity, :quote_quantity, :side, :is_buyer, :is_maker, :fee, :fee_currency, :traded_at, :is_margin, :is_isolated)`,
|
VALUES (:exchange, :uuid, :reward_type, :quantity, :state, :created_at)`,
|
||||||
trade)
|
reward)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,19 +2,124 @@ package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/time/rate"
|
||||||
|
|
||||||
"github.com/c9s/bbgo/pkg/exchange/batch"
|
"github.com/c9s/bbgo/pkg/exchange/batch"
|
||||||
"github.com/c9s/bbgo/pkg/types"
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var ErrNotImplemented = errors.New("exchange does not implement ExchangeRewardService interface")
|
||||||
|
|
||||||
type SyncService struct {
|
type SyncService struct {
|
||||||
TradeService *TradeService
|
TradeService *TradeService
|
||||||
OrderService *OrderService
|
OrderService *OrderService
|
||||||
|
RewardService *RewardService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SyncService) SyncRewards(ctx context.Context, exchange types.Exchange) error {
|
||||||
|
service, ok := exchange.(types.ExchangeRewardService)
|
||||||
|
if !ok {
|
||||||
|
return ErrNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var startTime time.Time
|
||||||
|
lastRecords, err := s.RewardService.QueryLast(exchange.Name(), 10)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(lastRecords) > 0 {
|
||||||
|
end := len(lastRecords) - 1
|
||||||
|
lastRecord := lastRecords[end]
|
||||||
|
startTime = lastRecord.CreatedAt.Time()
|
||||||
|
}
|
||||||
|
|
||||||
|
batchQuery := &RewardBatchQuery{Service: service}
|
||||||
|
rewardsC, errC := batchQuery.Query(ctx, startTime, time.Now())
|
||||||
|
|
||||||
|
for reward := range rewardsC {
|
||||||
|
select {
|
||||||
|
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
|
||||||
|
case err := <-errC:
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.RewardService.Insert(reward); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <-errC
|
||||||
|
}
|
||||||
|
|
||||||
|
type RewardBatchQuery struct {
|
||||||
|
Service types.ExchangeRewardService
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *RewardBatchQuery) Query(ctx context.Context, startTime, endTime time.Time) (c chan types.Reward, errC chan error) {
|
||||||
|
c = make(chan types.Reward, 500)
|
||||||
|
errC = make(chan error, 1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
limiter := rate.NewLimiter(rate.Every(5*time.Second), 2) // from binance (original 1200, use 1000 for safety)
|
||||||
|
|
||||||
|
defer close(c)
|
||||||
|
defer close(errC)
|
||||||
|
|
||||||
|
rewardKeys := make(map[string]struct{}, 500)
|
||||||
|
|
||||||
|
for startTime.Before(endTime) {
|
||||||
|
if err := limiter.Wait(ctx); err != nil {
|
||||||
|
logrus.WithError(err).Error("rate limit error")
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Infof("batch querying rewards %s <=> %s", startTime, endTime)
|
||||||
|
|
||||||
|
rewards, err := q.Service.QueryRewards(ctx, startTime)
|
||||||
|
if err != nil {
|
||||||
|
errC <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rewards) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, o := range rewards {
|
||||||
|
if _, ok := rewardKeys[o.UUID]; ok {
|
||||||
|
logrus.Infof("skipping duplicated order id: %s", o.UUID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.CreatedAt.Time().After(endTime) {
|
||||||
|
// stop batch query
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c <- o
|
||||||
|
startTime = o.CreatedAt.Time()
|
||||||
|
rewardKeys[o.UUID] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}()
|
||||||
|
|
||||||
|
return c, errC
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
func (s *SyncService) SyncOrders(ctx context.Context, exchange types.Exchange, symbol string, startTime time.Time) error {
|
func (s *SyncService) SyncOrders(ctx context.Context, exchange types.Exchange, symbol string, startTime time.Time) error {
|
||||||
isMargin := false
|
isMargin := false
|
||||||
isIsolated := false
|
isIsolated := false
|
||||||
|
@ -147,6 +252,15 @@ func (s *SyncService) SyncSessionSymbols(ctx context.Context, exchange types.Exc
|
||||||
if err := s.SyncOrders(ctx, exchange, symbol, startTime); err != nil {
|
if err := s.SyncOrders(ctx, exchange, symbol, startTime); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if err := s.SyncRewards(ctx, exchange) ; err != nil {
|
||||||
|
if err == ErrNotImplemented {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -85,6 +85,10 @@ type Exchange interface {
|
||||||
CancelOrders(ctx context.Context, orders ...Order) error
|
CancelOrders(ctx context.Context, orders ...Order) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ExchangeRewardService interface {
|
||||||
|
QueryRewards(ctx context.Context, startTime time.Time) ([]Reward, error)
|
||||||
|
}
|
||||||
|
|
||||||
type TradeQueryOptions struct {
|
type TradeQueryOptions struct {
|
||||||
StartTime *time.Time
|
StartTime *time.Time
|
||||||
EndTime *time.Time
|
EndTime *time.Time
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package types
|
package types
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/c9s/bbgo/pkg/datatype"
|
"github.com/c9s/bbgo/pkg/datatype"
|
||||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
)
|
)
|
||||||
|
@ -18,13 +20,30 @@ const (
|
||||||
|
|
||||||
type Reward struct {
|
type Reward struct {
|
||||||
UUID string `json:"uuid" db:"uuid"`
|
UUID string `json:"uuid" db:"uuid"`
|
||||||
|
Exchange ExchangeName `json:"exchange" db:"exchange"`
|
||||||
Type RewardType `json:"reward_type" db:"reward_type"`
|
Type RewardType `json:"reward_type" db:"reward_type"`
|
||||||
Currency string `json:"currency" db:"currency"`
|
Currency string `json:"currency" db:"currency"`
|
||||||
Amount fixedpoint.Value `json:"quantity" db:"quantity"`
|
Quantity fixedpoint.Value `json:"quantity" db:"quantity"`
|
||||||
State string `json:"state" db:"state"`
|
State string `json:"state" db:"state"`
|
||||||
Note string `json:"note" db:"note"`
|
Note string `json:"note" db:"note"`
|
||||||
Used bool `json:"used" db:"used"`
|
Used bool `json:"spent" db:"spent"`
|
||||||
|
|
||||||
// Unix timestamp in seconds
|
// Unix timestamp in seconds
|
||||||
CreatedAt datatype.Time `json:"created_at"`
|
CreatedAt datatype.Time `json:"created_at" db:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RewardSlice []Reward
|
||||||
|
|
||||||
|
func (s RewardSlice) Len() int { return len(s) }
|
||||||
|
func (s RewardSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||||
|
|
||||||
|
type RewardSliceByCreationTime struct {
|
||||||
|
RewardSlice
|
||||||
|
}
|
||||||
|
|
||||||
|
// Less reports whether x[i] should be ordered before x[j]
|
||||||
|
func (s RewardSliceByCreationTime) Less(i, j int) bool {
|
||||||
|
return time.Time(s.RewardSlice[i].CreatedAt).After(
|
||||||
|
time.Time(s.RewardSlice[j].CreatedAt),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,7 +90,7 @@ func (trade Trade) SlackAttachment() slack.Attachment {
|
||||||
{Title: "Exchange", Value: trade.Exchange, Short: true},
|
{Title: "Exchange", Value: trade.Exchange, Short: true},
|
||||||
{Title: "Price", Value: util.FormatFloat(trade.Price, 2), Short: true},
|
{Title: "Price", Value: util.FormatFloat(trade.Price, 2), Short: true},
|
||||||
{Title: "Volume", Value: util.FormatFloat(trade.Quantity, 4), Short: true},
|
{Title: "Volume", Value: util.FormatFloat(trade.Quantity, 4), Short: true},
|
||||||
{Title: "Amount", Value: util.FormatFloat(trade.QuoteQuantity, 2)},
|
{Title: "Quantity", Value: util.FormatFloat(trade.QuoteQuantity, 2)},
|
||||||
{Title: "Fee", Value: util.FormatFloat(trade.Fee, 4), Short: true},
|
{Title: "Fee", Value: util.FormatFloat(trade.Fee, 4), Short: true},
|
||||||
{Title: "FeeCurrency", Value: trade.FeeCurrency, Short: true},
|
{Title: "FeeCurrency", Value: trade.FeeCurrency, Short: true},
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue
Block a user