1
This commit is contained in:
parent
6b7011becb
commit
b8c75bd0ef
@ -243,6 +243,37 @@ def _attach_paper_trade(item):
|
||||
return item
|
||||
|
||||
|
||||
def _attach_paper_order(item):
|
||||
order_id = item.get("paper_order_id")
|
||||
if not order_id:
|
||||
item["paper_order"] = None
|
||||
item["paper_order_status"] = ""
|
||||
return item
|
||||
order = {
|
||||
"id": order_id,
|
||||
"recommendation_id": item.get("paper_order_recommendation_id") or item.get("id"),
|
||||
"symbol": item.get("paper_order_symbol") or item.get("symbol"),
|
||||
"side": item.get("paper_order_side") or "long",
|
||||
"order_type": item.get("paper_order_type") or "limit",
|
||||
"status": item.get("paper_order_status_raw") or "",
|
||||
"target_price": item.get("paper_order_target_price") or 0,
|
||||
"current_price_at_create": item.get("paper_order_current_price_at_create") or 0,
|
||||
"fill_price": item.get("paper_order_fill_price") or 0,
|
||||
"stop_loss": item.get("paper_order_stop_loss") or 0,
|
||||
"tp1": item.get("paper_order_tp1") or 0,
|
||||
"tp2": item.get("paper_order_tp2") or 0,
|
||||
"created_at": item.get("paper_order_created_at") or "",
|
||||
"updated_at": item.get("paper_order_updated_at") or "",
|
||||
"expires_at": item.get("paper_order_expires_at") or "",
|
||||
"filled_at": item.get("paper_order_filled_at") or "",
|
||||
"canceled_at": item.get("paper_order_canceled_at") or "",
|
||||
"cancel_reason": item.get("paper_order_cancel_reason") or "",
|
||||
}
|
||||
item["paper_order"] = order
|
||||
item["paper_order_status"] = order["status"]
|
||||
return item
|
||||
|
||||
|
||||
def get_all_recommendations(limit=50, decision_only=False, version="", offset=0, with_meta=False, archive_filter=""):
|
||||
"""获取推荐列表。"""
|
||||
conn = get_conn()
|
||||
@ -379,10 +410,29 @@ def get_all_recommendations(limit=50, decision_only=False, version="", offset=0,
|
||||
pt.realized_pnl_pct AS paper_realized_pnl_pct,
|
||||
pt.realized_pnl_usdt AS paper_realized_pnl_usdt,
|
||||
pt.exit_reason AS paper_exit_reason,
|
||||
pt.updated_at AS paper_updated_at
|
||||
pt.updated_at AS paper_updated_at,
|
||||
po.id AS paper_order_id,
|
||||
po.recommendation_id AS paper_order_recommendation_id,
|
||||
po.symbol AS paper_order_symbol,
|
||||
po.side AS paper_order_side,
|
||||
po.order_type AS paper_order_type,
|
||||
po.status AS paper_order_status_raw,
|
||||
po.target_price AS paper_order_target_price,
|
||||
po.current_price_at_create AS paper_order_current_price_at_create,
|
||||
po.fill_price AS paper_order_fill_price,
|
||||
po.stop_loss AS paper_order_stop_loss,
|
||||
po.tp1 AS paper_order_tp1,
|
||||
po.tp2 AS paper_order_tp2,
|
||||
po.created_at AS paper_order_created_at,
|
||||
po.updated_at AS paper_order_updated_at,
|
||||
po.expires_at AS paper_order_expires_at,
|
||||
po.filled_at AS paper_order_filled_at,
|
||||
po.canceled_at AS paper_order_canceled_at,
|
||||
po.cancel_reason AS paper_order_cancel_reason
|
||||
FROM recommendation r
|
||||
LEFT JOIN latest_price_cache lpc ON lpc.symbol = r.symbol
|
||||
LEFT JOIN paper_trades pt ON pt.recommendation_id = r.id
|
||||
LEFT JOIN paper_orders po ON po.recommendation_id = r.id
|
||||
JOIN (
|
||||
SELECT symbol, MAX(id) AS max_id
|
||||
FROM recommendation
|
||||
@ -420,6 +470,7 @@ def get_all_recommendations(limit=50, decision_only=False, version="", offset=0,
|
||||
item["recommendation_result_label"] = rec_result_label
|
||||
_derive_execution_fields(item)
|
||||
_attach_paper_trade(item)
|
||||
_attach_paper_order(item)
|
||||
result.append(item)
|
||||
|
||||
if not with_meta:
|
||||
|
||||
@ -1355,7 +1355,29 @@ def sync_recommendation(rec: dict, current_price: float, event_time: str = "") -
|
||||
conn.commit()
|
||||
return result
|
||||
return {"skipped": True, "reason": "not_buy_now"}
|
||||
pending_order = conn.execute(
|
||||
"SELECT * FROM paper_orders WHERE recommendation_id=%s AND status='pending'",
|
||||
(rec_id,),
|
||||
).fetchone()
|
||||
if pending_order:
|
||||
conn.execute(
|
||||
"""
|
||||
UPDATE paper_orders
|
||||
SET status='canceled',
|
||||
cancel_reason='upgraded_to_buy_now',
|
||||
canceled_at=%s,
|
||||
updated_at=%s
|
||||
WHERE recommendation_id=%s AND status='pending'
|
||||
""",
|
||||
(event_time, event_time, rec_id),
|
||||
)
|
||||
result = _open_trade(conn, rec, current_price, event_time, config=cfg)
|
||||
if pending_order:
|
||||
result["paper_order"] = {
|
||||
"canceled": True,
|
||||
"order_id": pending_order["id"],
|
||||
"cancel_reason": "upgraded_to_buy_now",
|
||||
}
|
||||
conn.commit()
|
||||
return result
|
||||
except Exception:
|
||||
|
||||
@ -14,6 +14,37 @@ from app.db.tracking_queries import update_recommendation_tracking
|
||||
from app.services.llm_insights import attach_recommendation_insights
|
||||
|
||||
|
||||
def _attach_paper_order(item: dict) -> dict:
|
||||
order_id = item.get("paper_order_id")
|
||||
if not order_id:
|
||||
item["paper_order"] = None
|
||||
item["paper_order_status"] = ""
|
||||
return item
|
||||
order = {
|
||||
"id": order_id,
|
||||
"recommendation_id": item.get("paper_order_recommendation_id") or item.get("id"),
|
||||
"symbol": item.get("paper_order_symbol") or item.get("symbol"),
|
||||
"side": item.get("paper_order_side") or "long",
|
||||
"order_type": item.get("paper_order_type") or "limit",
|
||||
"status": item.get("paper_order_status_raw") or "",
|
||||
"target_price": item.get("paper_order_target_price") or 0,
|
||||
"current_price_at_create": item.get("paper_order_current_price_at_create") or 0,
|
||||
"fill_price": item.get("paper_order_fill_price") or 0,
|
||||
"stop_loss": item.get("paper_order_stop_loss") or 0,
|
||||
"tp1": item.get("paper_order_tp1") or 0,
|
||||
"tp2": item.get("paper_order_tp2") or 0,
|
||||
"created_at": item.get("paper_order_created_at") or "",
|
||||
"updated_at": item.get("paper_order_updated_at") or "",
|
||||
"expires_at": item.get("paper_order_expires_at") or "",
|
||||
"filled_at": item.get("paper_order_filled_at") or "",
|
||||
"canceled_at": item.get("paper_order_canceled_at") or "",
|
||||
"cancel_reason": item.get("paper_order_cancel_reason") or "",
|
||||
}
|
||||
item["paper_order"] = order
|
||||
item["paper_order_status"] = order["status"]
|
||||
return item
|
||||
|
||||
|
||||
def get_active_recommendations(actionable_only: bool = False):
|
||||
"""获取所有 active 推荐。"""
|
||||
conn = get_conn()
|
||||
@ -153,9 +184,28 @@ def get_active_recommendations_deduped(
|
||||
f"""
|
||||
SELECT r.*,
|
||||
lpc.price AS latest_cache_price,
|
||||
lpc.updated_at AS latest_cache_updated_at
|
||||
lpc.updated_at AS latest_cache_updated_at,
|
||||
po.id AS paper_order_id,
|
||||
po.recommendation_id AS paper_order_recommendation_id,
|
||||
po.symbol AS paper_order_symbol,
|
||||
po.side AS paper_order_side,
|
||||
po.order_type AS paper_order_type,
|
||||
po.status AS paper_order_status_raw,
|
||||
po.target_price AS paper_order_target_price,
|
||||
po.current_price_at_create AS paper_order_current_price_at_create,
|
||||
po.fill_price AS paper_order_fill_price,
|
||||
po.stop_loss AS paper_order_stop_loss,
|
||||
po.tp1 AS paper_order_tp1,
|
||||
po.tp2 AS paper_order_tp2,
|
||||
po.created_at AS paper_order_created_at,
|
||||
po.updated_at AS paper_order_updated_at,
|
||||
po.expires_at AS paper_order_expires_at,
|
||||
po.filled_at AS paper_order_filled_at,
|
||||
po.canceled_at AS paper_order_canceled_at,
|
||||
po.cancel_reason AS paper_order_cancel_reason
|
||||
FROM recommendation r
|
||||
LEFT JOIN latest_price_cache lpc ON lpc.symbol = r.symbol
|
||||
LEFT JOIN paper_orders po ON po.recommendation_id = r.id
|
||||
JOIN (
|
||||
SELECT symbol, MAX(id) AS max_id
|
||||
FROM recommendation
|
||||
@ -189,6 +239,7 @@ def get_active_recommendations_deduped(
|
||||
item["recommendation_result"] = rec_result
|
||||
item["recommendation_result_label"] = rec_result_label
|
||||
_derive_execution_fields(item)
|
||||
_attach_paper_order(item)
|
||||
|
||||
is_expired = False
|
||||
if hours > 0:
|
||||
|
||||
@ -6,10 +6,13 @@ in the live_trading DB/service layer.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import requests
|
||||
@ -21,13 +24,86 @@ class LiveTradingConfigError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def _cache_dir() -> Path:
|
||||
return Path(os.getenv("ALPHAX_EXCHANGE_CACHE_DIR", "/app/data/exchange_cache"))
|
||||
|
||||
|
||||
def _markets_cache_ttl_seconds() -> int:
|
||||
try:
|
||||
return max(300, int(os.getenv("ALPHAX_EXCHANGE_INFO_CACHE_SECONDS", "86400") or 86400))
|
||||
except Exception:
|
||||
return 86400
|
||||
|
||||
|
||||
def _safe_cache_key(value: str) -> str:
|
||||
return "".join(ch if ch.isalnum() or ch in ("-", "_") else "_" for ch in value)
|
||||
|
||||
|
||||
@dataclass
|
||||
class BinanceLiveClient:
|
||||
exchange: object
|
||||
market_type: str = "um_futures"
|
||||
|
||||
def _markets_cache_path(self) -> Path:
|
||||
sandbox = "sandbox" if bool(getattr(self.exchange, "sandbox", False)) else "live"
|
||||
exchange_id = str(getattr(self.exchange, "id", "binance") or "binance")
|
||||
return _cache_dir() / f"{_safe_cache_key(exchange_id)}_{_safe_cache_key(self.market_type)}_{sandbox}_markets.json"
|
||||
|
||||
def _restore_cached_markets(self, *, allow_stale: bool = False) -> bool:
|
||||
path = self._markets_cache_path()
|
||||
if not path.exists():
|
||||
return False
|
||||
try:
|
||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||
cached_at = float(payload.get("cached_at") or 0)
|
||||
age = time.time() - cached_at
|
||||
if not allow_stale and age > _markets_cache_ttl_seconds():
|
||||
return False
|
||||
markets = payload.get("markets") or {}
|
||||
markets_by_id = payload.get("markets_by_id") or {}
|
||||
symbols = payload.get("symbols") or sorted(markets.keys())
|
||||
ids = payload.get("ids") or sorted(markets_by_id.keys())
|
||||
if not markets:
|
||||
return False
|
||||
self.exchange.markets = markets
|
||||
self.exchange.markets_by_id = markets_by_id
|
||||
self.exchange.symbols = symbols
|
||||
self.exchange.ids = ids
|
||||
self.exchange.markets_loading = None
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _store_markets_cache(self, markets) -> None:
|
||||
if not markets:
|
||||
return
|
||||
path = self._markets_cache_path()
|
||||
try:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
payload = {
|
||||
"cached_at": time.time(),
|
||||
"exchange_id": str(getattr(self.exchange, "id", "binance") or "binance"),
|
||||
"market_type": self.market_type,
|
||||
"symbols": list(getattr(self.exchange, "symbols", []) or sorted(markets.keys())),
|
||||
"ids": list(getattr(self.exchange, "ids", []) or []),
|
||||
"markets": markets,
|
||||
"markets_by_id": getattr(self.exchange, "markets_by_id", {}) or {},
|
||||
}
|
||||
path.write_text(json.dumps(payload, ensure_ascii=False, default=str), encoding="utf-8")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def load_markets(self):
|
||||
return self.exchange.load_markets()
|
||||
if self._restore_cached_markets():
|
||||
return self.exchange.markets
|
||||
try:
|
||||
markets = self.exchange.load_markets()
|
||||
self._store_markets_cache(markets)
|
||||
return markets
|
||||
except Exception:
|
||||
if self._restore_cached_markets(allow_stale=True):
|
||||
return self.exchange.markets
|
||||
raise
|
||||
|
||||
def fetch_balance(self):
|
||||
return self.exchange.fetch_balance()
|
||||
|
||||
@ -62,6 +62,7 @@ from app.core.signal_taxonomy import signal_codes as build_signal_codes
|
||||
|
||||
exchange = ccxt.binance({"enableRateLimit": True})
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
BINANCE_SPOT_BASE_URL = os.getenv("ALPHAX_BINANCE_SPOT_BASE_URL", "https://api.binance.com").rstrip("/")
|
||||
|
||||
# ==================== 排除列表 ====================
|
||||
STABLECOINS = {
|
||||
@ -86,22 +87,51 @@ def get_dynamic_weights():
|
||||
|
||||
# ==================== 工具函数 ====================
|
||||
|
||||
def fetch_all_tickers():
|
||||
tickers = exchange.fetch_tickers()
|
||||
def _fetch_spot_24h_tickers():
|
||||
"""Fetch spot 24h tickers without ccxt market loading.
|
||||
|
||||
ccxt.fetch_tickers() can call Binance futures exchangeInfo as part of
|
||||
load_markets(), which is exactly the endpoint most likely to be IP-banned.
|
||||
The public spot 24h endpoint is enough for our broad universe scan.
|
||||
"""
|
||||
resp = requests.get(f"{BINANCE_SPOT_BASE_URL}/api/v3/ticker/24hr", timeout=15)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
if not isinstance(data, list):
|
||||
return {}
|
||||
tickers = {}
|
||||
for item in data:
|
||||
raw_symbol = str(item.get("symbol") or "")
|
||||
if not raw_symbol.endswith("USDT"):
|
||||
continue
|
||||
base = raw_symbol[:-4]
|
||||
if not base:
|
||||
continue
|
||||
close_time = item.get("closeTime")
|
||||
ticker_dt = ""
|
||||
try:
|
||||
markets = exchange.markets or exchange.load_markets()
|
||||
ticker_dt = datetime.utcfromtimestamp(float(close_time) / 1000).isoformat()
|
||||
except Exception:
|
||||
markets = {}
|
||||
ticker_dt = ""
|
||||
tickers[f"{base}/USDT"] = {
|
||||
"last": float(item.get("lastPrice") or 0),
|
||||
"percentage": float(item.get("priceChangePercent") or 0),
|
||||
"quoteVolume": float(item.get("quoteVolume") or 0),
|
||||
"high": float(item.get("highPrice") or 0),
|
||||
"low": float(item.get("lowPrice") or 0),
|
||||
"datetime": ticker_dt,
|
||||
}
|
||||
return tickers
|
||||
|
||||
|
||||
def fetch_all_tickers():
|
||||
tickers = _fetch_spot_24h_tickers()
|
||||
usdt_pairs = {}
|
||||
universe_exclusions = []
|
||||
for symbol, info in tickers.items():
|
||||
if "/USDT" in symbol:
|
||||
base = symbol.split("/")[0]
|
||||
vol_usd = info.get("quoteVolume", 0) or 0
|
||||
market = markets.get(symbol) or {}
|
||||
if market and (market.get("spot") is False or market.get("active") is False):
|
||||
universe_exclusions.append({"symbol": symbol, "base": base, "price": info.get("last", 0) or 0, "volume_24h": vol_usd, "reason_code": "inactive_market", "reason_label": "非活跃现货交易对"})
|
||||
continue
|
||||
ticker_dt = info.get("datetime")
|
||||
if ticker_dt:
|
||||
try:
|
||||
|
||||
@ -66,14 +66,8 @@ def _market_state(avg_change, advance_decline_ratio, hot_count, crash_count, ben
|
||||
}
|
||||
|
||||
|
||||
def _benchmark_overview():
|
||||
try:
|
||||
tickers = altcoin_screener.exchange.fetch_tickers(["BTC/USDT", "ETH/USDT"])
|
||||
except Exception:
|
||||
try:
|
||||
tickers = altcoin_screener.exchange.fetch_tickers()
|
||||
except Exception:
|
||||
tickers = {}
|
||||
def _benchmark_overview(tickers=None):
|
||||
tickers = tickers or {}
|
||||
result = {}
|
||||
for symbol in ("BTC/USDT", "ETH/USDT"):
|
||||
info = tickers.get(symbol) or {}
|
||||
@ -142,7 +136,7 @@ def _funding_overview(universe_symbols=None):
|
||||
|
||||
def compute_crypto_market_overview():
|
||||
pairs = altcoin_screener.fetch_all_tickers()
|
||||
benchmarks = _benchmark_overview()
|
||||
benchmarks = _benchmark_overview(pairs)
|
||||
items = []
|
||||
for symbol, info in (pairs or {}).items():
|
||||
volume = _safe_float(info.get("volume_24h"))
|
||||
|
||||
@ -209,6 +209,12 @@
|
||||
.regime-reason { color: var(--stone); font-size: 11px; line-height: 1.35; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: right; }
|
||||
.decision-log-brief { border: 1px dashed var(--hairline-strong); border-radius: var(--radius-lg); background: var(--canvas); padding: 8px 10px; color: var(--slate); font-size: 11px; line-height: 1.45; }
|
||||
.decision-log-brief b { color: var(--ink); font-weight: 950; }
|
||||
.paper-order-brief { margin: 0 18px 8px; border: 1px solid rgba(66,98,255,.14); border-radius: var(--radius-lg); background: rgba(66,98,255,.04); padding: 8px 10px; display:flex; align-items:center; justify-content:space-between; gap:10px; color: var(--slate); font-size: 11px; line-height: 1.4; }
|
||||
.paper-order-brief.pending { border-color: rgba(252,185,0,.24); background: var(--yellow-light); }
|
||||
.paper-order-brief.filled { border-color: rgba(0,180,115,.18); background: var(--green-light); }
|
||||
.paper-order-brief.canceled,.paper-order-brief.expired,.paper-order-brief.rejected { border-color: rgba(229,62,62,.18); background: var(--red-light); }
|
||||
.paper-order-title { color: var(--ink); font-size: 12px; font-weight: 950; white-space: nowrap; }
|
||||
.paper-order-meta { color: var(--stone); text-align:right; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||
|
||||
/* ===== K-LINE ===== */
|
||||
.kline-wrap { padding: 0 8px 4px; }
|
||||
@ -480,6 +486,23 @@ var MODULE_LABELS = {
|
||||
tracker: '跟踪',
|
||||
paper_trader: '策略交易'
|
||||
};
|
||||
var PAPER_ORDER_STATUS_LABELS = {
|
||||
pending: '挂单中',
|
||||
filled: '已成交',
|
||||
canceled: '已取消',
|
||||
expired: '已过期',
|
||||
rejected: '已拒绝'
|
||||
};
|
||||
var PAPER_ORDER_REASON_LABELS = {
|
||||
upgraded_to_buy_now: '信号已升级为入场窗口',
|
||||
too_far_from_entry: '距离计划价过远',
|
||||
recommendation_invalid: '信号已失效',
|
||||
recommendation_missing: '原信号不存在',
|
||||
expired: '超过有效时间',
|
||||
global_risk_rejected: '全局风控拦截',
|
||||
cumulative_leverage_exceeded: '累计杠杆超限',
|
||||
stop_loss_leverage_risk_exceeded: '止损杠杆风险过高'
|
||||
};
|
||||
var FLAG_LABELS = {
|
||||
market_regime: '市场环境',
|
||||
market_risk: '市场风险',
|
||||
@ -562,6 +585,19 @@ function renderStrategyDiagnostics(r) {
|
||||
var html = renderScoreComponents(r) + renderRegimeBrief(r) + renderDecisionLogBrief(r);
|
||||
return html ? '<div class="strategy-diagnostics">'+html+'</div>' : '';
|
||||
}
|
||||
function renderPaperOrderBrief(r) {
|
||||
var order = r && r.paper_order;
|
||||
if (!order || !order.id) return '';
|
||||
var status = String(order.status || '').toLowerCase();
|
||||
var label = PAPER_ORDER_STATUS_LABELS[status] || translateInternalToken(status || '挂单');
|
||||
var target = Number(order.target_price || 0);
|
||||
var current = Number(r.current_price || order.current_price_at_create || 0);
|
||||
var meta = [];
|
||||
if (target > 0) meta.push('计划价 $' + fmtPrice(target, priceDecimals(target)));
|
||||
if (current > 0 && status === 'pending') meta.push('现价 $' + fmtPrice(current, priceDecimals(current)));
|
||||
if (order.cancel_reason) meta.push(PAPER_ORDER_REASON_LABELS[order.cancel_reason] || translateInternalToken(order.cancel_reason));
|
||||
return '<div class="paper-order-brief '+esc(status)+'"><span class="paper-order-title">策略挂单 · '+esc(label)+'</span><span class="paper-order-meta">'+esc(meta.join(' · ') || '已关联到当前信号')+'</span></div>';
|
||||
}
|
||||
function normalizeTriggerCause(s) {
|
||||
return cleanDisplayText(s)
|
||||
.replace(/^15min入场窗口/, '15min 触发')
|
||||
@ -886,7 +922,7 @@ function renderRecCard(r) {
|
||||
} else {
|
||||
entryPlanHtml = '<div class="entry-plan"><div class="ep-item"><span class="ep-label">当前参考</span><span class="ep-val entry-ref">'+fmtP(price)+'</span><span class="ep-sub">不是入场价</span></div><div class="ep-item"><span class="ep-label">观察重点</span><span class="ep-val space-ref">待触发</span><span class="ep-sub">'+cleanDisplayText(entryModel || '需15m/1H当前信号')+'</span></div><div class="ep-item"><span class="ep-label">绩效口径</span><span class="ep-val risk-line">不计入</span><span class="ep-sub">未形成交易机会</span></div><div class="ep-item"><span class="ep-label">观察阶段</span><span class="ep-val level-ref">'+cleanDisplayText(horizon || '观察池候选')+'</span><span class="ep-sub">'+cleanDisplayText(levelLabel)+'</span></div></div>';
|
||||
}
|
||||
return '<div class="card '+(isWeakObserve?'weak-observe':'')+'"><div class="card-bar"><div class="coin-left"><div class="coin-icon">'+base.slice(0,2).toUpperCase()+'</div><div><span class="coin-symbol">'+base+'</span></div></div><div class="badge-group"><span class="score-badge '+st.cls+'"><span class="score-num">'+score+'</span><span class="score-label">总分</span></span></div></div><div class="price-bar"><span class="price">$'+priceFmt+'</span>'+changeHtml+'</div>'+decisionHtml+renderStrategyDiagnostics(r)+signalLevelHtml+onchainHtml+aiInsightHtml+'<div class="kline-wrap"><div class="kline-int-bar"><button class="kline-int-btn" data-int="15m" onclick="switchKlineInterval(this);event.stopPropagation()">15m</button><button class="kline-int-btn active" data-int="1h" onclick="switchKlineInterval(this);event.stopPropagation()">1H</button><button class="kline-int-btn" data-int="4h" onclick="switchKlineInterval(this);event.stopPropagation()">4H</button><button class="kline-int-btn" data-int="1d" onclick="switchKlineInterval(this);event.stopPropagation()">1D</button></div><div class="kline-container loading" data-symbol="'+r.symbol+'" data-entry-price="'+klineEntryPrice+'" data-stop-loss="'+klineStopLoss+'" data-tp1="'+klineTp1+'" data-rec-time="'+entryTime+'" data-tp1-time="'+tp1EventTime+'" data-sl-time="'+slEventTime+'" data-ref-price="'+price+'" data-action-status="'+(r.action_status||'')+'"><div class="chart-loading"><svg class="spin" width="16" height="16" color="#8e91a0"><use href="#svg-spinner"/></svg></div></div></div>'+(isWeakObserve ? weakNoteHtml : entryPlanHtml)+(sigHtml?'<div class="signals-row">'+sigHtml+'</div>':'')+'<div class="card-footer"><span>'+fmtTime(r.rec_time)+'</span><span class="card-ver">'+ver+'</span>'+(isTpOrSl?'<span class="pnl-block '+pnlCls+'">'+pnlSign+pnl.toFixed(1)+'%</span>':'')+'</div></div>';
|
||||
return '<div class="card '+(isWeakObserve?'weak-observe':'')+'"><div class="card-bar"><div class="coin-left"><div class="coin-icon">'+base.slice(0,2).toUpperCase()+'</div><div><span class="coin-symbol">'+base+'</span></div></div><div class="badge-group"><span class="score-badge '+st.cls+'"><span class="score-num">'+score+'</span><span class="score-label">总分</span></span></div></div><div class="price-bar"><span class="price">$'+priceFmt+'</span>'+changeHtml+'</div>'+decisionHtml+renderPaperOrderBrief(r)+renderStrategyDiagnostics(r)+signalLevelHtml+onchainHtml+aiInsightHtml+'<div class="kline-wrap"><div class="kline-int-bar"><button class="kline-int-btn" data-int="15m" onclick="switchKlineInterval(this);event.stopPropagation()">15m</button><button class="kline-int-btn active" data-int="1h" onclick="switchKlineInterval(this);event.stopPropagation()">1H</button><button class="kline-int-btn" data-int="4h" onclick="switchKlineInterval(this);event.stopPropagation()">4H</button><button class="kline-int-btn" data-int="1d" onclick="switchKlineInterval(this);event.stopPropagation()">1D</button></div><div class="kline-container loading" data-symbol="'+r.symbol+'" data-entry-price="'+klineEntryPrice+'" data-stop-loss="'+klineStopLoss+'" data-tp1="'+klineTp1+'" data-rec-time="'+entryTime+'" data-tp1-time="'+tp1EventTime+'" data-sl-time="'+slEventTime+'" data-ref-price="'+price+'" data-action-status="'+(r.action_status||'')+'"><div class="chart-loading"><svg class="spin" width="16" height="16" color="#8e91a0"><use href="#svg-spinner"/></svg></div></div></div>'+(isWeakObserve ? weakNoteHtml : entryPlanHtml)+(sigHtml?'<div class="signals-row">'+sigHtml+'</div>':'')+'<div class="card-footer"><span>'+fmtTime(r.rec_time)+'</span><span class="card-ver">'+ver+'</span>'+(isTpOrSl?'<span class="pnl-block '+pnlCls+'">'+pnlSign+pnl.toFixed(1)+'%</span>':'')+'</div></div>';
|
||||
} catch (e) {
|
||||
console.error('renderRecCard hard fail', r && r.symbol, e);
|
||||
return renderLiveFallbackCard(r);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user