From c0d8a9a3757f458a7a0a9afb652f2c79736897c6 Mon Sep 17 00:00:00 2001 From: tinkle-community Date: Thu, 11 Jun 2026 00:33:11 +0800 Subject: [PATCH] refactor(trader): name trading-logic magic numbers - marginOverheadFactor/takerFeeRate/positionSizeSafetyFactor in sizing math - aggressiveBuyPriceFactor/aggressiveSellPriceFactor in hyperliquid and aster simulated market orders - dustQuantityEpsilon in FIFO position rebuild --- trader/aster/trader_orders.go | 16 ++++++++++++---- trader/auto_trader_orders.go | 28 ++++++++++++++++++---------- trader/hyperliquid/trader_orders.go | 24 ++++++++++++++++-------- trader/position_rebuild.go | 10 +++++++--- 4 files changed, 53 insertions(+), 25 deletions(-) diff --git a/trader/aster/trader_orders.go b/trader/aster/trader_orders.go index 12ce9502..616d85db 100644 --- a/trader/aster/trader_orders.go +++ b/trader/aster/trader_orders.go @@ -9,6 +9,14 @@ import ( "strings" ) +// Aggressive limit prices simulate market orders: buy slightly above and sell +// slightly below the current price so limit orders fill immediately while +// capping slippage at 1%. +const ( + aggressiveBuyPriceFactor = 1.01 + aggressiveSellPriceFactor = 0.99 +) + // OpenLong Open long position func (t *AsterTrader) OpenLong(symbol string, quantity float64, leverage int) (map[string]interface{}, error) { // Cancel all pending orders before opening position to prevent position stacking from residual orders @@ -34,7 +42,7 @@ func (t *AsterTrader) OpenLong(symbol string, quantity float64, leverage int) (m } // Use limit order to simulate market order (price set slightly higher to ensure execution) - limitPrice := price * 1.01 + limitPrice := price * aggressiveBuyPriceFactor // Format price and quantity to correct precision formattedPrice, err := t.formatPrice(symbol, limitPrice) @@ -107,7 +115,7 @@ func (t *AsterTrader) OpenShort(symbol string, quantity float64, leverage int) ( } // Use limit order to simulate market order (price set slightly lower to ensure execution) - limitPrice := price * 0.99 + limitPrice := price * aggressiveSellPriceFactor // Format price and quantity to correct precision formattedPrice, err := t.formatPrice(symbol, limitPrice) @@ -182,7 +190,7 @@ func (t *AsterTrader) CloseLong(symbol string, quantity float64) (map[string]int return nil, err } - limitPrice := price * 0.99 + limitPrice := price * aggressiveSellPriceFactor // Format price and quantity to correct precision formattedPrice, err := t.formatPrice(symbol, limitPrice) @@ -265,7 +273,7 @@ func (t *AsterTrader) CloseShort(symbol string, quantity float64) (map[string]in return nil, err } - limitPrice := price * 1.01 + limitPrice := price * aggressiveBuyPriceFactor // Format price and quantity to correct precision formattedPrice, err := t.formatPrice(symbol, limitPrice) diff --git a/trader/auto_trader_orders.go b/trader/auto_trader_orders.go index e18b2b8f..1635201b 100644 --- a/trader/auto_trader_orders.go +++ b/trader/auto_trader_orders.go @@ -9,6 +9,20 @@ import ( "time" ) +const ( + // marginOverheadFactor and takerFeeRate approximate the total funds an + // exchange reserves when opening a position: + // totalRequired ≈ positionSize/leverage + positionSize*takerFeeRate + positionSize/leverage*1% + // = positionSize * (marginOverheadFactor/leverage + takerFeeRate) + marginOverheadFactor = 1.01 + takerFeeRate = 0.001 + + // positionSizeSafetyFactor leaves a buffer below the maximum affordable + // position size so a price move between sizing and execution cannot + // trigger an insufficient-margin rejection. + positionSizeSafetyFactor = 0.98 +) + // executeDecisionWithRecord executes AI decision and records detailed information func (at *AutoTrader) executeDecisionWithRecord(decision *kernel.Decision, actionRecord *store.DecisionAction) error { switch decision.Action { @@ -83,15 +97,12 @@ func (at *AutoTrader) executeOpenLongWithRecord(decision *kernel.Decision, actio } // ⚠️ Auto-adjust position size if insufficient margin - // Formula: totalRequired = positionSize/leverage + positionSize*0.001 + positionSize/leverage*0.01 - // = positionSize * (1.01/leverage + 0.001) - marginFactor := 1.01/float64(decision.Leverage) + 0.001 + marginFactor := marginOverheadFactor/float64(decision.Leverage) + takerFeeRate maxAffordablePositionSize := availableBalance / marginFactor actualPositionSize := decision.PositionSizeUSD if actualPositionSize > maxAffordablePositionSize { - // Use 98% of max to leave buffer for price fluctuation - adjustedSize := maxAffordablePositionSize * 0.98 + adjustedSize := maxAffordablePositionSize * positionSizeSafetyFactor logger.Infof(" ⚠️ Position size %.2f exceeds max affordable %.2f, auto-reducing to %.2f", actualPositionSize, maxAffordablePositionSize, adjustedSize) actualPositionSize = adjustedSize @@ -200,15 +211,12 @@ func (at *AutoTrader) executeOpenShortWithRecord(decision *kernel.Decision, acti } // ⚠️ Auto-adjust position size if insufficient margin - // Formula: totalRequired = positionSize/leverage + positionSize*0.001 + positionSize/leverage*0.01 - // = positionSize * (1.01/leverage + 0.001) - marginFactor := 1.01/float64(decision.Leverage) + 0.001 + marginFactor := marginOverheadFactor/float64(decision.Leverage) + takerFeeRate maxAffordablePositionSize := availableBalance / marginFactor actualPositionSize := decision.PositionSizeUSD if actualPositionSize > maxAffordablePositionSize { - // Use 98% of max to leave buffer for price fluctuation - adjustedSize := maxAffordablePositionSize * 0.98 + adjustedSize := maxAffordablePositionSize * positionSizeSafetyFactor logger.Infof(" ⚠️ Position size %.2f exceeds max affordable %.2f, auto-reducing to %.2f", actualPositionSize, maxAffordablePositionSize, adjustedSize) actualPositionSize = adjustedSize diff --git a/trader/hyperliquid/trader_orders.go b/trader/hyperliquid/trader_orders.go index 7c8a82ce..95884d22 100644 --- a/trader/hyperliquid/trader_orders.go +++ b/trader/hyperliquid/trader_orders.go @@ -15,6 +15,14 @@ import ( "github.com/sonirico/go-hyperliquid" ) +// Aggressive limit prices simulate market orders: buy slightly above and sell +// slightly below the current price so IOC limit orders fill immediately while +// capping slippage at 1%. +const ( + aggressiveBuyPriceFactor = 1.01 + aggressiveSellPriceFactor = 0.99 +) + func (t *HyperliquidTrader) placeOrderWithBuilderFee(order hyperliquid.CreateOrderRequest) error { _, err := t.exchange.Order(t.ctx, order, defaultBuilder) if err == nil { @@ -66,8 +74,8 @@ func (t *HyperliquidTrader) OpenLong(symbol string, quantity float64, leverage i } // Price needs to be processed to 5 significant figures - aggressivePrice := t.roundPriceToSigfigs(price * 1.01) - logger.Infof(" 💰 Price precision handling: %.8f -> %.8f (5 significant figures)", price*1.01, aggressivePrice) + aggressivePrice := t.roundPriceToSigfigs(price * aggressiveBuyPriceFactor) + logger.Infof(" 💰 Price precision handling: %.8f -> %.8f (5 significant figures)", price*aggressiveBuyPriceFactor, aggressivePrice) // Handle xyz dex assets differently if isXyz { @@ -138,8 +146,8 @@ func (t *HyperliquidTrader) OpenShort(symbol string, quantity float64, leverage } // Price needs to be processed to 5 significant figures - aggressivePrice := t.roundPriceToSigfigs(price * 0.99) - logger.Infof(" 💰 Price precision handling: %.8f -> %.8f (5 significant figures)", price*0.99, aggressivePrice) + aggressivePrice := t.roundPriceToSigfigs(price * aggressiveSellPriceFactor) + logger.Infof(" 💰 Price precision handling: %.8f -> %.8f (5 significant figures)", price*aggressiveSellPriceFactor, aggressivePrice) // Handle xyz dex assets differently if isXyz { @@ -220,8 +228,8 @@ func (t *HyperliquidTrader) CloseLong(symbol string, quantity float64) (map[stri } // Price needs to be processed to 5 significant figures - aggressivePrice := t.roundPriceToSigfigs(price * 0.99) - logger.Infof(" 💰 Price precision handling: %.8f -> %.8f (5 significant figures)", price*0.99, aggressivePrice) + aggressivePrice := t.roundPriceToSigfigs(price * aggressiveSellPriceFactor) + logger.Infof(" 💰 Price precision handling: %.8f -> %.8f (5 significant figures)", price*aggressiveSellPriceFactor, aggressivePrice) // Handle xyz dex assets differently if isXyz { @@ -307,8 +315,8 @@ func (t *HyperliquidTrader) CloseShort(symbol string, quantity float64) (map[str } // Price needs to be processed to 5 significant figures - aggressivePrice := t.roundPriceToSigfigs(price * 1.01) - logger.Infof(" 💰 Price precision handling: %.8f -> %.8f (5 significant figures)", price*1.01, aggressivePrice) + aggressivePrice := t.roundPriceToSigfigs(price * aggressiveBuyPriceFactor) + logger.Infof(" 💰 Price precision handling: %.8f -> %.8f (5 significant figures)", price*aggressiveBuyPriceFactor, aggressivePrice) // Handle xyz dex assets differently if isXyz { diff --git a/trader/position_rebuild.go b/trader/position_rebuild.go index 79c8cf2e..ac7b5518 100644 --- a/trader/position_rebuild.go +++ b/trader/position_rebuild.go @@ -11,6 +11,10 @@ import ( // All exchanges use this same algorithm to reconstruct position history from trades // ============================================================================= +// dustQuantityEpsilon is the threshold below which a residual quantity is +// treated as zero, absorbing float rounding noise from FIFO trade matching. +const dustQuantityEpsilon = 0.00000001 + // openTradeEntry represents an opening trade for position tracking type openTradeEntry struct { Price float64 @@ -130,7 +134,7 @@ func buildClosedPosition(trade TradeRecord, side string, state *positionState) * var weightedSum float64 var matchedQty float64 - for i := 0; i < len(state.OpenTrades) && remainingQty > 0.00000001; i++ { + for i := 0; i < len(state.OpenTrades) && remainingQty > dustQuantityEpsilon; i++ { ot := &state.OpenTrades[i] matchQty := ot.Quantity if matchQty > remainingQty { @@ -149,13 +153,13 @@ func buildClosedPosition(trade TradeRecord, side string, state *positionState) * ot.Quantity -= matchQty // Remove fully consumed open trade - if ot.Quantity <= 0.00000001 { + if ot.Quantity <= dustQuantityEpsilon { state.OpenTrades = append(state.OpenTrades[:i], state.OpenTrades[i+1:]...) i-- } } - if matchedQty > 0.00000001 { + if matchedQty > dustQuantityEpsilon { entryPrice = weightedSum / matchedQty } state.TotalQty -= trade.Quantity