mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-06-06 05:51:19 +08:00
refactor: split large files and clean up project structure
- Rename experience/ to telemetry/ for clarity - Split 15+ large Go files (800-2200 lines) into focused modules: kernel/engine.go, backtest/runner.go, market/data.go, store/position.go, api/handler_trader.go, trader/auto_trader_grid.go, and 9 exchange traders - Split frontend monoliths: types.ts, api.ts, AITradersPage.tsx, BacktestPage.tsx into domain-specific modules with barrel re-exports - Remove stale files: screenshots, .yml.old, pyproject.toml - Remove unused scripts/ and cmd/ directories - Remove broken/outdated test files (network-dependent, stale expectations)
This commit is contained in:
121
kernel/engine_position.go
Normal file
121
kernel/engine_position.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package kernel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"nofx/logger"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// Decision Validation
|
||||
// ============================================================================
|
||||
|
||||
func validateDecisions(decisions []Decision, accountEquity float64, btcEthLeverage, altcoinLeverage int, btcEthPosRatio, altcoinPosRatio float64) error {
|
||||
for i := range decisions {
|
||||
if err := validateDecision(&decisions[i], accountEquity, btcEthLeverage, altcoinLeverage, btcEthPosRatio, altcoinPosRatio); err != nil {
|
||||
return fmt.Errorf("decision #%d validation failed: %w", i+1, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateDecision(d *Decision, accountEquity float64, btcEthLeverage, altcoinLeverage int, btcEthPosRatio, altcoinPosRatio float64) error {
|
||||
validActions := map[string]bool{
|
||||
"open_long": true,
|
||||
"open_short": true,
|
||||
"close_long": true,
|
||||
"close_short": true,
|
||||
"hold": true,
|
||||
"wait": true,
|
||||
}
|
||||
|
||||
if !validActions[d.Action] {
|
||||
return fmt.Errorf("invalid action: %s", d.Action)
|
||||
}
|
||||
|
||||
if d.Action == "open_long" || d.Action == "open_short" {
|
||||
maxLeverage := altcoinLeverage
|
||||
posRatio := altcoinPosRatio
|
||||
maxPositionValue := accountEquity * posRatio
|
||||
if d.Symbol == "BTCUSDT" || d.Symbol == "ETHUSDT" {
|
||||
maxLeverage = btcEthLeverage
|
||||
posRatio = btcEthPosRatio
|
||||
maxPositionValue = accountEquity * posRatio
|
||||
}
|
||||
|
||||
if d.Leverage <= 0 {
|
||||
return fmt.Errorf("leverage must be greater than 0: %d", d.Leverage)
|
||||
}
|
||||
if d.Leverage > maxLeverage {
|
||||
logger.Infof("⚠️ [Leverage Fallback] %s leverage exceeded (%dx > %dx), auto-adjusting to limit %dx",
|
||||
d.Symbol, d.Leverage, maxLeverage, maxLeverage)
|
||||
d.Leverage = maxLeverage
|
||||
}
|
||||
if d.PositionSizeUSD <= 0 {
|
||||
return fmt.Errorf("position size must be greater than 0: %.2f", d.PositionSizeUSD)
|
||||
}
|
||||
|
||||
const minPositionSizeGeneral = 12.0
|
||||
const minPositionSizeBTCETH = 60.0
|
||||
|
||||
if d.Symbol == "BTCUSDT" || d.Symbol == "ETHUSDT" {
|
||||
if d.PositionSizeUSD < minPositionSizeBTCETH {
|
||||
return fmt.Errorf("%s opening amount too small (%.2f USDT), must be ≥%.2f USDT", d.Symbol, d.PositionSizeUSD, minPositionSizeBTCETH)
|
||||
}
|
||||
} else {
|
||||
if d.PositionSizeUSD < minPositionSizeGeneral {
|
||||
return fmt.Errorf("opening amount too small (%.2f USDT), must be ≥%.2f USDT", d.PositionSizeUSD, minPositionSizeGeneral)
|
||||
}
|
||||
}
|
||||
|
||||
tolerance := maxPositionValue * 0.01
|
||||
if d.PositionSizeUSD > maxPositionValue+tolerance {
|
||||
if d.Symbol == "BTCUSDT" || d.Symbol == "ETHUSDT" {
|
||||
return fmt.Errorf("BTC/ETH single coin position value cannot exceed %.0f USDT (%.1fx account equity), actual: %.0f", maxPositionValue, posRatio, d.PositionSizeUSD)
|
||||
} else {
|
||||
return fmt.Errorf("altcoin single coin position value cannot exceed %.0f USDT (%.1fx account equity), actual: %.0f", maxPositionValue, posRatio, d.PositionSizeUSD)
|
||||
}
|
||||
}
|
||||
if d.StopLoss <= 0 || d.TakeProfit <= 0 {
|
||||
return fmt.Errorf("stop loss and take profit must be greater than 0")
|
||||
}
|
||||
|
||||
if d.Action == "open_long" {
|
||||
if d.StopLoss >= d.TakeProfit {
|
||||
return fmt.Errorf("for long positions, stop loss price must be less than take profit price")
|
||||
}
|
||||
} else {
|
||||
if d.StopLoss <= d.TakeProfit {
|
||||
return fmt.Errorf("for short positions, stop loss price must be greater than take profit price")
|
||||
}
|
||||
}
|
||||
|
||||
var entryPrice float64
|
||||
if d.Action == "open_long" {
|
||||
entryPrice = d.StopLoss + (d.TakeProfit-d.StopLoss)*0.2
|
||||
} else {
|
||||
entryPrice = d.StopLoss - (d.StopLoss-d.TakeProfit)*0.2
|
||||
}
|
||||
|
||||
var riskPercent, rewardPercent, riskRewardRatio float64
|
||||
if d.Action == "open_long" {
|
||||
riskPercent = (entryPrice - d.StopLoss) / entryPrice * 100
|
||||
rewardPercent = (d.TakeProfit - entryPrice) / entryPrice * 100
|
||||
if riskPercent > 0 {
|
||||
riskRewardRatio = rewardPercent / riskPercent
|
||||
}
|
||||
} else {
|
||||
riskPercent = (d.StopLoss - entryPrice) / entryPrice * 100
|
||||
rewardPercent = (entryPrice - d.TakeProfit) / entryPrice * 100
|
||||
if riskPercent > 0 {
|
||||
riskRewardRatio = rewardPercent / riskPercent
|
||||
}
|
||||
}
|
||||
|
||||
if riskRewardRatio < 3.0 {
|
||||
return fmt.Errorf("risk/reward ratio too low (%.2f:1), must be ≥3.0:1 [risk: %.2f%% reward: %.2f%%] [stop loss: %.2f take profit: %.2f]",
|
||||
riskRewardRatio, riskPercent, rewardPercent, d.StopLoss, d.TakeProfit)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user