mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-20 02:37:42 +08:00
Compare commits
15 Commits
ai-grid
...
2f54d1d4c0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f54d1d4c0 | ||
|
|
0b448558ca | ||
|
|
84276f64ae | ||
|
|
5560df133e | ||
|
|
f43c63699b | ||
|
|
7b1edaa51f | ||
|
|
ed8ad63288 | ||
|
|
a7370efc2f | ||
|
|
5b384d126f | ||
|
|
1532b55d77 | ||
|
|
0e75b80d95 | ||
|
|
9c57134dfb | ||
|
|
7ce7361cef | ||
|
|
7a1643c56c | ||
|
|
7e96c5d0f2 |
17
README.md
17
README.md
@@ -488,6 +488,23 @@ All contributions are tracked on GitHub. When NOFX generates revenue, contributo
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Sponsors
|
||||||
|
|
||||||
|
Thanks to all our sponsors!
|
||||||
|
|
||||||
|
<a href="https://github.com/pjl914335852-ux"><img src="https://github.com/pjl914335852-ux.png" width="60" height="60" style="border-radius:50%" alt="pjl914335852-ux" /></a>
|
||||||
|
<a href="https://github.com/cat9999aaa"><img src="https://github.com/cat9999aaa.png" width="60" height="60" style="border-radius:50%" alt="cat9999aaa" /></a>
|
||||||
|
<a href="https://github.com/1733055465"><img src="https://github.com/1733055465.png" width="60" height="60" style="border-radius:50%" alt="1733055465" /></a>
|
||||||
|
<a href="https://github.com/kolal2020"><img src="https://github.com/kolal2020.png" width="60" height="60" style="border-radius:50%" alt="kolal2020" /></a>
|
||||||
|
<a href="https://github.com/CyberFFarm"><img src="https://github.com/CyberFFarm.png" width="60" height="60" style="border-radius:50%" alt="CyberFFarm" /></a>
|
||||||
|
<a href="https://github.com/vip3001003"><img src="https://github.com/vip3001003.png" width="60" height="60" style="border-radius:50%" alt="vip3001003" /></a>
|
||||||
|
<a href="https://github.com/mrtluh"><img src="https://github.com/mrtluh.png" width="60" height="60" style="border-radius:50%" alt="mrtluh" /></a>
|
||||||
|
<a href="https://github.com/cpcp1117-source"><img src="https://github.com/cpcp1117-source.png" width="60" height="60" style="border-radius:50%" alt="cpcp1117-source" /></a>
|
||||||
|
|
||||||
|
[Become a sponsor](https://github.com/sponsors/NoFxAiOS)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Star History
|
## Star History
|
||||||
|
|
||||||
[](https://star-history.com/#NoFxAiOS/nofx&Date)
|
[](https://star-history.com/#NoFxAiOS/nofx&Date)
|
||||||
|
|||||||
@@ -1767,8 +1767,8 @@ func compactArrayOpen(s string) string {
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
func validateDecisions(decisions []Decision, accountEquity float64, btcEthLeverage, altcoinLeverage int, btcEthPosRatio, altcoinPosRatio float64) error {
|
func validateDecisions(decisions []Decision, accountEquity float64, btcEthLeverage, altcoinLeverage int, btcEthPosRatio, altcoinPosRatio float64) error {
|
||||||
for i, decision := range decisions {
|
for i := range decisions {
|
||||||
if err := validateDecision(&decision, accountEquity, btcEthLeverage, altcoinLeverage, btcEthPosRatio, altcoinPosRatio); err != nil {
|
if err := validateDecision(&decisions[i], accountEquity, btcEthLeverage, altcoinLeverage, btcEthPosRatio, altcoinPosRatio); err != nil {
|
||||||
return fmt.Errorf("decision #%d validation failed: %w", i+1, err)
|
return fmt.Errorf("decision #%d validation failed: %w", i+1, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,16 +158,19 @@ func (s *PositionStore) UpdatePositionQuantityAndPrice(id int64, addQty float64,
|
|||||||
newEntryPrice := (pos.EntryPrice*pos.Quantity + addPrice*addQty) / newQty
|
newEntryPrice := (pos.EntryPrice*pos.Quantity + addPrice*addQty) / newQty
|
||||||
newEntryPrice = math.Round(newEntryPrice*100) / 100
|
newEntryPrice = math.Round(newEntryPrice*100) / 100
|
||||||
newFee := pos.Fee + addFee
|
newFee := pos.Fee + addFee
|
||||||
|
nowMs := time.Now().UTC().UnixMilli()
|
||||||
|
|
||||||
return s.db.Model(&TraderPosition{}).Where("id = ?", id).Updates(map[string]interface{}{
|
return s.db.Model(&TraderPosition{}).Where("id = ?", id).Updates(map[string]interface{}{
|
||||||
"quantity": newQty,
|
"quantity": newQty,
|
||||||
"entry_quantity": newEntryQty,
|
"entry_quantity": newEntryQty,
|
||||||
"entry_price": newEntryPrice,
|
"entry_price": newEntryPrice,
|
||||||
"fee": newFee,
|
"fee": newFee,
|
||||||
|
"updated_at": nowMs,
|
||||||
}).Error
|
}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReducePositionQuantity reduces position quantity for partial close
|
// ReducePositionQuantity reduces position quantity for partial close
|
||||||
|
// If quantity reaches 0 (or near 0), automatically closes the position
|
||||||
func (s *PositionStore) ReducePositionQuantity(id int64, reduceQty float64, exitPrice float64, addFee float64, addPnL float64) error {
|
func (s *PositionStore) ReducePositionQuantity(id int64, reduceQty float64, exitPrice float64, addFee float64, addPnL float64) error {
|
||||||
var pos TraderPosition
|
var pos TraderPosition
|
||||||
if err := s.db.First(&pos, id).Error; err != nil {
|
if err := s.db.First(&pos, id).Error; err != nil {
|
||||||
@@ -187,19 +190,40 @@ func (s *PositionStore) ReducePositionQuantity(id int64, reduceQty float64, exit
|
|||||||
newExitPrice = math.Round(newExitPrice*100) / 100
|
newExitPrice = math.Round(newExitPrice*100) / 100
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nowMs := time.Now().UTC().UnixMilli()
|
||||||
|
|
||||||
|
// Check if position should be fully closed (quantity reduced to ~0)
|
||||||
|
const QUANTITY_TOLERANCE = 0.0001
|
||||||
|
if newQty <= QUANTITY_TOLERANCE {
|
||||||
|
// Auto-close: set status to CLOSED
|
||||||
|
return s.db.Model(&TraderPosition{}).Where("id = ?", id).Updates(map[string]interface{}{
|
||||||
|
"quantity": 0,
|
||||||
|
"fee": newFee,
|
||||||
|
"exit_price": newExitPrice,
|
||||||
|
"realized_pnl": newPnL,
|
||||||
|
"status": "CLOSED",
|
||||||
|
"exit_time": nowMs,
|
||||||
|
"close_reason": "sync",
|
||||||
|
"updated_at": nowMs,
|
||||||
|
}).Error
|
||||||
|
}
|
||||||
|
|
||||||
return s.db.Model(&TraderPosition{}).Where("id = ?", id).Updates(map[string]interface{}{
|
return s.db.Model(&TraderPosition{}).Where("id = ?", id).Updates(map[string]interface{}{
|
||||||
"quantity": newQty,
|
"quantity": newQty,
|
||||||
"fee": newFee,
|
"fee": newFee,
|
||||||
"exit_price": newExitPrice,
|
"exit_price": newExitPrice,
|
||||||
"realized_pnl": newPnL,
|
"realized_pnl": newPnL,
|
||||||
|
"updated_at": nowMs,
|
||||||
}).Error
|
}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdatePositionExchangeInfo updates exchange_id and exchange_type
|
// UpdatePositionExchangeInfo updates exchange_id and exchange_type
|
||||||
func (s *PositionStore) UpdatePositionExchangeInfo(id int64, exchangeID, exchangeType string) error {
|
func (s *PositionStore) UpdatePositionExchangeInfo(id int64, exchangeID, exchangeType string) error {
|
||||||
|
nowMs := time.Now().UTC().UnixMilli()
|
||||||
return s.db.Model(&TraderPosition{}).Where("id = ?", id).Updates(map[string]interface{}{
|
return s.db.Model(&TraderPosition{}).Where("id = ?", id).Updates(map[string]interface{}{
|
||||||
"exchange_id": exchangeID,
|
"exchange_id": exchangeID,
|
||||||
"exchange_type": exchangeType,
|
"exchange_type": exchangeType,
|
||||||
|
"updated_at": nowMs,
|
||||||
}).Error
|
}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,12 +56,8 @@ func (t *FuturesTrader) SyncOrdersFromBinance(traderID string, exchangeID string
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Record current time BEFORE querying, to avoid missing trades during sync
|
logger.Infof("🔄 Syncing Binance trades from: %s (UTC) [ms: %d, now: %d]",
|
||||||
// This prevents race condition where trades happen between query and lastSyncTime update
|
time.UnixMilli(lastSyncTimeMs).UTC().Format("2006-01-02 15:04:05"), lastSyncTimeMs, nowMs)
|
||||||
syncStartTimeMs := nowMs
|
|
||||||
|
|
||||||
logger.Infof("🔄 Syncing Binance trades from: %s (UTC)",
|
|
||||||
time.UnixMilli(lastSyncTimeMs).UTC().Format("2006-01-02 15:04:05"))
|
|
||||||
|
|
||||||
// Step 1: Get max trade IDs from local DB for incremental sync
|
// Step 1: Get max trade IDs from local DB for incremental sync
|
||||||
maxTradeIDs, err := orderStore.GetMaxTradeIDsByExchange(exchangeID)
|
maxTradeIDs, err := orderStore.GetMaxTradeIDsByExchange(exchangeID)
|
||||||
@@ -100,18 +96,17 @@ func (t *FuturesTrader) SyncOrdersFromBinance(traderID string, exchangeID string
|
|||||||
symbolMap[s] = true
|
symbolMap[s] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Method 4: FALLBACK - Query REALIZED_PNL income to find symbols with closed trades
|
// Method 4: ALWAYS query REALIZED_PNL income to find symbols with closed trades
|
||||||
// This catches trades that COMMISSION missed (VIP users, BNB fee discount)
|
// This catches trades that COMMISSION missed (VIP users, BNB fee discount)
|
||||||
if len(symbolMap) == 0 {
|
// IMPORTANT: Must run always, not just when symbolMap is empty,
|
||||||
logger.Infof(" 🔍 No symbols found, trying REALIZED_PNL fallback...")
|
// because a position might be fully closed (no active position) but have PnL
|
||||||
pnlSymbols, err := t.GetPnLSymbols(lastSyncTime)
|
pnlSymbols, err := t.GetPnLSymbols(lastSyncTime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Infof(" ⚠️ Failed to get PnL symbols: %v", err)
|
logger.Infof(" ⚠️ Failed to get PnL symbols: %v", err)
|
||||||
} else {
|
} else {
|
||||||
logger.Infof(" 📋 REALIZED_PNL symbols found: %d - %v", len(pnlSymbols), pnlSymbols)
|
logger.Infof(" 📋 REALIZED_PNL symbols found: %d - %v", len(pnlSymbols), pnlSymbols)
|
||||||
for _, s := range pnlSymbols {
|
for _, s := range pnlSymbols {
|
||||||
symbolMap[s] = true
|
symbolMap[s] = true
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,10 +117,9 @@ func (t *FuturesTrader) SyncOrdersFromBinance(traderID string, exchangeID string
|
|||||||
|
|
||||||
if len(changedSymbols) == 0 {
|
if len(changedSymbols) == 0 {
|
||||||
logger.Infof("📭 No symbols with new trades to sync")
|
logger.Infof("📭 No symbols with new trades to sync")
|
||||||
// Update last sync time even if no changes
|
// DON'T update lastSyncTime to current time here!
|
||||||
binanceSyncStateMutex.Lock()
|
// Keep using the last actual trade time from DB to avoid creating gaps
|
||||||
binanceSyncState[exchangeID] = syncStartTimeMs
|
// The lastSyncTimeMs from DB already has +1000ms buffer added
|
||||||
binanceSyncStateMutex.Unlock()
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,17 +152,12 @@ func (t *FuturesTrader) SyncOrdersFromBinance(traderID string, exchangeID string
|
|||||||
|
|
||||||
logger.Infof("📥 Received %d trades from Binance (%d API calls)", len(allTrades), apiCalls)
|
logger.Infof("📥 Received %d trades from Binance (%d API calls)", len(allTrades), apiCalls)
|
||||||
|
|
||||||
// Only update last sync time if ALL symbols were successfully queried
|
|
||||||
// This prevents data loss when some symbols fail due to rate limit or network issues
|
|
||||||
if len(failedSymbols) == 0 {
|
|
||||||
binanceSyncStateMutex.Lock()
|
|
||||||
binanceSyncState[exchangeID] = syncStartTimeMs
|
|
||||||
binanceSyncStateMutex.Unlock()
|
|
||||||
} else {
|
|
||||||
logger.Infof(" ⚠️ %d symbols failed, not updating lastSyncTime to retry next time: %v", len(failedSymbols), failedSymbols)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(allTrades) == 0 {
|
if len(allTrades) == 0 {
|
||||||
|
// No trades returned, but symbols were detected - might be false positive from COMMISSION/PnL detection
|
||||||
|
// Don't update lastSyncTime, keep using DB value
|
||||||
|
if len(failedSymbols) > 0 {
|
||||||
|
logger.Infof(" ⚠️ %d symbols failed: %v", len(failedSymbols), failedSymbols)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,10 +171,12 @@ func (t *FuturesTrader) SyncOrdersFromBinance(traderID string, exchangeID string
|
|||||||
posBuilder := store.NewPositionBuilder(positionStore)
|
posBuilder := store.NewPositionBuilder(positionStore)
|
||||||
syncedCount := 0
|
syncedCount := 0
|
||||||
|
|
||||||
|
skippedCount := 0
|
||||||
for _, trade := range allTrades {
|
for _, trade := range allTrades {
|
||||||
// Check if trade already exists
|
// Check if trade already exists
|
||||||
existing, err := orderStore.GetOrderByExchangeID(exchangeID, trade.TradeID)
|
existing, err := orderStore.GetOrderByExchangeID(exchangeID, trade.TradeID)
|
||||||
if err == nil && existing != nil {
|
if err == nil && existing != nil {
|
||||||
|
skippedCount++
|
||||||
continue // Trade already exists, skip
|
continue // Trade already exists, skip
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,7 +271,21 @@ func (t *FuturesTrader) SyncOrdersFromBinance(traderID string, exchangeID string
|
|||||||
trade.Time.UTC().Format("01-02 15:04:05"))
|
trade.Time.UTC().Format("01-02 15:04:05"))
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Infof("✅ Binance order sync completed: %d new trades synced", syncedCount)
|
// Update lastSyncTime to the LATEST trade time (not current time!)
|
||||||
|
// This ensures next sync starts from where we left off, not from "now"
|
||||||
|
// allTrades is already sorted by time ASC, so last element is the latest
|
||||||
|
if len(allTrades) > 0 && len(failedSymbols) == 0 {
|
||||||
|
latestTradeTimeMs := allTrades[len(allTrades)-1].Time.UTC().UnixMilli()
|
||||||
|
binanceSyncStateMutex.Lock()
|
||||||
|
binanceSyncState[exchangeID] = latestTradeTimeMs
|
||||||
|
binanceSyncStateMutex.Unlock()
|
||||||
|
logger.Infof("📅 Updated lastSyncTime to latest trade: %s (UTC)",
|
||||||
|
time.UnixMilli(latestTradeTimeMs).UTC().Format("2006-01-02 15:04:05"))
|
||||||
|
} else if len(failedSymbols) > 0 {
|
||||||
|
logger.Infof(" ⚠️ %d symbols failed, not updating lastSyncTime to retry next time: %v", len(failedSymbols), failedSymbols)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Infof("✅ Binance order sync completed: %d new trades synced, %d skipped (already exist)", syncedCount, skippedCount)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export function GridConfigEditor({
|
|||||||
totalInvestment: { zh: '投资金额 (USDT)', en: 'Investment (USDT)' },
|
totalInvestment: { zh: '投资金额 (USDT)', en: 'Investment (USDT)' },
|
||||||
totalInvestmentDesc: { zh: '网格策略的总投资金额', en: 'Total investment for grid strategy' },
|
totalInvestmentDesc: { zh: '网格策略的总投资金额', en: 'Total investment for grid strategy' },
|
||||||
leverage: { zh: '杠杆倍数', en: 'Leverage' },
|
leverage: { zh: '杠杆倍数', en: 'Leverage' },
|
||||||
leverageDesc: { zh: '交易使用的杠杆倍数 (1-20)', en: 'Leverage for trading (1-20)' },
|
leverageDesc: { zh: '交易使用的杠杆倍数 (1-5)', en: 'Leverage for trading (1-5)' },
|
||||||
|
|
||||||
// Grid parameters
|
// Grid parameters
|
||||||
gridCount: { zh: '网格数量', en: 'Grid Count' },
|
gridCount: { zh: '网格数量', en: 'Grid Count' },
|
||||||
@@ -171,7 +171,7 @@ export function GridConfigEditor({
|
|||||||
onChange={(e) => updateField('leverage', parseInt(e.target.value) || 5)}
|
onChange={(e) => updateField('leverage', parseInt(e.target.value) || 5)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
min={1}
|
min={1}
|
||||||
max={20}
|
max={5}
|
||||||
className="w-full px-3 py-2 rounded"
|
className="w-full px-3 py-2 rounded"
|
||||||
style={inputStyle}
|
style={inputStyle}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user