This commit is contained in:
aaron 2026-05-18 12:44:53 +08:00
parent a1031a8e06
commit c2e5a73aba
7 changed files with 143 additions and 46 deletions

View File

@ -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()
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":
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":
return """
AND 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'
)
"""
return ""
return f"({invalid_where})"
# “全部”只展示归档口径:已执行 + 失效。
return f"(({executed_where}) OR ({invalid_where}))"
def _attach_paper_trade(item):
@ -251,9 +256,7 @@ def get_all_recommendations(limit=50, decision_only=False, version="", offset=0,
except Exception:
offset = 0
archive_where = "(status != 'active' OR COALESCE(display_bucket, '') = 'history' OR COALESCE(execution_status, '') IN ('invalid','completed'))"
archive_filter_where = _archive_filter_where(archive_filter)
filtered_archive_where = archive_where + archive_filter_where
filtered_archive_where = _decision_archive_where(archive_filter)
version_where = " AND strategy_version=%s" 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
FROM recommendation
WHERE """
+ archive_where
+ filtered_archive_where
+ version_where
+ """
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
FROM recommendation
WHERE """
+ archive_where
+ filtered_archive_where
+ """
GROUP BY symbol
) latest ON latest.max_id = r.id

View File

@ -1,5 +1,5 @@
{% extends "base.html" %}
{% block title %}AlphaX Agent — 看板{% endblock %}
{% block title %}AlphaX Agent — 机会总览{% endblock %}
<!-- BUILD: 2026-05-09T18:25:00 grid+kline-autoload -->
{% block extra_head_css %}
@ -285,11 +285,11 @@
{% block content %}
<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="tabs">
<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 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>
</div>
</div>
@ -589,7 +589,7 @@ function renderLiveCards(data, weakCount) {
var items = Array.isArray(data) ? data : [];
if (!items.length) {
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 };
items.sort(function(a,b){
@ -748,9 +748,9 @@ function renderRecCard(r) {
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>';
} 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) {
console.error('renderRecCard hard fail', r && r.symbol, e);
return renderLiveFallbackCard(r);
@ -918,7 +918,7 @@ function historyOutcome(r) {
return { resolved: true, type: 'executed_failed', label: '执行后止损', detail: '执行样本触发风险边界' };
}
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: '尚未归档' };
}
@ -948,7 +948,7 @@ async function loadHistoryRecommendations(reset) {
var notExecutedCount = Number(summary.not_executed_count || 0);
$('histCount').textContent = totalCount ? ' ' + totalCount : '';
$('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(--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>';
@ -962,7 +962,7 @@ async function loadHistoryRecommendations(reset) {
historyOffset += completed.length;
}
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 base = (r.symbol||'').replace('/USDT',''), outcome = historyOutcome(r);
var paper = r.paper_trade || null;
@ -1000,7 +1000,7 @@ async function loadHistoryRecommendations(reset) {
var outcomeDetail = outcome.detail;
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=\"h-pnl-row\">'+(hasPaper ? '<span class=\"price h-entry-price\">$'+fmtN(entryP)+'</span><span class=\"h-arrow neutral\">&rarr;</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\">&rarr;</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\">&rarr;</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\">&rarr;</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=\"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>':'')+

View File

@ -144,11 +144,6 @@ a { color: inherit; text-decoration: none; }
<span class="brand-name">AlphaX Agent</span>
</a>
<div class="notice" style="margin-bottom:16px">
<strong>AlphaX Agent</strong><br>
提前发现机会,别在强信号后追高。登录或开启免费体验,创建账号后可前往订阅中心。
</div>
<div class="tabs">
<button class="tab active" id="tabRegister" onclick="setTab('register')">创建账号</button>
<button class="tab" id="tabLogin" onclick="setTab('login')">会员登录</button>
@ -211,11 +206,10 @@ a { color: inherit; text-decoration: none; }
</button>
</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>
<a class="back-link" href="/subscription">前往订阅中心</a>
</div>
<script>
@ -297,16 +291,35 @@ async function resendCode(){
setMsg('regMsg', data.dev_verification_code ? '验证码:'+data.dev_verification_code : '验证码已重新发送','ok');
}catch(e){ setMsg('regMsg', e.message, 'err'); }
}
var loginSubmitting = false;
async function loginUser(){
if(loginSubmitting) return;
loginSubmitting = true;
var btn = $('loginSubmitBtn');
if(btn) btn.disabled = true;
try{
var data = await post('/api/auth/login',{email:$('loginEmail').value,password:$('loginPassword').value});
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);
}catch(e){ setMsg('loginMsg', e.message, 'err'); }
}catch(e){
setMsg('loginMsg', e.message, 'err');
loginSubmitting = false;
if(btn) btn.disabled = false;
}
}
(function(){
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>
</body>

View File

@ -174,24 +174,20 @@ a { color: inherit; text-decoration: none; }
<span class="brand-name">AlphaX Agent</span>
</a>
<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 == '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 | default('app') == 'app' %}active{% endif %}" href="/app"><svg class="link-icon"><use href="#svg-dashboard"/></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 == '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 == '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 == '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 == '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>
<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 == '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>

View File

@ -153,7 +153,7 @@ h1 { font-size: clamp(36px, 5.5vw, 56px); font-weight: 500; line-height: 1.1; le
</div>
<div class="user-dropdown" id="userDropdown">
<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>
</div>
</div>

View File

@ -77,7 +77,7 @@
<div id="guideBox" class="guide-box">
<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 class="status-bar" id="statusBar" style="display:none">
@ -152,7 +152,7 @@ async function loadMe() {
document.getElementById('guideBox').classList.add('show');
if (params.get('expired') === '1') {
document.getElementById('guideTitle').textContent = '订阅已到期,请先续订';
document.getElementById('guideText').textContent = '当前账号没有有效订阅。续订或开通套餐后,才能继续访问看板、策略、迭代和舆情页面。';
document.getElementById('guideText').textContent = '当前账号没有有效订阅。续订或开通套餐后,才能继续访问机会总览、策略、迭代和舆情页面。';
} else if (!s) {
document.getElementById('guideTitle').textContent = '欢迎使用 AlphaX Agent请先开通套餐';
document.getElementById('guideText').textContent = '新用户可领取 30 天免费体验。开通后即可进入完整功能页面。';

View File

@ -94,6 +94,91 @@ def test_archive_filter_separates_executed_and_invalid(temp_db):
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):
_insert_recommendation(
temp_db,