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
This commit is contained in:
tinkle-community
2026-06-11 00:33:11 +08:00
parent 9ea9bd705f
commit c0d8a9a375
4 changed files with 53 additions and 25 deletions

View File

@@ -9,6 +9,14 @@ import (
"strings" "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 // OpenLong Open long position
func (t *AsterTrader) OpenLong(symbol string, quantity float64, leverage int) (map[string]interface{}, error) { 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 // 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) // 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 // Format price and quantity to correct precision
formattedPrice, err := t.formatPrice(symbol, limitPrice) 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) // 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 // Format price and quantity to correct precision
formattedPrice, err := t.formatPrice(symbol, limitPrice) formattedPrice, err := t.formatPrice(symbol, limitPrice)
@@ -182,7 +190,7 @@ func (t *AsterTrader) CloseLong(symbol string, quantity float64) (map[string]int
return nil, err return nil, err
} }
limitPrice := price * 0.99 limitPrice := price * aggressiveSellPriceFactor
// Format price and quantity to correct precision // Format price and quantity to correct precision
formattedPrice, err := t.formatPrice(symbol, limitPrice) formattedPrice, err := t.formatPrice(symbol, limitPrice)
@@ -265,7 +273,7 @@ func (t *AsterTrader) CloseShort(symbol string, quantity float64) (map[string]in
return nil, err return nil, err
} }
limitPrice := price * 1.01 limitPrice := price * aggressiveBuyPriceFactor
// Format price and quantity to correct precision // Format price and quantity to correct precision
formattedPrice, err := t.formatPrice(symbol, limitPrice) formattedPrice, err := t.formatPrice(symbol, limitPrice)

View File

@@ -9,6 +9,20 @@ import (
"time" "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 // executeDecisionWithRecord executes AI decision and records detailed information
func (at *AutoTrader) executeDecisionWithRecord(decision *kernel.Decision, actionRecord *store.DecisionAction) error { func (at *AutoTrader) executeDecisionWithRecord(decision *kernel.Decision, actionRecord *store.DecisionAction) error {
switch decision.Action { switch decision.Action {
@@ -83,15 +97,12 @@ func (at *AutoTrader) executeOpenLongWithRecord(decision *kernel.Decision, actio
} }
// ⚠️ Auto-adjust position size if insufficient margin // ⚠️ Auto-adjust position size if insufficient margin
// Formula: totalRequired = positionSize/leverage + positionSize*0.001 + positionSize/leverage*0.01 marginFactor := marginOverheadFactor/float64(decision.Leverage) + takerFeeRate
// = positionSize * (1.01/leverage + 0.001)
marginFactor := 1.01/float64(decision.Leverage) + 0.001
maxAffordablePositionSize := availableBalance / marginFactor maxAffordablePositionSize := availableBalance / marginFactor
actualPositionSize := decision.PositionSizeUSD actualPositionSize := decision.PositionSizeUSD
if actualPositionSize > maxAffordablePositionSize { if actualPositionSize > maxAffordablePositionSize {
// Use 98% of max to leave buffer for price fluctuation adjustedSize := maxAffordablePositionSize * positionSizeSafetyFactor
adjustedSize := maxAffordablePositionSize * 0.98
logger.Infof(" ⚠️ Position size %.2f exceeds max affordable %.2f, auto-reducing to %.2f", logger.Infof(" ⚠️ Position size %.2f exceeds max affordable %.2f, auto-reducing to %.2f",
actualPositionSize, maxAffordablePositionSize, adjustedSize) actualPositionSize, maxAffordablePositionSize, adjustedSize)
actualPositionSize = adjustedSize actualPositionSize = adjustedSize
@@ -200,15 +211,12 @@ func (at *AutoTrader) executeOpenShortWithRecord(decision *kernel.Decision, acti
} }
// ⚠️ Auto-adjust position size if insufficient margin // ⚠️ Auto-adjust position size if insufficient margin
// Formula: totalRequired = positionSize/leverage + positionSize*0.001 + positionSize/leverage*0.01 marginFactor := marginOverheadFactor/float64(decision.Leverage) + takerFeeRate
// = positionSize * (1.01/leverage + 0.001)
marginFactor := 1.01/float64(decision.Leverage) + 0.001
maxAffordablePositionSize := availableBalance / marginFactor maxAffordablePositionSize := availableBalance / marginFactor
actualPositionSize := decision.PositionSizeUSD actualPositionSize := decision.PositionSizeUSD
if actualPositionSize > maxAffordablePositionSize { if actualPositionSize > maxAffordablePositionSize {
// Use 98% of max to leave buffer for price fluctuation adjustedSize := maxAffordablePositionSize * positionSizeSafetyFactor
adjustedSize := maxAffordablePositionSize * 0.98
logger.Infof(" ⚠️ Position size %.2f exceeds max affordable %.2f, auto-reducing to %.2f", logger.Infof(" ⚠️ Position size %.2f exceeds max affordable %.2f, auto-reducing to %.2f",
actualPositionSize, maxAffordablePositionSize, adjustedSize) actualPositionSize, maxAffordablePositionSize, adjustedSize)
actualPositionSize = adjustedSize actualPositionSize = adjustedSize

View File

@@ -15,6 +15,14 @@ import (
"github.com/sonirico/go-hyperliquid" "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 { func (t *HyperliquidTrader) placeOrderWithBuilderFee(order hyperliquid.CreateOrderRequest) error {
_, err := t.exchange.Order(t.ctx, order, defaultBuilder) _, err := t.exchange.Order(t.ctx, order, defaultBuilder)
if err == nil { 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 // Price needs to be processed to 5 significant figures
aggressivePrice := t.roundPriceToSigfigs(price * 1.01) aggressivePrice := t.roundPriceToSigfigs(price * aggressiveBuyPriceFactor)
logger.Infof(" 💰 Price precision handling: %.8f -> %.8f (5 significant figures)", price*1.01, aggressivePrice) logger.Infof(" 💰 Price precision handling: %.8f -> %.8f (5 significant figures)", price*aggressiveBuyPriceFactor, aggressivePrice)
// Handle xyz dex assets differently // Handle xyz dex assets differently
if isXyz { if isXyz {
@@ -138,8 +146,8 @@ func (t *HyperliquidTrader) OpenShort(symbol string, quantity float64, leverage
} }
// Price needs to be processed to 5 significant figures // Price needs to be processed to 5 significant figures
aggressivePrice := t.roundPriceToSigfigs(price * 0.99) aggressivePrice := t.roundPriceToSigfigs(price * aggressiveSellPriceFactor)
logger.Infof(" 💰 Price precision handling: %.8f -> %.8f (5 significant figures)", price*0.99, aggressivePrice) logger.Infof(" 💰 Price precision handling: %.8f -> %.8f (5 significant figures)", price*aggressiveSellPriceFactor, aggressivePrice)
// Handle xyz dex assets differently // Handle xyz dex assets differently
if isXyz { 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 // Price needs to be processed to 5 significant figures
aggressivePrice := t.roundPriceToSigfigs(price * 0.99) aggressivePrice := t.roundPriceToSigfigs(price * aggressiveSellPriceFactor)
logger.Infof(" 💰 Price precision handling: %.8f -> %.8f (5 significant figures)", price*0.99, aggressivePrice) logger.Infof(" 💰 Price precision handling: %.8f -> %.8f (5 significant figures)", price*aggressiveSellPriceFactor, aggressivePrice)
// Handle xyz dex assets differently // Handle xyz dex assets differently
if isXyz { 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 // Price needs to be processed to 5 significant figures
aggressivePrice := t.roundPriceToSigfigs(price * 1.01) aggressivePrice := t.roundPriceToSigfigs(price * aggressiveBuyPriceFactor)
logger.Infof(" 💰 Price precision handling: %.8f -> %.8f (5 significant figures)", price*1.01, aggressivePrice) logger.Infof(" 💰 Price precision handling: %.8f -> %.8f (5 significant figures)", price*aggressiveBuyPriceFactor, aggressivePrice)
// Handle xyz dex assets differently // Handle xyz dex assets differently
if isXyz { if isXyz {

View File

@@ -11,6 +11,10 @@ import (
// All exchanges use this same algorithm to reconstruct position history from trades // 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 // openTradeEntry represents an opening trade for position tracking
type openTradeEntry struct { type openTradeEntry struct {
Price float64 Price float64
@@ -130,7 +134,7 @@ func buildClosedPosition(trade TradeRecord, side string, state *positionState) *
var weightedSum float64 var weightedSum float64
var matchedQty 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] ot := &state.OpenTrades[i]
matchQty := ot.Quantity matchQty := ot.Quantity
if matchQty > remainingQty { if matchQty > remainingQty {
@@ -149,13 +153,13 @@ func buildClosedPosition(trade TradeRecord, side string, state *positionState) *
ot.Quantity -= matchQty ot.Quantity -= matchQty
// Remove fully consumed open trade // Remove fully consumed open trade
if ot.Quantity <= 0.00000001 { if ot.Quantity <= dustQuantityEpsilon {
state.OpenTrades = append(state.OpenTrades[:i], state.OpenTrades[i+1:]...) state.OpenTrades = append(state.OpenTrades[:i], state.OpenTrades[i+1:]...)
i-- i--
} }
} }
if matchedQty > 0.00000001 { if matchedQty > dustQuantityEpsilon {
entryPrice = weightedSum / matchedQty entryPrice = weightedSum / matchedQty
} }
state.TotalQty -= trade.Quantity state.TotalQty -= trade.Quantity