mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-06-06 05:51:19 +08:00
feat: upgrade Binance to Algo Order API and improve trading flow
- Upgrade go-binance to v2.8.9 with new Algo Order API - Migrate SetStopLoss/SetTakeProfit to use AlgoOrderTypeStopMarket/TakeProfitMarket - Update cancel functions to handle both legacy and Algo orders - Fix Lighter stop orders using correct order types (type=2/4) with TriggerPrice - Add CancelAllOrders before opening positions for Bybit and Lighter - Fix decision limit selector in API handler - Add stop_loss/take_profit/confidence fields to DecisionAction - Store decisions array in database with proper serialization - Redesign DecisionCard with beautiful entry/SL/TP display
This commit is contained in:
@@ -14,6 +14,7 @@ import (
|
||||
"nofx/manager"
|
||||
"nofx/store"
|
||||
"nofx/trader"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -1898,7 +1899,7 @@ func (s *Server) handleDecisions(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, records)
|
||||
}
|
||||
|
||||
// handleLatestDecisions Latest decision logs (most recent 5, newest first)
|
||||
// handleLatestDecisions Latest decision logs (newest first, supports limit parameter)
|
||||
func (s *Server) handleLatestDecisions(c *gin.Context) {
|
||||
_, traderID, err := s.getTraderFromQuery(c)
|
||||
if err != nil {
|
||||
@@ -1912,7 +1913,18 @@ func (s *Server) handleLatestDecisions(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
records, err := trader.GetStore().Decision().GetLatestRecords(trader.GetID(), 5)
|
||||
// Get limit from query parameter, default to 5
|
||||
limit := 5
|
||||
if limitStr := c.Query("limit"); limitStr != "" {
|
||||
if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 {
|
||||
limit = parsedLimit
|
||||
if limit > 100 {
|
||||
limit = 100 // Max 100 to prevent abuse
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
records, err := trader.GetStore().Decision().GetLatestRecords(trader.GetID(), limit)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": fmt.Sprintf("Failed to get decision log: %v", err),
|
||||
|
||||
2
go.mod
2
go.mod
@@ -3,7 +3,7 @@ module nofx
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/adshao/go-binance/v2 v2.8.7
|
||||
github.com/adshao/go-binance/v2 v2.8.9
|
||||
github.com/agiledragon/gomonkey/v2 v2.13.0
|
||||
github.com/ethereum/go-ethereum v1.16.5
|
||||
github.com/gin-gonic/gin v1.11.0
|
||||
|
||||
2
go.sum
2
go.sum
@@ -2,6 +2,8 @@ github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDO
|
||||
github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8=
|
||||
github.com/adshao/go-binance/v2 v2.8.7 h1:n7jkhwIHMdtd/9ZU2gTqFV15XVSbUCjyFlOUAtTd8uU=
|
||||
github.com/adshao/go-binance/v2 v2.8.7/go.mod h1:XkkuecSyJKPolaCGf/q4ovJYB3t0P+7RUYTbGr+LMGM=
|
||||
github.com/adshao/go-binance/v2 v2.8.9 h1:NX+4u/LgEmrjTS7OMWU+9ZgfHKFM61RPhnr9/SqWPhc=
|
||||
github.com/adshao/go-binance/v2 v2.8.9/go.mod h1:XkkuecSyJKPolaCGf/q4ovJYB3t0P+7RUYTbGr+LMGM=
|
||||
github.com/agiledragon/gomonkey/v2 v2.13.0 h1:B24Jg6wBI1iB8EFR1c+/aoTg7QN/Cum7YffG8KMIyYo=
|
||||
github.com/agiledragon/gomonkey/v2 v2.13.0/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY=
|
||||
github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
|
||||
|
||||
@@ -56,16 +56,20 @@ type PositionSnapshot struct {
|
||||
}
|
||||
|
||||
// DecisionAction decision action
|
||||
type DecisionAction struct{
|
||||
Action string `json:"action"`
|
||||
Symbol string `json:"symbol"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
Leverage int `json:"leverage"`
|
||||
Price float64 `json:"price"`
|
||||
OrderID int64 `json:"order_id"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Success bool `json:"success"`
|
||||
Error string `json:"error"`
|
||||
type DecisionAction struct {
|
||||
Action string `json:"action"`
|
||||
Symbol string `json:"symbol"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
Leverage int `json:"leverage"`
|
||||
Price float64 `json:"price"`
|
||||
StopLoss float64 `json:"stop_loss,omitempty"` // Stop loss price
|
||||
TakeProfit float64 `json:"take_profit,omitempty"` // Take profit price
|
||||
Confidence int `json:"confidence,omitempty"` // AI confidence (0-100)
|
||||
Reasoning string `json:"reasoning,omitempty"` // Brief reasoning
|
||||
OrderID int64 `json:"order_id"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Success bool `json:"success"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
// Statistics statistics information
|
||||
@@ -113,6 +117,9 @@ func (s *DecisionStore) initTables() error {
|
||||
// Migration: add raw_response column if not exists
|
||||
s.db.Exec(`ALTER TABLE decision_records ADD COLUMN raw_response TEXT DEFAULT ''`)
|
||||
|
||||
// Migration: add decisions column if not exists
|
||||
s.db.Exec(`ALTER TABLE decision_records ADD COLUMN decisions TEXT DEFAULT '[]'`)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -124,22 +131,23 @@ func (s *DecisionStore) LogDecision(record *DecisionRecord) error {
|
||||
record.Timestamp = record.Timestamp.UTC()
|
||||
}
|
||||
|
||||
// Serialize candidate coins and execution log to JSON
|
||||
// Serialize candidate coins, execution log and decisions to JSON
|
||||
candidateCoinsJSON, _ := json.Marshal(record.CandidateCoins)
|
||||
executionLogJSON, _ := json.Marshal(record.ExecutionLog)
|
||||
decisionsJSON, _ := json.Marshal(record.Decisions)
|
||||
|
||||
// Insert decision record main table (only save AI decision related content)
|
||||
result, err := s.db.Exec(`
|
||||
INSERT INTO decision_records (
|
||||
trader_id, cycle_number, timestamp, system_prompt, input_prompt,
|
||||
cot_trace, decision_json, raw_response, candidate_coins, execution_log,
|
||||
success, error_message, ai_request_duration_ms
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
decisions, success, error_message, ai_request_duration_ms
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
record.TraderID, record.CycleNumber, record.Timestamp.Format(time.RFC3339),
|
||||
record.SystemPrompt, record.InputPrompt, record.CoTTrace, record.DecisionJSON,
|
||||
record.RawResponse, string(candidateCoinsJSON), string(executionLogJSON),
|
||||
record.Success, record.ErrorMessage, record.AIRequestDurationMs,
|
||||
string(decisionsJSON), record.Success, record.ErrorMessage, record.AIRequestDurationMs,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to insert decision record: %w", err)
|
||||
@@ -159,7 +167,7 @@ func (s *DecisionStore) GetLatestRecords(traderID string, n int) ([]*DecisionRec
|
||||
rows, err := s.db.Query(`
|
||||
SELECT id, trader_id, cycle_number, timestamp, system_prompt, input_prompt,
|
||||
cot_trace, decision_json, candidate_coins, execution_log,
|
||||
success, error_message, ai_request_duration_ms
|
||||
COALESCE(decisions, '[]'), success, error_message, ai_request_duration_ms
|
||||
FROM decision_records
|
||||
WHERE trader_id = ?
|
||||
ORDER BY timestamp DESC
|
||||
@@ -197,7 +205,7 @@ func (s *DecisionStore) GetAllLatestRecords(n int) ([]*DecisionRecord, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT id, trader_id, cycle_number, timestamp, system_prompt, input_prompt,
|
||||
cot_trace, decision_json, candidate_coins, execution_log,
|
||||
success, error_message, ai_request_duration_ms
|
||||
COALESCE(decisions, '[]'), success, error_message, ai_request_duration_ms
|
||||
FROM decision_records
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT ?
|
||||
@@ -231,7 +239,7 @@ func (s *DecisionStore) GetRecordsByDate(traderID string, date time.Time) ([]*De
|
||||
rows, err := s.db.Query(`
|
||||
SELECT id, trader_id, cycle_number, timestamp, system_prompt, input_prompt,
|
||||
cot_trace, decision_json, candidate_coins, execution_log,
|
||||
success, error_message, ai_request_duration_ms
|
||||
COALESCE(decisions, '[]'), success, error_message, ai_request_duration_ms
|
||||
FROM decision_records
|
||||
WHERE trader_id = ? AND DATE(timestamp) = ?
|
||||
ORDER BY timestamp ASC
|
||||
@@ -338,13 +346,13 @@ func (s *DecisionStore) GetLastCycleNumber(traderID string) (int, error) {
|
||||
func (s *DecisionStore) scanDecisionRecord(rows *sql.Rows) (*DecisionRecord, error) {
|
||||
var record DecisionRecord
|
||||
var timestampStr string
|
||||
var candidateCoinsJSON, executionLogJSON string
|
||||
var candidateCoinsJSON, executionLogJSON, decisionsJSON string
|
||||
|
||||
err := rows.Scan(
|
||||
&record.ID, &record.TraderID, &record.CycleNumber, ×tampStr,
|
||||
&record.SystemPrompt, &record.InputPrompt, &record.CoTTrace,
|
||||
&record.DecisionJSON, &candidateCoinsJSON, &executionLogJSON,
|
||||
&record.Success, &record.ErrorMessage, &record.AIRequestDurationMs,
|
||||
&decisionsJSON, &record.Success, &record.ErrorMessage, &record.AIRequestDurationMs,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -353,6 +361,7 @@ func (s *DecisionStore) scanDecisionRecord(rows *sql.Rows) (*DecisionRecord, err
|
||||
record.Timestamp, _ = time.Parse(time.RFC3339, timestampStr)
|
||||
json.Unmarshal([]byte(candidateCoinsJSON), &record.CandidateCoins)
|
||||
json.Unmarshal([]byte(executionLogJSON), &record.ExecutionLog)
|
||||
json.Unmarshal([]byte(decisionsJSON), &record.Decisions)
|
||||
|
||||
return &record, nil
|
||||
}
|
||||
|
||||
@@ -528,13 +528,17 @@ func (at *AutoTrader) runCycle() error {
|
||||
// Execute decisions and record results
|
||||
for _, d := range sortedDecisions {
|
||||
actionRecord := store.DecisionAction{
|
||||
Action: d.Action,
|
||||
Symbol: d.Symbol,
|
||||
Quantity: 0,
|
||||
Leverage: d.Leverage,
|
||||
Price: 0,
|
||||
Timestamp: time.Now(),
|
||||
Success: false,
|
||||
Action: d.Action,
|
||||
Symbol: d.Symbol,
|
||||
Quantity: 0,
|
||||
Leverage: d.Leverage,
|
||||
Price: 0,
|
||||
StopLoss: d.StopLoss,
|
||||
TakeProfit: d.TakeProfit,
|
||||
Confidence: d.Confidence,
|
||||
Reasoning: d.Reasoning,
|
||||
Timestamp: time.Now(),
|
||||
Success: false,
|
||||
}
|
||||
|
||||
if err := at.executeDecisionWithRecord(&d, &actionRecord); err != nil {
|
||||
@@ -816,9 +820,13 @@ func (at *AutoTrader) ExecuteDecision(d *decision.Decision) error {
|
||||
|
||||
// Create a minimal action record for tracking
|
||||
actionRecord := &store.DecisionAction{
|
||||
Symbol: d.Symbol,
|
||||
Action: d.Action,
|
||||
Leverage: d.Leverage,
|
||||
Symbol: d.Symbol,
|
||||
Action: d.Action,
|
||||
Leverage: d.Leverage,
|
||||
StopLoss: d.StopLoss,
|
||||
TakeProfit: d.TakeProfit,
|
||||
Confidence: d.Confidence,
|
||||
Reasoning: d.Reasoning,
|
||||
}
|
||||
|
||||
// Execute the decision
|
||||
|
||||
@@ -534,38 +534,64 @@ func (t *FuturesTrader) CloseShort(symbol string, quantity float64) (map[string]
|
||||
}
|
||||
|
||||
// CancelStopLossOrders cancels only stop-loss orders (doesn't affect take-profit orders)
|
||||
// Now uses both legacy API and new Algo Order API
|
||||
func (t *FuturesTrader) CancelStopLossOrders(symbol string) error {
|
||||
// Get all open orders for this symbol
|
||||
canceledCount := 0
|
||||
var cancelErrors []error
|
||||
|
||||
// 1. Cancel legacy stop-loss orders
|
||||
orders, err := t.client.NewListOpenOrdersService().
|
||||
Symbol(symbol).
|
||||
Do(context.Background())
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get open orders: %w", err)
|
||||
if err == nil {
|
||||
for _, order := range orders {
|
||||
orderType := string(order.Type)
|
||||
|
||||
// Only cancel stop-loss orders (don't cancel take-profit orders)
|
||||
// Use string comparison since OrderType constants were removed in v2.8.9
|
||||
if orderType == "STOP_MARKET" || orderType == "STOP" {
|
||||
_, err := t.client.NewCancelOrderService().
|
||||
Symbol(symbol).
|
||||
OrderID(order.OrderID).
|
||||
Do(context.Background())
|
||||
|
||||
if err != nil {
|
||||
errMsg := fmt.Sprintf("Order ID %d: %v", order.OrderID, err)
|
||||
cancelErrors = append(cancelErrors, fmt.Errorf("%s", errMsg))
|
||||
logger.Infof(" ⚠ Failed to cancel legacy stop-loss order: %s", errMsg)
|
||||
continue
|
||||
}
|
||||
|
||||
canceledCount++
|
||||
logger.Infof(" ✓ Canceled legacy stop-loss order (Order ID: %d, Type: %s, Side: %s)", order.OrderID, orderType, order.PositionSide)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out stop-loss orders and cancel them (cancel all directions including LONG and SHORT)
|
||||
canceledCount := 0
|
||||
var cancelErrors []error
|
||||
for _, order := range orders {
|
||||
orderType := order.Type
|
||||
// 2. Cancel Algo stop-loss orders
|
||||
algoOrders, err := t.client.NewListOpenAlgoOrdersService().
|
||||
Symbol(symbol).
|
||||
Do(context.Background())
|
||||
|
||||
// Only cancel stop-loss orders (don't cancel take-profit orders)
|
||||
if orderType == futures.OrderTypeStopMarket || orderType == futures.OrderTypeStop {
|
||||
_, err := t.client.NewCancelOrderService().
|
||||
Symbol(symbol).
|
||||
OrderID(order.OrderID).
|
||||
Do(context.Background())
|
||||
if err == nil {
|
||||
for _, algoOrder := range algoOrders {
|
||||
// Only cancel stop-loss orders
|
||||
if algoOrder.OrderType == futures.AlgoOrderTypeStopMarket || algoOrder.OrderType == futures.AlgoOrderTypeStop {
|
||||
_, err := t.client.NewCancelAlgoOrderService().
|
||||
AlgoID(algoOrder.AlgoId).
|
||||
Do(context.Background())
|
||||
|
||||
if err != nil {
|
||||
errMsg := fmt.Sprintf("Order ID %d: %v", order.OrderID, err)
|
||||
cancelErrors = append(cancelErrors, fmt.Errorf("%s", errMsg))
|
||||
logger.Infof(" ⚠ Failed to cancel stop-loss order: %s", errMsg)
|
||||
continue
|
||||
if err != nil {
|
||||
errMsg := fmt.Sprintf("Algo ID %d: %v", algoOrder.AlgoId, err)
|
||||
cancelErrors = append(cancelErrors, fmt.Errorf("%s", errMsg))
|
||||
logger.Infof(" ⚠ Failed to cancel Algo stop-loss order: %s", errMsg)
|
||||
continue
|
||||
}
|
||||
|
||||
canceledCount++
|
||||
logger.Infof(" ✓ Canceled Algo stop-loss order (Algo ID: %d, Type: %s)", algoOrder.AlgoId, algoOrder.OrderType)
|
||||
}
|
||||
|
||||
canceledCount++
|
||||
logger.Infof(" ✓ Canceled stop-loss order (Order ID: %d, Type: %s, Side: %s)", order.OrderID, orderType, order.PositionSide)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -584,38 +610,64 @@ func (t *FuturesTrader) CancelStopLossOrders(symbol string) error {
|
||||
}
|
||||
|
||||
// CancelTakeProfitOrders cancels only take-profit orders (doesn't affect stop-loss orders)
|
||||
// Now uses both legacy API and new Algo Order API
|
||||
func (t *FuturesTrader) CancelTakeProfitOrders(symbol string) error {
|
||||
// Get all open orders for this symbol
|
||||
canceledCount := 0
|
||||
var cancelErrors []error
|
||||
|
||||
// 1. Cancel legacy take-profit orders
|
||||
orders, err := t.client.NewListOpenOrdersService().
|
||||
Symbol(symbol).
|
||||
Do(context.Background())
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get open orders: %w", err)
|
||||
if err == nil {
|
||||
for _, order := range orders {
|
||||
orderType := string(order.Type)
|
||||
|
||||
// Only cancel take-profit orders (don't cancel stop-loss orders)
|
||||
// Use string comparison since OrderType constants were removed in v2.8.9
|
||||
if orderType == "TAKE_PROFIT_MARKET" || orderType == "TAKE_PROFIT" {
|
||||
_, err := t.client.NewCancelOrderService().
|
||||
Symbol(symbol).
|
||||
OrderID(order.OrderID).
|
||||
Do(context.Background())
|
||||
|
||||
if err != nil {
|
||||
errMsg := fmt.Sprintf("Order ID %d: %v", order.OrderID, err)
|
||||
cancelErrors = append(cancelErrors, fmt.Errorf("%s", errMsg))
|
||||
logger.Infof(" ⚠ Failed to cancel legacy take-profit order: %s", errMsg)
|
||||
continue
|
||||
}
|
||||
|
||||
canceledCount++
|
||||
logger.Infof(" ✓ Canceled legacy take-profit order (Order ID: %d, Type: %s, Side: %s)", order.OrderID, orderType, order.PositionSide)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out take-profit orders and cancel them (cancel all directions including LONG and SHORT)
|
||||
canceledCount := 0
|
||||
var cancelErrors []error
|
||||
for _, order := range orders {
|
||||
orderType := order.Type
|
||||
// 2. Cancel Algo take-profit orders
|
||||
algoOrders, err := t.client.NewListOpenAlgoOrdersService().
|
||||
Symbol(symbol).
|
||||
Do(context.Background())
|
||||
|
||||
// Only cancel take-profit orders (don't cancel stop-loss orders)
|
||||
if orderType == futures.OrderTypeTakeProfitMarket || orderType == futures.OrderTypeTakeProfit {
|
||||
_, err := t.client.NewCancelOrderService().
|
||||
Symbol(symbol).
|
||||
OrderID(order.OrderID).
|
||||
Do(context.Background())
|
||||
if err == nil {
|
||||
for _, algoOrder := range algoOrders {
|
||||
// Only cancel take-profit orders
|
||||
if algoOrder.OrderType == futures.AlgoOrderTypeTakeProfitMarket || algoOrder.OrderType == futures.AlgoOrderTypeTakeProfit {
|
||||
_, err := t.client.NewCancelAlgoOrderService().
|
||||
AlgoID(algoOrder.AlgoId).
|
||||
Do(context.Background())
|
||||
|
||||
if err != nil {
|
||||
errMsg := fmt.Sprintf("Order ID %d: %v", order.OrderID, err)
|
||||
cancelErrors = append(cancelErrors, fmt.Errorf("%s", errMsg))
|
||||
logger.Infof(" ⚠ Failed to cancel take-profit order: %s", errMsg)
|
||||
continue
|
||||
if err != nil {
|
||||
errMsg := fmt.Sprintf("Algo ID %d: %v", algoOrder.AlgoId, err)
|
||||
cancelErrors = append(cancelErrors, fmt.Errorf("%s", errMsg))
|
||||
logger.Infof(" ⚠ Failed to cancel Algo take-profit order: %s", errMsg)
|
||||
continue
|
||||
}
|
||||
|
||||
canceledCount++
|
||||
logger.Infof(" ✓ Canceled Algo take-profit order (Algo ID: %d, Type: %s)", algoOrder.AlgoId, algoOrder.OrderType)
|
||||
}
|
||||
|
||||
canceledCount++
|
||||
logger.Infof(" ✓ Canceled take-profit order (Order ID: %d, Type: %s, Side: %s)", order.OrderID, orderType, order.PositionSide)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -634,61 +686,91 @@ func (t *FuturesTrader) CancelTakeProfitOrders(symbol string) error {
|
||||
}
|
||||
|
||||
// CancelAllOrders cancels all pending orders for this symbol
|
||||
// Now uses both legacy API and new Algo Order API
|
||||
func (t *FuturesTrader) CancelAllOrders(symbol string) error {
|
||||
// 1. Cancel all legacy orders
|
||||
err := t.client.NewCancelAllOpenOrdersService().
|
||||
Symbol(symbol).
|
||||
Do(context.Background())
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to cancel pending orders: %w", err)
|
||||
logger.Infof(" ⚠ Failed to cancel legacy orders: %v", err)
|
||||
} else {
|
||||
logger.Infof(" ✓ Canceled all legacy pending orders for %s", symbol)
|
||||
}
|
||||
|
||||
logger.Infof(" ✓ Canceled all pending orders for %s", symbol)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CancelStopOrders cancels take-profit/stop-loss orders for this symbol (used to adjust TP/SL positions)
|
||||
func (t *FuturesTrader) CancelStopOrders(symbol string) error {
|
||||
// Get all open orders for this symbol
|
||||
orders, err := t.client.NewListOpenOrdersService().
|
||||
// 2. Cancel all Algo orders
|
||||
err = t.client.NewCancelAllAlgoOpenOrdersService().
|
||||
Symbol(symbol).
|
||||
Do(context.Background())
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get open orders: %w", err)
|
||||
// Ignore "no algo orders" error
|
||||
if !contains(err.Error(), "no algo") && !contains(err.Error(), "No algo") {
|
||||
logger.Infof(" ⚠ Failed to cancel Algo orders: %v", err)
|
||||
}
|
||||
} else {
|
||||
logger.Infof(" ✓ Canceled all Algo orders for %s", symbol)
|
||||
}
|
||||
|
||||
// Filter out take-profit and stop-loss orders and cancel them
|
||||
return nil
|
||||
}
|
||||
|
||||
// CancelStopOrders cancels take-profit/stop-loss orders for this symbol (used to adjust TP/SL positions)
|
||||
// Now uses both legacy API and new Algo Order API (Binance migrated stop orders to Algo system)
|
||||
func (t *FuturesTrader) CancelStopOrders(symbol string) error {
|
||||
canceledCount := 0
|
||||
for _, order := range orders {
|
||||
orderType := order.Type
|
||||
|
||||
// Only cancel stop-loss and take-profit orders
|
||||
if orderType == futures.OrderTypeStopMarket ||
|
||||
orderType == futures.OrderTypeTakeProfitMarket ||
|
||||
orderType == futures.OrderTypeStop ||
|
||||
orderType == futures.OrderTypeTakeProfit {
|
||||
// 1. Cancel legacy stop orders (for backward compatibility)
|
||||
orders, err := t.client.NewListOpenOrdersService().
|
||||
Symbol(symbol).
|
||||
Do(context.Background())
|
||||
|
||||
_, err := t.client.NewCancelOrderService().
|
||||
Symbol(symbol).
|
||||
OrderID(order.OrderID).
|
||||
Do(context.Background())
|
||||
if err == nil {
|
||||
for _, order := range orders {
|
||||
orderType := string(order.Type)
|
||||
|
||||
if err != nil {
|
||||
logger.Infof(" ⚠ Failed to cancel order %d: %v", order.OrderID, err)
|
||||
continue
|
||||
// Only cancel stop-loss and take-profit orders
|
||||
// Use string comparison since OrderType constants were removed in v2.8.9
|
||||
if orderType == "STOP_MARKET" ||
|
||||
orderType == "TAKE_PROFIT_MARKET" ||
|
||||
orderType == "STOP" ||
|
||||
orderType == "TAKE_PROFIT" {
|
||||
|
||||
_, err := t.client.NewCancelOrderService().
|
||||
Symbol(symbol).
|
||||
OrderID(order.OrderID).
|
||||
Do(context.Background())
|
||||
|
||||
if err != nil {
|
||||
logger.Infof(" ⚠ Failed to cancel legacy order %d: %v", order.OrderID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
canceledCount++
|
||||
logger.Infof(" ✓ Canceled legacy stop order for %s (Order ID: %d, Type: %s)",
|
||||
symbol, order.OrderID, orderType)
|
||||
}
|
||||
|
||||
canceledCount++
|
||||
logger.Infof(" ✓ Canceled take-profit/stop-loss order for %s (Order ID: %d, Type: %s)",
|
||||
symbol, order.OrderID, orderType)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Cancel Algo orders (new API)
|
||||
err = t.client.NewCancelAllAlgoOpenOrdersService().
|
||||
Symbol(symbol).
|
||||
Do(context.Background())
|
||||
|
||||
if err != nil {
|
||||
// Ignore "no algo orders" error
|
||||
if !contains(err.Error(), "no algo") && !contains(err.Error(), "No algo") {
|
||||
logger.Infof(" ⚠ Failed to cancel Algo orders: %v", err)
|
||||
}
|
||||
} else {
|
||||
logger.Infof(" ✓ Canceled all Algo orders for %s", symbol)
|
||||
canceledCount++
|
||||
}
|
||||
|
||||
if canceledCount == 0 {
|
||||
logger.Infof(" ℹ %s has no take-profit/stop-loss orders to cancel", symbol)
|
||||
} else {
|
||||
logger.Infof(" ✓ Canceled %d take-profit/stop-loss order(s) for %s", canceledCount, symbol)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -721,7 +803,8 @@ func (t *FuturesTrader) CalculatePositionSize(balance, riskPercent, price float6
|
||||
return quantity
|
||||
}
|
||||
|
||||
// SetStopLoss sets stop-loss order
|
||||
// SetStopLoss sets stop-loss order using new Algo Order API
|
||||
// Binance has migrated stop orders to Algo Order system (error -4120 STOP_ORDER_SWITCH_ALGO)
|
||||
func (t *FuturesTrader) SetStopLoss(symbol string, positionSide string, quantity, stopPrice float64) error {
|
||||
var side futures.SideType
|
||||
var posSide futures.PositionSideType
|
||||
@@ -734,33 +817,28 @@ func (t *FuturesTrader) SetStopLoss(symbol string, positionSide string, quantity
|
||||
posSide = futures.PositionSideTypeShort
|
||||
}
|
||||
|
||||
// Format quantity
|
||||
quantityStr, err := t.FormatQuantity(symbol, quantity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = t.client.NewCreateOrderService().
|
||||
// Use new Algo Order API
|
||||
_, err := t.client.NewCreateAlgoOrderService().
|
||||
Symbol(symbol).
|
||||
Side(side).
|
||||
PositionSide(posSide).
|
||||
Type(futures.OrderTypeStopMarket).
|
||||
StopPrice(fmt.Sprintf("%.8f", stopPrice)).
|
||||
Quantity(quantityStr).
|
||||
Type(futures.AlgoOrderTypeStopMarket).
|
||||
TriggerPrice(fmt.Sprintf("%.8f", stopPrice)).
|
||||
WorkingType(futures.WorkingTypeContractPrice).
|
||||
ClosePosition(true).
|
||||
NewClientOrderID(getBrOrderID()).
|
||||
ClientAlgoId(getBrOrderID()).
|
||||
Do(context.Background())
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set stop-loss: %w", err)
|
||||
}
|
||||
|
||||
logger.Infof(" Stop-loss price set: %.4f", stopPrice)
|
||||
logger.Infof(" Stop-loss price set (Algo Order): %.4f", stopPrice)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetTakeProfit sets take-profit order
|
||||
// SetTakeProfit sets take-profit order using new Algo Order API
|
||||
// Binance has migrated stop orders to Algo Order system (error -4120 STOP_ORDER_SWITCH_ALGO)
|
||||
func (t *FuturesTrader) SetTakeProfit(symbol string, positionSide string, quantity, takeProfitPrice float64) error {
|
||||
var side futures.SideType
|
||||
var posSide futures.PositionSideType
|
||||
@@ -773,29 +851,23 @@ func (t *FuturesTrader) SetTakeProfit(symbol string, positionSide string, quanti
|
||||
posSide = futures.PositionSideTypeShort
|
||||
}
|
||||
|
||||
// Format quantity
|
||||
quantityStr, err := t.FormatQuantity(symbol, quantity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = t.client.NewCreateOrderService().
|
||||
// Use new Algo Order API
|
||||
_, err := t.client.NewCreateAlgoOrderService().
|
||||
Symbol(symbol).
|
||||
Side(side).
|
||||
PositionSide(posSide).
|
||||
Type(futures.OrderTypeTakeProfitMarket).
|
||||
StopPrice(fmt.Sprintf("%.8f", takeProfitPrice)).
|
||||
Quantity(quantityStr).
|
||||
Type(futures.AlgoOrderTypeTakeProfitMarket).
|
||||
TriggerPrice(fmt.Sprintf("%.8f", takeProfitPrice)).
|
||||
WorkingType(futures.WorkingTypeContractPrice).
|
||||
ClosePosition(true).
|
||||
NewClientOrderID(getBrOrderID()).
|
||||
ClientAlgoId(getBrOrderID()).
|
||||
Do(context.Background())
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set take-profit: %w", err)
|
||||
}
|
||||
|
||||
logger.Infof(" Take-profit price set: %.4f", takeProfitPrice)
|
||||
logger.Infof(" Take-profit price set (Algo Order): %.4f", takeProfitPrice)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -280,6 +280,15 @@ func (t *BybitTrader) GetPositions() ([]map[string]interface{}, error) {
|
||||
func (t *BybitTrader) OpenLong(symbol string, quantity float64, leverage int) (map[string]interface{}, error) {
|
||||
logger.Infof("[Bybit] ===== OpenLong called: symbol=%s, qty=%.6f, leverage=%d =====", symbol, quantity, leverage)
|
||||
|
||||
// First cancel all pending orders for this symbol (clean up old orders)
|
||||
if err := t.CancelAllOrders(symbol); err != nil {
|
||||
logger.Infof("⚠️ [Bybit] Failed to cancel old pending orders: %v", err)
|
||||
}
|
||||
// Also cancel conditional orders (stop-loss/take-profit) - Bybit keeps them separate
|
||||
if err := t.CancelStopOrders(symbol); err != nil {
|
||||
logger.Infof("⚠️ [Bybit] Failed to cancel old stop orders: %v", err)
|
||||
}
|
||||
|
||||
// Set leverage first
|
||||
if err := t.SetLeverage(symbol, leverage); err != nil {
|
||||
logger.Infof("⚠️ [Bybit] Failed to set leverage: %v", err)
|
||||
@@ -314,6 +323,15 @@ func (t *BybitTrader) OpenLong(symbol string, quantity float64, leverage int) (m
|
||||
func (t *BybitTrader) OpenShort(symbol string, quantity float64, leverage int) (map[string]interface{}, error) {
|
||||
logger.Infof("[Bybit] ===== OpenShort called: symbol=%s, qty=%.6f, leverage=%d =====", symbol, quantity, leverage)
|
||||
|
||||
// First cancel all pending orders for this symbol (clean up old orders)
|
||||
if err := t.CancelAllOrders(symbol); err != nil {
|
||||
logger.Infof("⚠️ [Bybit] Failed to cancel old pending orders: %v", err)
|
||||
}
|
||||
// Also cancel conditional orders (stop-loss/take-profit) - Bybit keeps them separate
|
||||
if err := t.CancelStopOrders(symbol); err != nil {
|
||||
logger.Infof("⚠️ [Bybit] Failed to cancel old stop orders: %v", err)
|
||||
}
|
||||
|
||||
// Set leverage first
|
||||
if err := t.SetLeverage(symbol, leverage); err != nil {
|
||||
logger.Infof("⚠️ [Bybit] Failed to set leverage: %v", err)
|
||||
|
||||
@@ -14,44 +14,46 @@ import (
|
||||
)
|
||||
|
||||
// SetStopLoss Set stop-loss order (implements Trader interface)
|
||||
// IMPORTANT: Uses StopLossOrder type (type=2) with TriggerPrice, NOT regular limit order
|
||||
func (t *LighterTraderV2) SetStopLoss(symbol string, positionSide string, quantity, stopPrice float64) error {
|
||||
if t.txClient == nil {
|
||||
return fmt.Errorf("TxClient not initialized")
|
||||
}
|
||||
|
||||
logger.Infof("🛑 LIGHTER Setting stop-loss: %s %s qty=%.4f, stop=%.2f", symbol, positionSide, quantity, stopPrice)
|
||||
logger.Infof("🛑 LIGHTER Setting stop-loss: %s %s qty=%.4f, trigger=%.2f", symbol, positionSide, quantity, stopPrice)
|
||||
|
||||
// Determine order direction (short position uses buy order, long position uses sell order)
|
||||
// Determine order direction (long position uses sell order, short position uses buy order)
|
||||
isAsk := (positionSide == "LONG" || positionSide == "long")
|
||||
|
||||
// Create limit stop-loss order
|
||||
_, err := t.CreateOrder(symbol, isAsk, quantity, stopPrice, "limit")
|
||||
// Create stop-loss order with TriggerPrice (type=2: StopLossOrder)
|
||||
_, err := t.CreateStopOrder(symbol, isAsk, quantity, stopPrice, "stop_loss")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set stop-loss: %w", err)
|
||||
}
|
||||
|
||||
logger.Infof("✓ LIGHTER stop-loss set: %.2f", stopPrice)
|
||||
logger.Infof("✓ LIGHTER stop-loss set: trigger=%.2f", stopPrice)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetTakeProfit Set take-profit order (implements Trader interface)
|
||||
// IMPORTANT: Uses TakeProfitOrder type (type=4) with TriggerPrice, NOT regular limit order
|
||||
func (t *LighterTraderV2) SetTakeProfit(symbol string, positionSide string, quantity, takeProfitPrice float64) error {
|
||||
if t.txClient == nil {
|
||||
return fmt.Errorf("TxClient not initialized")
|
||||
}
|
||||
|
||||
logger.Infof("🎯 LIGHTER Setting take-profit: %s %s qty=%.4f, tp=%.2f", symbol, positionSide, quantity, takeProfitPrice)
|
||||
logger.Infof("🎯 LIGHTER Setting take-profit: %s %s qty=%.4f, trigger=%.2f", symbol, positionSide, quantity, takeProfitPrice)
|
||||
|
||||
// Determine order direction (short position uses buy order, long position uses sell order)
|
||||
// Determine order direction (long position uses sell order, short position uses buy order)
|
||||
isAsk := (positionSide == "LONG" || positionSide == "long")
|
||||
|
||||
// Create limit take-profit order
|
||||
_, err := t.CreateOrder(symbol, isAsk, quantity, takeProfitPrice, "limit")
|
||||
// Create take-profit order with TriggerPrice (type=4: TakeProfitOrder)
|
||||
_, err := t.CreateStopOrder(symbol, isAsk, quantity, takeProfitPrice, "take_profit")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set take-profit: %w", err)
|
||||
}
|
||||
|
||||
logger.Infof("✓ LIGHTER take-profit set: %.2f", takeProfitPrice)
|
||||
logger.Infof("✓ LIGHTER take-profit set: trigger=%.2f", takeProfitPrice)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -23,18 +23,23 @@ func (t *LighterTraderV2) OpenLong(symbol string, quantity float64, leverage int
|
||||
|
||||
logger.Infof("📈 LIGHTER opening long: %s, qty=%.4f, leverage=%dx", symbol, quantity, leverage)
|
||||
|
||||
// 1. Set leverage (if needed)
|
||||
// 1. First cancel all pending orders for this symbol (clean up old stop-loss and take-profit orders)
|
||||
if err := t.CancelAllOrders(symbol); err != nil {
|
||||
logger.Infof("⚠️ Failed to cancel old pending orders: %v", err)
|
||||
}
|
||||
|
||||
// 2. Set leverage (if needed)
|
||||
if err := t.SetLeverage(symbol, leverage); err != nil {
|
||||
logger.Infof("⚠️ Failed to set leverage: %v", err)
|
||||
}
|
||||
|
||||
// 2. Get market price
|
||||
// 3. Get market price
|
||||
marketPrice, err := t.GetMarketPrice(symbol)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get market price: %w", err)
|
||||
}
|
||||
|
||||
// 3. Create market buy order (open long)
|
||||
// 4. Create market buy order (open long)
|
||||
orderResult, err := t.CreateOrder(symbol, false, quantity, 0, "market")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open long: %w", err)
|
||||
@@ -59,18 +64,23 @@ func (t *LighterTraderV2) OpenShort(symbol string, quantity float64, leverage in
|
||||
|
||||
logger.Infof("📉 LIGHTER opening short: %s, qty=%.4f, leverage=%dx", symbol, quantity, leverage)
|
||||
|
||||
// 1. Set leverage
|
||||
// 1. First cancel all pending orders for this symbol (clean up old stop-loss and take-profit orders)
|
||||
if err := t.CancelAllOrders(symbol); err != nil {
|
||||
logger.Infof("⚠️ Failed to cancel old pending orders: %v", err)
|
||||
}
|
||||
|
||||
// 2. Set leverage
|
||||
if err := t.SetLeverage(symbol, leverage); err != nil {
|
||||
logger.Infof("⚠️ Failed to set leverage: %v", err)
|
||||
}
|
||||
|
||||
// 2. Get market price
|
||||
// 3. Get market price
|
||||
marketPrice, err := t.GetMarketPrice(symbol)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get market price: %w", err)
|
||||
}
|
||||
|
||||
// 3. Create market sell order (open short)
|
||||
// 4. Create market sell order (open short)
|
||||
orderResult, err := t.CreateOrder(symbol, true, quantity, 0, "market")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open short: %w", err)
|
||||
@@ -563,6 +573,107 @@ func (t *LighterTraderV2) SetMarginMode(symbol string, isCrossMargin bool) error
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateStopOrder Create stop-loss or take-profit order with TriggerPrice
|
||||
// Order types: "stop_loss" (type=2), "take_profit" (type=4)
|
||||
func (t *LighterTraderV2) CreateStopOrder(symbol string, isAsk bool, quantity float64, triggerPrice float64, orderType string) (map[string]interface{}, error) {
|
||||
if t.txClient == nil {
|
||||
return nil, fmt.Errorf("TxClient not initialized")
|
||||
}
|
||||
|
||||
// Get market index
|
||||
marketIndexU16, err := t.getMarketIndex(symbol)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get market index: %w", err)
|
||||
}
|
||||
marketIndex := uint8(marketIndexU16)
|
||||
|
||||
// Build order request
|
||||
clientOrderIndex := time.Now().UnixMilli() % 281474976710655
|
||||
|
||||
// Order type: StopLossOrder=2, TakeProfitOrder=4
|
||||
var orderTypeValue uint8 = 2 // Default: StopLossOrder
|
||||
if orderType == "take_profit" {
|
||||
orderTypeValue = 4 // TakeProfitOrder
|
||||
}
|
||||
|
||||
// Convert quantity to base amount
|
||||
sizeDecimals := 4
|
||||
normalizedSymbol := normalizeSymbol(symbol)
|
||||
switch normalizedSymbol {
|
||||
case "BTC":
|
||||
sizeDecimals = 5
|
||||
case "SOL":
|
||||
sizeDecimals = 3
|
||||
case "ETH":
|
||||
sizeDecimals = 4
|
||||
}
|
||||
baseAmount := int64(quantity * float64(pow10(sizeDecimals)))
|
||||
|
||||
// TriggerPrice: price precision is 2 decimals (multiply by 100)
|
||||
triggerPriceValue := uint32(triggerPrice * 1e2)
|
||||
|
||||
// For stop orders, Price should be set to a reasonable execution price
|
||||
// Stop-loss sell: price slightly below trigger (95% of trigger)
|
||||
// Take-profit sell: price slightly below trigger (95% of trigger)
|
||||
// Stop-loss buy: price slightly above trigger (105% of trigger)
|
||||
// Take-profit buy: price slightly above trigger (105% of trigger)
|
||||
var priceValue uint32
|
||||
if isAsk {
|
||||
// Sell order - set price at 95% of trigger to ensure execution
|
||||
priceValue = uint32(triggerPrice * 0.95 * 1e2)
|
||||
} else {
|
||||
// Buy order - set price at 105% of trigger to ensure execution
|
||||
priceValue = uint32(triggerPrice * 1.05 * 1e2)
|
||||
}
|
||||
|
||||
// Stop orders use GoodTillTime with expiry
|
||||
orderExpiry := time.Now().Add(30 * 24 * time.Hour).UnixMilli() // 30 days
|
||||
|
||||
txReq := &types.CreateOrderTxReq{
|
||||
MarketIndex: marketIndex,
|
||||
ClientOrderIndex: clientOrderIndex,
|
||||
BaseAmount: baseAmount,
|
||||
Price: priceValue,
|
||||
IsAsk: boolToUint8(isAsk),
|
||||
Type: orderTypeValue,
|
||||
TimeInForce: 1, // GoodTillTime
|
||||
ReduceOnly: 1, // Stop orders should be reduce-only
|
||||
TriggerPrice: triggerPriceValue,
|
||||
OrderExpiry: orderExpiry,
|
||||
}
|
||||
|
||||
// Sign transaction
|
||||
nonce := int64(-1)
|
||||
tx, err := t.txClient.GetCreateOrderTransaction(txReq, &types.TransactOpts{
|
||||
Nonce: &nonce,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to sign stop order: %w", err)
|
||||
}
|
||||
|
||||
// Get tx_info
|
||||
txInfo, err := tx.GetTxInfo()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get tx info: %w", err)
|
||||
}
|
||||
|
||||
logger.Infof("DEBUG stop order - type: %d, trigger: %.2f, price: %.2f, isAsk: %v", orderTypeValue, triggerPrice, float64(priceValue)/100, isAsk)
|
||||
|
||||
// Submit order
|
||||
orderResp, err := t.submitOrder(int(tx.GetTxType()), txInfo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to submit stop order: %w", err)
|
||||
}
|
||||
|
||||
side := "buy"
|
||||
if isAsk {
|
||||
side = "sell"
|
||||
}
|
||||
logger.Infof("✓ LIGHTER %s order created: %s %s qty=%.4f trigger=%.2f", orderType, symbol, side, quantity, triggerPrice)
|
||||
|
||||
return orderResp, nil
|
||||
}
|
||||
|
||||
// boolToUint8 Convert boolean to uint8
|
||||
func boolToUint8(b bool) uint8 {
|
||||
if b {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react'
|
||||
import type { DecisionRecord } from '../types'
|
||||
import type { DecisionRecord, DecisionAction } from '../types'
|
||||
import { t, type Language } from '../i18n/translations'
|
||||
|
||||
interface DecisionCardProps {
|
||||
@@ -7,154 +7,339 @@ interface DecisionCardProps {
|
||||
language: Language
|
||||
}
|
||||
|
||||
// Action type configuration
|
||||
const ACTION_CONFIG: Record<string, { color: string; bg: string; icon: string; label: string }> = {
|
||||
open_long: { color: '#0ECB81', bg: 'rgba(14, 203, 129, 0.15)', icon: '📈', label: 'LONG' },
|
||||
open_short: { color: '#F6465D', bg: 'rgba(246, 70, 93, 0.15)', icon: '📉', label: 'SHORT' },
|
||||
close_long: { color: '#F0B90B', bg: 'rgba(240, 185, 11, 0.15)', icon: '💰', label: 'CLOSE' },
|
||||
close_short: { color: '#F0B90B', bg: 'rgba(240, 185, 11, 0.15)', icon: '💰', label: 'CLOSE' },
|
||||
hold: { color: '#848E9C', bg: 'rgba(132, 142, 156, 0.15)', icon: '⏸️', label: 'HOLD' },
|
||||
wait: { color: '#848E9C', bg: 'rgba(132, 142, 156, 0.15)', icon: '⏳', label: 'WAIT' },
|
||||
}
|
||||
|
||||
// Format price with proper decimals
|
||||
function formatPrice(price: number | undefined): string {
|
||||
if (!price || price === 0) return '-'
|
||||
if (price >= 1000) return price.toFixed(2)
|
||||
if (price >= 1) return price.toFixed(4)
|
||||
return price.toFixed(6)
|
||||
}
|
||||
|
||||
// Calculate percentage change
|
||||
function calcPctChange(entry: number | undefined, target: number | undefined, isLong: boolean): string {
|
||||
if (!entry || !target || entry === 0) return '-'
|
||||
const pct = ((target - entry) / entry) * 100
|
||||
const adjustedPct = isLong ? pct : -pct
|
||||
return `${adjustedPct >= 0 ? '+' : ''}${adjustedPct.toFixed(2)}%`
|
||||
}
|
||||
|
||||
// Get confidence color
|
||||
function getConfidenceColor(confidence: number | undefined): string {
|
||||
if (!confidence) return '#848E9C'
|
||||
if (confidence >= 80) return '#0ECB81'
|
||||
if (confidence >= 60) return '#F0B90B'
|
||||
return '#F6465D'
|
||||
}
|
||||
|
||||
// Single Action Card Component
|
||||
function ActionCard({ action, language }: { action: DecisionAction; language: Language }) {
|
||||
const config = ACTION_CONFIG[action.action] || ACTION_CONFIG.wait
|
||||
const isLong = action.action.includes('long')
|
||||
const isOpen = action.action.includes('open')
|
||||
|
||||
return (
|
||||
<div
|
||||
className="rounded-lg p-4 transition-all duration-200 hover:scale-[1.01]"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #1E2329 0%, #181C21 100%)',
|
||||
border: `1px solid ${config.color}33`,
|
||||
boxShadow: `0 4px 12px rgba(0, 0, 0, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.03)`,
|
||||
}}
|
||||
>
|
||||
{/* Header Row */}
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xl">{config.icon}</span>
|
||||
<span className="font-mono font-bold text-lg" style={{ color: '#EAECEF' }}>
|
||||
{action.symbol.replace('USDT', '')}
|
||||
</span>
|
||||
<span
|
||||
className="px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider"
|
||||
style={{ background: config.bg, color: config.color, border: `1px solid ${config.color}55` }}
|
||||
>
|
||||
{config.label}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Status Badge */}
|
||||
<div className="flex items-center gap-2">
|
||||
{action.confidence !== undefined && action.confidence > 0 && (
|
||||
<div
|
||||
className="px-2 py-1 rounded text-xs font-semibold"
|
||||
style={{
|
||||
background: `${getConfidenceColor(action.confidence)}22`,
|
||||
color: getConfidenceColor(action.confidence)
|
||||
}}
|
||||
>
|
||||
{action.confidence.toFixed(0)}%
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="w-2 h-2 rounded-full"
|
||||
style={{ background: action.success ? '#0ECB81' : '#F6465D' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Trading Details Grid */}
|
||||
{isOpen && (
|
||||
<div className="grid grid-cols-4 gap-3 mt-3 pt-3" style={{ borderTop: '1px solid #2B3139' }}>
|
||||
{/* Entry Price */}
|
||||
<div className="text-center">
|
||||
<div className="text-xs mb-1" style={{ color: '#848E9C' }}>
|
||||
{t('entryPrice', language)}
|
||||
</div>
|
||||
<div className="font-mono font-semibold" style={{ color: '#EAECEF' }}>
|
||||
{formatPrice(action.price)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stop Loss */}
|
||||
<div className="text-center">
|
||||
<div className="text-xs mb-1" style={{ color: '#F6465D' }}>
|
||||
{t('stopLoss', language)}
|
||||
</div>
|
||||
<div className="font-mono font-semibold" style={{ color: '#F6465D' }}>
|
||||
{formatPrice(action.stop_loss)}
|
||||
</div>
|
||||
{action.stop_loss && action.price && (
|
||||
<div className="text-xs mt-0.5" style={{ color: '#848E9C' }}>
|
||||
{calcPctChange(action.price, action.stop_loss, isLong)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Take Profit */}
|
||||
<div className="text-center">
|
||||
<div className="text-xs mb-1" style={{ color: '#0ECB81' }}>
|
||||
{t('takeProfit', language)}
|
||||
</div>
|
||||
<div className="font-mono font-semibold" style={{ color: '#0ECB81' }}>
|
||||
{formatPrice(action.take_profit)}
|
||||
</div>
|
||||
{action.take_profit && action.price && (
|
||||
<div className="text-xs mt-0.5" style={{ color: '#848E9C' }}>
|
||||
{calcPctChange(action.price, action.take_profit, isLong)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Leverage */}
|
||||
<div className="text-center">
|
||||
<div className="text-xs mb-1" style={{ color: '#848E9C' }}>
|
||||
{t('leverage', language)}
|
||||
</div>
|
||||
<div className="font-mono font-semibold" style={{ color: '#F0B90B' }}>
|
||||
{action.leverage}x
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Risk/Reward Ratio for open positions */}
|
||||
{isOpen && action.stop_loss && action.take_profit && action.price && (
|
||||
<div className="mt-3 pt-3 flex items-center justify-between" style={{ borderTop: '1px solid #2B3139' }}>
|
||||
<span className="text-xs" style={{ color: '#848E9C' }}>{t('riskReward', language)}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{(() => {
|
||||
const slDist = Math.abs(action.price - action.stop_loss)
|
||||
const tpDist = Math.abs(action.take_profit - action.price)
|
||||
const ratio = slDist > 0 ? (tpDist / slDist) : 0
|
||||
const ratioColor = ratio >= 3 ? '#0ECB81' : ratio >= 2 ? '#F0B90B' : '#F6465D'
|
||||
return (
|
||||
<>
|
||||
<div className="flex gap-1">
|
||||
<span style={{ color: '#F6465D' }}>1</span>
|
||||
<span style={{ color: '#848E9C' }}>:</span>
|
||||
<span style={{ color: '#0ECB81' }}>{ratio.toFixed(1)}</span>
|
||||
</div>
|
||||
<div
|
||||
className="h-1.5 rounded-full"
|
||||
style={{
|
||||
width: '60px',
|
||||
background: '#2B3139',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="h-full rounded-full transition-all duration-300"
|
||||
style={{
|
||||
width: `${Math.min(ratio / 5 * 100, 100)}%`,
|
||||
background: ratioColor
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Reasoning */}
|
||||
{action.reasoning && (
|
||||
<div className="mt-3 pt-3" style={{ borderTop: '1px solid #2B3139' }}>
|
||||
<div className="text-xs line-clamp-2" style={{ color: '#848E9C' }}>
|
||||
💡 {action.reasoning}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error Message */}
|
||||
{action.error && (
|
||||
<div
|
||||
className="mt-3 rounded p-2 text-xs"
|
||||
style={{
|
||||
background: 'rgba(246, 70, 93, 0.1)',
|
||||
border: '1px solid rgba(246, 70, 93, 0.3)',
|
||||
color: '#F6465D',
|
||||
}}
|
||||
>
|
||||
❌ {action.error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function DecisionCard({ decision, language }: DecisionCardProps) {
|
||||
const [showInputPrompt, setShowInputPrompt] = useState(false)
|
||||
const [showCoT, setShowCoT] = useState(false)
|
||||
|
||||
return (
|
||||
<div
|
||||
className="rounded p-5 transition-all duration-300 hover:translate-y-[-2px]"
|
||||
className="rounded-xl p-5 transition-all duration-300 hover:translate-y-[-2px]"
|
||||
style={{
|
||||
border: '1px solid #2B3139',
|
||||
background: '#1E2329',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.3)',
|
||||
background: 'linear-gradient(180deg, #1E2329 0%, #181C21 100%)',
|
||||
boxShadow: '0 4px 16px rgba(0, 0, 0, 0.3)',
|
||||
}}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div>
|
||||
<div className="font-semibold" style={{ color: '#EAECEF' }}>
|
||||
{t('cycle', language)} #{decision.cycle_number}
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className="w-10 h-10 rounded-lg flex items-center justify-center"
|
||||
style={{ background: 'rgba(240, 185, 11, 0.15)' }}
|
||||
>
|
||||
<span className="text-xl">🤖</span>
|
||||
</div>
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
||||
{new Date(decision.timestamp).toLocaleString()}
|
||||
<div>
|
||||
<div className="font-bold" style={{ color: '#EAECEF' }}>
|
||||
{t('cycle', language)} #{decision.cycle_number}
|
||||
</div>
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
||||
{new Date(decision.timestamp).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="px-3 py-1 rounded text-xs font-bold"
|
||||
className="px-4 py-1.5 rounded-full text-xs font-bold tracking-wider"
|
||||
style={
|
||||
decision.success
|
||||
? { background: 'rgba(14, 203, 129, 0.1)', color: '#0ECB81' }
|
||||
: { background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }
|
||||
? { background: 'rgba(14, 203, 129, 0.15)', color: '#0ECB81', border: '1px solid rgba(14, 203, 129, 0.3)' }
|
||||
: { background: 'rgba(246, 70, 93, 0.15)', color: '#F6465D', border: '1px solid rgba(246, 70, 93, 0.3)' }
|
||||
}
|
||||
>
|
||||
{t(decision.success ? 'success' : 'failed', language)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{decision.input_prompt && (
|
||||
<div className="mb-3">
|
||||
<button
|
||||
onClick={() => setShowInputPrompt(!showInputPrompt)}
|
||||
className="flex items-center gap-2 text-sm transition-colors"
|
||||
style={{ color: '#60a5fa' }}
|
||||
>
|
||||
<span className="font-semibold">
|
||||
📥 {t('inputPrompt', language)}
|
||||
</span>
|
||||
<span className="text-xs">
|
||||
{showInputPrompt ? t('collapse', language) : t('expand', language)}
|
||||
</span>
|
||||
</button>
|
||||
{showInputPrompt && (
|
||||
<div
|
||||
className="mt-2 rounded p-4 text-sm font-mono whitespace-pre-wrap max-h-96 overflow-y-auto"
|
||||
style={{
|
||||
background: '#0B0E11',
|
||||
border: '1px solid #2B3139',
|
||||
color: '#EAECEF',
|
||||
}}
|
||||
>
|
||||
{decision.input_prompt}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{decision.cot_trace && (
|
||||
<div className="mb-3">
|
||||
<button
|
||||
onClick={() => setShowCoT(!showCoT)}
|
||||
className="flex items-center gap-2 text-sm transition-colors"
|
||||
style={{ color: '#F0B90B' }}
|
||||
>
|
||||
<span className="font-semibold">
|
||||
📤 {t('aiThinking', language)}
|
||||
</span>
|
||||
<span className="text-xs">
|
||||
{showCoT ? t('collapse', language) : t('expand', language)}
|
||||
</span>
|
||||
</button>
|
||||
{showCoT && (
|
||||
<div
|
||||
className="mt-2 rounded p-4 text-sm font-mono whitespace-pre-wrap max-h-96 overflow-y-auto"
|
||||
style={{
|
||||
background: '#0B0E11',
|
||||
border: '1px solid #2B3139',
|
||||
color: '#EAECEF',
|
||||
}}
|
||||
>
|
||||
{decision.cot_trace}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Decision Actions - Beautiful Grid */}
|
||||
{decision.decisions && decision.decisions.length > 0 && (
|
||||
<div className="space-y-2 mb-3">
|
||||
<div className="space-y-3 mb-4">
|
||||
{decision.decisions.map((action, index) => (
|
||||
<div
|
||||
key={`${action.symbol}-${index}`}
|
||||
className="flex items-center gap-2 text-sm rounded px-3 py-2"
|
||||
style={{ background: '#0B0E11' }}
|
||||
>
|
||||
<span
|
||||
className="font-mono font-bold"
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
{action.symbol}
|
||||
</span>
|
||||
<span
|
||||
className="px-2 py-0.5 rounded text-xs font-bold"
|
||||
style={
|
||||
action.action.includes('open')
|
||||
? {
|
||||
background: 'rgba(96, 165, 250, 0.1)',
|
||||
color: '#60a5fa',
|
||||
}
|
||||
: action.action.includes('close')
|
||||
? {
|
||||
background: 'rgba(14, 203, 129, 0.1)',
|
||||
color: '#0ECB81',
|
||||
}
|
||||
: action.action === 'wait' || action.action === 'hold'
|
||||
? {
|
||||
background: 'rgba(132, 142, 156, 0.1)',
|
||||
color: '#848E9C',
|
||||
}
|
||||
: {
|
||||
background: 'rgba(248, 113, 113, 0.1)',
|
||||
color: '#F87171',
|
||||
}
|
||||
}
|
||||
>
|
||||
{action.action}
|
||||
</span>
|
||||
{action.reasoning && (
|
||||
<span
|
||||
className="text-xs"
|
||||
style={{ color: '#848E9C', flex: 1 }}
|
||||
>
|
||||
{action.reasoning}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<ActionCard key={`${action.symbol}-${index}`} action={action} language={language} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Collapsible Sections */}
|
||||
<div className="space-y-2">
|
||||
{/* Input Prompt */}
|
||||
{decision.input_prompt && (
|
||||
<div>
|
||||
<button
|
||||
onClick={() => setShowInputPrompt(!showInputPrompt)}
|
||||
className="flex items-center gap-2 text-sm transition-colors w-full justify-between p-2 rounded hover:bg-white/5"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-base">📥</span>
|
||||
<span className="font-semibold" style={{ color: '#60a5fa' }}>
|
||||
{t('inputPrompt', language)}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
className="text-xs px-2 py-0.5 rounded"
|
||||
style={{ background: 'rgba(96, 165, 250, 0.15)', color: '#60a5fa' }}
|
||||
>
|
||||
{showInputPrompt ? t('collapse', language) : t('expand', language)}
|
||||
</span>
|
||||
</button>
|
||||
{showInputPrompt && (
|
||||
<div
|
||||
className="mt-2 rounded-lg p-4 text-sm font-mono whitespace-pre-wrap max-h-96 overflow-y-auto"
|
||||
style={{
|
||||
background: '#0B0E11',
|
||||
border: '1px solid #2B3139',
|
||||
color: '#EAECEF',
|
||||
}}
|
||||
>
|
||||
{decision.input_prompt}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* AI Thinking */}
|
||||
{decision.cot_trace && (
|
||||
<div>
|
||||
<button
|
||||
onClick={() => setShowCoT(!showCoT)}
|
||||
className="flex items-center gap-2 text-sm transition-colors w-full justify-between p-2 rounded hover:bg-white/5"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-base">🧠</span>
|
||||
<span className="font-semibold" style={{ color: '#F0B90B' }}>
|
||||
{t('aiThinking', language)}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
className="text-xs px-2 py-0.5 rounded"
|
||||
style={{ background: 'rgba(240, 185, 11, 0.15)', color: '#F0B90B' }}
|
||||
>
|
||||
{showCoT ? t('collapse', language) : t('expand', language)}
|
||||
</span>
|
||||
</button>
|
||||
{showCoT && (
|
||||
<div
|
||||
className="mt-2 rounded-lg p-4 text-sm font-mono whitespace-pre-wrap max-h-96 overflow-y-auto"
|
||||
style={{
|
||||
background: '#0B0E11',
|
||||
border: '1px solid #2B3139',
|
||||
color: '#EAECEF',
|
||||
}}
|
||||
>
|
||||
{decision.cot_trace}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Execution Log */}
|
||||
{decision.execution_log && decision.execution_log.length > 0 && (
|
||||
<div
|
||||
className="rounded p-3 text-xs font-mono space-y-1"
|
||||
className="rounded-lg p-3 mt-4 text-xs font-mono space-y-1"
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
|
||||
>
|
||||
{decision.execution_log.map((log, index) => (
|
||||
@@ -165,9 +350,10 @@ export function DecisionCard({ decision, language }: DecisionCardProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error Message */}
|
||||
{decision.error_message && (
|
||||
<div
|
||||
className="rounded p-3 mt-3 text-sm"
|
||||
className="rounded-lg p-3 mt-4 text-sm"
|
||||
style={{
|
||||
background: 'rgba(246, 70, 93, 0.1)',
|
||||
border: '1px solid rgba(246, 70, 93, 0.4)',
|
||||
|
||||
@@ -43,6 +43,9 @@ export const translations = {
|
||||
symbol: 'Symbol',
|
||||
side: 'Side',
|
||||
entryPrice: 'Entry Price',
|
||||
stopLoss: 'Stop Loss',
|
||||
takeProfit: 'Take Profit',
|
||||
riskReward: 'Risk/Reward',
|
||||
markPrice: 'Mark Price',
|
||||
quantity: 'Quantity',
|
||||
positionValue: 'Position Value',
|
||||
@@ -1200,6 +1203,9 @@ export const translations = {
|
||||
symbol: '币种',
|
||||
side: '方向',
|
||||
entryPrice: '入场价',
|
||||
stopLoss: '止损',
|
||||
takeProfit: '止盈',
|
||||
riskReward: '风险回报比',
|
||||
markPrice: '标记价',
|
||||
quantity: '数量',
|
||||
positionValue: '仓位价值',
|
||||
|
||||
@@ -46,11 +46,14 @@ export interface DecisionAction {
|
||||
quantity: number
|
||||
leverage: number
|
||||
price: number
|
||||
stop_loss?: number // Stop loss price
|
||||
take_profit?: number // Take profit price
|
||||
confidence?: number // AI confidence (0-100)
|
||||
reasoning?: string // Brief reasoning
|
||||
order_id: number
|
||||
timestamp: string
|
||||
success: boolean
|
||||
error?: string
|
||||
reasoning?: string
|
||||
}
|
||||
|
||||
export interface AccountSnapshot {
|
||||
|
||||
Reference in New Issue
Block a user