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:
tinkle-community
2025-12-15 21:22:22 +08:00
parent aeede956e6
commit 3f084005e4
12 changed files with 699 additions and 270 deletions

View File

@@ -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
View File

@@ -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
View File

@@ -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=

View File

@@ -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, &timestampStr,
&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
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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)',

View File

@@ -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: '仓位价值',

View File

@@ -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 {