2021-03-21 02:44:06 +00:00
package xmaker
import (
"context"
"fmt"
2024-11-20 09:56:45 +00:00
"io"
2024-08-30 14:41:13 +00:00
"math"
2024-11-20 09:56:45 +00:00
"os"
2021-03-21 02:44:06 +00:00
"sync"
"time"
2024-11-20 09:56:45 +00:00
"github.com/jedib0t/go-pretty/v6/table"
2021-12-26 04:10:10 +00:00
"github.com/pkg/errors"
2024-08-24 13:17:32 +00:00
"github.com/prometheus/client_golang/prometheus"
2021-12-26 04:10:10 +00:00
"github.com/sirupsen/logrus"
2022-01-09 03:33:34 +00:00
"golang.org/x/time/rate"
2021-12-26 04:10:10 +00:00
2021-03-21 02:44:06 +00:00
"github.com/c9s/bbgo/pkg/bbgo"
2023-07-04 13:42:24 +00:00
"github.com/c9s/bbgo/pkg/core"
2024-11-20 09:56:45 +00:00
"github.com/c9s/bbgo/pkg/dynamic"
2021-03-21 02:44:06 +00:00
"github.com/c9s/bbgo/pkg/fixedpoint"
2024-08-24 05:28:32 +00:00
indicatorv2 "github.com/c9s/bbgo/pkg/indicator/v2"
2024-08-24 04:28:05 +00:00
"github.com/c9s/bbgo/pkg/pricesolver"
2024-10-09 03:47:10 +00:00
"github.com/c9s/bbgo/pkg/profile/timeprofile"
2024-08-24 03:42:07 +00:00
"github.com/c9s/bbgo/pkg/risk/circuitbreaker"
2024-08-26 04:45:18 +00:00
"github.com/c9s/bbgo/pkg/strategy/common"
2024-11-20 09:56:45 +00:00
"github.com/c9s/bbgo/pkg/style"
2021-03-21 02:44:06 +00:00
"github.com/c9s/bbgo/pkg/types"
2022-01-06 16:14:24 +00:00
"github.com/c9s/bbgo/pkg/util"
2024-10-28 09:28:56 +00:00
"github.com/c9s/bbgo/pkg/util/timejitter"
2021-03-21 02:44:06 +00:00
)
2021-05-17 13:33:55 +00:00
var defaultMargin = fixedpoint . NewFromFloat ( 0.003 )
2024-08-26 04:45:18 +00:00
var two = fixedpoint . NewFromInt ( 2 )
2024-10-07 09:12:49 +00:00
const feeTokenQuote = "USDT"
2022-01-12 06:33:55 +00:00
const priceUpdateTimeout = 30 * time . Second
2022-01-12 03:55:45 +00:00
2021-03-21 02:44:06 +00:00
const ID = "xmaker"
var log = logrus . WithField ( "strategy" , ID )
2024-10-09 03:54:13 +00:00
type MutexFloat64 struct {
value float64
mu sync . Mutex
}
func ( m * MutexFloat64 ) Set ( v float64 ) {
m . mu . Lock ( )
m . value = v
m . mu . Unlock ( )
}
func ( m * MutexFloat64 ) Get ( ) float64 {
m . mu . Lock ( )
v := m . value
m . mu . Unlock ( )
return v
}
2024-08-30 07:44:55 +00:00
type Quote struct {
BestBidPrice , BestAskPrice fixedpoint . Value
BidMargin , AskMargin fixedpoint . Value
// BidLayerPips is the price pips between each layer
BidLayerPips , AskLayerPips fixedpoint . Value
}
type SessionBinder interface {
Bind ( ctx context . Context , session * bbgo . ExchangeSession , symbol string ) error
}
type SignalNumber float64
const (
SignalNumberMaxLong = 2.0
SignalNumberMaxShort = - 2.0
)
type SignalProvider interface {
CalculateSignal ( ctx context . Context ) ( float64 , error )
}
type KLineShapeSignal struct {
FullBodyThreshold float64 ` json:"fullBodyThreshold" `
}
type SignalConfig struct {
Weight float64 ` json:"weight" `
BollingerBandTrendSignal * BollingerBandTrendSignal ` json:"bollingerBandTrend,omitempty" `
OrderBookBestPriceSignal * OrderBookBestPriceVolumeSignal ` json:"orderBookBestPrice,omitempty" `
2024-09-30 08:21:22 +00:00
DepthRatioSignal * DepthRatioSignal ` json:"depthRatio,omitempty" `
2024-08-30 07:44:55 +00:00
KLineShapeSignal * KLineShapeSignal ` json:"klineShape,omitempty" `
2024-09-04 07:59:21 +00:00
TradeVolumeWindowSignal * TradeVolumeWindowSignal ` json:"tradeVolumeWindow,omitempty" `
2024-08-30 07:44:55 +00:00
}
2024-10-09 04:33:11 +00:00
func ( c * SignalConfig ) Get ( ) SignalProvider {
if c . OrderBookBestPriceSignal != nil {
return c . OrderBookBestPriceSignal
} else if c . DepthRatioSignal != nil {
return c . DepthRatioSignal
} else if c . BollingerBandTrendSignal != nil {
return c . BollingerBandTrendSignal
} else if c . TradeVolumeWindowSignal != nil {
return c . TradeVolumeWindowSignal
}
panic ( fmt . Errorf ( "no valid signal provider found, please check your config" ) )
}
2021-03-21 02:44:06 +00:00
func init ( ) {
bbgo . RegisterStrategy ( ID , & Strategy { } )
}
2024-11-20 08:42:19 +00:00
type SignalMargin struct {
Enabled bool ` json:"enabled" `
Scale * bbgo . SlideRule ` json:"scale,omitempty" `
Threshold float64 ` json:"threshold,omitempty" `
}
2024-11-20 09:56:45 +00:00
type DelayedHedge struct {
// EnableDelayHedge enables the delay hedge feature
Enabled bool ` json:"enabled" `
// MaxDelayDuration is the maximum delay duration to hedge the position
MaxDelayDuration types . Duration ` json:"maxDelay" `
// FixedDelayDuration is the fixed delay duration
FixedDelayDuration types . Duration ` json:"fixedDelay" `
// SignalThreshold is the signal threshold to trigger the delay hedge
SignalThreshold float64 ` json:"signalThreshold" `
// DynamicDelayScale is the dynamic delay scale
DynamicDelayScale * bbgo . SlideRule ` json:"dynamicDelayScale,omitempty" `
}
2021-03-21 02:44:06 +00:00
type Strategy struct {
2022-03-11 13:27:45 +00:00
Environment * bbgo . Environment
2021-03-21 02:44:06 +00:00
2021-05-30 06:46:48 +00:00
Symbol string ` json:"symbol" `
// SourceExchange session name
2021-03-21 02:44:06 +00:00
SourceExchange string ` json:"sourceExchange" `
2021-05-30 06:46:48 +00:00
// MakerExchange session name
MakerExchange string ` json:"makerExchange" `
2021-03-21 02:44:06 +00:00
2021-05-09 13:14:51 +00:00
UpdateInterval types . Duration ` json:"updateInterval" `
HedgeInterval types . Duration ` json:"hedgeInterval" `
2021-05-09 10:55:56 +00:00
OrderCancelWaitTime types . Duration ` json:"orderCancelWaitTime" `
2021-03-21 02:44:06 +00:00
2024-10-07 09:07:09 +00:00
SubscribeFeeTokenMarkets bool ` json:"subscribeFeeTokenMarkets" `
2024-11-20 08:42:19 +00:00
EnableSignalMargin bool ` json:"enableSignalMargin" `
SignalConfigList [ ] SignalConfig ` json:"signals" `
2024-11-20 09:56:45 +00:00
SignalReverseSideMargin * SignalMargin ` json:"signalReverseSideMargin,omitempty" `
SignalTrendSideMarginDiscount * SignalMargin ` json:"signalTrendSideMarginDiscount,omitempty" `
2024-11-20 08:42:19 +00:00
// Margin is the default margin for the quote
Margin fixedpoint . Value ` json:"margin" `
BidMargin fixedpoint . Value ` json:"bidMargin" `
AskMargin fixedpoint . Value ` json:"askMargin" `
// MinMargin is the minimum margin protection for signal margin
MinMargin * fixedpoint . Value ` json:"minMargin" `
2024-08-30 07:44:55 +00:00
2024-09-06 13:47:43 +00:00
UseDepthPrice bool ` json:"useDepthPrice" `
DepthQuantity fixedpoint . Value ` json:"depthQuantity" `
SourceDepthLevel types . Depth ` json:"sourceDepthLevel" `
2021-05-09 18:52:41 +00:00
2024-10-15 10:51:37 +00:00
// EnableDelayHedge enables the delay hedge feature
EnableDelayHedge bool ` json:"enableDelayHedge" `
2024-10-15 09:29:12 +00:00
// MaxHedgeDelayDuration is the maximum delay duration to hedge the position
MaxDelayHedgeDuration types . Duration ` json:"maxHedgeDelayDuration" `
DelayHedgeSignalThreshold float64 ` json:"delayHedgeSignalThreshold" `
2024-11-20 09:56:45 +00:00
DelayedHedge * DelayedHedge ` json:"delayedHedge,omitempty" `
2021-05-17 12:03:42 +00:00
EnableBollBandMargin bool ` json:"enableBollBandMargin" `
BollBandInterval types . Interval ` json:"bollBandInterval" `
BollBandMargin fixedpoint . Value ` json:"bollBandMargin" `
BollBandMarginFactor fixedpoint . Value ` json:"bollBandMarginFactor" `
2024-08-28 06:48:38 +00:00
// MinMarginLevel is the minimum margin level to trigger the hedge
MinMarginLevel fixedpoint . Value ` json:"minMarginLevel" `
2021-05-11 04:47:45 +00:00
StopHedgeQuoteBalance fixedpoint . Value ` json:"stopHedgeQuoteBalance" `
StopHedgeBaseBalance fixedpoint . Value ` json:"stopHedgeBaseBalance" `
2021-05-09 18:52:41 +00:00
// Quantity is used for fixed quantity of the first layer
Quantity fixedpoint . Value ` json:"quantity" `
// QuantityMultiplier is the factor that multiplies the quantity of the previous layer
QuantityMultiplier fixedpoint . Value ` json:"quantityMultiplier" `
// QuantityScale helps user to define the quantity by layer scale
QuantityScale * bbgo . LayerScale ` json:"quantityScale,omitempty" `
2021-05-11 04:47:45 +00:00
// MaxExposurePosition defines the unhedged quantity of stop
2021-04-04 03:14:09 +00:00
MaxExposurePosition fixedpoint . Value ` json:"maxExposurePosition" `
2021-05-11 04:47:45 +00:00
2024-09-01 07:42:36 +00:00
MaxHedgeAccountLeverage fixedpoint . Value ` json:"maxHedgeAccountLeverage" `
2021-05-11 04:47:45 +00:00
DisableHedge bool ` json:"disableHedge" `
2021-03-21 02:44:06 +00:00
2021-05-28 17:31:13 +00:00
NotifyTrade bool ` json:"notifyTrade" `
2024-10-16 03:44:30 +00:00
EnableArbitrage bool ` json:"enableArbitrage" `
2024-09-07 05:47:21 +00:00
2023-07-22 09:29:16 +00:00
// RecoverTrade tries to find the missing trades via the REStful API
RecoverTrade bool ` json:"recoverTrade" `
RecoverTradeScanPeriod types . Duration ` json:"recoverTradeScanPeriod" `
2024-11-20 08:42:19 +00:00
MaxQuoteQuotaRatio fixedpoint . Value ` json:"maxQuoteQuotaRatio,omitempty" `
2024-11-20 05:44:06 +00:00
2021-03-21 02:44:06 +00:00
NumLayers int ` json:"numLayers" `
2021-05-11 04:47:45 +00:00
// Pips is the pips of the layer prices
2021-05-17 15:55:09 +00:00
Pips fixedpoint . Value ` json:"pips" `
2021-05-11 04:47:45 +00:00
2024-08-26 04:45:18 +00:00
// ProfitFixerConfig is the profit fixer configuration
ProfitFixerConfig * common . ProfitFixerConfig ` json:"profitFixer,omitempty" `
2021-05-11 04:47:45 +00:00
// --------------------------------
// private field
2021-03-21 02:44:06 +00:00
2021-12-26 04:10:10 +00:00
makerSession , sourceSession * bbgo . ExchangeSession
2021-03-21 02:44:06 +00:00
2021-12-26 04:10:10 +00:00
makerMarket , sourceMarket types . Market
2021-03-21 02:44:06 +00:00
2021-05-17 12:03:42 +00:00
// boll is the BOLLINGER indicator we used for predicting the price.
2024-08-24 05:28:32 +00:00
boll * indicatorv2 . BOLLStream
2021-05-17 12:03:42 +00:00
2021-03-21 02:44:06 +00:00
state * State
2024-08-24 04:28:05 +00:00
priceSolver * pricesolver . SimplePriceSolver
2024-08-24 03:42:07 +00:00
CircuitBreaker * circuitbreaker . BasicCircuitBreaker ` json:"circuitBreaker" `
2022-05-05 07:05:38 +00:00
// persistence fields
2024-10-16 06:39:09 +00:00
Position * types . Position ` json:"position,omitempty" persistence:"position" `
ProfitStats * ProfitStats ` json:"profitStats,omitempty" persistence:"profit_stats" `
coveredPosition fixedpoint . MutexValue
2022-05-05 07:05:38 +00:00
2024-09-07 05:47:21 +00:00
sourceBook , makerBook * types . StreamOrderBook
activeMakerOrders * bbgo . ActiveOrderBook
2021-03-21 02:44:06 +00:00
2022-01-11 14:47:40 +00:00
hedgeErrorLimiter * rate . Limiter
2022-01-10 04:25:13 +00:00
hedgeErrorRateReservation * rate . Reservation
2022-01-09 03:33:34 +00:00
2023-07-04 13:42:24 +00:00
orderStore * core . OrderStore
2023-07-05 07:26:36 +00:00
tradeCollector * core . TradeCollector
2021-03-21 02:44:06 +00:00
2023-11-30 05:44:35 +00:00
askPriceHeartBeat , bidPriceHeartBeat * types . PriceHeartBeat
2022-01-12 04:14:51 +00:00
2024-08-28 08:07:11 +00:00
accountValueCalculator * bbgo . AccountValueCalculator
2024-10-15 10:51:37 +00:00
lastPrice fixedpoint . MutexValue
2021-03-22 09:27:07 +00:00
groupID uint32
2021-03-21 02:44:06 +00:00
stopC chan struct { }
2024-08-24 04:15:52 +00:00
reportProfitStatsRateLimiter * rate . Limiter
circuitBreakerAlertLimiter * rate . Limiter
2024-08-27 09:41:37 +00:00
logger logrus . FieldLogger
2024-08-30 07:44:55 +00:00
metricsLabels prometheus . Labels
2024-10-05 05:09:31 +00:00
2024-10-24 08:12:09 +00:00
sourceMarketDataConnectivity , sourceUserDataConnectivity * types . Connectivity
connectivityGroup * types . ConnectivityGroup
2024-10-09 03:54:13 +00:00
// lastAggregatedSignal stores the last aggregated signal with mutex
// TODO: use float64 series instead, so that we can store history signal values
lastAggregatedSignal MutexFloat64
2024-10-15 09:29:12 +00:00
positionStartedAt * time . Time
positionStartedAtMutex sync . Mutex
2021-03-21 02:44:06 +00:00
}
2021-05-09 12:03:06 +00:00
func ( s * Strategy ) ID ( ) string {
return ID
}
2022-05-05 07:05:38 +00:00
func ( s * Strategy ) InstanceID ( ) string {
return fmt . Sprintf ( "%s:%s" , ID , s . Symbol )
}
2021-03-21 02:44:06 +00:00
func ( s * Strategy ) CrossSubscribe ( sessions map [ string ] * bbgo . ExchangeSession ) {
sourceSession , ok := sessions [ s . SourceExchange ]
if ! ok {
2021-03-21 04:43:41 +00:00
panic ( fmt . Errorf ( "source session %s is not defined" , s . SourceExchange ) )
2021-03-21 02:44:06 +00:00
}
2021-05-17 11:19:32 +00:00
sourceSession . Subscribe ( types . KLineChannel , s . Symbol , types . SubscribeOptions { Interval : "1m" } )
2021-05-17 13:33:55 +00:00
makerSession , ok := sessions [ s . MakerExchange ]
if ! ok {
panic ( fmt . Errorf ( "maker session %s is not defined" , s . MakerExchange ) )
}
2024-09-04 07:59:21 +00:00
2021-05-17 13:33:55 +00:00
makerSession . Subscribe ( types . KLineChannel , s . Symbol , types . SubscribeOptions { Interval : "1m" } )
2024-09-04 07:59:21 +00:00
for _ , sig := range s . SignalConfigList {
if sig . TradeVolumeWindowSignal != nil {
sourceSession . Subscribe ( types . MarketTradeChannel , s . Symbol , types . SubscribeOptions { } )
2024-09-06 13:47:43 +00:00
} else if sig . BollingerBandTrendSignal != nil {
sourceSession . Subscribe ( types . KLineChannel , s . Symbol , types . SubscribeOptions { Interval : sig . BollingerBandTrendSignal . Interval } )
2024-09-04 07:59:21 +00:00
}
}
2024-10-07 09:07:09 +00:00
if s . SubscribeFeeTokenMarkets {
subscribeOpts := types . SubscribeOptions { Interval : "1m" }
sourceSession . Subscribe ( types . KLineChannel , sourceSession . Exchange . PlatformFeeCurrency ( ) + feeTokenQuote , subscribeOpts )
makerSession . Subscribe ( types . KLineChannel , makerSession . Exchange . PlatformFeeCurrency ( ) + feeTokenQuote , subscribeOpts )
}
2021-03-21 02:44:06 +00:00
}
2021-05-10 16:58:11 +00:00
func aggregatePrice ( pvs types . PriceVolumeSlice , requiredQuantity fixedpoint . Value ) ( price fixedpoint . Value ) {
if len ( pvs ) == 0 {
fix bollgrid, emstop, flashcrash, funding, grid, pricealert, pricedrop, rebalance, schedule, swing, xbalance, xgap, xmaker and speedup fixedpoint
2022-02-04 11:39:23 +00:00
price = fixedpoint . Zero
2021-05-10 16:58:11 +00:00
return price
}
2024-08-28 14:32:27 +00:00
sumAmount := fixedpoint . Zero
sumQty := fixedpoint . Zero
2021-05-10 16:58:11 +00:00
for i := 0 ; i < len ( pvs ) ; i ++ {
pv := pvs [ i ]
2024-08-28 14:32:27 +00:00
sumQty = sumQty . Add ( pv . Volume )
sumAmount = sumAmount . Add ( pv . Volume . Mul ( pv . Price ) )
if sumQty . Compare ( requiredQuantity ) >= 0 {
2021-05-10 16:58:11 +00:00
break
}
}
2024-08-28 14:32:27 +00:00
return sumAmount . Div ( sumQty )
2021-05-10 16:10:49 +00:00
}
2023-11-30 05:44:35 +00:00
func ( s * Strategy ) Initialize ( ) error {
s . bidPriceHeartBeat = types . NewPriceHeartBeat ( priceUpdateTimeout )
s . askPriceHeartBeat = types . NewPriceHeartBeat ( priceUpdateTimeout )
2024-08-27 09:41:37 +00:00
s . logger = logrus . WithFields ( logrus . Fields {
"symbol" : s . Symbol ,
"strategy" : ID ,
"strategy_id" : s . InstanceID ( ) ,
} )
2024-08-27 09:30:43 +00:00
2024-08-30 07:44:55 +00:00
s . metricsLabels = prometheus . Labels {
"strategy_type" : ID ,
"strategy_id" : s . InstanceID ( ) ,
"exchange" : s . MakerExchange ,
"symbol" : s . Symbol ,
}
2024-11-20 07:09:18 +00:00
2024-11-20 08:42:19 +00:00
if s . SignalReverseSideMargin != nil && s . SignalReverseSideMargin . Scale != nil {
scale , err := s . SignalReverseSideMargin . Scale . Scale ( )
2024-11-20 07:09:18 +00:00
if err != nil {
return err
}
if solveErr := scale . Solve ( ) ; solveErr != nil {
return solveErr
}
}
2024-11-20 09:56:45 +00:00
if s . SignalTrendSideMarginDiscount != nil && s . SignalTrendSideMarginDiscount . Scale != nil {
scale , err := s . SignalTrendSideMarginDiscount . Scale . Scale ( )
2024-11-20 07:09:18 +00:00
if err != nil {
return err
}
if solveErr := scale . Solve ( ) ; solveErr != nil {
return solveErr
}
}
2024-11-20 09:56:45 +00:00
if s . DelayedHedge != nil && s . DelayedHedge . DynamicDelayScale != nil {
if scale , _ := s . DelayedHedge . DynamicDelayScale . Scale ( ) ; scale != nil {
if err := scale . Solve ( ) ; err != nil {
return err
}
}
}
s . PrintConfig ( os . Stdout , true , false )
2024-08-30 07:44:55 +00:00
return nil
2024-08-27 09:30:43 +00:00
}
2024-11-20 09:56:45 +00:00
func ( s * Strategy ) PrintConfig ( f io . Writer , pretty bool , withColor ... bool ) {
var tableStyle * table . Style
if pretty {
tableStyle = style . NewDefaultTableStyle ( )
}
dynamic . PrintConfig ( s , f , tableStyle , len ( withColor ) > 0 && withColor [ 0 ] , dynamic . DefaultWhiteList ( ) ... )
}
2024-08-27 09:30:43 +00:00
// getBollingerTrend returns -1 when the price is in the downtrend, 1 when the price is in the uptrend, 0 when the price is in the band
func ( s * Strategy ) getBollingerTrend ( quote * Quote ) int {
// when bid price is lower than the down band, then it's in the downtrend
// when ask price is higher than the up band, then it's in the uptrend
lastDownBand := fixedpoint . NewFromFloat ( s . boll . DownBand . Last ( 0 ) )
lastUpBand := fixedpoint . NewFromFloat ( s . boll . UpBand . Last ( 0 ) )
if quote . BestAskPrice . Compare ( lastDownBand ) < 0 {
return - 1
} else if quote . BestBidPrice . Compare ( lastUpBand ) > 0 {
return 1
} else {
return 0
}
}
2024-10-15 09:29:12 +00:00
// setPositionStartTime sets the position start time only if it's not set
func ( s * Strategy ) setPositionStartTime ( now time . Time ) {
s . positionStartedAtMutex . Lock ( )
if s . positionStartedAt == nil {
s . positionStartedAt = & now
}
s . positionStartedAtMutex . Unlock ( )
}
func ( s * Strategy ) resetPositionStartTime ( ) {
s . positionStartedAtMutex . Lock ( )
s . positionStartedAt = nil
s . positionStartedAtMutex . Unlock ( )
}
func ( s * Strategy ) getPositionHoldingPeriod ( now time . Time ) ( time . Duration , bool ) {
s . positionStartedAtMutex . Lock ( )
startedAt := s . positionStartedAt
s . positionStartedAtMutex . Unlock ( )
if startedAt == nil || startedAt . IsZero ( ) {
return 0 , false
}
return now . Sub ( * startedAt ) , true
}
2024-08-30 09:39:25 +00:00
func ( s * Strategy ) applySignalMargin ( ctx context . Context , quote * Quote ) error {
2024-09-07 05:47:21 +00:00
signal , err := s . aggregateSignal ( ctx )
2024-08-30 09:39:25 +00:00
if err != nil {
return err
}
2024-10-09 03:54:13 +00:00
s . lastAggregatedSignal . Set ( signal )
2024-08-30 09:42:20 +00:00
s . logger . Infof ( "aggregated signal: %f" , signal )
2024-08-30 09:39:25 +00:00
2024-08-30 09:52:28 +00:00
if signal == 0.0 {
return nil
}
2024-11-20 08:42:19 +00:00
signalAbs := math . Abs ( signal )
2024-08-30 09:39:25 +00:00
2024-11-20 08:42:19 +00:00
var trendSideMarginDiscount , reverseSideMargin float64
var trendSideMarginDiscountFp , reverseSideMarginFp fixedpoint . Value
2024-11-20 09:56:45 +00:00
if s . SignalTrendSideMarginDiscount != nil && s . SignalTrendSideMarginDiscount . Enabled {
trendSideMarginScale , err := s . SignalTrendSideMarginDiscount . Scale . Scale ( )
2024-11-20 08:42:19 +00:00
if err != nil {
return err
}
2024-08-30 09:39:25 +00:00
2024-11-20 09:56:45 +00:00
if signalAbs > s . SignalTrendSideMarginDiscount . Threshold {
2024-11-20 08:42:19 +00:00
// trendSideMarginDiscount is the discount for the trend side margin
trendSideMarginDiscount = trendSideMarginScale . Call ( math . Abs ( signal ) )
trendSideMarginDiscountFp = fixedpoint . NewFromFloat ( trendSideMarginDiscount )
2024-08-30 09:39:25 +00:00
2024-11-20 08:42:19 +00:00
if signal > 0.0 {
quote . BidMargin = quote . BidMargin . Sub ( trendSideMarginDiscountFp )
} else if signal < 0.0 {
quote . AskMargin = quote . AskMargin . Sub ( trendSideMarginDiscountFp )
}
2024-08-30 09:39:25 +00:00
}
2024-11-20 08:42:19 +00:00
}
2024-08-30 09:42:20 +00:00
2024-11-20 08:42:19 +00:00
if s . SignalReverseSideMargin != nil && s . SignalReverseSideMargin . Enabled {
reverseSideMarginScale , err := s . SignalReverseSideMargin . Scale . Scale ( )
if err != nil {
return err
}
if signalAbs > s . SignalReverseSideMargin . Threshold {
reverseSideMargin = reverseSideMarginScale . Call ( math . Abs ( signal ) )
reverseSideMarginFp = fixedpoint . NewFromFloat ( reverseSideMargin )
if signal < 0.0 {
quote . BidMargin = quote . BidMargin . Add ( reverseSideMarginFp )
} else if signal > 0.0 {
quote . AskMargin = quote . AskMargin . Add ( reverseSideMarginFp )
}
2024-08-30 09:39:25 +00:00
}
2024-11-20 08:42:19 +00:00
}
2024-08-30 09:42:20 +00:00
2024-11-20 08:42:19 +00:00
s . logger . Infof ( "signal margin params: signal = %f, reverseSideMargin = %f, trendSideMarginDiscount = %f" , signal , reverseSideMargin , trendSideMarginDiscount )
s . logger . Infof ( "calculated signal margin: signal = %f, askMargin = %s, bidMargin = %s" ,
signal ,
quote . AskMargin ,
quote . BidMargin ,
)
if s . MinMargin != nil {
quote . AskMargin = fixedpoint . Max ( * s . MinMargin , quote . AskMargin )
quote . BidMargin = fixedpoint . Max ( * s . MinMargin , quote . BidMargin )
2024-08-30 09:39:25 +00:00
}
2024-11-20 08:42:19 +00:00
s . logger . Infof ( "final signal margin: signal = %f, askMargin = %s, bidMargin = %s" ,
signal ,
quote . AskMargin ,
quote . BidMargin ,
)
2024-08-30 09:39:25 +00:00
return nil
}
2024-08-27 09:30:43 +00:00
// applyBollingerMargin applies the bollinger band margin to the quote
func ( s * Strategy ) applyBollingerMargin (
quote * Quote ,
) error {
lastDownBand := fixedpoint . NewFromFloat ( s . boll . DownBand . Last ( 0 ) )
lastUpBand := fixedpoint . NewFromFloat ( s . boll . UpBand . Last ( 0 ) )
if lastUpBand . IsZero ( ) || lastDownBand . IsZero ( ) {
2024-08-27 09:41:37 +00:00
s . logger . Warnf ( "bollinger band value is zero, skipping" )
2024-08-27 09:30:43 +00:00
return nil
}
factor := fixedpoint . Min ( s . BollBandMarginFactor , fixedpoint . One )
switch s . getBollingerTrend ( quote ) {
case - 1 :
// for the downtrend, increase the bid margin
// ratio here should be greater than 1.00
ratio := fixedpoint . Min ( lastDownBand . Div ( quote . BestAskPrice ) , fixedpoint . One )
// so that 1.x can multiply the original bid margin
bollMargin := s . BollBandMargin . Mul ( ratio ) . Mul ( factor )
2024-08-27 09:41:37 +00:00
s . logger . Infof ( "%s bollband downtrend: increasing bid margin %f (bidMargin) + %f (bollMargin) = %f (finalBidMargin)" ,
2024-08-27 09:30:43 +00:00
s . Symbol ,
quote . BidMargin . Float64 ( ) ,
bollMargin . Float64 ( ) ,
quote . BidMargin . Add ( bollMargin ) . Float64 ( ) )
quote . BidMargin = quote . BidMargin . Add ( bollMargin )
quote . BidLayerPips = quote . BidLayerPips . Mul ( ratio )
case 1 :
// for the uptrend, increase the ask margin
// ratio here should be greater than 1.00
ratio := fixedpoint . Min ( quote . BestAskPrice . Div ( lastUpBand ) , fixedpoint . One )
// so that the original bid margin can be multiplied by 1.x
bollMargin := s . BollBandMargin . Mul ( ratio ) . Mul ( factor )
2024-08-28 08:07:11 +00:00
s . logger . Infof ( "%s bollband uptrend adjusting ask margin %f (askMargin) + %f (bollMargin) = %f (finalAskMargin)" ,
2024-08-27 09:30:43 +00:00
s . Symbol ,
quote . AskMargin . Float64 ( ) ,
bollMargin . Float64 ( ) ,
quote . AskMargin . Add ( bollMargin ) . Float64 ( ) )
quote . AskMargin = quote . AskMargin . Add ( bollMargin )
quote . AskLayerPips = quote . AskLayerPips . Mul ( ratio )
default :
// default, in the band
}
return nil
}
2024-09-07 05:47:21 +00:00
func ( s * Strategy ) aggregateSignal ( ctx context . Context ) ( float64 , error ) {
2024-08-30 07:44:55 +00:00
sum := 0.0
voters := 0.0
2024-10-09 04:33:11 +00:00
for _ , signalConfig := range s . SignalConfigList {
signalProvider := signalConfig . Get ( )
sig , err := signalProvider . CalculateSignal ( ctx )
2024-09-04 07:59:21 +00:00
if err != nil {
return 0 , err
} else if sig == 0.0 {
continue
}
2024-10-09 04:33:11 +00:00
if signalConfig . Weight > 0.0 {
sum += sig * signalConfig . Weight
voters += signalConfig . Weight
2024-09-04 07:59:21 +00:00
} else {
sum += sig
voters ++
}
}
if sum == 0.0 {
return 0.0 , nil
2024-08-30 07:44:55 +00:00
}
return sum / voters , nil
}
2024-09-07 06:19:07 +00:00
// getInitialLayerQuantity returns the initial quantity for the layer
// i is the layer index, starting from 0
func ( s * Strategy ) getInitialLayerQuantity ( i int ) ( fixedpoint . Value , error ) {
if s . QuantityScale != nil {
qf , err := s . QuantityScale . Scale ( i + 1 )
if err != nil {
return fixedpoint . Zero , fmt . Errorf ( "quantityScale error: %w" , err )
}
log . Infof ( "%s scaling bid #%d quantity to %f" , s . Symbol , i + 1 , qf )
// override the default quantity
return fixedpoint . NewFromFloat ( qf ) , nil
}
q := s . Quantity
if s . QuantityMultiplier . Sign ( ) > 0 && i > 0 {
q = fixedpoint . NewFromFloat (
q . Float64 ( ) * math . Pow (
s . QuantityMultiplier . Float64 ( ) , float64 ( i + 1 ) ) )
}
// fallback to the fixed quantity
return q , nil
}
2024-09-09 07:04:56 +00:00
// getLayerPrice returns the price for the layer
// i is the layer index, starting from 0
// side is the side of the order
// sourceBook is the source order book
2024-09-09 06:41:41 +00:00
func ( s * Strategy ) getLayerPrice (
i int ,
side types . SideType ,
sourceBook * types . StreamOrderBook ,
quote * Quote ,
requiredDepth fixedpoint . Value ,
) ( price fixedpoint . Value ) {
var margin , delta , pips fixedpoint . Value
switch side {
case types . SideTypeSell :
margin = quote . AskMargin
delta = margin
if quote . AskLayerPips . Sign ( ) > 0 {
pips = quote . AskLayerPips
} else {
pips = fixedpoint . One
}
case types . SideTypeBuy :
margin = quote . BidMargin
delta = margin . Neg ( )
if quote . BidLayerPips . Sign ( ) > 0 {
pips = quote . BidLayerPips . Neg ( )
} else {
pips = fixedpoint . One . Neg ( )
}
}
2024-09-15 16:29:08 +00:00
sideBook := sourceBook . SideBook ( side )
if pv , ok := sideBook . First ( ) ; ok {
price = pv . Price
}
2024-09-09 07:04:56 +00:00
if requiredDepth . Sign ( ) > 0 {
2024-09-15 16:29:08 +00:00
price = aggregatePrice ( sideBook , requiredDepth )
2024-09-09 06:41:41 +00:00
price = price . Mul ( fixedpoint . One . Add ( delta ) )
if i > 0 {
price = price . Add ( pips . Mul ( s . makerMarket . TickSize ) )
}
} else {
price = price . Mul ( fixedpoint . One . Add ( delta ) )
if i > 0 {
price = price . Add ( pips . Mul ( s . makerMarket . TickSize ) )
}
}
return price
}
2024-09-07 06:19:07 +00:00
func ( s * Strategy ) updateQuote ( ctx context . Context ) error {
2024-10-09 09:09:28 +00:00
cancelMakerOrdersProfile := timeprofile . Start ( "cancelMakerOrders" )
2022-01-13 03:01:46 +00:00
if err := s . activeMakerOrders . GracefulCancel ( ctx , s . makerSession . Exchange ) ; err != nil {
2024-08-27 09:41:37 +00:00
s . logger . Warnf ( "there are some %s orders not canceled, skipping placing maker orders" , s . Symbol )
2022-01-13 03:01:46 +00:00
s . activeMakerOrders . Print ( )
2024-09-07 06:19:07 +00:00
return nil
2021-03-21 02:44:06 +00:00
}
2024-10-09 09:09:28 +00:00
cancelOrderDurationMetrics . With ( s . metricsLabels ) . Observe ( float64 ( cancelMakerOrdersProfile . Stop ( ) . Milliseconds ( ) ) )
2022-06-05 10:12:26 +00:00
if s . activeMakerOrders . NumOfOrders ( ) > 0 {
2024-09-07 05:47:21 +00:00
s . logger . Warnf ( "unable to cancel all %s orders, skipping placing maker orders" , s . Symbol )
2024-09-07 06:19:07 +00:00
return nil
2021-05-09 10:32:05 +00:00
}
2021-03-21 02:44:06 +00:00
2024-11-18 06:11:08 +00:00
if ! s . sourceSession . Connectivity . IsConnected ( ) {
s . logger . Warnf ( "source session is disconnected, skipping update quote" )
return nil
}
if ! s . makerSession . Connectivity . IsConnected ( ) {
s . logger . Warnf ( "maker session is disconnected, skipping update quote" )
2024-10-24 08:12:09 +00:00
return nil
}
2024-09-07 05:47:21 +00:00
signal , err := s . aggregateSignal ( ctx )
2024-08-30 07:44:55 +00:00
if err != nil {
2024-09-07 06:19:07 +00:00
return err
2024-08-30 07:44:55 +00:00
}
2024-08-30 09:39:25 +00:00
s . logger . Infof ( "aggregated signal: %f" , signal )
aggregatedSignalMetrics . With ( s . metricsLabels ) . Set ( signal )
2024-08-30 07:44:55 +00:00
2024-08-24 03:58:09 +00:00
if s . CircuitBreaker != nil {
now := time . Now ( )
if reason , halted := s . CircuitBreaker . IsHalted ( now ) ; halted {
2024-09-02 07:30:14 +00:00
s . logger . Warnf ( "strategy %s is halted, reason: %s" , ID , reason )
2024-08-24 03:58:09 +00:00
2024-08-24 04:15:52 +00:00
if s . circuitBreakerAlertLimiter . AllowN ( now , 1 ) {
2024-09-02 07:30:14 +00:00
bbgo . Notify ( "Strategy %s is halted, reason: %s" , ID , reason )
2024-08-24 03:58:09 +00:00
}
2024-09-07 06:19:07 +00:00
return nil
2024-08-24 03:58:09 +00:00
}
}
2024-09-06 13:47:43 +00:00
bestBid , bestAsk , hasPrice := s . sourceBook . BestBidAndAsk ( )
2021-06-07 09:02:24 +00:00
if ! hasPrice {
2024-09-02 07:30:14 +00:00
s . logger . Warnf ( "no valid price, skip quoting" )
2024-09-07 06:19:07 +00:00
return fmt . Errorf ( "no valid book price" )
2021-06-07 09:02:24 +00:00
}
2024-09-07 05:47:21 +00:00
bestBidPrice := bestBid . Price
bestAskPrice := bestAsk . Price
s . logger . Infof ( "%s book ticker: best ask / best bid = %v / %v" , s . Symbol , bestAskPrice , bestBidPrice )
if bestBidPrice . Compare ( bestAskPrice ) > 0 {
2024-09-07 06:19:07 +00:00
return fmt . Errorf ( "best bid price %f is higher than best ask price %f, skip quoting" ,
2024-09-07 05:47:21 +00:00
bestBidPrice . Float64 ( ) ,
bestAskPrice . Float64 ( ) ,
)
}
2021-12-26 04:10:10 +00:00
// use mid-price for the last price
2024-10-15 10:51:37 +00:00
midPrice := bestBid . Price . Add ( bestAsk . Price ) . Div ( two )
s . lastPrice . Set ( midPrice )
s . priceSolver . Update ( s . Symbol , midPrice )
2024-08-24 04:28:05 +00:00
2024-09-06 13:47:43 +00:00
bookLastUpdateTime := s . sourceBook . LastUpdateTime ( )
2022-01-12 14:11:28 +00:00
2023-11-30 05:44:35 +00:00
if _ , err := s . bidPriceHeartBeat . Update ( bestBid ) ; err != nil {
2024-08-27 09:41:37 +00:00
s . logger . WithError ( err ) . Errorf ( "quote update error, %s price not updating, order book last update: %s ago" ,
2022-01-12 14:11:28 +00:00
s . Symbol ,
time . Since ( bookLastUpdateTime ) )
2024-10-09 04:33:11 +00:00
s . sourceSession . MarketDataStream . Reconnect ( )
2024-11-08 00:37:08 +00:00
s . sourceBook . Reset ( )
2024-09-07 06:19:07 +00:00
return err
2022-01-12 03:55:45 +00:00
}
2023-11-30 05:44:35 +00:00
if _ , err := s . askPriceHeartBeat . Update ( bestAsk ) ; err != nil {
2024-08-27 09:41:37 +00:00
s . logger . WithError ( err ) . Errorf ( "quote update error, %s price not updating, order book last update: %s ago" ,
2022-01-12 14:11:28 +00:00
s . Symbol ,
time . Since ( bookLastUpdateTime ) )
2024-10-09 04:33:11 +00:00
s . sourceSession . MarketDataStream . Reconnect ( )
2024-11-08 00:37:08 +00:00
s . sourceBook . Reset ( )
2024-09-07 06:19:07 +00:00
return err
2022-01-12 03:55:45 +00:00
}
2024-09-06 13:47:43 +00:00
sourceBook := s . sourceBook . CopyDepth ( 10 )
2021-03-21 02:44:06 +00:00
if valid , err := sourceBook . IsValid ( ) ; ! valid {
2024-08-27 09:41:37 +00:00
s . logger . WithError ( err ) . Errorf ( "%s invalid copied order book, skip quoting: %v" , s . Symbol , err )
2024-09-07 06:19:07 +00:00
return err
2021-03-21 02:44:06 +00:00
}
2021-03-21 03:16:15 +00:00
var disableMakerBid = false
var disableMakerAsk = false
2021-03-21 02:44:06 +00:00
2021-05-10 16:10:49 +00:00
// check maker's balance quota
// we load the balances from the account while we're generating the orders,
2021-03-21 03:16:15 +00:00
// the balance may have a chance to be deducted by other strategies or manual orders submitted by the user
2024-09-02 08:08:51 +00:00
makerBalances := s . makerSession . GetAccount ( ) . Balances ( ) . NotZero ( )
s . logger . Infof ( "maker balances: %+v" , makerBalances )
2021-03-21 02:44:06 +00:00
makerQuota := & bbgo . QuotaTransaction { }
2021-03-21 03:16:15 +00:00
if b , ok := makerBalances [ s . makerMarket . BaseCurrency ] ; ok {
2024-10-15 10:51:37 +00:00
if s . makerMarket . IsDustQuantity ( b . Available , s . lastPrice . Get ( ) ) {
2021-03-21 04:43:41 +00:00
disableMakerAsk = true
2024-09-01 08:41:16 +00:00
s . logger . Infof ( "%s maker ask disabled: insufficient base balance %s" , s . Symbol , b . String ( ) )
2024-09-02 07:30:14 +00:00
} else {
makerQuota . BaseAsset . Add ( b . Available )
2021-03-21 04:43:41 +00:00
}
2024-09-02 08:08:51 +00:00
} else {
disableMakerAsk = true
s . logger . Infof ( "%s maker ask disabled: base balance %s not found" , s . Symbol , b . String ( ) )
2021-03-21 02:44:06 +00:00
}
2021-03-21 04:43:41 +00:00
2021-03-21 03:16:15 +00:00
if b , ok := makerBalances [ s . makerMarket . QuoteCurrency ] ; ok {
fix bollgrid, emstop, flashcrash, funding, grid, pricealert, pricedrop, rebalance, schedule, swing, xbalance, xgap, xmaker and speedup fixedpoint
2022-02-04 11:39:23 +00:00
if b . Available . Compare ( s . makerMarket . MinNotional ) > 0 {
2024-11-20 08:42:19 +00:00
if s . MaxQuoteQuotaRatio . Sign ( ) > 0 {
quoteAvailable := b . Available . Mul ( s . MaxQuoteQuotaRatio )
2024-11-20 05:44:06 +00:00
makerQuota . QuoteAsset . Add ( quoteAvailable )
} else {
// use all quote balances as much as possible
makerQuota . QuoteAsset . Add ( b . Available )
}
2021-05-10 16:10:49 +00:00
} else {
2021-04-04 03:14:09 +00:00
disableMakerBid = true
2024-09-01 08:41:16 +00:00
s . logger . Infof ( "%s maker bid disabled: insufficient quote balance %s" , s . Symbol , b . String ( ) )
2021-04-04 03:14:09 +00:00
}
2024-09-02 08:08:51 +00:00
} else {
disableMakerBid = true
s . logger . Infof ( "%s maker bid disabled: quote balance %s not found" , s . Symbol , b . String ( ) )
2021-04-04 03:14:09 +00:00
}
2024-09-02 14:18:13 +00:00
s . logger . Infof ( "maker quota: %+v" , makerQuota )
2024-08-28 08:07:11 +00:00
// if
// 1) the source session is a margin session
// 2) the min margin level is configured
// 3) the hedge account's margin level is lower than the min margin level
hedgeAccount := s . sourceSession . GetAccount ( )
hedgeBalances := hedgeAccount . Balances ( )
2021-03-21 02:44:06 +00:00
hedgeQuota := & bbgo . QuotaTransaction { }
2024-08-28 08:07:11 +00:00
if s . sourceSession . Margin &&
! s . MinMarginLevel . IsZero ( ) &&
! hedgeAccount . MarginLevel . IsZero ( ) {
if hedgeAccount . MarginLevel . Compare ( s . MinMarginLevel ) < 0 {
2024-08-31 16:58:50 +00:00
s . logger . Infof ( "hedge account margin level %s is less then the min margin level %s, calculating the borrowed positions" ,
hedgeAccount . MarginLevel . String ( ) ,
s . MinMarginLevel . String ( ) )
2024-09-02 07:30:14 +00:00
// TODO: should consider base asset debt as well.
2024-08-28 08:07:11 +00:00
if quote , ok := hedgeAccount . Balance ( s . sourceMarket . QuoteCurrency ) ; ok {
quoteDebt := quote . Debt ( )
if quoteDebt . Sign ( ) > 0 {
hedgeQuota . BaseAsset . Add ( quoteDebt . Div ( bestBid . Price ) )
}
}
if base , ok := hedgeAccount . Balance ( s . sourceMarket . BaseCurrency ) ; ok {
baseDebt := base . Debt ( )
if baseDebt . Sign ( ) > 0 {
hedgeQuota . QuoteAsset . Add ( baseDebt . Mul ( bestAsk . Price ) )
}
}
} else {
2024-08-31 16:58:50 +00:00
s . logger . Infof ( "hedge account margin level %s is greater than the min margin level %s, calculating the net value" ,
hedgeAccount . MarginLevel . String ( ) ,
s . MinMarginLevel . String ( ) )
2024-10-05 05:09:31 +00:00
netValueInUsd := s . accountValueCalculator . NetValue ( )
2024-08-31 16:58:50 +00:00
2024-10-05 05:09:31 +00:00
// calculate credit buffer
s . logger . Infof ( "hedge account net value in usd: %f" , netValueInUsd . Float64 ( ) )
2024-08-31 17:31:50 +00:00
2024-10-05 05:09:31 +00:00
maximumValueInUsd := netValueInUsd . Mul ( s . MaxHedgeAccountLeverage )
2024-08-31 17:31:50 +00:00
2024-10-05 05:09:31 +00:00
s . logger . Infof ( "hedge account maximum leveraged value in usd: %f (%f x)" , maximumValueInUsd . Float64 ( ) , s . MaxHedgeAccountLeverage . Float64 ( ) )
2024-08-31 16:58:50 +00:00
2024-10-05 05:09:31 +00:00
if quote , ok := hedgeAccount . Balance ( s . sourceMarket . QuoteCurrency ) ; ok {
debt := quote . Debt ( )
quota := maximumValueInUsd . Sub ( debt )
2024-08-31 16:58:50 +00:00
2024-10-05 05:09:31 +00:00
s . logger . Infof ( "hedge account quote balance: %s, debt: %s, quota: %s" ,
quote . String ( ) ,
debt . String ( ) ,
quota . String ( ) )
2024-08-28 08:07:11 +00:00
2024-10-05 05:09:31 +00:00
hedgeQuota . QuoteAsset . Add ( quota )
}
2024-08-31 16:58:50 +00:00
2024-10-05 05:09:31 +00:00
if base , ok := hedgeAccount . Balance ( s . sourceMarket . BaseCurrency ) ; ok {
debt := base . Debt ( )
quota := maximumValueInUsd . Div ( bestAsk . Price ) . Sub ( debt )
2024-08-31 16:58:50 +00:00
2024-10-05 05:09:31 +00:00
s . logger . Infof ( "hedge account base balance: %s, debt: %s, quota: %s" ,
base . String ( ) ,
debt . String ( ) ,
quota . String ( ) )
hedgeQuota . BaseAsset . Add ( quota )
2024-08-28 08:07:11 +00:00
}
}
} else {
if b , ok := hedgeBalances [ s . sourceMarket . BaseCurrency ] ; ok {
// to make bid orders, we need enough base asset in the foreign exchange,
// if the base asset balance is not enough for selling
if s . StopHedgeBaseBalance . Sign ( ) > 0 {
minAvailable := s . StopHedgeBaseBalance . Add ( s . sourceMarket . MinQuantity )
if b . Available . Compare ( minAvailable ) > 0 {
hedgeQuota . BaseAsset . Add ( b . Available . Sub ( minAvailable ) )
} else {
2024-09-01 08:41:16 +00:00
s . logger . Warnf ( "%s maker bid disabled: insufficient hedge base balance %s" , s . Symbol , b . String ( ) )
2024-08-28 08:07:11 +00:00
disableMakerBid = true
}
} else if b . Available . Compare ( s . sourceMarket . MinQuantity ) > 0 {
hedgeQuota . BaseAsset . Add ( b . Available )
2021-05-26 15:04:25 +00:00
} else {
2024-09-01 08:41:16 +00:00
s . logger . Warnf ( "%s maker bid disabled: insufficient hedge base balance %s" , s . Symbol , b . String ( ) )
2021-05-26 15:04:25 +00:00
disableMakerBid = true
}
2021-03-21 03:16:15 +00:00
}
2024-08-28 08:07:11 +00:00
if b , ok := hedgeBalances [ s . sourceMarket . QuoteCurrency ] ; ok {
// to make ask orders, we need enough quote asset in the foreign exchange,
// if the quote asset balance is not enough for buying
if s . StopHedgeQuoteBalance . Sign ( ) > 0 {
minAvailable := s . StopHedgeQuoteBalance . Add ( s . sourceMarket . MinNotional )
if b . Available . Compare ( minAvailable ) > 0 {
hedgeQuota . QuoteAsset . Add ( b . Available . Sub ( minAvailable ) )
} else {
2024-09-01 08:41:16 +00:00
s . logger . Warnf ( "%s maker ask disabled: insufficient hedge quote balance %s" , s . Symbol , b . String ( ) )
2024-08-28 08:07:11 +00:00
disableMakerAsk = true
}
} else if b . Available . Compare ( s . sourceMarket . MinNotional ) > 0 {
hedgeQuota . QuoteAsset . Add ( b . Available )
2021-05-26 15:04:25 +00:00
} else {
2024-09-01 08:41:16 +00:00
s . logger . Warnf ( "%s maker ask disabled: insufficient hedge quote balance %s" , s . Symbol , b . String ( ) )
2021-05-26 15:04:25 +00:00
disableMakerAsk = true
}
2021-03-21 03:16:15 +00:00
}
}
2021-03-21 02:44:06 +00:00
2021-05-10 16:10:49 +00:00
// if max exposure position is configured, we should not:
// 1. place bid orders when we already bought too much
// 2. place ask orders when we already sold too much
fix bollgrid, emstop, flashcrash, funding, grid, pricealert, pricedrop, rebalance, schedule, swing, xbalance, xgap, xmaker and speedup fixedpoint
2022-02-04 11:39:23 +00:00
if s . MaxExposurePosition . Sign ( ) > 0 {
2022-05-05 07:05:38 +00:00
pos := s . Position . GetBase ( )
2021-12-31 07:13:26 +00:00
2024-09-02 19:25:37 +00:00
if pos . Compare ( s . MaxExposurePosition . Neg ( ) ) <= 0 {
2021-05-10 16:10:49 +00:00
// stop sell if we over-sell
disableMakerAsk = true
2024-09-02 19:25:37 +00:00
s . logger . Warnf ( "%s ask maker is disabled: %f exceeded max exposure %f" , s . Symbol , pos . Float64 ( ) , s . MaxExposurePosition . Float64 ( ) )
} else if pos . Compare ( s . MaxExposurePosition ) >= 0 {
2021-05-10 16:10:49 +00:00
// stop buy if we over buy
disableMakerBid = true
2024-09-02 19:25:37 +00:00
s . logger . Warnf ( "%s bid maker is disabled: %f exceeded max exposure %f" , s . Symbol , pos . Float64 ( ) , s . MaxExposurePosition . Float64 ( ) )
2021-05-10 16:10:49 +00:00
}
}
2021-03-21 04:43:41 +00:00
if disableMakerAsk && disableMakerBid {
2024-11-20 06:21:17 +00:00
log . Warnf ( "%s bid/ask maker is disabled" , s . Symbol )
2024-09-07 06:19:07 +00:00
return nil
2021-03-21 04:43:41 +00:00
}
2021-05-10 16:10:49 +00:00
var submitOrders [ ] types . SubmitOrder
2021-05-10 16:58:11 +00:00
var accumulativeBidQuantity , accumulativeAskQuantity fixedpoint . Value
2022-06-14 17:18:46 +00:00
2024-08-27 09:30:43 +00:00
var quote = & Quote {
BestBidPrice : bestBidPrice ,
BestAskPrice : bestAskPrice ,
BidMargin : s . BidMargin ,
AskMargin : s . AskMargin ,
BidLayerPips : s . Pips ,
AskLayerPips : s . Pips ,
}
2021-05-17 12:03:42 +00:00
2024-08-30 09:39:25 +00:00
if s . EnableSignalMargin {
if err := s . applySignalMargin ( ctx , quote ) ; err != nil {
s . logger . WithError ( err ) . Errorf ( "unable to apply signal margin" )
}
} else if s . EnableBollBandMargin {
2024-08-27 09:30:43 +00:00
if err := s . applyBollingerMargin ( quote ) ; err != nil {
log . WithError ( err ) . Errorf ( "unable to apply bollinger margin" )
2021-05-17 12:03:42 +00:00
}
}
2024-08-24 13:17:32 +00:00
bidExposureInUsd := fixedpoint . Zero
askExposureInUsd := fixedpoint . Zero
2024-08-27 07:39:38 +00:00
2024-08-30 07:44:55 +00:00
bidMarginMetrics . With ( s . metricsLabels ) . Set ( quote . BidMargin . Float64 ( ) )
askMarginMetrics . With ( s . metricsLabels ) . Set ( quote . AskMargin . Float64 ( ) )
2024-08-27 07:39:38 +00:00
2024-09-09 09:49:53 +00:00
if s . EnableArbitrage {
2024-09-09 14:03:06 +00:00
done , err := s . tryArbitrage ( ctx , quote , makerBalances , hedgeBalances )
2024-09-09 09:49:53 +00:00
if err != nil {
s . logger . WithError ( err ) . Errorf ( "unable to arbitrage" )
} else if done {
return nil
}
}
2024-09-07 06:19:07 +00:00
if ! disableMakerBid {
for i := 0 ; i < s . NumLayers ; i ++ {
bidQuantity , err := s . getInitialLayerQuantity ( i )
if err != nil {
return err
2021-05-09 18:52:41 +00:00
}
2024-09-07 06:19:07 +00:00
// for maker bid orders
fix bollgrid, emstop, flashcrash, funding, grid, pricealert, pricedrop, rebalance, schedule, swing, xbalance, xgap, xmaker and speedup fixedpoint
2022-02-04 11:39:23 +00:00
accumulativeBidQuantity = accumulativeBidQuantity . Add ( bidQuantity )
2024-08-27 10:09:14 +00:00
2024-09-09 07:04:56 +00:00
requiredDepth := fixedpoint . Zero
2021-06-06 18:49:44 +00:00
if s . UseDepthPrice {
fix bollgrid, emstop, flashcrash, funding, grid, pricealert, pricedrop, rebalance, schedule, swing, xbalance, xgap, xmaker and speedup fixedpoint
2022-02-04 11:39:23 +00:00
if s . DepthQuantity . Sign ( ) > 0 {
2024-09-09 07:04:56 +00:00
requiredDepth = s . DepthQuantity
2022-01-11 14:47:40 +00:00
} else {
2024-09-09 07:04:56 +00:00
requiredDepth = accumulativeBidQuantity
2022-01-11 14:47:40 +00:00
}
2021-06-06 18:49:44 +00:00
}
2021-05-17 15:55:09 +00:00
2024-09-09 09:49:53 +00:00
bidPrice := s . getLayerPrice ( i , types . SideTypeBuy , s . sourceBook , quote , requiredDepth )
2024-09-09 07:04:56 +00:00
2024-08-27 07:39:38 +00:00
if i == 0 {
2024-08-28 14:32:41 +00:00
s . logger . Infof ( "maker best bid price %f" , bidPrice . Float64 ( ) )
2024-08-30 07:44:55 +00:00
makerBestBidPriceMetrics . With ( s . metricsLabels ) . Set ( bidPrice . Float64 ( ) )
2021-05-10 17:06:39 +00:00
}
2021-05-10 16:58:11 +00:00
2021-03-21 03:16:15 +00:00
if makerQuota . QuoteAsset . Lock ( bidQuantity . Mul ( bidPrice ) ) && hedgeQuota . BaseAsset . Lock ( bidQuantity ) {
// if we bought, then we need to sell the base from the hedge session
submitOrders = append ( submitOrders , types . SubmitOrder {
Symbol : s . Symbol ,
Type : types . OrderTypeLimit ,
Side : types . SideTypeBuy ,
fix bollgrid, emstop, flashcrash, funding, grid, pricealert, pricedrop, rebalance, schedule, swing, xbalance, xgap, xmaker and speedup fixedpoint
2022-02-04 11:39:23 +00:00
Price : bidPrice ,
Quantity : bidQuantity ,
2022-02-18 05:52:13 +00:00
TimeInForce : types . TimeInForceGTC ,
2021-03-21 03:16:15 +00:00
GroupID : s . groupID ,
} )
makerQuota . Commit ( )
hedgeQuota . Commit ( )
2024-08-24 13:17:32 +00:00
bidExposureInUsd = bidExposureInUsd . Add ( bidQuantity . Mul ( bidPrice ) )
2021-03-21 03:16:15 +00:00
} else {
makerQuota . Rollback ( )
hedgeQuota . Rollback ( )
}
2021-05-09 18:52:41 +00:00
2021-03-21 02:44:06 +00:00
}
2024-09-07 06:19:07 +00:00
}
2021-03-21 02:44:06 +00:00
2024-09-09 06:41:41 +00:00
// for maker ask orders
if ! disableMakerAsk {
for i := 0 ; i < s . NumLayers ; i ++ {
askQuantity , err := s . getInitialLayerQuantity ( i )
if err != nil {
return err
2021-05-09 18:52:41 +00:00
}
2024-09-09 06:41:41 +00:00
fix bollgrid, emstop, flashcrash, funding, grid, pricealert, pricedrop, rebalance, schedule, swing, xbalance, xgap, xmaker and speedup fixedpoint
2022-02-04 11:39:23 +00:00
accumulativeAskQuantity = accumulativeAskQuantity . Add ( askQuantity )
2021-05-10 16:58:11 +00:00
2024-09-09 07:04:56 +00:00
requiredDepth := fixedpoint . Zero
2021-06-06 18:49:44 +00:00
if s . UseDepthPrice {
fix bollgrid, emstop, flashcrash, funding, grid, pricealert, pricedrop, rebalance, schedule, swing, xbalance, xgap, xmaker and speedup fixedpoint
2022-02-04 11:39:23 +00:00
if s . DepthQuantity . Sign ( ) > 0 {
2024-09-09 07:04:56 +00:00
requiredDepth = s . DepthQuantity
2022-01-11 14:47:40 +00:00
} else {
2024-09-09 07:04:56 +00:00
requiredDepth = accumulativeAskQuantity
2022-01-11 14:47:40 +00:00
}
2021-06-06 18:49:44 +00:00
}
2024-09-09 09:49:53 +00:00
askPrice := s . getLayerPrice ( i , types . SideTypeSell , s . sourceBook , quote , requiredDepth )
2024-09-09 07:04:56 +00:00
2024-08-27 07:39:38 +00:00
if i == 0 {
2024-08-28 14:32:41 +00:00
s . logger . Infof ( "maker best ask price %f" , askPrice . Float64 ( ) )
2024-08-30 07:44:55 +00:00
makerBestAskPriceMetrics . With ( s . metricsLabels ) . Set ( askPrice . Float64 ( ) )
2021-05-10 17:06:39 +00:00
}
2021-05-09 18:52:41 +00:00
2021-03-21 03:16:15 +00:00
if makerQuota . BaseAsset . Lock ( askQuantity ) && hedgeQuota . QuoteAsset . Lock ( askQuantity . Mul ( askPrice ) ) {
2024-08-24 13:17:32 +00:00
2021-03-21 03:16:15 +00:00
// if we bought, then we need to sell the base from the hedge session
submitOrders = append ( submitOrders , types . SubmitOrder {
Symbol : s . Symbol ,
2021-06-06 18:03:58 +00:00
Market : s . makerMarket ,
2021-03-21 03:16:15 +00:00
Type : types . OrderTypeLimit ,
Side : types . SideTypeSell ,
fix bollgrid, emstop, flashcrash, funding, grid, pricealert, pricedrop, rebalance, schedule, swing, xbalance, xgap, xmaker and speedup fixedpoint
2022-02-04 11:39:23 +00:00
Price : askPrice ,
Quantity : askQuantity ,
2022-02-18 05:52:13 +00:00
TimeInForce : types . TimeInForceGTC ,
2021-03-21 03:16:15 +00:00
GroupID : s . groupID ,
} )
makerQuota . Commit ( )
hedgeQuota . Commit ( )
2024-08-24 13:17:32 +00:00
askExposureInUsd = askExposureInUsd . Add ( askQuantity . Mul ( askPrice ) )
2021-03-21 03:16:15 +00:00
} else {
makerQuota . Rollback ( )
hedgeQuota . Rollback ( )
}
2021-05-09 18:52:41 +00:00
fix bollgrid, emstop, flashcrash, funding, grid, pricealert, pricedrop, rebalance, schedule, swing, xbalance, xgap, xmaker and speedup fixedpoint
2022-02-04 11:39:23 +00:00
if s . QuantityMultiplier . Sign ( ) > 0 {
2021-05-09 18:52:41 +00:00
askQuantity = askQuantity . Mul ( s . QuantityMultiplier )
}
2021-03-21 02:44:06 +00:00
}
}
if len ( submitOrders ) == 0 {
2021-05-17 13:33:55 +00:00
log . Warnf ( "no orders generated" )
2024-09-07 06:19:07 +00:00
return nil
2021-03-21 02:44:06 +00:00
}
2024-08-24 13:17:32 +00:00
formattedOrders , err := s . makerSession . FormatOrders ( submitOrders )
2021-03-21 02:44:06 +00:00
if err != nil {
2024-09-07 06:19:07 +00:00
return err
2021-03-21 02:44:06 +00:00
}
2024-08-24 13:17:32 +00:00
defer s . tradeCollector . Process ( )
2024-10-09 03:47:10 +00:00
makerOrderPlacementProfile := timeprofile . Start ( "makerOrderPlacement" )
2024-09-09 09:49:53 +00:00
createdOrders , errIdx , err := bbgo . BatchPlaceOrder ( ctx , s . makerSession . Exchange , s . makerOrderCreateCallback , formattedOrders ... )
2024-08-24 13:17:32 +00:00
if err != nil {
log . WithError ( err ) . Errorf ( "unable to place maker orders: %+v" , formattedOrders )
2024-09-07 06:19:07 +00:00
return err
2024-08-24 13:17:32 +00:00
}
2024-10-09 03:47:10 +00:00
makerOrderPlacementDurationMetrics . With ( s . metricsLabels ) . Observe ( float64 ( makerOrderPlacementProfile . Stop ( ) . Milliseconds ( ) ) )
2024-08-30 07:44:55 +00:00
openOrderBidExposureInUsdMetrics . With ( s . metricsLabels ) . Set ( bidExposureInUsd . Float64 ( ) )
openOrderAskExposureInUsdMetrics . With ( s . metricsLabels ) . Set ( askExposureInUsd . Float64 ( ) )
2024-08-24 13:17:32 +00:00
_ = errIdx
_ = createdOrders
2024-09-07 06:19:07 +00:00
return nil
2021-03-21 02:44:06 +00:00
}
2024-09-09 09:49:53 +00:00
func ( s * Strategy ) makerOrderCreateCallback ( createdOrder types . Order ) {
s . orderStore . Add ( createdOrder )
s . activeMakerOrders . Add ( createdOrder )
}
2024-10-06 04:18:03 +00:00
func aggregatePriceVolumeSliceWithPriceFilter (
side types . SideType ,
pvs types . PriceVolumeSlice ,
filterPrice fixedpoint . Value ,
) types . PriceVolume {
2024-09-09 09:49:53 +00:00
var totalVolume = fixedpoint . Zero
var lastPrice = fixedpoint . Zero
for _ , pv := range pvs {
2024-10-06 04:18:03 +00:00
if side == types . SideTypeSell && pv . Price . Compare ( filterPrice ) > 0 {
break
} else if side == types . SideTypeBuy && pv . Price . Compare ( filterPrice ) < 0 {
2024-09-09 09:49:53 +00:00
break
}
lastPrice = pv . Price
totalVolume = totalVolume . Add ( pv . Volume )
}
return types . PriceVolume {
Price : lastPrice ,
Volume : totalVolume ,
}
}
// tryArbitrage tries to arbitrage between the source and maker exchange
2024-09-09 14:03:06 +00:00
func ( s * Strategy ) tryArbitrage ( ctx context . Context , quote * Quote , makerBalances , hedgeBalances types . BalanceMap ) ( bool , error ) {
2024-11-16 06:09:54 +00:00
if s . makerBook == nil {
return false , nil
}
2024-09-09 09:49:53 +00:00
marginBidPrice := quote . BestBidPrice . Mul ( fixedpoint . One . Sub ( quote . BidMargin ) )
2024-09-09 10:03:03 +00:00
marginAskPrice := quote . BestAskPrice . Mul ( fixedpoint . One . Add ( quote . AskMargin ) )
2024-09-09 09:49:53 +00:00
2024-09-09 14:03:06 +00:00
makerBid , makerAsk , ok := s . makerBook . BestBidAndAsk ( )
if ! ok {
return false , nil
}
2024-09-09 10:12:46 +00:00
2024-09-09 09:49:53 +00:00
var iocOrders [ ] types . SubmitOrder
2024-09-09 14:03:06 +00:00
if makerAsk . Price . Compare ( marginBidPrice ) <= 0 {
quoteBalance , hasQuote := makerBalances [ s . makerMarket . QuoteCurrency ]
if ! hasQuote {
return false , nil
2024-09-09 09:49:53 +00:00
}
2024-11-04 09:23:36 +00:00
availableQuote := s . makerMarket . TruncateQuoteQuantity ( quoteBalance . Available )
2024-09-09 14:03:06 +00:00
askPvs := s . makerBook . SideBook ( types . SideTypeSell )
2024-10-06 04:18:03 +00:00
sumPv := aggregatePriceVolumeSliceWithPriceFilter ( types . SideTypeSell , askPvs , marginBidPrice )
2024-11-04 09:23:36 +00:00
qty := fixedpoint . Min ( availableQuote . Div ( sumPv . Price ) , sumPv . Volume )
2024-09-09 14:03:06 +00:00
if sourceBase , ok := hedgeBalances [ s . sourceMarket . BaseCurrency ] ; ok {
qty = fixedpoint . Min ( qty , sourceBase . Available )
} else {
// insufficient hedge base balance for arbitrage
2024-09-09 09:49:53 +00:00
return false , nil
}
2024-10-15 05:40:31 +00:00
if qty . IsZero ( ) || s . makerMarket . IsDustQuantity ( qty , sumPv . Price ) {
2024-10-06 04:18:03 +00:00
return false , nil
}
2024-09-09 14:03:06 +00:00
iocOrders = append ( iocOrders , types . SubmitOrder {
Symbol : s . Symbol ,
2024-11-04 09:23:36 +00:00
Market : s . makerMarket ,
2024-09-09 14:03:06 +00:00
Type : types . OrderTypeLimit ,
Side : types . SideTypeBuy ,
Price : sumPv . Price ,
Quantity : qty ,
TimeInForce : types . TimeInForceIOC ,
} )
} else if makerBid . Price . Compare ( marginAskPrice ) >= 0 {
baseBalance , hasBase := makerBalances [ s . makerMarket . BaseCurrency ]
if ! hasBase {
return false , nil
2024-09-09 09:49:53 +00:00
}
2024-11-04 09:23:36 +00:00
availableBase := s . makerMarket . TruncateQuantity ( baseBalance . Available )
2024-09-09 14:03:06 +00:00
bidPvs := s . makerBook . SideBook ( types . SideTypeBuy )
2024-10-06 04:18:03 +00:00
sumPv := aggregatePriceVolumeSliceWithPriceFilter ( types . SideTypeBuy , bidPvs , marginAskPrice )
2024-11-04 09:23:36 +00:00
qty := fixedpoint . Min ( availableBase , sumPv . Volume )
2024-09-09 09:49:53 +00:00
2024-09-09 14:03:06 +00:00
if sourceQuote , ok := hedgeBalances [ s . sourceMarket . QuoteCurrency ] ; ok {
qty = fixedpoint . Min ( qty , quote . BestAskPrice . Div ( sourceQuote . Available ) )
} else {
// insufficient hedge quote balance for arbitrage
return false , nil
2024-09-09 09:49:53 +00:00
}
2024-10-15 05:40:31 +00:00
if qty . IsZero ( ) || s . makerMarket . IsDustQuantity ( qty , sumPv . Price ) {
2024-10-06 04:18:03 +00:00
return false , nil
}
2024-09-09 14:03:06 +00:00
// send ioc order for arbitrage
iocOrders = append ( iocOrders , types . SubmitOrder {
Symbol : s . Symbol ,
2024-11-04 09:23:36 +00:00
Market : s . makerMarket ,
2024-09-09 14:03:06 +00:00
Type : types . OrderTypeLimit ,
Side : types . SideTypeSell ,
Price : sumPv . Price ,
Quantity : qty ,
TimeInForce : types . TimeInForceIOC ,
} )
}
if len ( iocOrders ) == 0 {
return false , nil
}
// send ioc order for arbitrage
formattedOrders , err := s . makerSession . FormatOrders ( iocOrders )
if err != nil {
return false , err
}
defer s . tradeCollector . Process ( )
createdOrders , _ , err := bbgo . BatchPlaceOrder (
ctx ,
s . makerSession . Exchange ,
s . makerOrderCreateCallback ,
formattedOrders ... )
if err != nil {
return len ( createdOrders ) > 0 , err
2024-09-09 09:49:53 +00:00
}
2024-09-09 14:03:06 +00:00
s . logger . Infof ( "sent arbitrage IOC order: %+v" , createdOrders )
return true , nil
2024-09-09 09:49:53 +00:00
}
2024-09-23 14:16:53 +00:00
func AdjustHedgeQuantityWithAvailableBalance (
account * types . Account ,
market types . Market ,
side types . SideType , quantity , lastPrice fixedpoint . Value ,
2024-08-28 08:07:11 +00:00
) fixedpoint . Value {
2024-08-28 06:48:38 +00:00
switch side {
case types . SideTypeBuy :
// check quote quantity
2024-09-23 14:16:53 +00:00
if quote , ok := account . Balance ( market . QuoteCurrency ) ; ok {
if quote . Available . Compare ( market . MinNotional ) < 0 {
2024-08-28 06:48:38 +00:00
// adjust price to higher 0.1%, so that we can ensure that the order can be executed
2024-09-23 14:16:53 +00:00
availableQuote := market . TruncateQuoteQuantity ( quote . Available )
2024-08-28 06:48:38 +00:00
quantity = bbgo . AdjustQuantityByMaxAmount ( quantity , lastPrice , availableQuote )
}
}
case types . SideTypeSell :
// check quote quantity
2024-09-23 14:16:53 +00:00
if base , ok := account . Balance ( market . BaseCurrency ) ; ok {
2024-08-28 06:48:38 +00:00
if base . Available . Compare ( quantity ) < 0 {
quantity = base . Available
}
}
}
// truncate the quantity to the supported precision
2024-09-23 14:16:53 +00:00
return market . TruncateQuantity ( quantity )
2024-08-28 06:48:38 +00:00
}
2024-11-20 09:56:45 +00:00
// canDelayHedge returns true if the hedge can be delayed
func ( s * Strategy ) canDelayHedge ( hedgeSide types . SideType , pos fixedpoint . Value ) bool {
if s . DelayedHedge == nil || ! s . DelayedHedge . Enabled {
2024-10-15 15:00:09 +00:00
return false
}
signal := s . lastAggregatedSignal . Get ( )
2024-11-20 09:56:45 +00:00
signalAbs := math . Abs ( signal )
if signalAbs < s . DelayedHedge . SignalThreshold {
2024-10-16 07:40:50 +00:00
return false
}
2024-10-15 15:00:09 +00:00
// if the signal is strong enough, we can delay the hedge and wait for the next tick
2024-10-16 07:40:50 +00:00
period , ok := s . getPositionHoldingPeriod ( time . Now ( ) )
if ! ok {
return false
}
2024-11-20 09:56:45 +00:00
var maxDelay = s . DelayedHedge . MaxDelayDuration . Duration ( )
var delay = s . DelayedHedge . FixedDelayDuration . Duration ( )
if s . DelayedHedge . DynamicDelayScale != nil {
if scale , _ := s . DelayedHedge . DynamicDelayScale . Scale ( ) ; scale != nil {
delay = time . Duration ( scale . Call ( signalAbs ) ) * time . Millisecond
}
}
if delay > maxDelay {
delay = maxDelay
}
if ( signal > 0 && hedgeSide == types . SideTypeSell ) || ( signal < 0 && hedgeSide == types . SideTypeBuy ) {
if period < delay {
2024-10-16 07:40:50 +00:00
s . logger . Infof ( "delay hedge enabled, signal %f is strong enough, waiting for the next tick to hedge %s quantity (max period %s)" , signal , pos , s . MaxDelayHedgeDuration . Duration ( ) . String ( ) )
delayHedgeCounterMetrics . With ( s . metricsLabels ) . Inc ( )
return true
2024-10-15 15:00:09 +00:00
}
}
return false
}
2021-05-30 06:46:48 +00:00
func ( s * Strategy ) Hedge ( ctx context . Context , pos fixedpoint . Value ) {
2021-05-28 16:28:13 +00:00
side := types . SideTypeBuy
fix bollgrid, emstop, flashcrash, funding, grid, pricealert, pricedrop, rebalance, schedule, swing, xbalance, xgap, xmaker and speedup fixedpoint
2022-02-04 11:39:23 +00:00
if pos . IsZero ( ) {
2021-03-21 02:44:06 +00:00
return
}
fix bollgrid, emstop, flashcrash, funding, grid, pricealert, pricedrop, rebalance, schedule, swing, xbalance, xgap, xmaker and speedup fixedpoint
2022-02-04 11:39:23 +00:00
quantity := pos . Abs ( )
2021-05-30 06:46:48 +00:00
fix bollgrid, emstop, flashcrash, funding, grid, pricealert, pricedrop, rebalance, schedule, swing, xbalance, xgap, xmaker and speedup fixedpoint
2022-02-04 11:39:23 +00:00
if pos . Sign ( ) < 0 {
2021-03-21 02:44:06 +00:00
side = types . SideTypeSell
}
2024-10-15 15:00:09 +00:00
if s . canDelayHedge ( side , pos ) {
return
2024-10-15 10:51:37 +00:00
}
lastPrice := s . lastPrice . Get ( )
2021-03-21 02:44:06 +00:00
2024-10-22 03:45:13 +00:00
bestBid , bestAsk , ok := s . sourceBook . BestBidAndAsk ( )
if ok {
switch side {
case types . SideTypeBuy :
fix bollgrid, emstop, flashcrash, funding, grid, pricealert, pricedrop, rebalance, schedule, swing, xbalance, xgap, xmaker and speedup fixedpoint
2022-02-04 11:39:23 +00:00
lastPrice = bestAsk . Price
2024-10-22 03:45:13 +00:00
case types . SideTypeSell :
fix bollgrid, emstop, flashcrash, funding, grid, pricealert, pricedrop, rebalance, schedule, swing, xbalance, xgap, xmaker and speedup fixedpoint
2022-02-04 11:39:23 +00:00
lastPrice = bestBid . Price
2021-03-21 02:44:06 +00:00
}
}
2024-08-28 08:07:11 +00:00
account := s . sourceSession . GetAccount ( )
2024-08-28 06:48:38 +00:00
if s . sourceSession . Margin {
// check the margin level
if ! s . MinMarginLevel . IsZero ( ) && ! account . MarginLevel . IsZero ( ) && account . MarginLevel . Compare ( s . MinMarginLevel ) < 0 {
2024-10-15 09:29:12 +00:00
s . logger . Errorf ( "margin level %f is too low (< %f), skip hedge" , account . MarginLevel . Float64 ( ) , s . MinMarginLevel . Float64 ( ) )
2024-08-28 06:48:38 +00:00
return
2021-05-10 12:13:23 +00:00
}
2024-08-28 06:48:38 +00:00
} else {
2024-09-23 14:16:53 +00:00
quantity = AdjustHedgeQuantityWithAvailableBalance (
account , s . sourceMarket , side , quantity , lastPrice )
2022-01-08 16:30:18 +00:00
}
2021-05-10 12:13:23 +00:00
2022-01-08 16:30:18 +00:00
// truncate quantity for the supported precision
quantity = s . sourceMarket . TruncateQuantity ( quantity )
2024-08-27 07:48:02 +00:00
if s . sourceMarket . IsDustQuantity ( quantity , lastPrice ) {
2024-10-15 09:29:12 +00:00
s . logger . Warnf ( "skip dust quantity: %s @ price %f" , quantity . String ( ) , lastPrice . Float64 ( ) )
2022-01-08 16:30:18 +00:00
return
}
2022-01-10 04:25:13 +00:00
if s . hedgeErrorRateReservation != nil {
if ! s . hedgeErrorRateReservation . OK ( ) {
return
}
2024-10-15 09:29:12 +00:00
2022-06-19 04:29:36 +00:00
bbgo . Notify ( "Hit hedge error rate limit, waiting..." )
2022-01-10 04:25:13 +00:00
time . Sleep ( s . hedgeErrorRateReservation . Delay ( ) )
s . hedgeErrorRateReservation = nil
2021-05-10 12:13:23 +00:00
}
2022-06-19 04:29:36 +00:00
bbgo . Notify ( "Submitting %s hedge order %s %v" , s . Symbol , side . String ( ) , quantity )
2024-08-24 03:58:09 +00:00
2024-08-24 04:13:15 +00:00
submitOrders := [ ] types . SubmitOrder {
{
Market : s . sourceMarket ,
Symbol : s . Symbol ,
Type : types . OrderTypeMarket ,
Side : side ,
Quantity : quantity ,
MarginSideEffect : types . SideEffectTypeMarginBuy ,
} ,
}
formattedOrders , err := s . sourceSession . FormatOrders ( submitOrders )
if err != nil {
2024-08-24 04:13:44 +00:00
log . WithError ( err ) . Errorf ( "unable to format hedge orders" )
2024-08-24 04:13:15 +00:00
return
}
2021-03-21 02:44:06 +00:00
2024-08-24 04:13:15 +00:00
orderCreateCallback := func ( createdOrder types . Order ) {
s . orderStore . Add ( createdOrder )
}
defer s . tradeCollector . Process ( )
createdOrders , _ , err := bbgo . BatchPlaceOrder ( ctx , s . sourceSession . Exchange , orderCreateCallback , formattedOrders ... )
2021-03-21 02:44:06 +00:00
if err != nil {
2022-01-10 04:25:13 +00:00
s . hedgeErrorRateReservation = s . hedgeErrorLimiter . Reserve ( )
2021-03-21 02:44:06 +00:00
log . WithError ( err ) . Errorf ( "market order submit error: %s" , err . Error ( ) )
return
}
2024-08-24 04:13:15 +00:00
log . Infof ( "submitted hedge orders: %+v" , createdOrders )
// if it's selling, then we should add a positive position
2021-05-30 06:46:48 +00:00
if side == types . SideTypeSell {
2024-10-16 06:39:09 +00:00
s . coveredPosition . Add ( quantity )
2021-05-30 06:46:48 +00:00
} else {
2024-10-16 06:39:09 +00:00
s . coveredPosition . Add ( quantity . Neg ( ) )
2021-05-30 06:46:48 +00:00
}
2024-10-17 05:13:45 +00:00
s . resetPositionStartTime ( )
2021-03-21 02:44:06 +00:00
}
2023-07-22 09:29:16 +00:00
func ( s * Strategy ) tradeRecover ( ctx context . Context ) {
tradeScanInterval := s . RecoverTradeScanPeriod . Duration ( )
if tradeScanInterval == 0 {
tradeScanInterval = 30 * time . Minute
}
2023-07-22 09:30:24 +00:00
tradeScanOverlapBufferPeriod := 5 * time . Minute
2023-07-22 09:29:16 +00:00
tradeScanTicker := time . NewTicker ( tradeScanInterval )
defer tradeScanTicker . Stop ( )
for {
select {
case <- ctx . Done ( ) :
return
case <- tradeScanTicker . C :
log . Infof ( "scanning trades from %s ago..." , tradeScanInterval )
if s . RecoverTrade {
2023-07-22 09:30:24 +00:00
startTime := time . Now ( ) . Add ( - tradeScanInterval ) . Add ( - tradeScanOverlapBufferPeriod )
2023-07-22 09:29:16 +00:00
if err := s . tradeCollector . Recover ( ctx , s . sourceSession . Exchange . ( types . ExchangeTradeHistoryService ) , s . Symbol , startTime ) ; err != nil {
log . WithError ( err ) . Errorf ( "query trades error" )
}
if err := s . tradeCollector . Recover ( ctx , s . makerSession . Exchange . ( types . ExchangeTradeHistoryService ) , s . Symbol , startTime ) ; err != nil {
log . WithError ( err ) . Errorf ( "query trades error" )
}
}
}
}
}
2024-08-24 03:42:07 +00:00
func ( s * Strategy ) Defaults ( ) error {
2021-05-17 12:03:42 +00:00
if s . BollBandInterval == "" {
s . BollBandInterval = types . Interval1m
}
2024-10-15 10:51:37 +00:00
if s . MaxDelayHedgeDuration == 0 {
s . MaxDelayHedgeDuration = types . Duration ( 10 * time . Second )
}
if s . DelayHedgeSignalThreshold == 0.0 {
s . DelayHedgeSignalThreshold = 0.5
}
2024-09-06 13:47:43 +00:00
if s . SourceDepthLevel == "" {
s . SourceDepthLevel = types . DepthLevelMedium
}
fix bollgrid, emstop, flashcrash, funding, grid, pricealert, pricedrop, rebalance, schedule, swing, xbalance, xgap, xmaker and speedup fixedpoint
2022-02-04 11:39:23 +00:00
if s . BollBandMarginFactor . IsZero ( ) {
s . BollBandMarginFactor = fixedpoint . One
2021-05-17 12:03:42 +00:00
}
2024-11-20 08:42:19 +00:00
fix bollgrid, emstop, flashcrash, funding, grid, pricealert, pricedrop, rebalance, schedule, swing, xbalance, xgap, xmaker and speedup fixedpoint
2022-02-04 11:39:23 +00:00
if s . BollBandMargin . IsZero ( ) {
2021-05-17 12:03:42 +00:00
s . BollBandMargin = fixedpoint . NewFromFloat ( 0.001 )
}
2021-03-21 02:44:06 +00:00
// configure default values
if s . UpdateInterval == 0 {
s . UpdateInterval = types . Duration ( time . Second )
}
if s . HedgeInterval == 0 {
s . HedgeInterval = types . Duration ( 10 * time . Second )
}
if s . NumLayers == 0 {
s . NumLayers = 1
}
2024-08-28 06:48:38 +00:00
if s . MinMarginLevel . IsZero ( ) {
s . MinMarginLevel = fixedpoint . NewFromFloat ( 3.0 )
}
2024-09-01 07:42:36 +00:00
if s . MaxHedgeAccountLeverage . IsZero ( ) {
s . MaxHedgeAccountLeverage = fixedpoint . NewFromFloat ( 1.2 )
}
fix bollgrid, emstop, flashcrash, funding, grid, pricealert, pricedrop, rebalance, schedule, swing, xbalance, xgap, xmaker and speedup fixedpoint
2022-02-04 11:39:23 +00:00
if s . BidMargin . IsZero ( ) {
if ! s . Margin . IsZero ( ) {
2021-03-21 02:44:06 +00:00
s . BidMargin = s . Margin
} else {
s . BidMargin = defaultMargin
}
}
fix bollgrid, emstop, flashcrash, funding, grid, pricealert, pricedrop, rebalance, schedule, swing, xbalance, xgap, xmaker and speedup fixedpoint
2022-02-04 11:39:23 +00:00
if s . AskMargin . IsZero ( ) {
if ! s . Margin . IsZero ( ) {
2021-03-21 02:44:06 +00:00
s . AskMargin = s . Margin
} else {
s . AskMargin = defaultMargin
}
}
2024-08-27 06:48:30 +00:00
if s . CircuitBreaker == nil {
2024-11-14 08:31:08 +00:00
s . CircuitBreaker = circuitbreaker . NewBasicCircuitBreaker ( ID , s . InstanceID ( ) , s . Symbol )
} else {
s . CircuitBreaker . SetMetricsInfo ( ID , s . InstanceID ( ) , s . Symbol )
2024-08-27 06:48:30 +00:00
}
2024-11-20 06:21:17 +00:00
if s . EnableSignalMargin {
2024-11-20 08:42:19 +00:00
if s . SignalReverseSideMargin . Scale == nil {
s . SignalReverseSideMargin . Scale = & bbgo . SlideRule {
2024-11-20 06:21:17 +00:00
ExpScale : & bbgo . ExponentialScale {
Domain : [ 2 ] float64 { 0 , 2.0 } ,
Range : [ 2 ] float64 { 0.00010 , 0.00500 } ,
} ,
QuadraticScale : nil ,
}
}
2024-11-20 09:56:45 +00:00
if s . SignalTrendSideMarginDiscount . Scale == nil {
s . SignalTrendSideMarginDiscount . Scale = & bbgo . SlideRule {
2024-11-20 06:21:17 +00:00
ExpScale : & bbgo . ExponentialScale {
Domain : [ 2 ] float64 { 0 , 2.0 } ,
Range : [ 2 ] float64 { 0.00010 , 0.00500 } ,
} ,
}
}
2024-11-20 08:42:19 +00:00
2024-11-20 09:56:45 +00:00
if s . SignalTrendSideMarginDiscount . Threshold == 0.0 {
s . SignalTrendSideMarginDiscount . Threshold = 1.0
}
}
if s . DelayedHedge != nil {
// default value protection for delayed hedge
if s . DelayedHedge . MaxDelayDuration == 0 {
s . DelayedHedge . MaxDelayDuration = types . Duration ( 3 * time . Second )
}
if s . DelayedHedge . SignalThreshold == 0.0 {
s . DelayedHedge . SignalThreshold = 0.5
2024-11-20 08:42:19 +00:00
}
2024-11-20 06:21:17 +00:00
}
2024-08-27 06:48:30 +00:00
// circuitBreakerAlertLimiter is for CircuitBreaker alerts
s . circuitBreakerAlertLimiter = rate . NewLimiter ( rate . Every ( 3 * time . Minute ) , 2 )
2024-08-27 07:53:48 +00:00
s . reportProfitStatsRateLimiter = rate . NewLimiter ( rate . Every ( 3 * time . Minute ) , 1 )
2024-08-27 07:22:06 +00:00
s . hedgeErrorLimiter = rate . NewLimiter ( rate . Every ( 1 * time . Minute ) , 1 )
2024-08-27 06:48:30 +00:00
return nil
}
func ( s * Strategy ) Validate ( ) error {
2024-09-09 07:04:56 +00:00
if s . Quantity . IsZero ( ) && s . QuantityScale == nil {
2024-08-27 06:48:30 +00:00
return errors . New ( "quantity or quantityScale can not be empty" )
}
if ! s . QuantityMultiplier . IsZero ( ) && s . QuantityMultiplier . Sign ( ) < 0 {
return errors . New ( "quantityMultiplier can not be a negative number" )
}
if len ( s . Symbol ) == 0 {
return errors . New ( "symbol is required" )
}
return nil
}
func ( s * Strategy ) quoteWorker ( ctx context . Context ) {
2024-10-28 09:28:56 +00:00
ticker := time . NewTicker ( timejitter . Milliseconds ( s . UpdateInterval . Duration ( ) , 200 ) )
2024-08-27 07:22:06 +00:00
defer ticker . Stop ( )
2024-08-27 06:48:30 +00:00
defer func ( ) {
if err := s . activeMakerOrders . GracefulCancel ( context . Background ( ) , s . makerSession . Exchange ) ; err != nil {
log . WithError ( err ) . Errorf ( "can not cancel %s orders" , s . Symbol )
}
} ( )
for {
select {
case <- s . stopC :
2024-10-15 09:29:12 +00:00
s . logger . Warnf ( "%s maker goroutine stopped, due to the stop signal" , s . Symbol )
2024-08-27 06:48:30 +00:00
return
case <- ctx . Done ( ) :
2024-10-15 09:29:12 +00:00
s . logger . Warnf ( "%s maker goroutine stopped, due to the cancelled context" , s . Symbol )
2024-08-27 06:48:30 +00:00
return
2024-08-27 07:22:06 +00:00
case <- ticker . C :
2024-10-24 08:12:09 +00:00
2024-10-15 09:29:12 +00:00
if err := s . updateQuote ( ctx ) ; err != nil {
s . logger . WithError ( err ) . Errorf ( "unable to place maker orders" )
}
2024-08-27 06:48:30 +00:00
}
}
}
2024-08-28 08:07:11 +00:00
func ( s * Strategy ) accountUpdater ( ctx context . Context ) {
2024-08-28 08:41:40 +00:00
ticker := time . NewTicker ( 3 * time . Minute )
2024-08-28 08:07:11 +00:00
defer ticker . Stop ( )
for {
select {
case <- ctx . Done ( ) :
return
case <- ticker . C :
if _ , err := s . sourceSession . UpdateAccount ( ctx ) ; err != nil {
2024-10-15 09:29:12 +00:00
s . logger . WithError ( err ) . Errorf ( "unable to update account" )
2024-08-28 08:07:11 +00:00
}
if err := s . accountValueCalculator . UpdatePrices ( ctx ) ; err != nil {
2024-10-15 09:29:12 +00:00
s . logger . WithError ( err ) . Errorf ( "unable to update account value with prices" )
2024-08-28 08:07:11 +00:00
return
}
2024-10-05 05:09:31 +00:00
netValue := s . accountValueCalculator . NetValue ( )
2024-08-28 08:07:11 +00:00
s . logger . Infof ( "hedge session net value ~= %f USD" , netValue . Float64 ( ) )
}
}
}
2024-10-17 05:02:44 +00:00
func ( s * Strategy ) houseCleanWorker ( ctx context . Context ) {
expiryDuration := 3 * time . Hour
ticker := time . NewTicker ( time . Hour )
defer ticker . Stop ( )
for {
select {
case <- ctx . Done ( ) :
return
case <- ticker . C :
s . orderStore . Prune ( expiryDuration )
}
}
}
2024-08-27 06:48:30 +00:00
func ( s * Strategy ) hedgeWorker ( ctx context . Context ) {
2024-10-28 09:28:56 +00:00
ticker := time . NewTicker ( timejitter . Milliseconds ( s . HedgeInterval . Duration ( ) , 200 ) )
2024-08-27 06:48:30 +00:00
defer ticker . Stop ( )
2024-08-27 07:53:48 +00:00
profitChanged := false
reportTicker := time . NewTicker ( 5 * time . Minute )
2024-08-27 06:48:30 +00:00
for {
select {
case <- ctx . Done ( ) :
return
2024-10-15 09:29:12 +00:00
case tt := <- ticker . C :
2024-08-27 06:48:30 +00:00
// For positive position and positive covered position:
// uncover position = +5 - +3 (covered position) = 2
//
// For positive position and negative covered position:
// uncover position = +5 - (-3) (covered position) = 8
//
// meaning we bought 5 on MAX and sent buy order with 3 on binance
//
// For negative position:
// uncover position = -5 - -3 (covered position) = -2
s . tradeCollector . Process ( )
position := s . Position . GetBase ( )
2024-10-15 09:29:12 +00:00
if position . IsZero ( ) || s . Position . IsDust ( ) {
s . resetPositionStartTime ( )
} else {
s . setPositionStartTime ( tt )
}
2024-10-16 06:39:09 +00:00
coveredPosition := s . coveredPosition . Get ( )
2024-10-15 08:24:35 +00:00
uncoverPosition := position . Sub ( coveredPosition )
2024-08-27 06:48:30 +00:00
absPos := uncoverPosition . Abs ( )
2024-10-15 10:45:16 +00:00
2024-10-15 10:51:37 +00:00
if s . sourceMarket . IsDustQuantity ( absPos , s . lastPrice . Get ( ) ) {
continue
}
2024-10-16 09:36:05 +00:00
if s . DisableHedge {
2024-10-15 10:45:16 +00:00
continue
2024-08-27 06:48:30 +00:00
}
2024-10-15 10:45:16 +00:00
s . logger . Infof ( "%s base position %v coveredPosition: %v uncoverPosition: %v" ,
s . Symbol ,
position ,
coveredPosition ,
uncoverPosition ,
)
s . Hedge ( ctx , uncoverPosition . Neg ( ) )
profitChanged = true
2024-08-27 07:53:48 +00:00
case <- reportTicker . C :
if profitChanged {
if s . reportProfitStatsRateLimiter . Allow ( ) {
bbgo . Notify ( s . ProfitStats )
}
profitChanged = false
2024-08-27 06:48:30 +00:00
}
}
}
}
func ( s * Strategy ) CrossRun (
ctx context . Context , orderExecutionRouter bbgo . OrderExecutionRouter , sessions map [ string ] * bbgo . ExchangeSession ,
) error {
2024-08-27 07:22:06 +00:00
instanceID := s . InstanceID ( )
2022-01-09 03:33:34 +00:00
2021-03-21 02:44:06 +00:00
// configure sessions
sourceSession , ok := sessions [ s . SourceExchange ]
if ! ok {
return fmt . Errorf ( "source exchange session %s is not defined" , s . SourceExchange )
}
s . sourceSession = sourceSession
2024-08-24 04:28:05 +00:00
// initialize the price resolver
sourceMarkets := s . sourceSession . Markets ( )
2024-11-14 13:16:41 +00:00
if len ( sourceMarkets ) == 0 {
return fmt . Errorf ( "source exchange %s has no markets" , s . SourceExchange )
}
2024-08-24 04:28:05 +00:00
2021-03-21 02:44:06 +00:00
makerSession , ok := sessions [ s . MakerExchange ]
if ! ok {
return fmt . Errorf ( "maker exchange session %s is not defined" , s . MakerExchange )
}
s . makerSession = makerSession
s . sourceMarket , ok = s . sourceSession . Market ( s . Symbol )
if ! ok {
return fmt . Errorf ( "source session market %s is not defined" , s . Symbol )
}
s . makerMarket , ok = s . makerSession . Market ( s . Symbol )
if ! ok {
return fmt . Errorf ( "maker session market %s is not defined" , s . Symbol )
}
2024-08-24 05:28:32 +00:00
indicators := s . sourceSession . Indicators ( s . Symbol )
2021-05-17 12:03:42 +00:00
2024-08-24 05:28:32 +00:00
s . boll = indicators . BOLL ( types . IntervalWindow {
2021-05-17 12:03:42 +00:00
Interval : s . BollBandInterval ,
Window : 21 ,
} , 1.0 )
2021-03-21 02:44:06 +00:00
// restore state
2022-05-05 06:39:29 +00:00
s . groupID = util . FNV32 ( instanceID )
2024-09-02 07:30:14 +00:00
s . logger . Infof ( "using group id %d from fnv(%s)" , s . groupID , instanceID )
2021-03-21 02:44:06 +00:00
2024-08-27 07:22:06 +00:00
configLabels := prometheus . Labels { "strategy_id" : s . InstanceID ( ) , "strategy_type" : ID , "symbol" : s . Symbol }
configNumOfLayersMetrics . With ( configLabels ) . Set ( float64 ( s . NumLayers ) )
configMaxExposureMetrics . With ( configLabels ) . Set ( s . MaxExposurePosition . Float64 ( ) )
2024-08-27 07:35:39 +00:00
configBidMarginMetrics . With ( configLabels ) . Set ( s . BidMargin . Float64 ( ) )
configAskMarginMetrics . With ( configLabels ) . Set ( s . AskMargin . Float64 ( ) )
2024-08-27 07:22:06 +00:00
2022-05-05 07:05:38 +00:00
if s . Position == nil {
2022-10-03 10:37:53 +00:00
s . Position = types . NewPositionFromMarket ( s . makerMarket )
2024-08-24 04:01:11 +00:00
s . Position . Strategy = ID
s . Position . StrategyInstanceID = instanceID
2024-09-02 07:51:07 +00:00
} else {
s . Position . Strategy = ID
s . Position . StrategyInstanceID = instanceID
2022-05-05 07:05:38 +00:00
}
2024-10-07 09:03:45 +00:00
if s . makerSession . MakerFeeRate . Sign ( ) > 0 || s . makerSession . TakerFeeRate . Sign ( ) > 0 {
s . Position . SetExchangeFeeRate ( types . ExchangeName ( s . MakerExchange ) , types . ExchangeFee {
MakerFeeRate : s . makerSession . MakerFeeRate ,
TakerFeeRate : s . makerSession . TakerFeeRate ,
} )
}
if s . sourceSession . MakerFeeRate . Sign ( ) > 0 || s . sourceSession . TakerFeeRate . Sign ( ) > 0 {
s . Position . SetExchangeFeeRate ( types . ExchangeName ( s . SourceExchange ) , types . ExchangeFee {
MakerFeeRate : s . sourceSession . MakerFeeRate ,
TakerFeeRate : s . sourceSession . TakerFeeRate ,
} )
}
2024-09-02 07:51:07 +00:00
s . Position . UpdateMetrics ( )
2022-06-19 04:29:36 +00:00
bbgo . Notify ( "xmaker: %s position is restored" , s . Symbol , s . Position )
2022-06-13 04:04:35 +00:00
2022-05-05 07:05:38 +00:00
if s . ProfitStats == nil {
2022-10-03 10:37:53 +00:00
s . ProfitStats = & ProfitStats {
ProfitStats : types . NewProfitStats ( s . makerMarket ) ,
MakerExchange : s . makerSession . ExchangeName ,
2022-05-05 07:05:38 +00:00
}
}
2024-09-27 10:31:18 +00:00
s . priceSolver = pricesolver . NewSimplePriceResolver ( sourceMarkets )
s . priceSolver . BindStream ( s . sourceSession . MarketDataStream )
2024-10-07 09:12:49 +00:00
s . sourceSession . UserDataStream . OnTradeUpdate ( s . priceSolver . UpdateFromTrade )
2024-09-27 10:31:18 +00:00
2024-10-05 06:22:13 +00:00
s . accountValueCalculator = bbgo . NewAccountValueCalculator ( s . sourceSession , s . priceSolver , s . sourceMarket . QuoteCurrency )
if err := s . accountValueCalculator . UpdatePrices ( ctx ) ; err != nil {
return err
}
2024-08-30 07:44:55 +00:00
s . sourceSession . MarketDataStream . OnKLineClosed ( types . KLineWith ( s . Symbol , types . Interval1m , func ( k types . KLine ) {
feeToken := s . sourceSession . Exchange . PlatformFeeCurrency ( )
2024-10-07 09:12:49 +00:00
if feePrice , ok := s . priceSolver . ResolvePrice ( feeToken , feeTokenQuote ) ; ok {
s . Position . SetFeeAverageCost ( feeToken , feePrice )
}
} ) )
s . makerSession . MarketDataStream . OnKLineClosed ( types . KLineWith ( s . Symbol , types . Interval1m , func ( k types . KLine ) {
feeToken := s . makerSession . Exchange . PlatformFeeCurrency ( )
if feePrice , ok := s . priceSolver . ResolvePrice ( feeToken , feeTokenQuote ) ; ok {
2024-08-30 07:44:55 +00:00
s . Position . SetFeeAverageCost ( feeToken , feePrice )
}
} ) )
2024-08-26 04:45:18 +00:00
if s . ProfitFixerConfig != nil {
bbgo . Notify ( "Fixing %s profitStats and position..." , s . Symbol )
log . Infof ( "profitFixer is enabled, checking checkpoint: %+v" , s . ProfitFixerConfig . TradesSince )
if s . ProfitFixerConfig . TradesSince . Time ( ) . IsZero ( ) {
return errors . New ( "tradesSince time can not be zero" )
}
2024-09-01 09:54:32 +00:00
position := types . NewPositionFromMarket ( s . makerMarket )
2024-10-07 09:03:45 +00:00
position . ExchangeFeeRates = s . Position . ExchangeFeeRates
position . FeeRate = s . Position . FeeRate
position . StrategyInstanceID = s . Position . StrategyInstanceID
position . Strategy = s . Position . Strategy
2024-09-01 09:54:32 +00:00
profitStats := types . NewProfitStats ( s . makerMarket )
2024-08-26 04:45:18 +00:00
fixer := common . NewProfitFixer ( )
// fixer.ConverterManager = s.ConverterManager
if ss , ok := makerSession . Exchange . ( types . ExchangeTradeHistoryService ) ; ok {
log . Infof ( "adding makerSession %s to profitFixer" , makerSession . Name )
fixer . AddExchange ( makerSession . Name , ss )
}
if ss , ok := sourceSession . Exchange . ( types . ExchangeTradeHistoryService ) ; ok {
log . Infof ( "adding hedgeSession %s to profitFixer" , sourceSession . Name )
fixer . AddExchange ( sourceSession . Name , ss )
}
2024-09-01 09:54:32 +00:00
if err2 := fixer . Fix ( ctx , s . makerMarket . Symbol ,
2024-08-26 04:45:18 +00:00
s . ProfitFixerConfig . TradesSince . Time ( ) ,
time . Now ( ) ,
profitStats ,
position ) ; err2 != nil {
return err2
}
bbgo . Notify ( "Fixed %s position" , s . Symbol , position )
bbgo . Notify ( "Fixed %s profitStats" , s . Symbol , profitStats )
s . Position = position
s . ProfitStats . ProfitStats = profitStats
}
2024-11-16 06:09:54 +00:00
if s . EnableArbitrage {
makerMarketStream := s . makerSession . Exchange . NewStream ( )
makerMarketStream . SetPublicOnly ( )
makerMarketStream . Subscribe ( types . BookChannel , s . Symbol , types . SubscribeOptions {
Depth : types . DepthLevelFull ,
Speed : types . SpeedLow ,
} )
s . makerBook = types . NewStreamBook ( s . Symbol , s . makerSession . ExchangeName )
2024-11-18 08:46:39 +00:00
s . makerBook . BindStream ( makerMarketStream )
2024-11-16 06:09:54 +00:00
if err := makerMarketStream . Connect ( ctx ) ; err != nil {
return err
}
}
sourceMarketStream := s . sourceSession . Exchange . NewStream ( )
sourceMarketStream . SetPublicOnly ( )
sourceMarketStream . Subscribe ( types . BookChannel , s . Symbol , types . SubscribeOptions {
Depth : types . DepthLevelFull ,
Speed : types . SpeedLow ,
} )
2024-09-07 05:47:21 +00:00
2024-09-06 13:47:43 +00:00
s . sourceBook = types . NewStreamBook ( s . Symbol , s . sourceSession . ExchangeName )
2024-11-16 06:09:54 +00:00
s . sourceBook . BindStream ( sourceMarketStream )
if err := sourceMarketStream . Connect ( ctx ) ; err != nil {
return err
}
2021-03-21 02:44:06 +00:00
2024-08-30 09:39:25 +00:00
if s . EnableSignalMargin {
2024-10-09 04:33:11 +00:00
s . logger . Infof ( "signal margin is enabled" )
2024-11-20 08:42:19 +00:00
if s . SignalReverseSideMargin == nil || s . SignalReverseSideMargin . Scale == nil {
2024-11-20 06:21:17 +00:00
return errors . New ( "signalReverseSideMarginScale can not be nil when signal margin is enabled" )
}
2024-11-20 09:56:45 +00:00
if s . SignalTrendSideMarginDiscount == nil || s . SignalTrendSideMarginDiscount . Scale == nil {
2024-11-20 06:21:17 +00:00
return errors . New ( "signalTrendSideMarginScale can not be nil when signal margin is enabled" )
}
2024-11-20 08:42:19 +00:00
scale , err := s . SignalReverseSideMargin . Scale . Scale ( )
2024-08-30 09:39:25 +00:00
if err != nil {
return err
}
2024-10-09 04:07:33 +00:00
minAdditionalMargin := scale . Call ( 0.0 )
middleAdditionalMargin := scale . Call ( 1.0 )
maxAdditionalMargin := scale . Call ( 2.0 )
s . logger . Infof ( "signal margin range: %.2f%% @ 0.0 ~ %.2f%% @ 1.0 ~ %.2f%% @ 2.0" ,
minAdditionalMargin * 100.0 ,
middleAdditionalMargin * 100.0 ,
maxAdditionalMargin * 100.0 )
2024-08-30 09:39:25 +00:00
}
2024-08-30 07:44:55 +00:00
for _ , signalConfig := range s . SignalConfigList {
2024-10-09 04:33:11 +00:00
signal := signalConfig . Get ( )
if setter , ok := signal . ( StreamBookSetter ) ; ok {
s . logger . Infof ( "setting stream book on signal %T" , signal )
setter . SetStreamBook ( s . sourceBook )
}
if binder , ok := signal . ( SessionBinder ) ; ok {
s . logger . Infof ( "binding session on signal %T" , signal )
if err := binder . Bind ( ctx , s . sourceSession , s . Symbol ) ; err != nil {
2024-09-04 08:07:28 +00:00
return err
}
2024-08-30 07:44:55 +00:00
}
}
2022-06-05 22:57:25 +00:00
s . activeMakerOrders = bbgo . NewActiveOrderBook ( s . Symbol )
2021-05-27 06:45:06 +00:00
s . activeMakerOrders . BindStream ( s . makerSession . UserDataStream )
2021-03-21 02:44:06 +00:00
2023-07-04 13:42:24 +00:00
s . orderStore = core . NewOrderStore ( s . Symbol )
2021-05-27 06:45:06 +00:00
s . orderStore . BindStream ( s . sourceSession . UserDataStream )
s . orderStore . BindStream ( s . makerSession . UserDataStream )
2021-03-21 02:44:06 +00:00
2023-07-05 07:26:36 +00:00
s . tradeCollector = core . NewTradeCollector ( s . Symbol , s . Position , s . orderStore )
2024-10-17 04:53:51 +00:00
s . tradeCollector . TradeStore ( ) . SetPruneEnabled ( true )
2021-12-26 16:12:35 +00:00
if s . NotifyTrade {
2022-03-11 13:27:45 +00:00
s . tradeCollector . OnTrade ( func ( trade types . Trade , profit , netProfit fixedpoint . Value ) {
2022-06-19 04:29:36 +00:00
bbgo . Notify ( trade )
2021-12-26 16:12:35 +00:00
} )
}
2022-03-11 13:27:45 +00:00
s . tradeCollector . OnTrade ( func ( trade types . Trade , profit , netProfit fixedpoint . Value ) {
2021-12-26 04:10:10 +00:00
c := trade . PositionChange ( )
2021-12-31 06:23:02 +00:00
if trade . Exchange == s . sourceSession . ExchangeName {
2024-10-16 06:39:09 +00:00
s . coveredPosition . Add ( c )
2021-12-31 06:23:02 +00:00
}
2022-05-05 07:05:38 +00:00
s . ProfitStats . AddTrade ( trade )
2021-12-26 04:10:10 +00:00
2022-03-11 13:27:45 +00:00
if profit . Compare ( fixedpoint . Zero ) == 0 {
2022-05-05 07:05:38 +00:00
s . Environment . RecordPosition ( s . Position , trade , nil )
2024-08-24 04:13:15 +00:00
}
} )
2022-03-11 13:27:45 +00:00
2024-08-26 10:37:02 +00:00
// TODO: remove this nil value behavior, check all OnProfit usage and remove the EmitProfit call with nil profit
2024-08-24 04:13:15 +00:00
s . tradeCollector . OnProfit ( func ( trade types . Trade , profit * types . Profit ) {
2024-08-26 10:35:10 +00:00
if profit != nil {
if s . CircuitBreaker != nil {
s . CircuitBreaker . RecordProfit ( profit . Profit , trade . Time . Time ( ) )
}
2022-03-11 13:27:45 +00:00
2024-08-26 10:35:10 +00:00
bbgo . Notify ( profit )
2024-08-24 03:58:09 +00:00
2024-08-26 10:35:10 +00:00
s . ProfitStats . AddProfit ( * profit )
s . Environment . RecordPosition ( s . Position , trade , profit )
}
2021-12-26 04:10:10 +00:00
} )
2022-03-11 13:27:45 +00:00
2021-12-26 04:10:10 +00:00
s . tradeCollector . OnPositionUpdate ( func ( position * types . Position ) {
2022-06-19 04:29:36 +00:00
bbgo . Notify ( position )
2021-12-26 04:10:10 +00:00
} )
2024-08-27 06:48:30 +00:00
2022-01-09 07:39:59 +00:00
s . tradeCollector . OnRecover ( func ( trade types . Trade ) {
2023-07-22 09:34:09 +00:00
bbgo . Notify ( "Recovered trade" , trade )
2022-01-09 07:39:59 +00:00
} )
2024-08-24 04:13:15 +00:00
// bind two user data streams so that we can collect the trades together
2021-12-26 16:51:57 +00:00
s . tradeCollector . BindStream ( s . sourceSession . UserDataStream )
s . tradeCollector . BindStream ( s . makerSession . UserDataStream )
2021-12-26 04:10:10 +00:00
2021-03-21 02:44:06 +00:00
s . stopC = make ( chan struct { } )
2024-11-18 06:11:08 +00:00
s . sourceUserDataConnectivity = s . sourceSession . UserDataConnectivity
s . sourceMarketDataConnectivity = s . sourceSession . MarketDataConnectivity
2024-10-05 05:09:31 +00:00
2024-11-18 06:11:08 +00:00
s . connectivityGroup = types . NewConnectivityGroup ( s . sourceSession . UserDataConnectivity , s . makerSession . UserDataConnectivity )
2024-10-05 05:09:31 +00:00
go func ( ) {
2024-10-09 04:33:11 +00:00
s . logger . Infof ( "waiting for authentication connections to be ready..." )
2024-10-05 05:09:31 +00:00
select {
case <- ctx . Done ( ) :
2024-11-14 18:03:22 +00:00
case <- time . After ( 3 * time . Minute ) :
s . logger . Panicf ( "authentication timeout, exiting..." )
case <- s . connectivityGroup . AllAuthedC ( ctx ) :
2024-10-05 05:09:31 +00:00
}
s . logger . Infof ( "all user data streams are connected, starting workers..." )
go s . accountUpdater ( ctx )
go s . hedgeWorker ( ctx )
go s . quoteWorker ( ctx )
2024-10-17 05:02:44 +00:00
go s . houseCleanWorker ( ctx )
2024-10-09 04:33:11 +00:00
if s . RecoverTrade {
go s . tradeRecover ( ctx )
}
2024-10-05 05:09:31 +00:00
} ( )
2021-03-21 02:44:06 +00:00
2022-10-03 08:01:08 +00:00
bbgo . OnShutdown ( ctx , func ( ctx context . Context , wg * sync . WaitGroup ) {
2024-08-27 06:52:27 +00:00
// the ctx here is the shutdown context (not the strategy context)
// defer work group done to mark the strategy as stopped
2021-03-21 02:44:06 +00:00
defer wg . Done ( )
2024-08-27 06:52:27 +00:00
// send stop signal to the quoteWorker
2021-03-21 02:44:06 +00:00
close ( s . stopC )
2021-05-22 09:44:20 +00:00
// wait for the quoter to stop
2021-05-09 18:17:19 +00:00
time . Sleep ( s . UpdateInterval . Duration ( ) )
2021-05-09 11:04:44 +00:00
2024-08-24 05:28:32 +00:00
if err := s . activeMakerOrders . GracefulCancel ( ctx , s . makerSession . Exchange ) ; err != nil {
2022-01-06 16:10:40 +00:00
log . WithError ( err ) . Errorf ( "graceful cancel error" )
2021-05-09 10:48:25 +00:00
}
2024-08-27 06:52:27 +00:00
bbgo . Notify ( "Shutting down %s %s" , ID , s . Symbol , s . Position )
2021-03-21 02:44:06 +00:00
} )
return nil
}