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"
)
// 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)

View File

@@ -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

View File

@@ -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 {

View File

@@ -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