1
This commit is contained in:
parent
a1031a8e06
commit
c2e5a73aba
@ -186,19 +186,24 @@ def get_observation_candidates(limit=50):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _archive_filter_where(archive_filter):
|
def _decision_archive_where(archive_filter):
|
||||||
archive_filter = str(archive_filter or "").strip()
|
archive_filter = str(archive_filter or "").strip()
|
||||||
|
executed_where = "EXISTS (SELECT 1 FROM paper_trades ptf WHERE ptf.recommendation_id = recommendation.id)"
|
||||||
|
invalid_where = """
|
||||||
|
NOT EXISTS (SELECT 1 FROM paper_trades ptf WHERE ptf.recommendation_id = recommendation.id)
|
||||||
|
AND (
|
||||||
|
status IN ('expired','invalid','archived','stopped_out')
|
||||||
|
OR COALESCE(execution_status, '') = 'invalid'
|
||||||
|
)
|
||||||
|
"""
|
||||||
if archive_filter == "executed":
|
if archive_filter == "executed":
|
||||||
return " AND EXISTS (SELECT 1 FROM paper_trades ptf WHERE ptf.recommendation_id = recommendation.id)"
|
# 已执行口径以 paper trading 账本为准。正在持仓中的模拟交易,
|
||||||
|
# 其 recommendation 仍可能是 active/watch_pool,不能被归档条件挡掉。
|
||||||
|
return executed_where
|
||||||
if archive_filter == "invalid":
|
if archive_filter == "invalid":
|
||||||
return """
|
return f"({invalid_where})"
|
||||||
AND NOT EXISTS (SELECT 1 FROM paper_trades ptf WHERE ptf.recommendation_id = recommendation.id)
|
# “全部”只展示归档口径:已执行 + 失效。
|
||||||
AND (
|
return f"(({executed_where}) OR ({invalid_where}))"
|
||||||
status IN ('expired','invalid','archived','stopped_out')
|
|
||||||
OR COALESCE(execution_status, '') = 'invalid'
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
def _attach_paper_trade(item):
|
def _attach_paper_trade(item):
|
||||||
@ -251,9 +256,7 @@ def get_all_recommendations(limit=50, decision_only=False, version="", offset=0,
|
|||||||
except Exception:
|
except Exception:
|
||||||
offset = 0
|
offset = 0
|
||||||
|
|
||||||
archive_where = "(status != 'active' OR COALESCE(display_bucket, '') = 'history' OR COALESCE(execution_status, '') IN ('invalid','completed'))"
|
filtered_archive_where = _decision_archive_where(archive_filter)
|
||||||
archive_filter_where = _archive_filter_where(archive_filter)
|
|
||||||
filtered_archive_where = archive_where + archive_filter_where
|
|
||||||
version_where = " AND strategy_version=%s" if version else ""
|
version_where = " AND strategy_version=%s" if version else ""
|
||||||
params = [version] if version else []
|
params = [version] if version else []
|
||||||
|
|
||||||
@ -309,7 +312,7 @@ def get_all_recommendations(limit=50, decision_only=False, version="", offset=0,
|
|||||||
SELECT symbol, MAX(id) AS max_id
|
SELECT symbol, MAX(id) AS max_id
|
||||||
FROM recommendation
|
FROM recommendation
|
||||||
WHERE """
|
WHERE """
|
||||||
+ archive_where
|
+ filtered_archive_where
|
||||||
+ version_where
|
+ version_where
|
||||||
+ """
|
+ """
|
||||||
GROUP BY symbol
|
GROUP BY symbol
|
||||||
@ -342,7 +345,7 @@ def get_all_recommendations(limit=50, decision_only=False, version="", offset=0,
|
|||||||
SELECT symbol, MAX(id) AS max_id
|
SELECT symbol, MAX(id) AS max_id
|
||||||
FROM recommendation
|
FROM recommendation
|
||||||
WHERE """
|
WHERE """
|
||||||
+ archive_where
|
+ filtered_archive_where
|
||||||
+ """
|
+ """
|
||||||
GROUP BY symbol
|
GROUP BY symbol
|
||||||
) latest ON latest.max_id = r.id
|
) latest ON latest.max_id = r.id
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}AlphaX Agent — 看板{% endblock %}
|
{% block title %}AlphaX Agent — 机会总览{% endblock %}
|
||||||
<!-- BUILD: 2026-05-09T18:25:00 grid+kline-autoload -->
|
<!-- BUILD: 2026-05-09T18:25:00 grid+kline-autoload -->
|
||||||
|
|
||||||
{% block extra_head_css %}
|
{% block extra_head_css %}
|
||||||
@ -285,11 +285,11 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="shell">
|
<div class="shell">
|
||||||
<!-- compatibility markers: 实时推荐 / 历史推荐 / drawPin / data-entry-price / v.count / 止损 / 止盈 -->
|
<!-- compatibility markers: 实时机会 / 历史机会 / drawPin / data-entry-price / v.count / 止损 / 止盈 -->
|
||||||
<div class="controls-row">
|
<div class="controls-row">
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<button class="tab-btn active" data-tab="live" onclick="switchTab('live')">实时推荐<span class="count" id="liveCount"></span></button>
|
<button class="tab-btn active" data-tab="live" onclick="switchTab('live')">实时机会<span class="count" id="liveCount"></span></button>
|
||||||
<button class="tab-btn" data-tab="history" onclick="switchTab('history')">推荐归档<span class="count" id="histCount"></span></button>
|
<button class="tab-btn" data-tab="history" onclick="switchTab('history')">机会归档<span class="count" id="histCount"></span></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -589,7 +589,7 @@ function renderLiveCards(data, weakCount) {
|
|||||||
var items = Array.isArray(data) ? data : [];
|
var items = Array.isArray(data) ? data : [];
|
||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
var weakOnly = weakCount ? '<div class="weak-summary"><span>当前只有 '+weakCount+' 个弱观察候选,已默认收起,避免干扰主机会流。</span><button onclick="setFilter(\'weak_observe\')">查看弱观察</button></div>' : '';
|
var weakOnly = weakCount ? '<div class="weak-summary"><span>当前只有 '+weakCount+' 个弱观察候选,已默认收起,避免干扰主机会流。</span><button onclick="setFilter(\'weak_observe\')">查看弱观察</button></div>' : '';
|
||||||
$('liveCards').innerHTML = weakOnly || '<div class="empty-state"><p>暂无实时推荐或观察候选<br>系统持续扫描中,有机会会实时更新</p></div>'; return;
|
$('liveCards').innerHTML = weakOnly || '<div class="empty-state"><p>暂无实时机会或观察候选<br>系统持续扫描中,有机会会实时更新</p></div>'; return;
|
||||||
}
|
}
|
||||||
var order = { buy_now: 0, wait_pullback: 1, observe: 2, holding: 3, completed: 4, invalid: 9 };
|
var order = { buy_now: 0, wait_pullback: 1, observe: 2, holding: 3, completed: 4, invalid: 9 };
|
||||||
items.sort(function(a,b){
|
items.sort(function(a,b){
|
||||||
@ -748,9 +748,9 @@ function renderRecCard(r) {
|
|||||||
if (isTradePlan) {
|
if (isTradePlan) {
|
||||||
entryPlanHtml = '<div class="entry-plan"><div class="ep-item"><span class="ep-label">入场参考</span><span class="ep-val entry-ref">'+fmtP(entryRef)+'</span><span class="ep-sub">'+cleanDisplayText(entryLabel+' · '+(entryModel || '触发/计划价'))+'</span></div><div class="ep-item"><span class="ep-label">风险边界</span><span class="ep-val risk-line">'+fmtP(riskLine)+'</span><span class="ep-sub">'+cleanDisplayText(stopModel)+'</span></div><div class="ep-item"><span class="ep-label">上方空间</span><span class="ep-val space-ref">'+(upsidePct?('+'+upsidePct.toFixed(1)+'%'):'--')+'</span><span class="ep-sub">'+cleanDisplayText(tpModel)+' · '+fmtP(spaceRef)+'</span></div><div class="ep-item"><span class="ep-label">持有阶段</span><span class="ep-val level-ref">'+cleanDisplayText(horizon || phase.short)+'</span><span class="ep-sub">'+cleanDisplayText(levelLabel)+'</span></div></div>';
|
entryPlanHtml = '<div class="entry-plan"><div class="ep-item"><span class="ep-label">入场参考</span><span class="ep-val entry-ref">'+fmtP(entryRef)+'</span><span class="ep-sub">'+cleanDisplayText(entryLabel+' · '+(entryModel || '触发/计划价'))+'</span></div><div class="ep-item"><span class="ep-label">风险边界</span><span class="ep-val risk-line">'+fmtP(riskLine)+'</span><span class="ep-sub">'+cleanDisplayText(stopModel)+'</span></div><div class="ep-item"><span class="ep-label">上方空间</span><span class="ep-val space-ref">'+(upsidePct?('+'+upsidePct.toFixed(1)+'%'):'--')+'</span><span class="ep-sub">'+cleanDisplayText(tpModel)+' · '+fmtP(spaceRef)+'</span></div><div class="ep-item"><span class="ep-label">持有阶段</span><span class="ep-val level-ref">'+cleanDisplayText(horizon || phase.short)+'</span><span class="ep-sub">'+cleanDisplayText(levelLabel)+'</span></div></div>';
|
||||||
} else {
|
} 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>';
|
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">'+st.label+'</span></span></div></div><div class="price-bar"><span class="price">$'+priceFmt+'</span>'+changeHtml+'</div>'+decisionHtml+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+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) {
|
} catch (e) {
|
||||||
console.error('renderRecCard hard fail', r && r.symbol, e);
|
console.error('renderRecCard hard fail', r && r.symbol, e);
|
||||||
return renderLiveFallbackCard(r);
|
return renderLiveFallbackCard(r);
|
||||||
@ -918,7 +918,7 @@ function historyOutcome(r) {
|
|||||||
return { resolved: true, type: 'executed_failed', label: '执行后止损', detail: '执行样本触发风险边界' };
|
return { resolved: true, type: 'executed_failed', label: '执行后止损', detail: '执行样本触发风险边界' };
|
||||||
}
|
}
|
||||||
if (status === 'expired' || status === 'invalid' || status === 'archived' || execution === 'invalid' || bucket === 'history') {
|
if (status === 'expired' || status === 'invalid' || status === 'archived' || execution === 'invalid' || bucket === 'history') {
|
||||||
return { resolved: true, type: triggered ? 'executed_invalid' : 'not_executed', label: triggered ? '执行后失效' : '未执行失效', detail: triggered ? '曾进入执行态,后续失效' : '推荐/观察后未形成真实交易' };
|
return { resolved: true, type: triggered ? 'executed_invalid' : 'not_executed', label: triggered ? '执行后失效' : '未执行失效', detail: triggered ? '曾进入执行态,后续失效' : '机会/观察后未形成真实交易' };
|
||||||
}
|
}
|
||||||
return { resolved: false, type: 'pending', label: '仍在跟踪', detail: '尚未归档' };
|
return { resolved: false, type: 'pending', label: '仍在跟踪', detail: '尚未归档' };
|
||||||
}
|
}
|
||||||
@ -948,7 +948,7 @@ async function loadHistoryRecommendations(reset) {
|
|||||||
var notExecutedCount = Number(summary.not_executed_count || 0);
|
var notExecutedCount = Number(summary.not_executed_count || 0);
|
||||||
$('histCount').textContent = totalCount ? ' ' + totalCount : '';
|
$('histCount').textContent = totalCount ? ' ' + totalCount : '';
|
||||||
$('historyStats').innerHTML =
|
$('historyStats').innerHTML =
|
||||||
'<div class="hstat"><div class="num" style="color:var(--blue)">'+totalCount+'</div><div class="lbl"><svg width="14" height="14" color="var(--blue)"><use href="#svg-target"/></svg> 归档信号</div><div class="sub">推荐/观察历史</div></div>'+
|
'<div class="hstat"><div class="num" style="color:var(--blue)">'+totalCount+'</div><div class="lbl"><svg width="14" height="14" color="var(--blue)"><use href="#svg-target"/></svg> 归档信号</div><div class="sub">机会/观察历史</div></div>'+
|
||||||
'<div class="hstat"><div class="num" style="color:var(--green)">'+executedCount+'</div><div class="lbl"><svg width="14" height="14" color="var(--green)"><use href="#svg-trendup"/></svg> 进入执行</div><div class="sub">收益见模拟交易</div></div>'+
|
'<div class="hstat"><div class="num" style="color:var(--green)">'+executedCount+'</div><div class="lbl"><svg width="14" height="14" color="var(--green)"><use href="#svg-trendup"/></svg> 进入执行</div><div class="sub">收益见模拟交易</div></div>'+
|
||||||
'<div class="hstat"><div class="num" style="color:var(--yellow-dark)">'+notExecutedCount+'</div><div class="lbl"><svg width="14" height="14" color="var(--yellow-dark)"><use href="#svg-star"/></svg> 未执行归档</div><div class="sub">观察/等回踩失效</div></div>'+
|
'<div class="hstat"><div class="num" style="color:var(--yellow-dark)">'+notExecutedCount+'</div><div class="lbl"><svg width="14" height="14" color="var(--yellow-dark)"><use href="#svg-star"/></svg> 未执行归档</div><div class="sub">观察/等回踩失效</div></div>'+
|
||||||
'<div class="hstat"><div class="num" style="color:var(--red)">'+invalidCount+'</div><div class="lbl"><svg width="14" height="14" color="var(--red)"><use href="#svg-shield"/></svg> 信号失效</div><div class="sub">含过期/风控失效</div></div>';
|
'<div class="hstat"><div class="num" style="color:var(--red)">'+invalidCount+'</div><div class="lbl"><svg width="14" height="14" color="var(--red)"><use href="#svg-shield"/></svg> 信号失效</div><div class="sub">含过期/风控失效</div></div>';
|
||||||
@ -962,7 +962,7 @@ async function loadHistoryRecommendations(reset) {
|
|||||||
historyOffset += completed.length;
|
historyOffset += completed.length;
|
||||||
}
|
}
|
||||||
historyHasMore = !!page.has_more;
|
historyHasMore = !!page.has_more;
|
||||||
if(!historyItems.length){ $('historyCards').innerHTML='<div class="empty-state"><p>暂无归档推荐<br>推荐过期、失效或完成后会出现在这里</p></div>'; return; }
|
if(!historyItems.length){ $('historyCards').innerHTML='<div class="empty-state"><p>暂无归档机会<br>机会过期、失效或完成后会出现在这里</p></div>'; return; }
|
||||||
var cardsHtml = historyItems.map(function(r,idx) {
|
var cardsHtml = historyItems.map(function(r,idx) {
|
||||||
var base = (r.symbol||'').replace('/USDT',''), outcome = historyOutcome(r);
|
var base = (r.symbol||'').replace('/USDT',''), outcome = historyOutcome(r);
|
||||||
var paper = r.paper_trade || null;
|
var paper = r.paper_trade || null;
|
||||||
@ -1000,7 +1000,7 @@ async function loadHistoryRecommendations(reset) {
|
|||||||
var outcomeDetail = outcome.detail;
|
var outcomeDetail = outcome.detail;
|
||||||
return '<div class=\"card\">'+
|
return '<div class=\"card\">'+
|
||||||
'<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><span class=\"hist-result-badge '+resultCls+'\">'+outcomeText+'</span></div>'+
|
'<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><span class=\"hist-result-badge '+resultCls+'\">'+outcomeText+'</span></div>'+
|
||||||
'<div class=\"h-pnl-row\">'+(hasPaper ? '<span class=\"price h-entry-price\">$'+fmtN(entryP)+'</span><span class=\"h-arrow neutral\">→</span><span class=\"price h-exit-price '+(paper.status === 'closed' ? resultCls : 'muted')+'\">'+(paper.status === 'closed' ? '$'+fmtN(exitP) : '持有中')+'</span>' : '<span class=\"price h-entry-price muted\">未执行</span><span class=\"h-arrow neutral\">→</span><span class=\"price h-exit-price muted\">失效/归档</span>')+'<span class=\"hist-score-pill '+scoreCls+'\">评分 '+score+' · '+st.label+'</span><span class=\"h-duration\">'+duration+'</span></div>'+
|
'<div class=\"h-pnl-row\">'+(hasPaper ? '<span class=\"price h-entry-price\">$'+fmtN(entryP)+'</span><span class=\"h-arrow neutral\">→</span><span class=\"price h-exit-price '+(paper.status === 'closed' ? resultCls : 'muted')+'\">'+(paper.status === 'closed' ? '$'+fmtN(exitP) : '持有中')+'</span>' : '<span class=\"price h-entry-price muted\">未执行</span><span class=\"h-arrow neutral\">→</span><span class=\"price h-exit-price muted\">失效/归档</span>')+'<span class=\"hist-score-pill '+scoreCls+'\">机会分 '+score+'</span><span class=\"h-duration\">'+duration+'</span></div>'+
|
||||||
'<div class=\"hist-metric-row\"><div class=\"hist-metric\"><span class=\"hm-label\">交易阶段</span><span class=\"hm-val '+(hasPaper ? 'win' : 'blue')+'\">'+(hasPaper ? (paper.status === 'closed' ? '已完成模拟交易' : '模拟交易持有中') : '未执行归档')+'</span></div><div class=\"hist-metric\"><span class=\"hm-label\">结果说明</span><span class=\"hm-val blue\">'+outcomeDetail+'</span></div><div class=\"hist-metric\"><span class=\"hm-label\">执行状态</span><span class=\"hm-val '+(Number(r.entry_triggered||0)?'win':'blue')+'\">'+execText+'</span></div></div>'+
|
'<div class=\"hist-metric-row\"><div class=\"hist-metric\"><span class=\"hm-label\">交易阶段</span><span class=\"hm-val '+(hasPaper ? 'win' : 'blue')+'\">'+(hasPaper ? (paper.status === 'closed' ? '已完成模拟交易' : '模拟交易持有中') : '未执行归档')+'</span></div><div class=\"hist-metric\"><span class=\"hm-label\">结果说明</span><span class=\"hm-val blue\">'+outcomeDetail+'</span></div><div class=\"hist-metric\"><span class=\"hm-label\">执行状态</span><span class=\"hm-val '+(Number(r.entry_triggered||0)?'win':'blue')+'\">'+execText+'</span></div></div>'+
|
||||||
'<div class=\"kline-wrap\" id=\"wrap_'+hid+'\"><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\" id=\"'+hid+'\" data-symbol=\"'+(r.symbol||'')+'\" data-entry-price=\"'+hEntryPrice+'\" data-stop-loss=\"'+hSl+'\" data-tp1=\"'+hTp+'\" data-rec-time=\"'+hEntryTime+'\" data-tp1-time=\"'+hTpTime+'\" data-sl-time=\"'+hSlTime+'\" data-ref-price=\"'+(r.current_price||hEntryPrice||hTp||hSl||0)+'\" data-status=\"'+(r.status||'')+'\" ><div class=\"chart-loading\"><svg class=\"spin\" width=\"16\" height=\"16\" color=\"#8e91a0\"><use href=\"#svg-spinner\"/></svg></div></div></div>'+
|
'<div class=\"kline-wrap\" id=\"wrap_'+hid+'\"><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\" id=\"'+hid+'\" data-symbol=\"'+(r.symbol||'')+'\" data-entry-price=\"'+hEntryPrice+'\" data-stop-loss=\"'+hSl+'\" data-tp1=\"'+hTp+'\" data-rec-time=\"'+hEntryTime+'\" data-tp1-time=\"'+hTpTime+'\" data-sl-time=\"'+hSlTime+'\" data-ref-price=\"'+(r.current_price||hEntryPrice||hTp||hSl||0)+'\" data-status=\"'+(r.status||'')+'\" ><div class=\"chart-loading\"><svg class=\"spin\" width=\"16\" height=\"16\" color=\"#8e91a0\"><use href=\"#svg-spinner\"/></svg></div></div></div>'+
|
||||||
(sigHtml?'<div class=\"signals-row\">'+sigHtml+'</div>':'')+
|
(sigHtml?'<div class=\"signals-row\">'+sigHtml+'</div>':'')+
|
||||||
|
|||||||
@ -144,11 +144,6 @@ a { color: inherit; text-decoration: none; }
|
|||||||
<span class="brand-name">AlphaX Agent</span>
|
<span class="brand-name">AlphaX Agent</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div class="notice" style="margin-bottom:16px">
|
|
||||||
<strong>AlphaX Agent</strong><br>
|
|
||||||
提前发现机会,别在强信号后追高。登录或开启免费体验,创建账号后可前往订阅中心。
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<button class="tab active" id="tabRegister" onclick="setTab('register')">创建账号</button>
|
<button class="tab active" id="tabRegister" onclick="setTab('register')">创建账号</button>
|
||||||
<button class="tab" id="tabLogin" onclick="setTab('login')">会员登录</button>
|
<button class="tab" id="tabLogin" onclick="setTab('login')">会员登录</button>
|
||||||
@ -211,11 +206,10 @@ a { color: inherit; text-decoration: none; }
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-primary" onclick="loginUser()">登录</button>
|
<button class="btn-primary" id="loginSubmitBtn" onclick="loginUser()">登录</button>
|
||||||
<div id="loginMsg" class="msg"></div>
|
<div id="loginMsg" class="msg"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a class="back-link" href="/subscription">前往订阅中心</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@ -297,16 +291,35 @@ async function resendCode(){
|
|||||||
setMsg('regMsg', data.dev_verification_code ? '验证码:'+data.dev_verification_code : '验证码已重新发送','ok');
|
setMsg('regMsg', data.dev_verification_code ? '验证码:'+data.dev_verification_code : '验证码已重新发送','ok');
|
||||||
}catch(e){ setMsg('regMsg', e.message, 'err'); }
|
}catch(e){ setMsg('regMsg', e.message, 'err'); }
|
||||||
}
|
}
|
||||||
|
var loginSubmitting = false;
|
||||||
async function loginUser(){
|
async function loginUser(){
|
||||||
|
if(loginSubmitting) return;
|
||||||
|
loginSubmitting = true;
|
||||||
|
var btn = $('loginSubmitBtn');
|
||||||
|
if(btn) btn.disabled = true;
|
||||||
try{
|
try{
|
||||||
var data = await post('/api/auth/login',{email:$('loginEmail').value,password:$('loginPassword').value});
|
var data = await post('/api/auth/login',{email:$('loginEmail').value,password:$('loginPassword').value});
|
||||||
var next = data.next || (data.subscription_active ? '/app' : '/subscription?welcome=1');
|
var next = data.next || (data.subscription_active ? '/app' : '/subscription?welcome=1');
|
||||||
setMsg('loginMsg', data.subscription_active ? '登录成功,正在进入看板…' : '登录成功,先开通免费体验套餐…','ok');
|
setMsg('loginMsg', data.subscription_active ? '登录成功,正在进入机会总览…' : '登录成功,先开通免费体验套餐…','ok');
|
||||||
setTimeout(function(){ window.location.href = next; }, 500);
|
setTimeout(function(){ window.location.href = next; }, 500);
|
||||||
}catch(e){ setMsg('loginMsg', e.message, 'err'); }
|
}catch(e){
|
||||||
|
setMsg('loginMsg', e.message, 'err');
|
||||||
|
loginSubmitting = false;
|
||||||
|
if(btn) btn.disabled = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
(function(){
|
(function(){
|
||||||
if (location.search.indexOf('tab=login') !== -1) setTab('login');
|
if (location.search.indexOf('tab=login') !== -1) setTab('login');
|
||||||
|
['loginEmail','loginPassword'].forEach(function(id){
|
||||||
|
var input = $(id);
|
||||||
|
if(!input) return;
|
||||||
|
input.addEventListener('keydown', function(e){
|
||||||
|
if(e.key === 'Enter'){
|
||||||
|
e.preventDefault();
|
||||||
|
loginUser();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@ -174,24 +174,20 @@ a { color: inherit; text-decoration: none; }
|
|||||||
<span class="brand-name">AlphaX Agent</span>
|
<span class="brand-name">AlphaX Agent</span>
|
||||||
</a>
|
</a>
|
||||||
<nav class="sidebar-nav">
|
<nav class="sidebar-nav">
|
||||||
<div class="sidebar-section-label">交易</div>
|
<a class="sidebar-link {% if active_nav | default('app') == 'app' %}active{% endif %}" href="/app"><svg class="link-icon"><use href="#svg-dashboard"/></svg>机会总览</a>
|
||||||
<a class="sidebar-link {% if active_nav | default('app') == 'app' %}active{% endif %}" href="/app"><svg class="link-icon"><use href="#svg-dashboard"/></svg>看板</a>
|
|
||||||
<a class="sidebar-link {% if active_nav == 'chat' %}active{% endif %}" href="/chat"><svg class="link-icon"><use href="#svg-chat"/></svg>智能问答</a>
|
|
||||||
<a class="sidebar-link admin-link {% if active_nav == 'paper_trading' %}active{% endif %}" href="/paper-trading" style="display:none"><svg class="link-icon"><use href="#svg-paper"/></svg>模拟交易</a>
|
|
||||||
<div class="sidebar-section-label">研究</div>
|
|
||||||
<a class="sidebar-link {% if active_nav == 'market' %}active{% endif %}" href="/market"><svg class="link-icon"><use href="#svg-target"/></svg>市场总览</a>
|
<a class="sidebar-link {% if active_nav == 'market' %}active{% endif %}" href="/market"><svg class="link-icon"><use href="#svg-target"/></svg>市场总览</a>
|
||||||
<a class="sidebar-link {% if active_nav == 'sentiment' %}active{% endif %}" href="/sentiment"><svg class="link-icon"><use href="#svg-sentiment"/></svg>实时舆情</a>
|
<a class="sidebar-link {% if active_nav == 'sentiment' %}active{% endif %}" href="/sentiment"><svg class="link-icon"><use href="#svg-sentiment"/></svg>实时舆情</a>
|
||||||
<a class="sidebar-link {% if active_nav == 'onchain' %}active{% endif %}" href="/onchain"><svg class="link-icon"><use href="#svg-onchain"/></svg>链上异动</a>
|
<a class="sidebar-link {% if active_nav == 'onchain' %}active{% endif %}" href="/onchain"><svg class="link-icon"><use href="#svg-onchain"/></svg>链上异动</a>
|
||||||
<div class="sidebar-section-label">账户</div>
|
<a class="sidebar-link {% if active_nav == 'chat' %}active{% endif %}" href="/chat"><svg class="link-icon"><use href="#svg-chat"/></svg>智能问答</a>
|
||||||
<a class="sidebar-link {% if active_nav == 'subscription' %}active{% endif %}" href="/subscription"><svg class="link-icon"><use href="#svg-subscribe"/></svg>订阅</a>
|
<a class="sidebar-link {% if active_nav == 'subscription' %}active{% endif %}" href="/subscription"><svg class="link-icon"><use href="#svg-subscribe"/></svg>订阅</a>
|
||||||
<a class="sidebar-link {% if active_nav == 'referral' %}active{% endif %}" href="/referral"><svg class="link-icon"><use href="#svg-referral"/></svg>邀请</a>
|
<a class="sidebar-link {% if active_nav == 'referral' %}active{% endif %}" href="/referral"><svg class="link-icon"><use href="#svg-referral"/></svg>邀请</a>
|
||||||
<div class="sidebar-section-label admin-link" style="display:none">研发</div>
|
<div class="sidebar-section-label admin-link" style="display:none">管理员菜单</div>
|
||||||
|
<a class="sidebar-link admin-link {% if active_nav == 'paper_trading' %}active{% endif %}" href="/paper-trading" style="display:none"><svg class="link-icon"><use href="#svg-paper"/></svg>模拟交易</a>
|
||||||
<a class="sidebar-link admin-link {% if active_nav == 'pipeline' %}active{% endif %}" href="/pipeline" style="display:none"><svg class="link-icon"><use href="#svg-pipeline"/></svg>链路日志</a>
|
<a class="sidebar-link admin-link {% if active_nav == 'pipeline' %}active{% endif %}" href="/pipeline" style="display:none"><svg class="link-icon"><use href="#svg-pipeline"/></svg>链路日志</a>
|
||||||
<a class="sidebar-link admin-link {% if active_nav == 'llm_insights' %}active{% endif %}" href="/llm-insights" style="display:none"><svg class="link-icon"><use href="#svg-ai"/></svg>AI 记录</a>
|
<a class="sidebar-link admin-link {% if active_nav == 'llm_insights' %}active{% endif %}" href="/llm-insights" style="display:none"><svg class="link-icon"><use href="#svg-ai"/></svg>AI 记录</a>
|
||||||
<a class="sidebar-link admin-link {% if active_nav == 'strategy' %}active{% endif %}" href="/strategy" style="display:none"><svg class="link-icon"><use href="#svg-target"/></svg>策略</a>
|
<a class="sidebar-link admin-link {% if active_nav == 'strategy' %}active{% endif %}" href="/strategy" style="display:none"><svg class="link-icon"><use href="#svg-target"/></svg>策略</a>
|
||||||
<a class="sidebar-link admin-link {% if active_nav == 'iteration' %}active{% endif %}" href="/iteration" style="display:none"><svg class="link-icon"><use href="#svg-iterate"/></svg>迭代</a>
|
<a class="sidebar-link admin-link {% if active_nav == 'iteration' %}active{% endif %}" href="/iteration" style="display:none"><svg class="link-icon"><use href="#svg-iterate"/></svg>迭代</a>
|
||||||
<a class="sidebar-link admin-link {% if active_nav == 'chat_logs' %}active{% endif %}" href="/chat-logs" style="display:none"><svg class="link-icon"><use href="#svg-chat"/></svg>问答日志</a>
|
<a class="sidebar-link admin-link {% if active_nav == 'chat_logs' %}active{% endif %}" href="/chat-logs" style="display:none"><svg class="link-icon"><use href="#svg-chat"/></svg>问答日志</a>
|
||||||
<div class="sidebar-section-label admin-link" style="display:none">系统</div>
|
|
||||||
<a class="sidebar-link admin-link {% if active_nav == 'config' %}active{% endif %}" href="/config" style="display:none"><svg class="link-icon"><use href="#svg-config"/></svg>配置中心</a>
|
<a class="sidebar-link admin-link {% if active_nav == 'config' %}active{% endif %}" href="/config" style="display:none"><svg class="link-icon"><use href="#svg-config"/></svg>配置中心</a>
|
||||||
<a class="sidebar-link admin-link {% if active_nav == 'cron' %}active{% endif %}" href="/cron" style="display:none"><svg class="link-icon"><use href="#svg-cron"/></svg>调度中心</a>
|
<a class="sidebar-link admin-link {% if active_nav == 'cron' %}active{% endif %}" href="/cron" style="display:none"><svg class="link-icon"><use href="#svg-cron"/></svg>调度中心</a>
|
||||||
<a class="sidebar-link {% if active_nav == 'system_logs' %}active{% endif %} admin-link" href="/system-logs" style="display:none"><svg class="link-icon"><use href="#svg-admin"/></svg>系统日志</a>
|
<a class="sidebar-link {% if active_nav == 'system_logs' %}active{% endif %} admin-link" href="/system-logs" style="display:none"><svg class="link-icon"><use href="#svg-admin"/></svg>系统日志</a>
|
||||||
|
|||||||
@ -153,7 +153,7 @@ h1 { font-size: clamp(36px, 5.5vw, 56px); font-weight: 500; line-height: 1.1; le
|
|||||||
</div>
|
</div>
|
||||||
<div class="user-dropdown" id="userDropdown">
|
<div class="user-dropdown" id="userDropdown">
|
||||||
<div class="dd-email" id="ddEmail">--</div>
|
<div class="dd-email" id="ddEmail">--</div>
|
||||||
<a class="dd-item" href="/app">进入看板</a>
|
<a class="dd-item" href="/app">进入机会总览</a>
|
||||||
<a class="dd-item danger" href="#" onclick="doLogout()">退出登录</a>
|
<a class="dd-item danger" href="#" onclick="doLogout()">退出登录</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -77,7 +77,7 @@
|
|||||||
|
|
||||||
<div id="guideBox" class="guide-box">
|
<div id="guideBox" class="guide-box">
|
||||||
<div class="guide-title" id="guideTitle">先开通套餐,开始使用 AlphaX Agent</div>
|
<div class="guide-title" id="guideTitle">先开通套餐,开始使用 AlphaX Agent</div>
|
||||||
<div class="guide-text" id="guideText">新用户可领取 30 天免费体验。开通后即可进入看板、策略、迭代和舆情页面。</div>
|
<div class="guide-text" id="guideText">新用户可领取 30 天免费体验。开通后即可进入机会总览、策略、迭代和舆情页面。</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="status-bar" id="statusBar" style="display:none">
|
<div class="status-bar" id="statusBar" style="display:none">
|
||||||
@ -152,7 +152,7 @@ async function loadMe() {
|
|||||||
document.getElementById('guideBox').classList.add('show');
|
document.getElementById('guideBox').classList.add('show');
|
||||||
if (params.get('expired') === '1') {
|
if (params.get('expired') === '1') {
|
||||||
document.getElementById('guideTitle').textContent = '订阅已到期,请先续订';
|
document.getElementById('guideTitle').textContent = '订阅已到期,请先续订';
|
||||||
document.getElementById('guideText').textContent = '当前账号没有有效订阅。续订或开通套餐后,才能继续访问看板、策略、迭代和舆情页面。';
|
document.getElementById('guideText').textContent = '当前账号没有有效订阅。续订或开通套餐后,才能继续访问机会总览、策略、迭代和舆情页面。';
|
||||||
} else if (!s) {
|
} else if (!s) {
|
||||||
document.getElementById('guideTitle').textContent = '欢迎使用 AlphaX Agent,请先开通套餐';
|
document.getElementById('guideTitle').textContent = '欢迎使用 AlphaX Agent,请先开通套餐';
|
||||||
document.getElementById('guideText').textContent = '新用户可领取 30 天免费体验。开通后即可进入完整功能页面。';
|
document.getElementById('guideText').textContent = '新用户可领取 30 天免费体验。开通后即可进入完整功能页面。';
|
||||||
|
|||||||
@ -94,6 +94,91 @@ def test_archive_filter_separates_executed_and_invalid(temp_db):
|
|||||||
assert not row.get("paper_trade")
|
assert not row.get("paper_trade")
|
||||||
|
|
||||||
|
|
||||||
|
def test_archive_executed_includes_open_paper_trade_even_if_signal_still_active(temp_db):
|
||||||
|
_insert_recommendation(
|
||||||
|
temp_db,
|
||||||
|
symbol="LINK/USDT",
|
||||||
|
action_status="等回踩",
|
||||||
|
status="active",
|
||||||
|
execution_status="wait_pullback",
|
||||||
|
display_bucket="watch_pool",
|
||||||
|
entry_plan_json='{"entry_action": "等回踩", "entry_price": 9.5}',
|
||||||
|
)
|
||||||
|
|
||||||
|
conn = altcoin_db.sqlite3.connect(str(temp_db))
|
||||||
|
rec_id = conn.execute("SELECT id FROM recommendation WHERE symbol=%s", ("LINK/USDT",)).fetchone()[0]
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO paper_trades (
|
||||||
|
recommendation_id, symbol, status, opened_at,
|
||||||
|
entry_price, qty, notional_usdt,
|
||||||
|
stop_loss, tp1, tp2, current_price, pnl_pct,
|
||||||
|
created_at, updated_at
|
||||||
|
) VALUES (%s, %s, 'open', %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
rec_id,
|
||||||
|
"LINK/USDT",
|
||||||
|
"2026-05-16T10:00:00",
|
||||||
|
10.0,
|
||||||
|
500.0,
|
||||||
|
5000.0,
|
||||||
|
9.4,
|
||||||
|
10.8,
|
||||||
|
11.5,
|
||||||
|
9.9,
|
||||||
|
-1.0,
|
||||||
|
"2026-05-16T10:00:00",
|
||||||
|
"2026-05-16T10:05:00",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
client = TestClient(web_server.app)
|
||||||
|
executed_page = client.get("/api/recommendations?decision_only=true&compact=true&archive_filter=executed").json()
|
||||||
|
item = next(row for row in executed_page["items"] if row["symbol"] == "LINK/USDT")
|
||||||
|
assert item["paper_trade_executed"] is True
|
||||||
|
assert item["paper_trade"]["status"] == "open"
|
||||||
|
|
||||||
|
|
||||||
|
def test_archive_all_includes_executed_and_invalid_only(temp_db):
|
||||||
|
_insert_recommendation(
|
||||||
|
temp_db,
|
||||||
|
symbol="EXEC2/USDT",
|
||||||
|
action_status="止盈1",
|
||||||
|
status="hit_tp1",
|
||||||
|
execution_status="completed",
|
||||||
|
display_bucket="history",
|
||||||
|
entry_plan_json='{"entry_action": "可即刻买入"}',
|
||||||
|
)
|
||||||
|
_insert_recommendation(
|
||||||
|
temp_db,
|
||||||
|
symbol="LOSS2/USDT",
|
||||||
|
action_status="止损",
|
||||||
|
status="stopped_out",
|
||||||
|
execution_status="invalid",
|
||||||
|
display_bucket="history",
|
||||||
|
entry_plan_json='{"entry_action": "可即刻买入"}',
|
||||||
|
)
|
||||||
|
_insert_recommendation(
|
||||||
|
temp_db,
|
||||||
|
symbol="OBS2/USDT",
|
||||||
|
action_status="等回踩",
|
||||||
|
status="active",
|
||||||
|
execution_status="wait_pullback",
|
||||||
|
display_bucket="watch_pool",
|
||||||
|
entry_plan_json='{"entry_action": "等回踩"}',
|
||||||
|
)
|
||||||
|
|
||||||
|
client = TestClient(web_server.app)
|
||||||
|
page = client.get("/api/recommendations?decision_only=true&compact=true").json()
|
||||||
|
symbols = {row["symbol"] for row in page["items"]}
|
||||||
|
assert "EXEC2/USDT" in symbols
|
||||||
|
assert "LOSS2/USDT" in symbols
|
||||||
|
assert "OBS2/USDT" not in symbols
|
||||||
|
|
||||||
|
|
||||||
def test_unexecuted_archive_items_do_not_have_paper_trade_payload(temp_db):
|
def test_unexecuted_archive_items_do_not_have_paper_trade_payload(temp_db):
|
||||||
_insert_recommendation(
|
_insert_recommendation(
|
||||||
temp_db,
|
temp_db,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user