mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-11 23:07:01 +08:00
Sync manual closes into position history
This commit is contained in:
@@ -382,11 +382,54 @@ func (s *PositionStore) GetOpenPositionBySymbol(traderID, symbol, side string) (
|
||||
|
||||
// GetClosedPositions gets closed positions
|
||||
func (s *PositionStore) GetClosedPositions(traderID string, limit int) ([]*TraderPosition, error) {
|
||||
return s.GetClosedPositionsByTraderFilters([]string{traderID}, nil, limit)
|
||||
}
|
||||
|
||||
func (s *PositionStore) closedPositionsByTraderFilters(traderIDs []string, traderIDPatterns []string) *gorm.DB {
|
||||
query := s.db.Where("status = ?", "CLOSED")
|
||||
|
||||
conditions := make([]string, 0, len(traderIDs)+len(traderIDPatterns))
|
||||
args := make([]interface{}, 0, len(traderIDs)+len(traderIDPatterns))
|
||||
|
||||
cleanTraderIDs := make([]string, 0, len(traderIDs))
|
||||
for _, traderID := range traderIDs {
|
||||
traderID = strings.TrimSpace(traderID)
|
||||
if traderID != "" {
|
||||
cleanTraderIDs = append(cleanTraderIDs, traderID)
|
||||
}
|
||||
}
|
||||
if len(cleanTraderIDs) > 0 {
|
||||
conditions = append(conditions, "trader_id IN ?")
|
||||
args = append(args, cleanTraderIDs)
|
||||
}
|
||||
|
||||
for _, pattern := range traderIDPatterns {
|
||||
pattern = strings.TrimSpace(pattern)
|
||||
if pattern == "" {
|
||||
continue
|
||||
}
|
||||
conditions = append(conditions, "trader_id LIKE ?")
|
||||
args = append(args, pattern)
|
||||
}
|
||||
|
||||
if len(conditions) == 0 {
|
||||
return query.Where("1 = 0")
|
||||
}
|
||||
|
||||
return query.Where("("+strings.Join(conditions, " OR ")+")", args...)
|
||||
}
|
||||
|
||||
// GetClosedPositionsByTraderFilters gets closed positions for explicit trader IDs
|
||||
// and legacy trader ID patterns. Patterns are used only for same-user Autopilot
|
||||
// history continuity when an old trader row was deleted but its position records remain.
|
||||
func (s *PositionStore) GetClosedPositionsByTraderFilters(traderIDs []string, traderIDPatterns []string, limit int) ([]*TraderPosition, error) {
|
||||
var positions []*TraderPosition
|
||||
err := s.db.Where("trader_id = ? AND status = ?", traderID, "CLOSED").
|
||||
Order("exit_time DESC").
|
||||
Limit(limit).
|
||||
Find(&positions).Error
|
||||
query := s.closedPositionsByTraderFilters(traderIDs, traderIDPatterns).Order("exit_time DESC")
|
||||
if limit > 0 {
|
||||
query = query.Limit(limit)
|
||||
}
|
||||
|
||||
err := query.Find(&positions).Error
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query closed positions: %w", err)
|
||||
}
|
||||
|
||||
@@ -56,18 +56,16 @@ func (s *PositionStore) GetPositionStats(traderID string) (map[string]interface{
|
||||
|
||||
// GetFullStats gets complete trading statistics
|
||||
func (s *PositionStore) GetFullStats(traderID string) (*TraderStats, error) {
|
||||
return s.GetFullStatsByTraderFilters([]string{traderID}, nil)
|
||||
}
|
||||
|
||||
// GetFullStatsByTraderFilters gets complete trading statistics for explicit
|
||||
// trader IDs plus optional legacy trader ID patterns.
|
||||
func (s *PositionStore) GetFullStatsByTraderFilters(traderIDs []string, traderIDPatterns []string) (*TraderStats, error) {
|
||||
stats := &TraderStats{}
|
||||
|
||||
var count int64
|
||||
if err := s.db.Model(&TraderPosition{}).Where("trader_id = ? AND status = ?", traderID, "CLOSED").Count(&count).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if count == 0 {
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
var positions []TraderPosition
|
||||
err := s.db.Where("trader_id = ? AND status = ?", traderID, "CLOSED").
|
||||
err := s.closedPositionsByTraderFilters(traderIDs, traderIDPatterns).
|
||||
Order("exit_time ASC").
|
||||
Find(&positions).Error
|
||||
if err != nil {
|
||||
@@ -234,8 +232,14 @@ type SymbolStats struct {
|
||||
|
||||
// GetSymbolStats gets per-symbol trading statistics
|
||||
func (s *PositionStore) GetSymbolStats(traderID string, limit int) ([]SymbolStats, error) {
|
||||
return s.GetSymbolStatsByTraderFilters([]string{traderID}, nil, limit)
|
||||
}
|
||||
|
||||
// GetSymbolStatsByTraderFilters gets per-symbol trading statistics for explicit
|
||||
// trader IDs plus optional legacy trader ID patterns.
|
||||
func (s *PositionStore) GetSymbolStatsByTraderFilters(traderIDs []string, traderIDPatterns []string, limit int) ([]SymbolStats, error) {
|
||||
var positions []TraderPosition
|
||||
err := s.db.Where("trader_id = ? AND status = ?", traderID, "CLOSED").Find(&positions).Error
|
||||
err := s.closedPositionsByTraderFilters(traderIDs, traderIDPatterns).Find(&positions).Error
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query symbol stats: %w", err)
|
||||
}
|
||||
@@ -311,8 +315,8 @@ func (s *PositionStore) GetHoldingTimeStats(traderID string) ([]HoldingTimeStats
|
||||
}
|
||||
|
||||
rangeStats := map[string]*struct {
|
||||
count int
|
||||
wins int
|
||||
count int
|
||||
wins int
|
||||
totalPnL float64
|
||||
}{
|
||||
"<1h": {},
|
||||
@@ -374,8 +378,14 @@ type DirectionStats struct {
|
||||
|
||||
// GetDirectionStats analyzes long vs short performance
|
||||
func (s *PositionStore) GetDirectionStats(traderID string) ([]DirectionStats, error) {
|
||||
return s.GetDirectionStatsByTraderFilters([]string{traderID}, nil)
|
||||
}
|
||||
|
||||
// GetDirectionStatsByTraderFilters analyzes long vs short performance for
|
||||
// explicit trader IDs plus optional legacy trader ID patterns.
|
||||
func (s *PositionStore) GetDirectionStatsByTraderFilters(traderIDs []string, traderIDPatterns []string) ([]DirectionStats, error) {
|
||||
var positions []TraderPosition
|
||||
err := s.db.Where("trader_id = ? AND status = ?", traderID, "CLOSED").Find(&positions).Error
|
||||
err := s.closedPositionsByTraderFilters(traderIDs, traderIDPatterns).Find(&positions).Error
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query direction stats: %w", err)
|
||||
}
|
||||
|
||||
@@ -42,3 +42,95 @@ func TestGetOpenPositionBySymbolMatchesSideCaseInsensitively(t *testing.T) {
|
||||
t.Fatalf("entry time mismatch: got %d want %d", got.EntryTime, entryTime)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetClosedPositionsByTraderFiltersIncludesLegacyAutopilotIDs(t *testing.T) {
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("open in-memory sqlite: %v", err)
|
||||
}
|
||||
|
||||
positions := NewPositionStore(db)
|
||||
if err := positions.InitTables(); err != nil {
|
||||
t.Fatalf("init position table: %v", err)
|
||||
}
|
||||
|
||||
now := time.Now().UnixMilli()
|
||||
rows := []*TraderPosition{
|
||||
{
|
||||
TraderID: "current-trader",
|
||||
Symbol: "xyz:SP500",
|
||||
Side: "LONG",
|
||||
Quantity: 1,
|
||||
EntryPrice: 100,
|
||||
EntryTime: now - 3000,
|
||||
ExitPrice: 101,
|
||||
ExitTime: now - 2000,
|
||||
RealizedPnL: 1,
|
||||
Status: "CLOSED",
|
||||
CreatedAt: now - 3000,
|
||||
UpdatedAt: now - 2000,
|
||||
CloseReason: "sync",
|
||||
ExchangeType: "hyperliquid",
|
||||
},
|
||||
{
|
||||
TraderID: "exchange_user-123_claw402_111",
|
||||
Symbol: "AAVEUSDT",
|
||||
Side: "LONG",
|
||||
Quantity: 2,
|
||||
EntryPrice: 50,
|
||||
EntryTime: now - 5000,
|
||||
ExitPrice: 49,
|
||||
ExitTime: now - 4000,
|
||||
RealizedPnL: -2,
|
||||
Status: "CLOSED",
|
||||
CreatedAt: now - 5000,
|
||||
UpdatedAt: now - 4000,
|
||||
CloseReason: "sync",
|
||||
ExchangeType: "hyperliquid",
|
||||
},
|
||||
{
|
||||
TraderID: "exchange_other-user_claw402_222",
|
||||
Symbol: "LITUSDT",
|
||||
Side: "LONG",
|
||||
Quantity: 3,
|
||||
EntryPrice: 10,
|
||||
EntryTime: now - 7000,
|
||||
ExitPrice: 12,
|
||||
ExitTime: now - 6000,
|
||||
RealizedPnL: 6,
|
||||
Status: "CLOSED",
|
||||
CreatedAt: now - 7000,
|
||||
UpdatedAt: now - 6000,
|
||||
CloseReason: "sync",
|
||||
ExchangeType: "hyperliquid",
|
||||
},
|
||||
}
|
||||
for _, row := range rows {
|
||||
if err := db.Create(row).Error; err != nil {
|
||||
t.Fatalf("create position: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
got, err := positions.GetClosedPositionsByTraderFilters(
|
||||
[]string{"current-trader"},
|
||||
[]string{"%_user-123_claw402_%"},
|
||||
100,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("get closed positions: %v", err)
|
||||
}
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("expected current + same-user legacy positions, got %d", len(got))
|
||||
}
|
||||
|
||||
stats, err := positions.GetFullStatsByTraderFilters(
|
||||
[]string{"current-trader"},
|
||||
[]string{"%_user-123_claw402_%"},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("get stats: %v", err)
|
||||
}
|
||||
if stats.TotalTrades != 2 || stats.TotalPnL != -1 {
|
||||
t.Fatalf("unexpected stats: trades=%d pnl=%.2f", stats.TotalTrades, stats.TotalPnL)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user