mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-05 12:00:59 +08:00
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:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user