mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-04 01:48:22 +08:00
feat: add limit order support with AI context awareness
- Add OpenOrderInfo struct and OpenOrders field to trading context - Support new AI actions: place_buy_limit, place_sell_limit, cancel_order, cancel_all_orders - Include existing open orders in AI prompt to avoid duplicate orders - Add open orders display table in trader dashboard - Fix Hyperliquid symbol conversion and order status parsing - Add i18n translations for open orders
This commit is contained in:
+15
-1
@@ -6,13 +6,13 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"nofx/logger"
|
"nofx/logger"
|
||||||
"nofx/market"
|
"nofx/market"
|
||||||
"nofx/provider/hyperliquid"
|
"nofx/provider/hyperliquid"
|
||||||
"nofx/provider/nofxos"
|
"nofx/provider/nofxos"
|
||||||
"nofx/security"
|
"nofx/security"
|
||||||
"nofx/store"
|
"nofx/store"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -99,6 +99,7 @@ type Context struct {
|
|||||||
PromptVariant string `json:"prompt_variant,omitempty"`
|
PromptVariant string `json:"prompt_variant,omitempty"`
|
||||||
TradingStats *TradingStats `json:"trading_stats,omitempty"`
|
TradingStats *TradingStats `json:"trading_stats,omitempty"`
|
||||||
RecentOrders []RecentOrder `json:"recent_orders,omitempty"`
|
RecentOrders []RecentOrder `json:"recent_orders,omitempty"`
|
||||||
|
OpenOrders []OpenOrderInfo `json:"open_orders,omitempty"`
|
||||||
MarketDataMap map[string]*market.Data `json:"-"`
|
MarketDataMap map[string]*market.Data `json:"-"`
|
||||||
MultiTFMarket map[string]map[string]*market.Data `json:"-"`
|
MultiTFMarket map[string]map[string]*market.Data `json:"-"`
|
||||||
OITopDataMap map[string]*OITopData `json:"-"`
|
OITopDataMap map[string]*OITopData `json:"-"`
|
||||||
@@ -111,6 +112,19 @@ type Context struct {
|
|||||||
Timeframes []string `json:"-"`
|
Timeframes []string `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OpenOrderInfo describes an existing pending exchange order for AI context.
|
||||||
|
type OpenOrderInfo struct {
|
||||||
|
OrderID string `json:"order_id"`
|
||||||
|
Symbol string `json:"symbol"`
|
||||||
|
Side string `json:"side"`
|
||||||
|
PositionSide string `json:"position_side,omitempty"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Price float64 `json:"price,omitempty"`
|
||||||
|
StopPrice float64 `json:"stop_price,omitempty"`
|
||||||
|
Quantity float64 `json:"quantity"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
// Decision AI trading decision
|
// Decision AI trading decision
|
||||||
type Decision struct {
|
type Decision struct {
|
||||||
Symbol string `json:"symbol"`
|
Symbol string `json:"symbol"`
|
||||||
|
|||||||
@@ -20,12 +20,16 @@ func validateDecisions(decisions []Decision, accountEquity float64, btcEthLevera
|
|||||||
|
|
||||||
func validateDecision(d *Decision, accountEquity float64, btcEthLeverage, altcoinLeverage int, btcEthPosRatio, altcoinPosRatio float64) error {
|
func validateDecision(d *Decision, accountEquity float64, btcEthLeverage, altcoinLeverage int, btcEthPosRatio, altcoinPosRatio float64) error {
|
||||||
validActions := map[string]bool{
|
validActions := map[string]bool{
|
||||||
"open_long": true,
|
"open_long": true,
|
||||||
"open_short": true,
|
"open_short": true,
|
||||||
"close_long": true,
|
"close_long": true,
|
||||||
"close_short": true,
|
"close_short": true,
|
||||||
"hold": true,
|
"place_buy_limit": true,
|
||||||
"wait": true,
|
"place_sell_limit": true,
|
||||||
|
"cancel_order": true,
|
||||||
|
"cancel_all_orders": true,
|
||||||
|
"hold": true,
|
||||||
|
"wait": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
if !validActions[d.Action] {
|
if !validActions[d.Action] {
|
||||||
@@ -117,5 +121,33 @@ func validateDecision(d *Decision, accountEquity float64, btcEthLeverage, altcoi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if d.Action == "place_buy_limit" || d.Action == "place_sell_limit" {
|
||||||
|
if d.Symbol == "" {
|
||||||
|
return fmt.Errorf("symbol is required for limit orders")
|
||||||
|
}
|
||||||
|
if d.Price <= 0 {
|
||||||
|
return fmt.Errorf("limit order price must be greater than 0")
|
||||||
|
}
|
||||||
|
if d.Quantity <= 0 && d.PositionSizeUSD <= 0 {
|
||||||
|
return fmt.Errorf("limit order requires quantity or position_size_usd")
|
||||||
|
}
|
||||||
|
if d.Leverage < 0 {
|
||||||
|
return fmt.Errorf("leverage cannot be negative")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Action == "cancel_order" {
|
||||||
|
if d.Symbol == "" {
|
||||||
|
return fmt.Errorf("symbol is required for cancel_order")
|
||||||
|
}
|
||||||
|
if d.OrderID == "" {
|
||||||
|
return fmt.Errorf("order_id is required for cancel_order")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Action == "cancel_all_orders" && d.Symbol == "" {
|
||||||
|
return fmt.Errorf("symbol is required for cancel_all_orders")
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-1
@@ -132,13 +132,17 @@ func (e *StrategyEngine) BuildSystemPrompt(accountEquity float64, variant string
|
|||||||
examplePositionSize := accountEquity * btcEthPosValueRatio
|
examplePositionSize := accountEquity * btcEthPosValueRatio
|
||||||
sb.WriteString(fmt.Sprintf(" {\"symbol\": \"BTCUSDT\", \"action\": \"open_short\", \"leverage\": %d, \"position_size_usd\": %.0f, \"stop_loss\": 97000, \"take_profit\": 91000, \"confidence\": 85, \"risk_usd\": 300},\n",
|
sb.WriteString(fmt.Sprintf(" {\"symbol\": \"BTCUSDT\", \"action\": \"open_short\", \"leverage\": %d, \"position_size_usd\": %.0f, \"stop_loss\": 97000, \"take_profit\": 91000, \"confidence\": 85, \"risk_usd\": 300},\n",
|
||||||
riskControl.BTCETHMaxLeverage, examplePositionSize))
|
riskControl.BTCETHMaxLeverage, examplePositionSize))
|
||||||
|
sb.WriteString(" {\"symbol\": \"ETHUSDT\", \"action\": \"place_buy_limit\", \"price\": 3200, \"position_size_usd\": 500, \"leverage\": 3, \"confidence\": 75, \"reasoning\": \"Buy only on pullback; avoid chasing\"},\n")
|
||||||
|
sb.WriteString(" {\"symbol\": \"SOLUSDT\", \"action\": \"cancel_order\", \"order_id\": \"123456789\"},\n")
|
||||||
sb.WriteString(" {\"symbol\": \"ETHUSDT\", \"action\": \"close_long\"}\n")
|
sb.WriteString(" {\"symbol\": \"ETHUSDT\", \"action\": \"close_long\"}\n")
|
||||||
sb.WriteString("]\n```\n")
|
sb.WriteString("]\n```\n")
|
||||||
sb.WriteString("</decision>\n\n")
|
sb.WriteString("</decision>\n\n")
|
||||||
sb.WriteString("## Field Description\n\n")
|
sb.WriteString("## Field Description\n\n")
|
||||||
sb.WriteString("- `action`: open_long | open_short | close_long | close_short | hold | wait\n")
|
sb.WriteString("- `action`: open_long | open_short | close_long | close_short | place_buy_limit | place_sell_limit | cancel_order | cancel_all_orders | hold | wait\n")
|
||||||
sb.WriteString(fmt.Sprintf("- `confidence`: 0-100 (opening recommended ≥ %d)\n", riskControl.MinConfidence))
|
sb.WriteString(fmt.Sprintf("- `confidence`: 0-100 (opening recommended ≥ %d)\n", riskControl.MinConfidence))
|
||||||
sb.WriteString("- Required when opening: leverage, position_size_usd, stop_loss, take_profit, confidence, risk_usd\n")
|
sb.WriteString("- Required when opening: leverage, position_size_usd, stop_loss, take_profit, confidence, risk_usd\n")
|
||||||
|
sb.WriteString("- Required when placing limit orders: symbol, action, price, and either quantity or position_size_usd. Use `place_buy_limit` for a bid below current price and `place_sell_limit` for an ask above current price.\n")
|
||||||
|
sb.WriteString("- Existing open orders are shown in the user prompt. Do not place duplicate orders at similar prices; use `cancel_order` with `order_id` or `cancel_all_orders` before replacing stale/conflicting orders.\n")
|
||||||
sb.WriteString("- **IMPORTANT**: All numeric values must be calculated numbers, NOT formulas/expressions (e.g., use `27.76` not `3000 * 0.01`)\n\n")
|
sb.WriteString("- **IMPORTANT**: All numeric values must be calculated numbers, NOT formulas/expressions (e.g., use `27.76` not `3000 * 0.01`)\n\n")
|
||||||
|
|
||||||
// 8. Custom Prompt
|
// 8. Custom Prompt
|
||||||
@@ -265,6 +269,21 @@ func (e *StrategyEngine) BuildUserPrompt(ctx *Context) string {
|
|||||||
sb.WriteString("\n")
|
sb.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(ctx.OpenOrders) > 0 {
|
||||||
|
sb.WriteString("## Existing Open Orders\n")
|
||||||
|
for i, order := range ctx.OpenOrders {
|
||||||
|
price := order.Price
|
||||||
|
if price <= 0 {
|
||||||
|
price = order.StopPrice
|
||||||
|
}
|
||||||
|
sb.WriteString(fmt.Sprintf("%d. id=%s %s %s %s qty=%.6f price=%.4f status=%s\n",
|
||||||
|
i+1, order.OrderID, order.Symbol, order.Side, order.Type, order.Quantity, price, order.Status))
|
||||||
|
}
|
||||||
|
sb.WriteString("Before placing a new limit order, check these orders and cancel stale/conflicting duplicates when needed.\n\n")
|
||||||
|
} else {
|
||||||
|
sb.WriteString("Existing Open Orders: None\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
// Historical trading statistics (helps AI understand past performance)
|
// Historical trading statistics (helps AI understand past performance)
|
||||||
if ctx.TradingStats != nil && ctx.TradingStats.TotalTrades > 0 {
|
if ctx.TradingStats != nil && ctx.TradingStats.TotalTrades > 0 {
|
||||||
// Get language from strategy config
|
// Get language from strategy config
|
||||||
|
|||||||
@@ -468,6 +468,8 @@ func (at *AutoTrader) buildTradingContext() (*kernel.Context, error) {
|
|||||||
CandidateCoins: candidateCoins,
|
CandidateCoins: candidateCoins,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx.OpenOrders = at.collectOpenOrdersForAI(candidateCoins, positionInfos)
|
||||||
|
|
||||||
// 7. Add recent closed trades (if store is available)
|
// 7. Add recent closed trades (if store is available)
|
||||||
if at.store != nil {
|
if at.store != nil {
|
||||||
// Get recent 10 closed trades for AI context
|
// Get recent 10 closed trades for AI context
|
||||||
@@ -580,6 +582,51 @@ func (at *AutoTrader) buildTradingContext() (*kernel.Context, error) {
|
|||||||
return ctx, nil
|
return ctx, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (at *AutoTrader) collectOpenOrdersForAI(candidateCoins []kernel.CandidateCoin, positions []kernel.PositionInfo) []kernel.OpenOrderInfo {
|
||||||
|
symbolSet := make(map[string]struct{})
|
||||||
|
for _, coin := range candidateCoins {
|
||||||
|
if coin.Symbol != "" {
|
||||||
|
symbolSet[coin.Symbol] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, pos := range positions {
|
||||||
|
if pos.Symbol != "" {
|
||||||
|
symbolSet[pos.Symbol] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(symbolSet) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
openOrders := make([]kernel.OpenOrderInfo, 0)
|
||||||
|
for symbol := range symbolSet {
|
||||||
|
orders, err := at.trader.GetOpenOrders(symbol)
|
||||||
|
if err != nil {
|
||||||
|
at.logWarnf("⚠️ Failed to get open orders for %s: %v", symbol, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, order := range orders {
|
||||||
|
openOrders = append(openOrders, kernel.OpenOrderInfo{
|
||||||
|
OrderID: order.OrderID,
|
||||||
|
Symbol: order.Symbol,
|
||||||
|
Side: order.Side,
|
||||||
|
PositionSide: order.PositionSide,
|
||||||
|
Type: order.Type,
|
||||||
|
Price: order.Price,
|
||||||
|
StopPrice: order.StopPrice,
|
||||||
|
Quantity: order.Quantity,
|
||||||
|
Status: order.Status,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if len(openOrders) >= 20 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return openOrders
|
||||||
|
}
|
||||||
|
|
||||||
// sortDecisionsByPriority sorts decisions: close positions first, then open positions, finally hold/wait
|
// sortDecisionsByPriority sorts decisions: close positions first, then open positions, finally hold/wait
|
||||||
// This avoids position stacking overflow when changing positions
|
// This avoids position stacking overflow when changing positions
|
||||||
func sortDecisionsByPriority(decisions []kernel.Decision) []kernel.Decision {
|
func sortDecisionsByPriority(decisions []kernel.Decision) []kernel.Decision {
|
||||||
@@ -590,12 +637,16 @@ func sortDecisionsByPriority(decisions []kernel.Decision) []kernel.Decision {
|
|||||||
// Define priority
|
// Define priority
|
||||||
getActionPriority := func(action string) int {
|
getActionPriority := func(action string) int {
|
||||||
switch action {
|
switch action {
|
||||||
|
case "cancel_order", "cancel_all_orders":
|
||||||
|
return 0 // Cancel stale/conflicting pending orders before new actions
|
||||||
case "close_long", "close_short":
|
case "close_long", "close_short":
|
||||||
return 1 // Highest priority: close positions first
|
return 1 // Highest priority: close positions first
|
||||||
|
case "place_buy_limit", "place_sell_limit":
|
||||||
|
return 2 // Place pending orders before market opens if both are present
|
||||||
case "open_long", "open_short":
|
case "open_long", "open_short":
|
||||||
return 2 // Second priority: open positions later
|
return 3 // Market opens after pending-order maintenance
|
||||||
case "hold", "wait":
|
case "hold", "wait":
|
||||||
return 3 // Lowest priority: wait
|
return 4 // Lowest priority: wait
|
||||||
default:
|
default:
|
||||||
return 999 // Unknown actions at the end
|
return 999 // Unknown actions at the end
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ package trader
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"nofx/kernel"
|
"nofx/kernel"
|
||||||
"nofx/logger"
|
"nofx/logger"
|
||||||
"nofx/market"
|
"nofx/market"
|
||||||
"nofx/store"
|
"nofx/store"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -20,6 +22,14 @@ func (at *AutoTrader) executeDecisionWithRecord(decision *kernel.Decision, actio
|
|||||||
return at.executeCloseLongWithRecord(decision, actionRecord)
|
return at.executeCloseLongWithRecord(decision, actionRecord)
|
||||||
case "close_short":
|
case "close_short":
|
||||||
return at.executeCloseShortWithRecord(decision, actionRecord)
|
return at.executeCloseShortWithRecord(decision, actionRecord)
|
||||||
|
case "place_buy_limit":
|
||||||
|
return at.executeLimitOrderWithRecord(decision, actionRecord, "BUY")
|
||||||
|
case "place_sell_limit":
|
||||||
|
return at.executeLimitOrderWithRecord(decision, actionRecord, "SELL")
|
||||||
|
case "cancel_order":
|
||||||
|
return at.executeCancelOrderWithRecord(decision, actionRecord)
|
||||||
|
case "cancel_all_orders":
|
||||||
|
return at.executeCancelAllOrdersWithRecord(decision, actionRecord)
|
||||||
case "hold", "wait":
|
case "hold", "wait":
|
||||||
// No execution needed, just record
|
// No execution needed, just record
|
||||||
return nil
|
return nil
|
||||||
@@ -28,6 +38,135 @@ func (at *AutoTrader) executeDecisionWithRecord(decision *kernel.Decision, actio
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (at *AutoTrader) executeLimitOrderWithRecord(decision *kernel.Decision, actionRecord *store.DecisionAction, side string) error {
|
||||||
|
if decision.Price <= 0 {
|
||||||
|
return fmt.Errorf("limit order price must be greater than 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
quantity := decision.Quantity
|
||||||
|
if quantity <= 0 && decision.PositionSizeUSD > 0 {
|
||||||
|
quantity = decision.PositionSizeUSD / decision.Price
|
||||||
|
}
|
||||||
|
if quantity <= 0 {
|
||||||
|
return fmt.Errorf("limit order quantity must be greater than 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
gridTrader, ok := at.trader.(GridTrader)
|
||||||
|
if !ok {
|
||||||
|
gridTrader = NewGridTraderAdapter(at.trader)
|
||||||
|
}
|
||||||
|
|
||||||
|
if duplicated, existingOrderID, err := at.hasSimilarOpenOrder(decision.Symbol, side, decision.Price); err != nil {
|
||||||
|
logger.Warnf(" ⚠️ Failed to check existing open orders: %v", err)
|
||||||
|
} else if duplicated {
|
||||||
|
actionRecord.OrderID = parseOrderIDInt64(existingOrderID)
|
||||||
|
actionRecord.Quantity = quantity
|
||||||
|
actionRecord.Price = decision.Price
|
||||||
|
logger.Infof(" ℹ️ Similar %s limit order already exists for %s at %.4f (orderID=%s), skipping duplicate",
|
||||||
|
side, decision.Symbol, decision.Price, existingOrderID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
leverage := decision.Leverage
|
||||||
|
if leverage <= 0 {
|
||||||
|
leverage = at.defaultLeverageForSymbol(decision.Symbol)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := at.trader.SetMarginMode(decision.Symbol, at.config.IsCrossMargin); err != nil {
|
||||||
|
logger.Infof(" ⚠️ Failed to set margin mode: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := &LimitOrderRequest{
|
||||||
|
Symbol: decision.Symbol,
|
||||||
|
Side: side,
|
||||||
|
Price: decision.Price,
|
||||||
|
Quantity: quantity,
|
||||||
|
Leverage: leverage,
|
||||||
|
PostOnly: true,
|
||||||
|
ReduceOnly: false,
|
||||||
|
ClientID: fmt.Sprintf("ai-limit-%d", time.Now().UnixNano()%1000000000),
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := gridTrader.PlaceLimitOrder(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
actionRecord.OrderID = parseOrderIDInt64(result.OrderID)
|
||||||
|
actionRecord.Quantity = result.Quantity
|
||||||
|
actionRecord.Price = result.Price
|
||||||
|
actionRecord.Leverage = leverage
|
||||||
|
|
||||||
|
logger.Infof(" ✓ Limit order placed: %s %s %.6f @ %.4f orderID=%s",
|
||||||
|
decision.Symbol, side, result.Quantity, result.Price, result.OrderID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (at *AutoTrader) executeCancelOrderWithRecord(decision *kernel.Decision, actionRecord *store.DecisionAction) error {
|
||||||
|
gridTrader, ok := at.trader.(GridTrader)
|
||||||
|
if !ok {
|
||||||
|
gridTrader = NewGridTraderAdapter(at.trader)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := gridTrader.CancelOrder(decision.Symbol, decision.OrderID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
actionRecord.OrderID = parseOrderIDInt64(decision.OrderID)
|
||||||
|
logger.Infof(" ✓ Cancelled order: %s %s", decision.Symbol, decision.OrderID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (at *AutoTrader) executeCancelAllOrdersWithRecord(decision *kernel.Decision, actionRecord *store.DecisionAction) error {
|
||||||
|
if err := at.trader.CancelAllOrders(decision.Symbol); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Infof(" ✓ Cancelled all open orders for %s", decision.Symbol)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (at *AutoTrader) hasSimilarOpenOrder(symbol, side string, price float64) (bool, string, error) {
|
||||||
|
orders, err := at.trader.GetOpenOrders(symbol)
|
||||||
|
if err != nil {
|
||||||
|
return false, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
tolerance := price * 0.001
|
||||||
|
if tolerance < 0.01 {
|
||||||
|
tolerance = 0.01
|
||||||
|
}
|
||||||
|
for _, order := range orders {
|
||||||
|
if order.Side == side && math.Abs(order.Price-price) <= tolerance {
|
||||||
|
return true, order.OrderID, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (at *AutoTrader) defaultLeverageForSymbol(symbol string) int {
|
||||||
|
if at.config.StrategyConfig == nil {
|
||||||
|
return 3
|
||||||
|
}
|
||||||
|
risk := at.config.StrategyConfig.RiskControl
|
||||||
|
if symbol == "BTCUSDT" || symbol == "ETHUSDT" {
|
||||||
|
if risk.BTCETHMaxLeverage > 0 {
|
||||||
|
return risk.BTCETHMaxLeverage
|
||||||
|
}
|
||||||
|
return 3
|
||||||
|
}
|
||||||
|
if risk.AltcoinMaxLeverage > 0 {
|
||||||
|
return risk.AltcoinMaxLeverage
|
||||||
|
}
|
||||||
|
return 3
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseOrderIDInt64(orderID string) int64 {
|
||||||
|
id, _ := strconv.ParseInt(orderID, 10, 64)
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
// executeOpenLongWithRecord executes open long position and records detailed information
|
// executeOpenLongWithRecord executes open long position and records detailed information
|
||||||
func (at *AutoTrader) executeOpenLongWithRecord(decision *kernel.Decision, actionRecord *store.DecisionAction) error {
|
func (at *AutoTrader) executeOpenLongWithRecord(decision *kernel.Decision, actionRecord *store.DecisionAction) error {
|
||||||
logger.Infof(" 📈 Open long: %s", decision.Symbol)
|
logger.Infof(" 📈 Open long: %s", decision.Symbol)
|
||||||
|
|||||||
@@ -526,6 +526,8 @@ func (t *HyperliquidTrader) GetTrades(startTime time.Time, limit int) ([]types.T
|
|||||||
|
|
||||||
// GetOpenOrders gets all open/pending orders for a symbol
|
// GetOpenOrders gets all open/pending orders for a symbol
|
||||||
func (t *HyperliquidTrader) GetOpenOrders(symbol string) ([]types.OpenOrder, error) {
|
func (t *HyperliquidTrader) GetOpenOrders(symbol string) ([]types.OpenOrder, error) {
|
||||||
|
coin := convertSymbolToHyperliquid(symbol)
|
||||||
|
|
||||||
openOrders, err := t.exchange.Info().OpenOrders(t.ctx, t.walletAddr)
|
openOrders, err := t.exchange.Info().OpenOrders(t.ctx, t.walletAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get open orders: %w", err)
|
return nil, fmt.Errorf("failed to get open orders: %w", err)
|
||||||
@@ -533,7 +535,7 @@ func (t *HyperliquidTrader) GetOpenOrders(symbol string) ([]types.OpenOrder, err
|
|||||||
|
|
||||||
var result []types.OpenOrder
|
var result []types.OpenOrder
|
||||||
for _, order := range openOrders {
|
for _, order := range openOrders {
|
||||||
if order.Coin != symbol {
|
if order.Coin != coin {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -544,7 +546,7 @@ func (t *HyperliquidTrader) GetOpenOrders(symbol string) ([]types.OpenOrder, err
|
|||||||
|
|
||||||
result = append(result, types.OpenOrder{
|
result = append(result, types.OpenOrder{
|
||||||
OrderID: fmt.Sprintf("%d", order.Oid),
|
OrderID: fmt.Sprintf("%d", order.Oid),
|
||||||
Symbol: order.Coin,
|
Symbol: symbol,
|
||||||
Side: side,
|
Side: side,
|
||||||
PositionSide: "",
|
PositionSide: "",
|
||||||
Type: "LIMIT",
|
Type: "LIMIT",
|
||||||
|
|||||||
@@ -995,10 +995,23 @@ func (t *HyperliquidTrader) SetTakeProfit(symbol string, positionSide string, qu
|
|||||||
// PlaceLimitOrder places a limit order for grid trading
|
// PlaceLimitOrder places a limit order for grid trading
|
||||||
// Implements GridTrader interface
|
// Implements GridTrader interface
|
||||||
func (t *HyperliquidTrader) PlaceLimitOrder(req *types.LimitOrderRequest) (*types.LimitOrderResult, error) {
|
func (t *HyperliquidTrader) PlaceLimitOrder(req *types.LimitOrderRequest) (*types.LimitOrderResult, error) {
|
||||||
|
if req == nil {
|
||||||
|
return nil, fmt.Errorf("limit order request is nil")
|
||||||
|
}
|
||||||
|
if req.Price <= 0 {
|
||||||
|
return nil, fmt.Errorf("limit order price must be greater than 0")
|
||||||
|
}
|
||||||
|
if req.Quantity <= 0 {
|
||||||
|
return nil, fmt.Errorf("limit order quantity must be greater than 0")
|
||||||
|
}
|
||||||
|
|
||||||
coin := convertSymbolToHyperliquid(req.Symbol)
|
coin := convertSymbolToHyperliquid(req.Symbol)
|
||||||
|
|
||||||
// Set leverage if specified and not xyz dex
|
// Set leverage if specified and not xyz dex
|
||||||
isXyz := strings.HasPrefix(coin, "xyz:")
|
isXyz := strings.HasPrefix(coin, "xyz:")
|
||||||
|
if isXyz {
|
||||||
|
return nil, fmt.Errorf("hyperliquid xyz dex limit orders are not supported yet")
|
||||||
|
}
|
||||||
if req.Leverage > 0 && !isXyz {
|
if req.Leverage > 0 && !isXyz {
|
||||||
if err := t.SetLeverage(req.Symbol, req.Leverage); err != nil {
|
if err := t.SetLeverage(req.Symbol, req.Leverage); err != nil {
|
||||||
logger.Warnf("[Hyperliquid] Failed to set leverage: %v", err)
|
logger.Warnf("[Hyperliquid] Failed to set leverage: %v", err)
|
||||||
@@ -1013,6 +1026,10 @@ func (t *HyperliquidTrader) PlaceLimitOrder(req *types.LimitOrderRequest) (*type
|
|||||||
|
|
||||||
// Determine if buy or sell
|
// Determine if buy or sell
|
||||||
isBuy := req.Side == "BUY"
|
isBuy := req.Side == "BUY"
|
||||||
|
tif := hyperliquid.TifGtc
|
||||||
|
if req.PostOnly {
|
||||||
|
tif = hyperliquid.TifAlo
|
||||||
|
}
|
||||||
|
|
||||||
logger.Infof("[Hyperliquid] PlaceLimitOrder: %s %s @ %.4f, qty=%.4f", coin, req.Side, roundedPrice, roundedQuantity)
|
logger.Infof("[Hyperliquid] PlaceLimitOrder: %s %s @ %.4f, qty=%.4f", coin, req.Side, roundedPrice, roundedQuantity)
|
||||||
|
|
||||||
@@ -1023,21 +1040,21 @@ func (t *HyperliquidTrader) PlaceLimitOrder(req *types.LimitOrderRequest) (*type
|
|||||||
Price: roundedPrice,
|
Price: roundedPrice,
|
||||||
OrderType: hyperliquid.OrderType{
|
OrderType: hyperliquid.OrderType{
|
||||||
Limit: &hyperliquid.LimitOrderType{
|
Limit: &hyperliquid.LimitOrderType{
|
||||||
Tif: hyperliquid.TifGtc, // Good Till Cancel for grid orders
|
Tif: tif,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ReduceOnly: req.ReduceOnly,
|
ReduceOnly: req.ReduceOnly,
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := t.exchange.Order(t.ctx, order, defaultBuilder)
|
status, err := t.exchange.Order(t.ctx, order, defaultBuilder)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to place limit order: %w", err)
|
return nil, fmt.Errorf("failed to place limit order: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: Hyperliquid's Order response doesn't return the order ID directly
|
orderID, statusText, err := parseHyperliquidOrderStatus(status)
|
||||||
// We would need to query open orders to get it, but for grid trading
|
if err != nil {
|
||||||
// we can track orders by price level instead
|
return nil, err
|
||||||
orderID := fmt.Sprintf("%d", time.Now().UnixNano())
|
}
|
||||||
|
|
||||||
logger.Infof("✓ [Hyperliquid] Limit order placed: %s %s @ %.4f",
|
logger.Infof("✓ [Hyperliquid] Limit order placed: %s %s @ %.4f",
|
||||||
coin, req.Side, roundedPrice)
|
coin, req.Side, roundedPrice)
|
||||||
@@ -1050,10 +1067,23 @@ func (t *HyperliquidTrader) PlaceLimitOrder(req *types.LimitOrderRequest) (*type
|
|||||||
PositionSide: req.PositionSide,
|
PositionSide: req.PositionSide,
|
||||||
Price: roundedPrice,
|
Price: roundedPrice,
|
||||||
Quantity: roundedQuantity,
|
Quantity: roundedQuantity,
|
||||||
Status: "NEW",
|
Status: statusText,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseHyperliquidOrderStatus(status hyperliquid.OrderStatus) (orderID string, statusText string, err error) {
|
||||||
|
if status.Resting != nil {
|
||||||
|
return fmt.Sprintf("%d", status.Resting.Oid), "NEW", nil
|
||||||
|
}
|
||||||
|
if status.Filled != nil {
|
||||||
|
return fmt.Sprintf("%d", status.Filled.Oid), "FILLED", nil
|
||||||
|
}
|
||||||
|
if status.Error != nil {
|
||||||
|
return "", "", fmt.Errorf("hyperliquid order rejected: %s", *status.Error)
|
||||||
|
}
|
||||||
|
return "", "", fmt.Errorf("hyperliquid order response did not include order status")
|
||||||
|
}
|
||||||
|
|
||||||
// CancelOrder cancels a specific order by ID
|
// CancelOrder cancels a specific order by ID
|
||||||
// Implements GridTrader interface
|
// Implements GridTrader interface
|
||||||
func (t *HyperliquidTrader) CancelOrder(symbol, orderID string) error {
|
func (t *HyperliquidTrader) CancelOrder(symbol, orderID string) error {
|
||||||
|
|||||||
@@ -54,6 +54,10 @@ export const translations = {
|
|||||||
short: 'SHORT',
|
short: 'SHORT',
|
||||||
noPositions: 'No Positions',
|
noPositions: 'No Positions',
|
||||||
noActivePositions: 'No active trading positions',
|
noActivePositions: 'No active trading positions',
|
||||||
|
currentOpenOrders: 'Current Open Orders',
|
||||||
|
pending: 'Pending',
|
||||||
|
noOpenOrders: 'No Open Orders',
|
||||||
|
noPendingOrders: 'No pending exchange orders',
|
||||||
|
|
||||||
// Recent Decisions
|
// Recent Decisions
|
||||||
recentDecisions: 'Recent Decisions',
|
recentDecisions: 'Recent Decisions',
|
||||||
@@ -983,7 +987,10 @@ export const translations = {
|
|||||||
entry: 'Entry',
|
entry: 'Entry',
|
||||||
exit: 'Exit',
|
exit: 'Exit',
|
||||||
qty: 'Qty',
|
qty: 'Qty',
|
||||||
|
type: 'Type',
|
||||||
|
price: 'Price',
|
||||||
value: 'Value',
|
value: 'Value',
|
||||||
|
orderId: 'Order ID',
|
||||||
lev: 'Lev',
|
lev: 'Lev',
|
||||||
pnl: 'P&L',
|
pnl: 'P&L',
|
||||||
duration: 'Duration',
|
duration: 'Duration',
|
||||||
@@ -1173,6 +1180,7 @@ export const translations = {
|
|||||||
perPage: 'Per page',
|
perPage: 'Per page',
|
||||||
accountFetchFailed: 'DATA_FETCH::FAILED — Account data unavailable, check connection',
|
accountFetchFailed: 'DATA_FETCH::FAILED — Account data unavailable, check connection',
|
||||||
positionsFetchFailed: 'Position data unavailable',
|
positionsFetchFailed: 'Position data unavailable',
|
||||||
|
openOrdersFetchFailed: 'Open order data unavailable',
|
||||||
decisionsFetchFailed: 'Decision data unavailable',
|
decisionsFetchFailed: 'Decision data unavailable',
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1416,6 +1424,10 @@ export const translations = {
|
|||||||
short: '空头',
|
short: '空头',
|
||||||
noPositions: '无持仓',
|
noPositions: '无持仓',
|
||||||
noActivePositions: '当前没有活跃的交易持仓',
|
noActivePositions: '当前没有活跃的交易持仓',
|
||||||
|
currentOpenOrders: '当前挂单',
|
||||||
|
pending: '挂单',
|
||||||
|
noOpenOrders: '无挂单',
|
||||||
|
noPendingOrders: '当前没有未成交挂单',
|
||||||
|
|
||||||
// Recent Decisions
|
// Recent Decisions
|
||||||
recentDecisions: '最近决策',
|
recentDecisions: '最近决策',
|
||||||
@@ -2470,7 +2482,10 @@ export const translations = {
|
|||||||
entry: '入场价',
|
entry: '入场价',
|
||||||
mark: '标记价',
|
mark: '标记价',
|
||||||
qty: '数量',
|
qty: '数量',
|
||||||
|
type: '类型',
|
||||||
|
price: '价格',
|
||||||
value: '价值',
|
value: '价值',
|
||||||
|
orderId: '订单ID',
|
||||||
lev: '杠杆',
|
lev: '杠杆',
|
||||||
uPnL: '未实现盈亏',
|
uPnL: '未实现盈亏',
|
||||||
liq: '强平价',
|
liq: '强平价',
|
||||||
@@ -2480,6 +2495,7 @@ export const translations = {
|
|||||||
perPage: '每页',
|
perPage: '每页',
|
||||||
accountFetchFailed: 'DATA_FETCH::FAILED — 账户数据请求失败,请检查连接',
|
accountFetchFailed: 'DATA_FETCH::FAILED — 账户数据请求失败,请检查连接',
|
||||||
positionsFetchFailed: '持仓数据请求失败',
|
positionsFetchFailed: '持仓数据请求失败',
|
||||||
|
openOrdersFetchFailed: '挂单数据请求失败',
|
||||||
decisionsFetchFailed: '决策记录请求失败',
|
decisionsFetchFailed: '决策记录请求失败',
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -2718,6 +2734,10 @@ export const translations = {
|
|||||||
short: 'SHORT',
|
short: 'SHORT',
|
||||||
noPositions: 'Tidak Ada Posisi',
|
noPositions: 'Tidak Ada Posisi',
|
||||||
noActivePositions: 'Tidak ada posisi trading yang aktif',
|
noActivePositions: 'Tidak ada posisi trading yang aktif',
|
||||||
|
currentOpenOrders: 'Order Terbuka Saat Ini',
|
||||||
|
pending: 'Pending',
|
||||||
|
noOpenOrders: 'Tidak Ada Order Terbuka',
|
||||||
|
noPendingOrders: 'Tidak ada order exchange yang tertunda',
|
||||||
|
|
||||||
// Recent Decisions
|
// Recent Decisions
|
||||||
recentDecisions: 'Keputusan Terbaru',
|
recentDecisions: 'Keputusan Terbaru',
|
||||||
@@ -3580,7 +3600,10 @@ export const translations = {
|
|||||||
entry: 'Entry',
|
entry: 'Entry',
|
||||||
mark: 'Mark',
|
mark: 'Mark',
|
||||||
qty: 'Qty',
|
qty: 'Qty',
|
||||||
|
type: 'Tipe',
|
||||||
|
price: 'Harga',
|
||||||
value: 'Nilai',
|
value: 'Nilai',
|
||||||
|
orderId: 'ID Order',
|
||||||
lev: 'Lev.',
|
lev: 'Lev.',
|
||||||
uPnL: 'uPnL',
|
uPnL: 'uPnL',
|
||||||
liq: 'Liq.',
|
liq: 'Liq.',
|
||||||
@@ -3590,6 +3613,7 @@ export const translations = {
|
|||||||
perPage: 'Per halaman',
|
perPage: 'Per halaman',
|
||||||
accountFetchFailed: 'DATA_FETCH::FAILED — Data akun tidak tersedia, periksa koneksi',
|
accountFetchFailed: 'DATA_FETCH::FAILED — Data akun tidak tersedia, periksa koneksi',
|
||||||
positionsFetchFailed: 'Data posisi tidak tersedia',
|
positionsFetchFailed: 'Data posisi tidak tersedia',
|
||||||
|
openOrdersFetchFailed: 'Data order terbuka tidak tersedia',
|
||||||
decisionsFetchFailed: 'Data keputusan tidak tersedia',
|
decisionsFetchFailed: 'Data keputusan tidak tersedia',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type {
|
|||||||
SystemStatus,
|
SystemStatus,
|
||||||
AccountInfo,
|
AccountInfo,
|
||||||
Position,
|
Position,
|
||||||
|
OpenOrder,
|
||||||
DecisionRecord,
|
DecisionRecord,
|
||||||
Statistics,
|
Statistics,
|
||||||
CompetitionData,
|
CompetitionData,
|
||||||
@@ -40,6 +41,44 @@ export const dataApi = {
|
|||||||
return result.data!
|
return result.data!
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async getOpenOrders(
|
||||||
|
traderId: string,
|
||||||
|
symbol: string,
|
||||||
|
silent?: boolean
|
||||||
|
): Promise<OpenOrder[]> {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.append('trader_id', traderId)
|
||||||
|
params.append('symbol', symbol)
|
||||||
|
|
||||||
|
const result = await httpClient.request<OpenOrder[]>(
|
||||||
|
`${API_BASE}/open-orders?${params}`,
|
||||||
|
{ silent }
|
||||||
|
)
|
||||||
|
if (!result.success) throw new Error('Failed to fetch open orders')
|
||||||
|
return Array.isArray(result.data) ? result.data : []
|
||||||
|
},
|
||||||
|
|
||||||
|
async getOpenOrdersForSymbols(
|
||||||
|
traderId: string,
|
||||||
|
symbols: string[],
|
||||||
|
silent?: boolean
|
||||||
|
): Promise<OpenOrder[]> {
|
||||||
|
const uniqueSymbols = Array.from(new Set(symbols.filter(Boolean)))
|
||||||
|
if (uniqueSymbols.length === 0) return []
|
||||||
|
|
||||||
|
const results = await Promise.allSettled(
|
||||||
|
uniqueSymbols.map((symbol) => dataApi.getOpenOrders(traderId, symbol, silent))
|
||||||
|
)
|
||||||
|
|
||||||
|
const orders: OpenOrder[] = []
|
||||||
|
results.forEach((result) => {
|
||||||
|
if (result.status === 'fulfilled') {
|
||||||
|
orders.push(...result.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return orders
|
||||||
|
},
|
||||||
|
|
||||||
async getDecisions(traderId?: string): Promise<DecisionRecord[]> {
|
async getDecisions(traderId?: string): Promise<DecisionRecord[]> {
|
||||||
const url = traderId
|
const url = traderId
|
||||||
? `${API_BASE}/decisions?trader_id=${traderId}`
|
? `${API_BASE}/decisions?trader_id=${traderId}`
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useState, useRef } from 'react'
|
import { useEffect, useMemo, useState, useRef } from 'react'
|
||||||
import { mutate } from 'swr'
|
import { mutate } from 'swr'
|
||||||
|
import useSWR from 'swr'
|
||||||
import { api } from '../lib/api'
|
import { api } from '../lib/api'
|
||||||
import { ChartTabs } from '../components/charts/ChartTabs'
|
import { ChartTabs } from '../components/charts/ChartTabs'
|
||||||
import { DecisionCard } from '../components/trader/DecisionCard'
|
import { DecisionCard } from '../components/trader/DecisionCard'
|
||||||
@@ -16,6 +17,7 @@ import type {
|
|||||||
SystemStatus,
|
SystemStatus,
|
||||||
AccountInfo,
|
AccountInfo,
|
||||||
Position,
|
Position,
|
||||||
|
OpenOrder,
|
||||||
DecisionRecord,
|
DecisionRecord,
|
||||||
Statistics,
|
Statistics,
|
||||||
TraderInfo,
|
TraderInfo,
|
||||||
@@ -143,6 +145,41 @@ export function TraderDashboardPage({
|
|||||||
const [showWalletAddress, setShowWalletAddress] = useState<boolean>(false)
|
const [showWalletAddress, setShowWalletAddress] = useState<boolean>(false)
|
||||||
const [copiedAddress, setCopiedAddress] = useState<boolean>(false)
|
const [copiedAddress, setCopiedAddress] = useState<boolean>(false)
|
||||||
|
|
||||||
|
const openOrderSymbols = useMemo(() => {
|
||||||
|
const symbols = new Set<string>()
|
||||||
|
positions?.forEach((pos) => {
|
||||||
|
if (pos.symbol) symbols.add(pos.symbol)
|
||||||
|
})
|
||||||
|
decisions?.forEach((record) => {
|
||||||
|
record.decisions?.forEach((decision) => {
|
||||||
|
if (decision.symbol) symbols.add(decision.symbol)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if (status?.grid_symbol) symbols.add(status.grid_symbol)
|
||||||
|
if (selectedChartSymbol) symbols.add(selectedChartSymbol)
|
||||||
|
return Array.from(symbols)
|
||||||
|
}, [decisions, positions, selectedChartSymbol, status?.grid_symbol])
|
||||||
|
const openOrderSymbolsKey = useMemo(
|
||||||
|
() => [...openOrderSymbols].sort().join(','),
|
||||||
|
[openOrderSymbols]
|
||||||
|
)
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: openOrders,
|
||||||
|
error: openOrdersError,
|
||||||
|
isLoading: openOrdersLoading,
|
||||||
|
} = useSWR<OpenOrder[]>(
|
||||||
|
selectedTraderId && openOrderSymbols.length > 0
|
||||||
|
? `open-orders-${selectedTraderId}-${openOrderSymbolsKey}`
|
||||||
|
: null,
|
||||||
|
() => api.getOpenOrdersForSymbols(selectedTraderId!, openOrderSymbols, true),
|
||||||
|
{
|
||||||
|
refreshInterval: 15000,
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
dedupingInterval: 10000,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// Current positions pagination
|
// Current positions pagination
|
||||||
const [positionsPageSize, setPositionsPageSize] = useState<number>(20)
|
const [positionsPageSize, setPositionsPageSize] = useState<number>(20)
|
||||||
const [positionsCurrentPage, setPositionsCurrentPage] = useState<number>(1)
|
const [positionsCurrentPage, setPositionsCurrentPage] = useState<number>(1)
|
||||||
@@ -740,6 +777,109 @@ export function TraderDashboardPage({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Current Open Orders */}
|
||||||
|
<div
|
||||||
|
className="nofx-glass p-6 animate-slide-in relative overflow-hidden group"
|
||||||
|
style={{ animationDelay: '0.18s' }}
|
||||||
|
>
|
||||||
|
<div className="absolute top-0 right-0 p-3 opacity-10 group-hover:opacity-20 transition-opacity">
|
||||||
|
<div className="w-24 h-24 rounded-full bg-nofx-gold blur-3xl" />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between mb-5 relative z-10">
|
||||||
|
<h2 className="text-lg font-bold flex items-center gap-2 text-nofx-text-main uppercase tracking-wide">
|
||||||
|
<span className="text-nofx-gold">◇</span> {t('currentOpenOrders', language)}
|
||||||
|
</h2>
|
||||||
|
{openOrders && openOrders.length > 0 && (
|
||||||
|
<div className="text-xs px-2 py-1 rounded bg-nofx-gold/10 text-nofx-gold border border-nofx-gold/20 font-mono">
|
||||||
|
{openOrders.length} {t('pending', language)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{openOrders && openOrders.length > 0 ? (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full text-xs">
|
||||||
|
<thead className="text-left border-b border-white/5">
|
||||||
|
<tr>
|
||||||
|
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-left">{t('symbol', language)}</th>
|
||||||
|
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-center">{t('side', language)}</th>
|
||||||
|
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-center">{t('traderDashboard.type', language)}</th>
|
||||||
|
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right">{t('traderDashboard.price', language)}</th>
|
||||||
|
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right">{t('traderDashboard.qty', language)}</th>
|
||||||
|
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-right hidden md:table-cell">{t('traderDashboard.value', language)}</th>
|
||||||
|
<th className="px-1 pb-3 font-semibold text-nofx-text-muted whitespace-nowrap text-left hidden md:table-cell">{t('traderDashboard.orderId', language)}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{openOrders.map((order) => {
|
||||||
|
const orderPrice = order.price > 0 ? order.price : order.stop_price
|
||||||
|
const isBuy = order.side?.toUpperCase() === 'BUY'
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
key={`${order.symbol}-${order.order_id}`}
|
||||||
|
className="border-b border-white/5 last:border-0 transition-all hover:bg-white/5 cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedChartSymbol(order.symbol)
|
||||||
|
setChartUpdateKey(Date.now())
|
||||||
|
chartSectionRef.current?.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'start',
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<td className="px-1 py-3 font-mono font-semibold whitespace-nowrap text-left text-nofx-text-main">
|
||||||
|
{order.symbol}
|
||||||
|
</td>
|
||||||
|
<td className="px-1 py-3 whitespace-nowrap text-center">
|
||||||
|
<span
|
||||||
|
className={`px-1.5 py-0.5 rounded text-[10px] font-bold uppercase tracking-wider ${isBuy ? 'bg-nofx-green/10 text-nofx-green' : 'bg-nofx-red/10 text-nofx-red'}`}
|
||||||
|
>
|
||||||
|
{isBuy ? 'BUY' : 'SELL'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-1 py-3 whitespace-nowrap text-center">
|
||||||
|
<span className="px-1.5 py-0.5 rounded text-[10px] font-mono text-nofx-gold bg-nofx-gold/10 border border-nofx-gold/10">
|
||||||
|
{order.type || '--'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-1 py-3 font-mono whitespace-nowrap text-right text-nofx-text-main">
|
||||||
|
{orderPrice > 0 ? formatPrice(orderPrice) : '--'}
|
||||||
|
</td>
|
||||||
|
<td className="px-1 py-3 font-mono whitespace-nowrap text-right text-nofx-text-main">
|
||||||
|
{formatQuantity(order.quantity)}
|
||||||
|
</td>
|
||||||
|
<td className="px-1 py-3 font-mono whitespace-nowrap text-right text-nofx-text-main hidden md:table-cell">
|
||||||
|
{orderPrice > 0 ? (orderPrice * order.quantity).toFixed(2) : '--'}
|
||||||
|
</td>
|
||||||
|
<td className="px-1 py-3 font-mono whitespace-nowrap text-left text-nofx-text-muted hidden md:table-cell">
|
||||||
|
{order.order_id}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
) : openOrdersError ? (
|
||||||
|
<div className="text-center py-12 text-nofx-text-muted opacity-60">
|
||||||
|
<div className="text-4xl mb-4">⚠️</div>
|
||||||
|
<div className="text-lg font-semibold mb-2">{t('traderDashboard.openOrdersFetchFailed', language)}</div>
|
||||||
|
</div>
|
||||||
|
) : openOrdersLoading ? (
|
||||||
|
<div className="py-12 space-y-3">
|
||||||
|
{[0, 1, 2].map((i) => (
|
||||||
|
<div key={i} className="h-8 rounded bg-white/5 animate-pulse" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-12 text-nofx-text-muted opacity-60">
|
||||||
|
<div className="text-5xl mb-4 opacity-40 grayscale">⌁</div>
|
||||||
|
<div className="text-lg font-semibold mb-2">{t('noOpenOrders', language)}</div>
|
||||||
|
<div className="text-sm">{t('noPendingOrders', language)}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Column: Recent Decisions */}
|
{/* Right Column: Recent Decisions */}
|
||||||
|
|||||||
@@ -42,6 +42,18 @@ export interface Position {
|
|||||||
margin_used: number
|
margin_used: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OpenOrder {
|
||||||
|
order_id: string
|
||||||
|
symbol: string
|
||||||
|
side: string
|
||||||
|
position_side?: string
|
||||||
|
type: string
|
||||||
|
price: number
|
||||||
|
stop_price: number
|
||||||
|
quantity: number
|
||||||
|
status: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface DecisionAction {
|
export interface DecisionAction {
|
||||||
action: string
|
action: string
|
||||||
symbol: string
|
symbol: string
|
||||||
|
|||||||
Reference in New Issue
Block a user