mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-14 16:26:57 +08:00
Sync manual closes into position history
This commit is contained in:
@@ -3,6 +3,7 @@ package api
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"nofx/logger"
|
"nofx/logger"
|
||||||
"nofx/market"
|
"nofx/market"
|
||||||
@@ -206,21 +207,43 @@ func (s *Server) handlePositionHistory(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
userID := c.GetString("user_id")
|
||||||
|
if fullCfg, cfgErr := s.store.Trader().GetFullConfig(userID, traderID); cfgErr == nil && fullCfg.Exchange != nil {
|
||||||
|
if syncErr := s.syncOrdersFromExchange(
|
||||||
|
trader.GetUnderlyingTrader(),
|
||||||
|
trader.GetID(),
|
||||||
|
fullCfg.Exchange.ID,
|
||||||
|
fullCfg.Exchange.ExchangeType,
|
||||||
|
); syncErr != nil {
|
||||||
|
logger.Infof("⚠️ Position history refresh sync skipped: %v", syncErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
traderIDs := []string{trader.GetID()}
|
||||||
|
var traderIDPatterns []string
|
||||||
|
if strings.EqualFold(strings.TrimSpace(trader.GetName()), "NOFX Autopilot") && strings.TrimSpace(userID) != "" {
|
||||||
|
// Older one-click launches created new Autopilot trader rows. When a row was
|
||||||
|
// deleted, its closed position records remained under the old generated ID.
|
||||||
|
// The generated Autopilot ID embeds userID + "claw402", so this safely
|
||||||
|
// restores same-user history continuity without joining deleted rows.
|
||||||
|
traderIDPatterns = append(traderIDPatterns, "%_"+userID+"_claw402_%")
|
||||||
|
}
|
||||||
|
|
||||||
// Get closed positions
|
// Get closed positions
|
||||||
positions, err := store.Position().GetClosedPositions(trader.GetID(), limit)
|
positions, err := store.Position().GetClosedPositionsByTraderFilters(traderIDs, traderIDPatterns, limit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
SafeInternalError(c, "Get position history", err)
|
SafeInternalError(c, "Get position history", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get statistics
|
// Get statistics
|
||||||
stats, _ := store.Position().GetFullStats(trader.GetID())
|
stats, _ := store.Position().GetFullStatsByTraderFilters(traderIDs, traderIDPatterns)
|
||||||
|
|
||||||
// Get symbol stats
|
// Get symbol stats
|
||||||
symbolStats, _ := store.Position().GetSymbolStats(trader.GetID(), 10)
|
symbolStats, _ := store.Position().GetSymbolStatsByTraderFilters(traderIDs, traderIDPatterns, 10)
|
||||||
|
|
||||||
// Get direction stats
|
// Get direction stats
|
||||||
directionStats, _ := store.Position().GetDirectionStats(trader.GetID())
|
directionStats, _ := store.Position().GetDirectionStatsByTraderFilters(traderIDs, traderIDPatterns)
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"positions": positions,
|
"positions": positions,
|
||||||
|
|||||||
@@ -267,8 +267,12 @@ func (s *Server) handleClosePosition(c *gin.Context) {
|
|||||||
|
|
||||||
logger.Infof("✅ Position closed successfully: symbol=%s, side=%s, qty=%.6f, result=%v", req.Symbol, req.Side, posQty, result)
|
logger.Infof("✅ Position closed successfully: symbol=%s, side=%s, qty=%.6f, result=%v", req.Symbol, req.Side, posQty, result)
|
||||||
|
|
||||||
// Record order to database (for chart markers and history)
|
// Backfill the just-closed fill immediately. Manual closes may happen while
|
||||||
s.recordClosePositionOrder(traderID, exchangeCfg.ID, exchangeCfg.ExchangeType, req.Symbol, req.Side, posQty, entryPrice, result)
|
// the bot runtime is stopped, so the background OrderSync loop is not enough.
|
||||||
|
if syncErr := s.syncOrdersAfterManualClose(tempTrader, traderID, exchangeCfg.ID, exchangeCfg.ExchangeType); syncErr != nil {
|
||||||
|
logger.Infof(" ⚠️ Manual close sync failed: %v", syncErr)
|
||||||
|
s.recordClosePositionOrder(traderID, exchangeCfg.ID, exchangeCfg.ExchangeType, req.Symbol, req.Side, posQty, entryPrice, result)
|
||||||
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"message": "Position closed successfully",
|
"message": "Position closed successfully",
|
||||||
@@ -278,6 +282,49 @@ func (s *Server) handleClosePosition(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) syncOrdersFromExchange(exchangeTrader trader.Trader, traderID, exchangeID, exchangeType string) error {
|
||||||
|
switch t := exchangeTrader.(type) {
|
||||||
|
case *binance.FuturesTrader:
|
||||||
|
return t.SyncOrdersFromBinance(traderID, exchangeID, exchangeType, s.store)
|
||||||
|
case *hyperliquidtrader.HyperliquidTrader:
|
||||||
|
return t.SyncOrdersFromHyperliquid(traderID, exchangeID, exchangeType, s.store)
|
||||||
|
case *aster.AsterTrader:
|
||||||
|
return t.SyncOrdersFromAster(traderID, exchangeID, exchangeType, s.store)
|
||||||
|
case *bybit.BybitTrader:
|
||||||
|
return t.SyncOrdersFromBybit(traderID, exchangeID, exchangeType, s.store)
|
||||||
|
case *okx.OKXTrader:
|
||||||
|
return t.SyncOrdersFromOKX(traderID, exchangeID, exchangeType, s.store)
|
||||||
|
case *bitget.BitgetTrader:
|
||||||
|
return t.SyncOrdersFromBitget(traderID, exchangeID, exchangeType, s.store)
|
||||||
|
case *gate.GateTrader:
|
||||||
|
return t.SyncOrdersFromGate(traderID, exchangeID, exchangeType, s.store)
|
||||||
|
case *kucoin.KuCoinTrader:
|
||||||
|
return t.SyncOrdersFromKuCoin(traderID, exchangeID, exchangeType, s.store)
|
||||||
|
case *lighter.LighterTraderV2:
|
||||||
|
return t.SyncOrdersFromLighter(traderID, exchangeID, exchangeType, s.store)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("order sync is not available for exchange type %s", exchangeType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) syncOrdersAfterManualClose(exchangeTrader trader.Trader, traderID, exchangeID, exchangeType string) error {
|
||||||
|
var lastErr error
|
||||||
|
for attempt := 1; attempt <= 4; attempt++ {
|
||||||
|
if attempt > 1 {
|
||||||
|
time.Sleep(time.Duration(attempt-1) * 500 * time.Millisecond)
|
||||||
|
}
|
||||||
|
if err := s.syncOrdersFromExchange(exchangeTrader, traderID, exchangeID, exchangeType); err != nil {
|
||||||
|
lastErr = err
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if lastErr != nil {
|
||||||
|
return lastErr
|
||||||
|
}
|
||||||
|
return fmt.Errorf("manual close sync did not run")
|
||||||
|
}
|
||||||
|
|
||||||
// recordClosePositionOrder Record close position order to database (Lighter version - direct FILLED status)
|
// recordClosePositionOrder Record close position order to database (Lighter version - direct FILLED status)
|
||||||
func (s *Server) recordClosePositionOrder(traderID, exchangeID, exchangeType, symbol, side string, quantity, exitPrice float64, result map[string]interface{}) {
|
func (s *Server) recordClosePositionOrder(traderID, exchangeID, exchangeType, symbol, side string, quantity, exitPrice float64, result map[string]interface{}) {
|
||||||
// Skip for exchanges with OrderSync - let the background sync handle it to avoid duplicates
|
// Skip for exchanges with OrderSync - let the background sync handle it to avoid duplicates
|
||||||
|
|||||||
@@ -382,11 +382,54 @@ func (s *PositionStore) GetOpenPositionBySymbol(traderID, symbol, side string) (
|
|||||||
|
|
||||||
// GetClosedPositions gets closed positions
|
// GetClosedPositions gets closed positions
|
||||||
func (s *PositionStore) GetClosedPositions(traderID string, limit int) ([]*TraderPosition, error) {
|
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
|
var positions []*TraderPosition
|
||||||
err := s.db.Where("trader_id = ? AND status = ?", traderID, "CLOSED").
|
query := s.closedPositionsByTraderFilters(traderIDs, traderIDPatterns).Order("exit_time DESC")
|
||||||
Order("exit_time DESC").
|
if limit > 0 {
|
||||||
Limit(limit).
|
query = query.Limit(limit)
|
||||||
Find(&positions).Error
|
}
|
||||||
|
|
||||||
|
err := query.Find(&positions).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to query closed positions: %w", err)
|
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
|
// GetFullStats gets complete trading statistics
|
||||||
func (s *PositionStore) GetFullStats(traderID string) (*TraderStats, error) {
|
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{}
|
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
|
var positions []TraderPosition
|
||||||
err := s.db.Where("trader_id = ? AND status = ?", traderID, "CLOSED").
|
err := s.closedPositionsByTraderFilters(traderIDs, traderIDPatterns).
|
||||||
Order("exit_time ASC").
|
Order("exit_time ASC").
|
||||||
Find(&positions).Error
|
Find(&positions).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -234,8 +232,14 @@ type SymbolStats struct {
|
|||||||
|
|
||||||
// GetSymbolStats gets per-symbol trading statistics
|
// GetSymbolStats gets per-symbol trading statistics
|
||||||
func (s *PositionStore) GetSymbolStats(traderID string, limit int) ([]SymbolStats, error) {
|
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
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to query symbol stats: %w", err)
|
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 {
|
rangeStats := map[string]*struct {
|
||||||
count int
|
count int
|
||||||
wins int
|
wins int
|
||||||
totalPnL float64
|
totalPnL float64
|
||||||
}{
|
}{
|
||||||
"<1h": {},
|
"<1h": {},
|
||||||
@@ -374,8 +378,14 @@ type DirectionStats struct {
|
|||||||
|
|
||||||
// GetDirectionStats analyzes long vs short performance
|
// GetDirectionStats analyzes long vs short performance
|
||||||
func (s *PositionStore) GetDirectionStats(traderID string) ([]DirectionStats, error) {
|
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
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to query direction stats: %w", err)
|
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)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1545,7 +1545,7 @@ export function StrategyStudioPage() {
|
|||||||
resolveOneClickExchange(),
|
resolveOneClickExchange(),
|
||||||
])
|
])
|
||||||
|
|
||||||
const created = await api.createTrader({
|
const traderRequest = {
|
||||||
name: traderName,
|
name: traderName,
|
||||||
ai_model_id: model.id,
|
ai_model_id: model.id,
|
||||||
exchange_id: exchange.id,
|
exchange_id: exchange.id,
|
||||||
@@ -1553,17 +1553,27 @@ export function StrategyStudioPage() {
|
|||||||
scan_interval_minutes: 15,
|
scan_interval_minutes: 15,
|
||||||
is_cross_margin: true,
|
is_cross_margin: true,
|
||||||
show_in_competition: true,
|
show_in_competition: true,
|
||||||
})
|
|
||||||
|
|
||||||
if (created.startup_warning) {
|
|
||||||
notify.warning(created.startup_warning)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await api.startTrader(created.trader_id)
|
const existingTraders = await api.getTraders(true)
|
||||||
notify.success(`${traderName} created and started`)
|
const existingAutopilot = existingTraders.find(
|
||||||
|
(trader) => trader.trader_name === traderName
|
||||||
|
)
|
||||||
|
const autopilot = existingAutopilot
|
||||||
|
? await api.updateTrader(existingAutopilot.trader_id, traderRequest)
|
||||||
|
: await api.createTrader(traderRequest)
|
||||||
|
|
||||||
|
if (autopilot.startup_warning) {
|
||||||
|
notify.warning(autopilot.startup_warning)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!autopilot.is_running) {
|
||||||
|
await api.startTrader(autopilot.trader_id)
|
||||||
|
}
|
||||||
|
notify.success(`${traderName} started`)
|
||||||
setHasChanges(false)
|
setHasChanges(false)
|
||||||
await loadStrategies(selectedStrategy.id)
|
await loadStrategies(selectedStrategy.id)
|
||||||
navigate(buildDashboardPath(created.trader_id))
|
navigate(buildDashboardPath(autopilot.trader_id))
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notify.error(
|
notify.error(
|
||||||
err instanceof Error ? err.message : 'Failed to launch NOFX Autopilot'
|
err instanceof Error ? err.message : 'Failed to launch NOFX Autopilot'
|
||||||
|
|||||||
Reference in New Issue
Block a user