mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-16 09:11:03 +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/manager"
|
||||||
"nofx/store"
|
"nofx/store"
|
||||||
"nofx/trader"
|
"nofx/trader"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -1898,7 +1899,7 @@ func (s *Server) handleDecisions(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, records)
|
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) {
|
func (s *Server) handleLatestDecisions(c *gin.Context) {
|
||||||
_, traderID, err := s.getTraderFromQuery(c)
|
_, traderID, err := s.getTraderFromQuery(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1912,7 +1913,18 @@ func (s *Server) handleLatestDecisions(c *gin.Context) {
|
|||||||
return
|
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 {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
"error": fmt.Sprintf("Failed to get decision log: %v", err),
|
"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
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
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/agiledragon/gomonkey/v2 v2.13.0
|
||||||
github.com/ethereum/go-ethereum v1.16.5
|
github.com/ethereum/go-ethereum v1.16.5
|
||||||
github.com/gin-gonic/gin v1.11.0
|
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/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 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.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 h1:B24Jg6wBI1iB8EFR1c+/aoTg7QN/Cum7YffG8KMIyYo=
|
||||||
github.com/agiledragon/gomonkey/v2 v2.13.0/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY=
|
github.com/agiledragon/gomonkey/v2 v2.13.0/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY=
|
||||||
github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
|
github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
|
||||||
|
|||||||
@@ -56,16 +56,20 @@ type PositionSnapshot struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DecisionAction decision action
|
// DecisionAction decision action
|
||||||
type DecisionAction struct{
|
type DecisionAction struct {
|
||||||
Action string `json:"action"`
|
Action string `json:"action"`
|
||||||
Symbol string `json:"symbol"`
|
Symbol string `json:"symbol"`
|
||||||
Quantity float64 `json:"quantity"`
|
Quantity float64 `json:"quantity"`
|
||||||
Leverage int `json:"leverage"`
|
Leverage int `json:"leverage"`
|
||||||
Price float64 `json:"price"`
|
Price float64 `json:"price"`
|
||||||
OrderID int64 `json:"order_id"`
|
StopLoss float64 `json:"stop_loss,omitempty"` // Stop loss price
|
||||||
Timestamp time.Time `json:"timestamp"`
|
TakeProfit float64 `json:"take_profit,omitempty"` // Take profit price
|
||||||
Success bool `json:"success"`
|
Confidence int `json:"confidence,omitempty"` // AI confidence (0-100)
|
||||||
Error string `json:"error"`
|
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
|
// Statistics statistics information
|
||||||
@@ -113,6 +117,9 @@ func (s *DecisionStore) initTables() error {
|
|||||||
// Migration: add raw_response column if not exists
|
// Migration: add raw_response column if not exists
|
||||||
s.db.Exec(`ALTER TABLE decision_records ADD COLUMN raw_response TEXT DEFAULT ''`)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,22 +131,23 @@ func (s *DecisionStore) LogDecision(record *DecisionRecord) error {
|
|||||||
record.Timestamp = record.Timestamp.UTC()
|
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)
|
candidateCoinsJSON, _ := json.Marshal(record.CandidateCoins)
|
||||||
executionLogJSON, _ := json.Marshal(record.ExecutionLog)
|
executionLogJSON, _ := json.Marshal(record.ExecutionLog)
|
||||||
|
decisionsJSON, _ := json.Marshal(record.Decisions)
|
||||||
|
|
||||||
// Insert decision record main table (only save AI decision related content)
|
// Insert decision record main table (only save AI decision related content)
|
||||||
result, err := s.db.Exec(`
|
result, err := s.db.Exec(`
|
||||||
INSERT INTO decision_records (
|
INSERT INTO decision_records (
|
||||||
trader_id, cycle_number, timestamp, system_prompt, input_prompt,
|
trader_id, cycle_number, timestamp, system_prompt, input_prompt,
|
||||||
cot_trace, decision_json, raw_response, candidate_coins, execution_log,
|
cot_trace, decision_json, raw_response, candidate_coins, execution_log,
|
||||||
success, error_message, ai_request_duration_ms
|
decisions, success, error_message, ai_request_duration_ms
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`,
|
`,
|
||||||
record.TraderID, record.CycleNumber, record.Timestamp.Format(time.RFC3339),
|
record.TraderID, record.CycleNumber, record.Timestamp.Format(time.RFC3339),
|
||||||
record.SystemPrompt, record.InputPrompt, record.CoTTrace, record.DecisionJSON,
|
record.SystemPrompt, record.InputPrompt, record.CoTTrace, record.DecisionJSON,
|
||||||
record.RawResponse, string(candidateCoinsJSON), string(executionLogJSON),
|
record.RawResponse, string(candidateCoinsJSON), string(executionLogJSON),
|
||||||
record.Success, record.ErrorMessage, record.AIRequestDurationMs,
|
string(decisionsJSON), record.Success, record.ErrorMessage, record.AIRequestDurationMs,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to insert decision record: %w", err)
|
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(`
|
rows, err := s.db.Query(`
|
||||||
SELECT id, trader_id, cycle_number, timestamp, system_prompt, input_prompt,
|
SELECT id, trader_id, cycle_number, timestamp, system_prompt, input_prompt,
|
||||||
cot_trace, decision_json, candidate_coins, execution_log,
|
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
|
FROM decision_records
|
||||||
WHERE trader_id = ?
|
WHERE trader_id = ?
|
||||||
ORDER BY timestamp DESC
|
ORDER BY timestamp DESC
|
||||||
@@ -197,7 +205,7 @@ func (s *DecisionStore) GetAllLatestRecords(n int) ([]*DecisionRecord, error) {
|
|||||||
rows, err := s.db.Query(`
|
rows, err := s.db.Query(`
|
||||||
SELECT id, trader_id, cycle_number, timestamp, system_prompt, input_prompt,
|
SELECT id, trader_id, cycle_number, timestamp, system_prompt, input_prompt,
|
||||||
cot_trace, decision_json, candidate_coins, execution_log,
|
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
|
FROM decision_records
|
||||||
ORDER BY timestamp DESC
|
ORDER BY timestamp DESC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
@@ -231,7 +239,7 @@ func (s *DecisionStore) GetRecordsByDate(traderID string, date time.Time) ([]*De
|
|||||||
rows, err := s.db.Query(`
|
rows, err := s.db.Query(`
|
||||||
SELECT id, trader_id, cycle_number, timestamp, system_prompt, input_prompt,
|
SELECT id, trader_id, cycle_number, timestamp, system_prompt, input_prompt,
|
||||||
cot_trace, decision_json, candidate_coins, execution_log,
|
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
|
FROM decision_records
|
||||||
WHERE trader_id = ? AND DATE(timestamp) = ?
|
WHERE trader_id = ? AND DATE(timestamp) = ?
|
||||||
ORDER BY timestamp ASC
|
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) {
|
func (s *DecisionStore) scanDecisionRecord(rows *sql.Rows) (*DecisionRecord, error) {
|
||||||
var record DecisionRecord
|
var record DecisionRecord
|
||||||
var timestampStr string
|
var timestampStr string
|
||||||
var candidateCoinsJSON, executionLogJSON string
|
var candidateCoinsJSON, executionLogJSON, decisionsJSON string
|
||||||
|
|
||||||
err := rows.Scan(
|
err := rows.Scan(
|
||||||
&record.ID, &record.TraderID, &record.CycleNumber, ×tampStr,
|
&record.ID, &record.TraderID, &record.CycleNumber, ×tampStr,
|
||||||
&record.SystemPrompt, &record.InputPrompt, &record.CoTTrace,
|
&record.SystemPrompt, &record.InputPrompt, &record.CoTTrace,
|
||||||
&record.DecisionJSON, &candidateCoinsJSON, &executionLogJSON,
|
&record.DecisionJSON, &candidateCoinsJSON, &executionLogJSON,
|
||||||
&record.Success, &record.ErrorMessage, &record.AIRequestDurationMs,
|
&decisionsJSON, &record.Success, &record.ErrorMessage, &record.AIRequestDurationMs,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -353,6 +361,7 @@ func (s *DecisionStore) scanDecisionRecord(rows *sql.Rows) (*DecisionRecord, err
|
|||||||
record.Timestamp, _ = time.Parse(time.RFC3339, timestampStr)
|
record.Timestamp, _ = time.Parse(time.RFC3339, timestampStr)
|
||||||
json.Unmarshal([]byte(candidateCoinsJSON), &record.CandidateCoins)
|
json.Unmarshal([]byte(candidateCoinsJSON), &record.CandidateCoins)
|
||||||
json.Unmarshal([]byte(executionLogJSON), &record.ExecutionLog)
|
json.Unmarshal([]byte(executionLogJSON), &record.ExecutionLog)
|
||||||
|
json.Unmarshal([]byte(decisionsJSON), &record.Decisions)
|
||||||
|
|
||||||
return &record, nil
|
return &record, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -528,13 +528,17 @@ func (at *AutoTrader) runCycle() error {
|
|||||||
// Execute decisions and record results
|
// Execute decisions and record results
|
||||||
for _, d := range sortedDecisions {
|
for _, d := range sortedDecisions {
|
||||||
actionRecord := store.DecisionAction{
|
actionRecord := store.DecisionAction{
|
||||||
Action: d.Action,
|
Action: d.Action,
|
||||||
Symbol: d.Symbol,
|
Symbol: d.Symbol,
|
||||||
Quantity: 0,
|
Quantity: 0,
|
||||||
Leverage: d.Leverage,
|
Leverage: d.Leverage,
|
||||||
Price: 0,
|
Price: 0,
|
||||||
Timestamp: time.Now(),
|
StopLoss: d.StopLoss,
|
||||||
Success: false,
|
TakeProfit: d.TakeProfit,
|
||||||
|
Confidence: d.Confidence,
|
||||||
|
Reasoning: d.Reasoning,
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
Success: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := at.executeDecisionWithRecord(&d, &actionRecord); err != nil {
|
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
|
// Create a minimal action record for tracking
|
||||||
actionRecord := &store.DecisionAction{
|
actionRecord := &store.DecisionAction{
|
||||||
Symbol: d.Symbol,
|
Symbol: d.Symbol,
|
||||||
Action: d.Action,
|
Action: d.Action,
|
||||||
Leverage: d.Leverage,
|
Leverage: d.Leverage,
|
||||||
|
StopLoss: d.StopLoss,
|
||||||
|
TakeProfit: d.TakeProfit,
|
||||||
|
Confidence: d.Confidence,
|
||||||
|
Reasoning: d.Reasoning,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute the decision
|
// 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)
|
// 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 {
|
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().
|
orders, err := t.client.NewListOpenOrdersService().
|
||||||
Symbol(symbol).
|
Symbol(symbol).
|
||||||
Do(context.Background())
|
Do(context.Background())
|
||||||
|
|
||||||
if err != nil {
|
if err == nil {
|
||||||
return fmt.Errorf("failed to get open orders: %w", err)
|
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)
|
// 2. Cancel Algo stop-loss orders
|
||||||
canceledCount := 0
|
algoOrders, err := t.client.NewListOpenAlgoOrdersService().
|
||||||
var cancelErrors []error
|
Symbol(symbol).
|
||||||
for _, order := range orders {
|
Do(context.Background())
|
||||||
orderType := order.Type
|
|
||||||
|
|
||||||
// Only cancel stop-loss orders (don't cancel take-profit orders)
|
if err == nil {
|
||||||
if orderType == futures.OrderTypeStopMarket || orderType == futures.OrderTypeStop {
|
for _, algoOrder := range algoOrders {
|
||||||
_, err := t.client.NewCancelOrderService().
|
// Only cancel stop-loss orders
|
||||||
Symbol(symbol).
|
if algoOrder.OrderType == futures.AlgoOrderTypeStopMarket || algoOrder.OrderType == futures.AlgoOrderTypeStop {
|
||||||
OrderID(order.OrderID).
|
_, err := t.client.NewCancelAlgoOrderService().
|
||||||
Do(context.Background())
|
AlgoID(algoOrder.AlgoId).
|
||||||
|
Do(context.Background())
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errMsg := fmt.Sprintf("Order ID %d: %v", order.OrderID, err)
|
errMsg := fmt.Sprintf("Algo ID %d: %v", algoOrder.AlgoId, err)
|
||||||
cancelErrors = append(cancelErrors, fmt.Errorf("%s", errMsg))
|
cancelErrors = append(cancelErrors, fmt.Errorf("%s", errMsg))
|
||||||
logger.Infof(" ⚠ Failed to cancel stop-loss order: %s", errMsg)
|
logger.Infof(" ⚠ Failed to cancel Algo stop-loss order: %s", errMsg)
|
||||||
continue
|
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)
|
// 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 {
|
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().
|
orders, err := t.client.NewListOpenOrdersService().
|
||||||
Symbol(symbol).
|
Symbol(symbol).
|
||||||
Do(context.Background())
|
Do(context.Background())
|
||||||
|
|
||||||
if err != nil {
|
if err == nil {
|
||||||
return fmt.Errorf("failed to get open orders: %w", err)
|
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)
|
// 2. Cancel Algo take-profit orders
|
||||||
canceledCount := 0
|
algoOrders, err := t.client.NewListOpenAlgoOrdersService().
|
||||||
var cancelErrors []error
|
Symbol(symbol).
|
||||||
for _, order := range orders {
|
Do(context.Background())
|
||||||
orderType := order.Type
|
|
||||||
|
|
||||||
// Only cancel take-profit orders (don't cancel stop-loss orders)
|
if err == nil {
|
||||||
if orderType == futures.OrderTypeTakeProfitMarket || orderType == futures.OrderTypeTakeProfit {
|
for _, algoOrder := range algoOrders {
|
||||||
_, err := t.client.NewCancelOrderService().
|
// Only cancel take-profit orders
|
||||||
Symbol(symbol).
|
if algoOrder.OrderType == futures.AlgoOrderTypeTakeProfitMarket || algoOrder.OrderType == futures.AlgoOrderTypeTakeProfit {
|
||||||
OrderID(order.OrderID).
|
_, err := t.client.NewCancelAlgoOrderService().
|
||||||
Do(context.Background())
|
AlgoID(algoOrder.AlgoId).
|
||||||
|
Do(context.Background())
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errMsg := fmt.Sprintf("Order ID %d: %v", order.OrderID, err)
|
errMsg := fmt.Sprintf("Algo ID %d: %v", algoOrder.AlgoId, err)
|
||||||
cancelErrors = append(cancelErrors, fmt.Errorf("%s", errMsg))
|
cancelErrors = append(cancelErrors, fmt.Errorf("%s", errMsg))
|
||||||
logger.Infof(" ⚠ Failed to cancel take-profit order: %s", errMsg)
|
logger.Infof(" ⚠ Failed to cancel Algo take-profit order: %s", errMsg)
|
||||||
continue
|
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
|
// 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 {
|
func (t *FuturesTrader) CancelAllOrders(symbol string) error {
|
||||||
|
// 1. Cancel all legacy orders
|
||||||
err := t.client.NewCancelAllOpenOrdersService().
|
err := t.client.NewCancelAllOpenOrdersService().
|
||||||
Symbol(symbol).
|
Symbol(symbol).
|
||||||
Do(context.Background())
|
Do(context.Background())
|
||||||
|
|
||||||
if err != nil {
|
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)
|
// 2. Cancel all Algo orders
|
||||||
return nil
|
err = t.client.NewCancelAllAlgoOpenOrdersService().
|
||||||
}
|
|
||||||
|
|
||||||
// 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().
|
|
||||||
Symbol(symbol).
|
Symbol(symbol).
|
||||||
Do(context.Background())
|
Do(context.Background())
|
||||||
|
|
||||||
if err != nil {
|
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
|
canceledCount := 0
|
||||||
for _, order := range orders {
|
|
||||||
orderType := order.Type
|
|
||||||
|
|
||||||
// Only cancel stop-loss and take-profit orders
|
// 1. Cancel legacy stop orders (for backward compatibility)
|
||||||
if orderType == futures.OrderTypeStopMarket ||
|
orders, err := t.client.NewListOpenOrdersService().
|
||||||
orderType == futures.OrderTypeTakeProfitMarket ||
|
Symbol(symbol).
|
||||||
orderType == futures.OrderTypeStop ||
|
Do(context.Background())
|
||||||
orderType == futures.OrderTypeTakeProfit {
|
|
||||||
|
|
||||||
_, err := t.client.NewCancelOrderService().
|
if err == nil {
|
||||||
Symbol(symbol).
|
for _, order := range orders {
|
||||||
OrderID(order.OrderID).
|
orderType := string(order.Type)
|
||||||
Do(context.Background())
|
|
||||||
|
|
||||||
if err != nil {
|
// Only cancel stop-loss and take-profit orders
|
||||||
logger.Infof(" ⚠ Failed to cancel order %d: %v", order.OrderID, err)
|
// Use string comparison since OrderType constants were removed in v2.8.9
|
||||||
continue
|
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 {
|
if canceledCount == 0 {
|
||||||
logger.Infof(" ℹ %s has no take-profit/stop-loss orders to cancel", symbol)
|
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
|
return nil
|
||||||
@@ -721,7 +803,8 @@ func (t *FuturesTrader) CalculatePositionSize(balance, riskPercent, price float6
|
|||||||
return quantity
|
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 {
|
func (t *FuturesTrader) SetStopLoss(symbol string, positionSide string, quantity, stopPrice float64) error {
|
||||||
var side futures.SideType
|
var side futures.SideType
|
||||||
var posSide futures.PositionSideType
|
var posSide futures.PositionSideType
|
||||||
@@ -734,33 +817,28 @@ func (t *FuturesTrader) SetStopLoss(symbol string, positionSide string, quantity
|
|||||||
posSide = futures.PositionSideTypeShort
|
posSide = futures.PositionSideTypeShort
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format quantity
|
// Use new Algo Order API
|
||||||
quantityStr, err := t.FormatQuantity(symbol, quantity)
|
_, err := t.client.NewCreateAlgoOrderService().
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = t.client.NewCreateOrderService().
|
|
||||||
Symbol(symbol).
|
Symbol(symbol).
|
||||||
Side(side).
|
Side(side).
|
||||||
PositionSide(posSide).
|
PositionSide(posSide).
|
||||||
Type(futures.OrderTypeStopMarket).
|
Type(futures.AlgoOrderTypeStopMarket).
|
||||||
StopPrice(fmt.Sprintf("%.8f", stopPrice)).
|
TriggerPrice(fmt.Sprintf("%.8f", stopPrice)).
|
||||||
Quantity(quantityStr).
|
|
||||||
WorkingType(futures.WorkingTypeContractPrice).
|
WorkingType(futures.WorkingTypeContractPrice).
|
||||||
ClosePosition(true).
|
ClosePosition(true).
|
||||||
NewClientOrderID(getBrOrderID()).
|
ClientAlgoId(getBrOrderID()).
|
||||||
Do(context.Background())
|
Do(context.Background())
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to set stop-loss: %w", err)
|
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
|
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 {
|
func (t *FuturesTrader) SetTakeProfit(symbol string, positionSide string, quantity, takeProfitPrice float64) error {
|
||||||
var side futures.SideType
|
var side futures.SideType
|
||||||
var posSide futures.PositionSideType
|
var posSide futures.PositionSideType
|
||||||
@@ -773,29 +851,23 @@ func (t *FuturesTrader) SetTakeProfit(symbol string, positionSide string, quanti
|
|||||||
posSide = futures.PositionSideTypeShort
|
posSide = futures.PositionSideTypeShort
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format quantity
|
// Use new Algo Order API
|
||||||
quantityStr, err := t.FormatQuantity(symbol, quantity)
|
_, err := t.client.NewCreateAlgoOrderService().
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = t.client.NewCreateOrderService().
|
|
||||||
Symbol(symbol).
|
Symbol(symbol).
|
||||||
Side(side).
|
Side(side).
|
||||||
PositionSide(posSide).
|
PositionSide(posSide).
|
||||||
Type(futures.OrderTypeTakeProfitMarket).
|
Type(futures.AlgoOrderTypeTakeProfitMarket).
|
||||||
StopPrice(fmt.Sprintf("%.8f", takeProfitPrice)).
|
TriggerPrice(fmt.Sprintf("%.8f", takeProfitPrice)).
|
||||||
Quantity(quantityStr).
|
|
||||||
WorkingType(futures.WorkingTypeContractPrice).
|
WorkingType(futures.WorkingTypeContractPrice).
|
||||||
ClosePosition(true).
|
ClosePosition(true).
|
||||||
NewClientOrderID(getBrOrderID()).
|
ClientAlgoId(getBrOrderID()).
|
||||||
Do(context.Background())
|
Do(context.Background())
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to set take-profit: %w", err)
|
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
|
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) {
|
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)
|
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
|
// Set leverage first
|
||||||
if err := t.SetLeverage(symbol, leverage); err != nil {
|
if err := t.SetLeverage(symbol, leverage); err != nil {
|
||||||
logger.Infof("⚠️ [Bybit] Failed to set leverage: %v", err)
|
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) {
|
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)
|
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
|
// Set leverage first
|
||||||
if err := t.SetLeverage(symbol, leverage); err != nil {
|
if err := t.SetLeverage(symbol, leverage); err != nil {
|
||||||
logger.Infof("⚠️ [Bybit] Failed to set leverage: %v", err)
|
logger.Infof("⚠️ [Bybit] Failed to set leverage: %v", err)
|
||||||
|
|||||||
@@ -14,44 +14,46 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// SetStopLoss Set stop-loss order (implements Trader interface)
|
// 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 {
|
func (t *LighterTraderV2) SetStopLoss(symbol string, positionSide string, quantity, stopPrice float64) error {
|
||||||
if t.txClient == nil {
|
if t.txClient == nil {
|
||||||
return fmt.Errorf("TxClient not initialized")
|
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")
|
isAsk := (positionSide == "LONG" || positionSide == "long")
|
||||||
|
|
||||||
// Create limit stop-loss order
|
// Create stop-loss order with TriggerPrice (type=2: StopLossOrder)
|
||||||
_, err := t.CreateOrder(symbol, isAsk, quantity, stopPrice, "limit")
|
_, err := t.CreateStopOrder(symbol, isAsk, quantity, stopPrice, "stop_loss")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to set stop-loss: %w", err)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetTakeProfit Set take-profit order (implements Trader interface)
|
// 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 {
|
func (t *LighterTraderV2) SetTakeProfit(symbol string, positionSide string, quantity, takeProfitPrice float64) error {
|
||||||
if t.txClient == nil {
|
if t.txClient == nil {
|
||||||
return fmt.Errorf("TxClient not initialized")
|
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")
|
isAsk := (positionSide == "LONG" || positionSide == "long")
|
||||||
|
|
||||||
// Create limit take-profit order
|
// Create take-profit order with TriggerPrice (type=4: TakeProfitOrder)
|
||||||
_, err := t.CreateOrder(symbol, isAsk, quantity, takeProfitPrice, "limit")
|
_, err := t.CreateStopOrder(symbol, isAsk, quantity, takeProfitPrice, "take_profit")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to set take-profit: %w", err)
|
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
|
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)
|
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 {
|
if err := t.SetLeverage(symbol, leverage); err != nil {
|
||||||
logger.Infof("⚠️ Failed to set leverage: %v", err)
|
logger.Infof("⚠️ Failed to set leverage: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Get market price
|
// 3. Get market price
|
||||||
marketPrice, err := t.GetMarketPrice(symbol)
|
marketPrice, err := t.GetMarketPrice(symbol)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get market price: %w", err)
|
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")
|
orderResult, err := t.CreateOrder(symbol, false, quantity, 0, "market")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to open long: %w", err)
|
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)
|
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 {
|
if err := t.SetLeverage(symbol, leverage); err != nil {
|
||||||
logger.Infof("⚠️ Failed to set leverage: %v", err)
|
logger.Infof("⚠️ Failed to set leverage: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Get market price
|
// 3. Get market price
|
||||||
marketPrice, err := t.GetMarketPrice(symbol)
|
marketPrice, err := t.GetMarketPrice(symbol)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get market price: %w", err)
|
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")
|
orderResult, err := t.CreateOrder(symbol, true, quantity, 0, "market")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to open short: %w", err)
|
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
|
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
|
// boolToUint8 Convert boolean to uint8
|
||||||
func boolToUint8(b bool) uint8 {
|
func boolToUint8(b bool) uint8 {
|
||||||
if b {
|
if b {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import type { DecisionRecord } from '../types'
|
import type { DecisionRecord, DecisionAction } from '../types'
|
||||||
import { t, type Language } from '../i18n/translations'
|
import { t, type Language } from '../i18n/translations'
|
||||||
|
|
||||||
interface DecisionCardProps {
|
interface DecisionCardProps {
|
||||||
@@ -7,154 +7,339 @@ interface DecisionCardProps {
|
|||||||
language: Language
|
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) {
|
export function DecisionCard({ decision, language }: DecisionCardProps) {
|
||||||
const [showInputPrompt, setShowInputPrompt] = useState(false)
|
const [showInputPrompt, setShowInputPrompt] = useState(false)
|
||||||
const [showCoT, setShowCoT] = useState(false)
|
const [showCoT, setShowCoT] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<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={{
|
style={{
|
||||||
border: '1px solid #2B3139',
|
border: '1px solid #2B3139',
|
||||||
background: '#1E2329',
|
background: 'linear-gradient(180deg, #1E2329 0%, #181C21 100%)',
|
||||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.3)',
|
boxShadow: '0 4px 16px rgba(0, 0, 0, 0.3)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between mb-3">
|
{/* Header */}
|
||||||
<div>
|
<div className="flex items-center justify-between mb-4">
|
||||||
<div className="font-semibold" style={{ color: '#EAECEF' }}>
|
<div className="flex items-center gap-3">
|
||||||
{t('cycle', language)} #{decision.cycle_number}
|
<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>
|
||||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
<div>
|
||||||
{new Date(decision.timestamp).toLocaleString()}
|
<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>
|
</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={
|
style={
|
||||||
decision.success
|
decision.success
|
||||||
? { background: 'rgba(14, 203, 129, 0.1)', color: '#0ECB81' }
|
? { background: 'rgba(14, 203, 129, 0.15)', color: '#0ECB81', border: '1px solid rgba(14, 203, 129, 0.3)' }
|
||||||
: { background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }
|
: { background: 'rgba(246, 70, 93, 0.15)', color: '#F6465D', border: '1px solid rgba(246, 70, 93, 0.3)' }
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t(decision.success ? 'success' : 'failed', language)}
|
{t(decision.success ? 'success' : 'failed', language)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{decision.input_prompt && (
|
{/* Decision Actions - Beautiful Grid */}
|
||||||
<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.decisions && decision.decisions.length > 0 && (
|
{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) => (
|
{decision.decisions.map((action, index) => (
|
||||||
<div
|
<ActionCard key={`${action.symbol}-${index}`} action={action} language={language} />
|
||||||
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>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</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 && (
|
{decision.execution_log && decision.execution_log.length > 0 && (
|
||||||
<div
|
<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' }}
|
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
|
||||||
>
|
>
|
||||||
{decision.execution_log.map((log, index) => (
|
{decision.execution_log.map((log, index) => (
|
||||||
@@ -165,9 +350,10 @@ export function DecisionCard({ decision, language }: DecisionCardProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Error Message */}
|
||||||
{decision.error_message && (
|
{decision.error_message && (
|
||||||
<div
|
<div
|
||||||
className="rounded p-3 mt-3 text-sm"
|
className="rounded-lg p-3 mt-4 text-sm"
|
||||||
style={{
|
style={{
|
||||||
background: 'rgba(246, 70, 93, 0.1)',
|
background: 'rgba(246, 70, 93, 0.1)',
|
||||||
border: '1px solid rgba(246, 70, 93, 0.4)',
|
border: '1px solid rgba(246, 70, 93, 0.4)',
|
||||||
|
|||||||
@@ -43,6 +43,9 @@ export const translations = {
|
|||||||
symbol: 'Symbol',
|
symbol: 'Symbol',
|
||||||
side: 'Side',
|
side: 'Side',
|
||||||
entryPrice: 'Entry Price',
|
entryPrice: 'Entry Price',
|
||||||
|
stopLoss: 'Stop Loss',
|
||||||
|
takeProfit: 'Take Profit',
|
||||||
|
riskReward: 'Risk/Reward',
|
||||||
markPrice: 'Mark Price',
|
markPrice: 'Mark Price',
|
||||||
quantity: 'Quantity',
|
quantity: 'Quantity',
|
||||||
positionValue: 'Position Value',
|
positionValue: 'Position Value',
|
||||||
@@ -1200,6 +1203,9 @@ export const translations = {
|
|||||||
symbol: '币种',
|
symbol: '币种',
|
||||||
side: '方向',
|
side: '方向',
|
||||||
entryPrice: '入场价',
|
entryPrice: '入场价',
|
||||||
|
stopLoss: '止损',
|
||||||
|
takeProfit: '止盈',
|
||||||
|
riskReward: '风险回报比',
|
||||||
markPrice: '标记价',
|
markPrice: '标记价',
|
||||||
quantity: '数量',
|
quantity: '数量',
|
||||||
positionValue: '仓位价值',
|
positionValue: '仓位价值',
|
||||||
|
|||||||
@@ -46,11 +46,14 @@ export interface DecisionAction {
|
|||||||
quantity: number
|
quantity: number
|
||||||
leverage: number
|
leverage: number
|
||||||
price: 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
|
order_id: number
|
||||||
timestamp: string
|
timestamp: string
|
||||||
success: boolean
|
success: boolean
|
||||||
error?: string
|
error?: string
|
||||||
reasoning?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AccountSnapshot {
|
export interface AccountSnapshot {
|
||||||
|
|||||||
Reference in New Issue
Block a user