mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-17 09:24:36 +08:00
fix: reconcile local positions against the live exchange book — stop lost PnL
Root cause of the dashboard under-reporting: any missed or unmatched fill (position flip, sync gap, liquidation) left a 'zombie' OPEN row. Every later close on that symbol landed as a partial close against the zombie, so the row never reached CLOSED and its realized PnL never entered the closed-trade statistics — Edge Profile, realized P/L, win rate, and the AI's own track-record context all silently under-reported. Fix: after each Hyperliquid order sync, reconcile local OPEN rows against the exchange's live book (core perps + xyz dex), scoped by exchange account so rows left by prior autopilot incarnations (each relaunch mints a fresh trader_id sharing one exchange) are healed too. Rows the exchange no longer holds are closed with their accumulated PnL; oversized rows are trimmed to the live quantity. Live run confirmed: 9 zombie rows closed across incarnations, book now matches the exchange exactly. Also removes the header language switcher (desktop + mobile) and the login/setup LanguageSwitcher — the product UI is English-only. New store methods GetOpenPositionsByExchange + ReconcileOpenPositionsWithLive with cross-incarnation test coverage.
This commit is contained in:
@@ -347,6 +347,27 @@ func (s *PositionStore) GetOpenPositions(traderID string) ([]*TraderPosition, er
|
||||
return positions, nil
|
||||
}
|
||||
|
||||
// GetOpenPositionsByExchange returns every OPEN row on an exchange account,
|
||||
// across all trader IDs. An exchange account is shared by every "NOFX
|
||||
// Autopilot" relaunch (each relaunch mints a fresh trader_id), so reconciling
|
||||
// must be scoped to the exchange — not the current trader_id — or rows left by
|
||||
// prior incarnations become permanent orphans that never close.
|
||||
func (s *PositionStore) GetOpenPositionsByExchange(exchangeID string) ([]*TraderPosition, error) {
|
||||
var positions []*TraderPosition
|
||||
err := s.db.Where("exchange_id = ? AND status = ?", exchangeID, "OPEN").
|
||||
Order("entry_time DESC").
|
||||
Find(&positions).Error
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query open positions by exchange: %w", err)
|
||||
}
|
||||
for _, pos := range positions {
|
||||
if pos.EntryQuantity == 0 {
|
||||
pos.EntryQuantity = pos.Quantity
|
||||
}
|
||||
}
|
||||
return positions, nil
|
||||
}
|
||||
|
||||
// GetOpenPositionBySymbol gets open position for specified symbol and direction
|
||||
func (s *PositionStore) GetOpenPositionBySymbol(traderID, symbol, side string) (*TraderPosition, error) {
|
||||
var pos TraderPosition
|
||||
|
||||
Reference in New Issue
Block a user