mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-16 01:06: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"
|
"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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user