fix(hub): archive labels, symbol column, and time-close dropdown

Force exchange_key on archive sync; add contract column and tag select styles on inner-light-mind; serve time_close_ui.js on instances so order/key time-close duration can be selected.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-11 19:44:51 +08:00
parent 959593cdab
commit a9637fafb2
16 changed files with 147 additions and 44 deletions
+7 -5
View File
@@ -376,14 +376,16 @@
<label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
<input type="checkbox" name="breakeven_enabled" value="1" checked> 启用移动保本(关闭则仅保留初始止损与交易所挂单)
</label>
<label id="order-time-close-wrap" class="order-time-close-wrap" style="display:inline-flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
<input type="checkbox" name="time_close_enabled" value="1" id="order-time-close-cb"> 时间平仓
<select name="time_close_hours" id="order-time-close-hours" disabled>
<span id="order-time-close-wrap" class="order-time-close-wrap" style="display:inline-flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
<label style="display:inline-flex;align-items:center;gap:4px;margin:0;cursor:pointer">
<input type="checkbox" name="time_close_enabled" value="1" id="order-time-close-cb"> 时间平仓
</label>
<select name="time_close_hours" id="order-time-close-hours" title="持仓满该时长后自动平仓">
<option value="1">1h</option>
<option value="2">2h</option>
<option value="4" selected>4h</option>
</select>
</label>
</span>
<label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
<input type="checkbox" name="order_chart" value="true"> 开仓后生成多周期K线图(各周期100根,含开平仓标记)
</label>
@@ -799,7 +801,7 @@
</div>
<script src="/static/instance_ui.js?v=1"></script>
<script src="/static/time_close_ui.js?v=1"></script>
<script src="/static/time_close_ui.js?v=2"></script>
<script src="/static/ai_review_render.js?v=2"></script>
<script src="/static/form_submit_guard.js?v=2"></script>
<script>
+7 -5
View File
@@ -356,14 +356,16 @@
<label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
<input type="checkbox" name="breakeven_enabled" value="1" checked> 启用移动保本(关闭则仅保留初始止损与交易所挂单)
</label>
<label id="order-time-close-wrap" class="order-time-close-wrap" style="display:inline-flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
<input type="checkbox" name="time_close_enabled" value="1" id="order-time-close-cb"> 时间平仓
<select name="time_close_hours" id="order-time-close-hours" disabled>
<span id="order-time-close-wrap" class="order-time-close-wrap" style="display:inline-flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
<label style="display:inline-flex;align-items:center;gap:4px;margin:0;cursor:pointer">
<input type="checkbox" name="time_close_enabled" value="1" id="order-time-close-cb"> 时间平仓
</label>
<select name="time_close_hours" id="order-time-close-hours" title="持仓满该时长后自动平仓">
<option value="1">1h</option>
<option value="2">2h</option>
<option value="4" selected>4h</option>
</select>
</label>
</span>
<label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
<input type="checkbox" name="order_chart" value="true"> 开仓后生成多周期K线图(各周期100根,含开平仓标记)
</label>
@@ -779,7 +781,7 @@
</div>
<script src="/static/instance_ui.js?v=1"></script>
<script src="/static/time_close_ui.js?v=1"></script>
<script src="/static/time_close_ui.js?v=2"></script>
<script src="/static/ai_review_render.js?v=2"></script>
<script src="/static/form_submit_guard.js?v=2"></script>
<script>
+7 -5
View File
@@ -356,14 +356,16 @@
<label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
<input type="checkbox" name="breakeven_enabled" value="1" checked> 启用移动保本(关闭则仅保留初始止损与交易所挂单)
</label>
<label id="order-time-close-wrap" class="order-time-close-wrap" style="display:inline-flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
<input type="checkbox" name="time_close_enabled" value="1" id="order-time-close-cb"> 时间平仓
<select name="time_close_hours" id="order-time-close-hours" disabled>
<span id="order-time-close-wrap" class="order-time-close-wrap" style="display:inline-flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
<label style="display:inline-flex;align-items:center;gap:4px;margin:0;cursor:pointer">
<input type="checkbox" name="time_close_enabled" value="1" id="order-time-close-cb"> 时间平仓
</label>
<select name="time_close_hours" id="order-time-close-hours" title="持仓满该时长后自动平仓">
<option value="1">1h</option>
<option value="2">2h</option>
<option value="4" selected>4h</option>
</select>
</label>
</span>
<label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
<input type="checkbox" name="order_chart" value="true"> 开仓后生成多周期K线图(各周期100根,含开平仓标记)
</label>
@@ -779,7 +781,7 @@
</div>
<script src="/static/instance_ui.js?v=1"></script>
<script src="/static/time_close_ui.js?v=1"></script>
<script src="/static/time_close_ui.js?v=2"></script>
<script src="/static/ai_review_render.js?v=2"></script>
<script src="/static/form_submit_guard.js?v=2"></script>
<script>
+7 -5
View File
@@ -385,14 +385,16 @@
<label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
<input type="checkbox" name="breakeven_enabled" value="1" checked> 启用移动保本(关闭则仅保留初始止损与交易所挂单)
</label>
<label id="order-time-close-wrap" class="order-time-close-wrap" style="display:inline-flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
<input type="checkbox" name="time_close_enabled" value="1" id="order-time-close-cb"> 时间平仓
<select name="time_close_hours" id="order-time-close-hours" disabled>
<span id="order-time-close-wrap" class="order-time-close-wrap" style="display:inline-flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
<label style="display:inline-flex;align-items:center;gap:4px;margin:0;cursor:pointer">
<input type="checkbox" name="time_close_enabled" value="1" id="order-time-close-cb"> 时间平仓
</label>
<select name="time_close_hours" id="order-time-close-hours" title="持仓满该时长后自动平仓">
<option value="1">1h</option>
<option value="2">2h</option>
<option value="4" selected>4h</option>
</select>
</label>
</span>
<label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
<input type="checkbox" name="order_chart" value="true"> 开仓后生成多周期K线图(各周期100根,含开平仓标记)
</label>
@@ -808,7 +810,7 @@
</div>
<script src="/static/instance_ui.js?v=1"></script>
<script src="/static/time_close_ui.js?v=1"></script>
<script src="/static/time_close_ui.js?v=2"></script>
<script src="/static/ai_review_render.js?v=2"></script>
<script src="/static/form_submit_guard.js?v=2"></script>
<script>
+2
View File
@@ -55,6 +55,7 @@ def install_instance_theme_static(app) -> None:
"instance_ui.js": "application/javascript; charset=utf-8",
"ai_review_render.js": "application/javascript; charset=utf-8",
"form_submit_guard.js": "application/javascript; charset=utf-8",
"time_close_ui.js": "application/javascript; charset=utf-8",
"focus_chart_page.js": "application/javascript; charset=utf-8",
"focus_chart_page.css": "text/css; charset=utf-8",
}
@@ -450,6 +451,7 @@ def register_hub_routes(app):
try:
trades = fetch_trades_for_archive(
conn,
exchange_key=str(c.get("exchange") or ""),
days=days,
row_to_dict_fn=c.get("row_to_dict"),
reset_hour=reset_hour,
+8 -1
View File
@@ -320,7 +320,10 @@ def upsert_trades_cache(
if not sym:
continue
active_ids.append(tid)
payload = {k: t.get(k) for k in t.keys()}
row = dict(t)
row["exchange_key"] = ex_k
row.pop("account_exchange_key", None)
payload = {k: row.get(k) for k in row.keys()}
entry_label = _trade_entry_reason_for_cache(t)
conn.execute(
"""
@@ -441,6 +444,10 @@ def _trade_row_to_dict(row: sqlite3.Row, overlay: dict | None = None) -> dict[st
out["behavior_tag"] = ov.get("behavior_tag") or ""
out["note"] = ov.get("note") or ""
out["trade_id"] = out.get("trade_id") or out.get("id")
ex_col = str(d.get("exchange_key") or "").strip().lower()
if ex_col:
out["exchange_key"] = ex_col
out.pop("account_exchange_key", None)
return _enrich_trade_display_fields(out)
+11 -2
View File
@@ -422,6 +422,7 @@ def _existing_trend_plan_ids(conn) -> set[int]:
def _normalize_snapshot_archive_row(
snap: dict,
*,
exchange_key: str = "",
reset_hour: int = 8,
) -> dict[str, Any] | None:
result = str(snap.get("result_label") or "").strip()
@@ -458,6 +459,7 @@ def _normalize_snapshot_archive_row(
entry_type = entry_reason_for_monitor_type(monitor_type) or monitor_type
return {
"id": -snap_id,
"exchange_key": (exchange_key or "").strip().lower(),
"symbol": (snap.get("symbol") or "").strip().upper(),
"direction": snap.get("direction"),
"result": result,
@@ -495,6 +497,7 @@ def _parse_ms_from_row(raw: Any) -> int | None:
def _fetch_strategy_snapshots_for_archive(
conn,
*,
exchange_key: str = "",
days: int = 365,
reset_hour: int = 8,
limit: int = 2000,
@@ -527,7 +530,9 @@ def _fetch_strategy_snapshots_for_archive(
source_id = 0
if source_id > 0 and source_id in skip:
continue
norm = _normalize_snapshot_archive_row(d, reset_hour=reset_hour)
norm = _normalize_snapshot_archive_row(
d, exchange_key=exchange_key, reset_hour=reset_hour
)
if norm:
out.append(norm)
if len(out) >= lim:
@@ -538,6 +543,7 @@ def _fetch_strategy_snapshots_for_archive(
def fetch_trades_for_archive(
conn,
*,
exchange_key: str = "",
days: int = 365,
row_to_dict_fn: Optional[Callable] = None,
reset_hour: int = 8,
@@ -565,7 +571,9 @@ def fetch_trades_for_archive(
records = []
for row in rows:
d = _row_dict(row, row_to_dict_fn)
norm = _normalize_archive_trade_row(d, reset_hour=reset_hour)
norm = _normalize_archive_trade_row(
d, exchange_key=exchange_key, reset_hour=reset_hour
)
if norm:
records.append(norm)
if len(records) >= lim:
@@ -586,6 +594,7 @@ def fetch_trades_for_archive(
snaps = _fetch_strategy_snapshots_for_archive(
conn,
days=days,
exchange_key=exchange_key,
reset_hour=reset_hour,
limit=max(0, lim - len(records)),
skip_plan_ids=skip_ids,
+1 -1
View File
@@ -325,7 +325,7 @@ async def _run_archive_sync_once() -> dict:
trades = trades_resp.get("trades") or []
for t in trades:
if isinstance(t, dict):
t.setdefault("exchange_key", ex_key)
t["exchange_key"] = ex_key
def remote_fetch(**kwargs):
return _fetch_instance_ohlcv_sync(
+20 -1
View File
@@ -5757,7 +5757,7 @@ body.funds-fullscreen-open {
}
.archive-trades-table {
width: 100%;
min-width: 920px;
min-width: 1000px;
border-collapse: collapse;
font-size: 0.78rem;
}
@@ -5768,6 +5768,10 @@ body.funds-fullscreen-open {
.archive-trades-table .archive-hold {
white-space: nowrap;
}
.archive-trades-table .archive-symbol {
white-space: nowrap;
font-weight: 500;
}
.archive-review-mark {
display: inline-block;
margin-right: 4px;
@@ -5854,6 +5858,21 @@ body.funds-fullscreen-open {
color: var(--text);
font-size: 0.75rem;
}
.archive-tag-select.is-tag-sick {
color: var(--red);
border-color: color-mix(in srgb, var(--red) 45%, var(--border-soft));
background: color-mix(in srgb, var(--red) 14%, var(--inset-surface));
}
.archive-tag-select.is-tag-emotion {
color: #60a5fa;
border-color: color-mix(in srgb, #60a5fa 45%, var(--border-soft));
background: color-mix(in srgb, #60a5fa 14%, var(--inset-surface));
}
.archive-trade-row.archive-trade-sick .archive-tag-select.is-tag-sick {
color: var(--red);
border-color: color-mix(in srgb, var(--red) 50%, var(--border-soft));
background: color-mix(in srgb, var(--red) 18%, var(--inset-surface));
}
.archive-empty {
padding: 16px;
color: var(--muted);
+19 -7
View File
@@ -223,10 +223,17 @@
function tradeRowExchange(tr) {
if (!tr) return "—";
const exKey = tr.exchange_key || tr.account_exchange_key || "";
if (exKey) return exchangeLabel(exKey);
const name = tr.account_name || tr.exchange_name || "";
return name || "—";
const exKey = String(tr.exchange_key || "").toLowerCase();
return exKey ? exchangeLabel(exKey) : "—";
}
function applyTagSelectStyle(sel) {
if (!sel) return;
const v = sel.value || "";
sel.classList.remove("is-tag-empty", "is-tag-sick", "is-tag-emotion");
if (v === "sick") sel.classList.add("is-tag-sick");
else if (v === "emotion") sel.classList.add("is-tag-emotion");
else sel.classList.add("is-tag-empty");
}
function exchangeLabel(exKey) {
@@ -898,7 +905,7 @@
async function openTradeChart(tr) {
if (!tr) return;
const exKey = tr.exchange_key || tr.account_exchange_key || "";
const exKey = String(tr.exchange_key || "").toLowerCase();
const sym = tr.symbol || "";
if (!exKey || !sym) {
setStatus("该笔交易缺少交易所或合约,无法加载图表");
@@ -920,13 +927,13 @@
}
elTrades.innerHTML =
'<table class="archive-trades-table"><thead><tr>' +
"<th>交易所</th><th>开仓类型</th><th>开仓时间</th><th>平仓时间</th><th>持仓时长</th>" +
"<th>交易所</th><th>合约</th><th>开仓类型</th><th>开仓时间</th><th>平仓时间</th><th>持仓时长</th>" +
"<th>方向</th><th>结果</th><th>盈亏</th><th>标签</th><th>备注</th><th>操作</th>" +
"</tr></thead><tbody>" +
dailyTrades
.map(function (t) {
const tid = t.trade_id || t.id;
const exKey = t.exchange_key || t.account_exchange_key || "";
const exKey = String(t.exchange_key || "").toLowerCase();
const tag = t.behavior_tag || "";
const sick = tag === "sick";
const active = String(tid) === String(selectedTradeId) ? " is-active" : "";
@@ -945,6 +952,9 @@
"<td>" +
esc(tradeRowExchange(t)) +
"</td>" +
'<td class="archive-symbol">' +
esc(t.symbol || "—") +
"</td>" +
"<td>" +
(rev ? '<span class="archive-review-mark">' + rev + "</span>" : "") +
esc(fmtEntryType(t)) +
@@ -1027,7 +1037,9 @@
});
});
elTrades.querySelectorAll(".archive-tag-select").forEach(function (sel) {
applyTagSelectStyle(sel);
sel.addEventListener("change", function () {
applyTagSelectStyle(sel);
saveOverlay(sel.getAttribute("data-id"), sel.getAttribute("data-ex"), sel.value, null);
});
});
+3 -3
View File
@@ -15,7 +15,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'" />
<noscript><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" /></noscript>
<link rel="stylesheet" href="/assets/app.css?v=20260612-sick-row-red-text" />
<link rel="stylesheet" href="/assets/app.css?v=20260612-trade-symbol-col" />
<link rel="stylesheet" href="/assets/dashboard.css?v=20260612-dash-monitor-count" />
</head>
<body>
@@ -584,11 +584,11 @@
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
<script src="/assets/chart_draw.js?v=20260609-market-day-split"></script>
<script src="/assets/chart.js?v=20260609-market-day-split"></script>
<script src="/assets/archive.js?v=20260612-stats-table-layout"></script>
<script src="/assets/archive.js?v=20260612-trade-symbol-col"></script>
<script src="/assets/funds.js?v=20260609-hub-funds-fold"></script>
<script src="/assets/dashboard.js?v=20260612-dash-monitor-count"></script>
<script src="/assets/ai_review_render.js?v=3"></script>
<script src="/assets/time_close_ui.js?v=1"></script>
<script src="/assets/time_close_ui.js?v=2"></script>
<script src="/assets/app.js?v=20260612-time-close"></script>
</body>
</html>
+8 -1
View File
@@ -23,9 +23,16 @@
if (!cb || !sel) return;
function sync() {
const on = !!cb.checked;
sel.disabled = !on;
sel.disabled = false;
sel.tabIndex = 0;
if (wrap) wrap.classList.toggle("is-disabled", !on);
}
sel.addEventListener("mousedown", function (ev) {
ev.stopPropagation();
});
sel.addEventListener("click", function (ev) {
ev.stopPropagation();
});
cb.addEventListener("change", sync);
sync();
}
+7 -3
View File
@@ -348,9 +348,13 @@ html[data-theme="light"] .pos-meta-item::after {
font-variant-numeric: tabular-nums;
letter-spacing: 0.02em;
}
.key-time-close-wrap.is-disabled select,
.order-time-close-wrap.is-disabled select {
opacity: 0.55;
.key-time-close-wrap.is-disabled > label,
.order-time-close-wrap.is-disabled > label {
opacity: 0.72;
}
.key-time-close-wrap select,
.order-time-close-wrap select {
cursor: pointer;
}
html[data-theme="light"] .pos-meta-on {
color: #006e9a !important;
+8 -1
View File
@@ -23,9 +23,16 @@
if (!cb || !sel) return;
function sync() {
const on = !!cb.checked;
sel.disabled = !on;
sel.disabled = false;
sel.tabIndex = 0;
if (wrap) wrap.classList.toggle("is-disabled", !on);
}
sel.addEventListener("mousedown", function (ev) {
ev.stopPropagation();
});
sel.addEventListener("click", function (ev) {
ev.stopPropagation();
});
cb.addEventListener("change", sync);
sync();
}
+6 -4
View File
@@ -157,14 +157,16 @@
<label id="key-breakeven-wrap" style="display:inline-flex;align-items:center;gap:4px;font-size:.85rem;color:#9aa">
<input type="checkbox" name="breakeven_enabled" value="1" id="key-breakeven-cb"> 移动保本
</label>
<label id="key-time-close-wrap" class="key-time-close-wrap" style="display:inline-flex;align-items:center;gap:4px;font-size:.85rem;color:#9aa">
<input type="checkbox" name="time_close_enabled" value="1" id="key-time-close-cb"> 时间平仓
<select name="time_close_hours" id="key-time-close-hours" disabled>
<span id="key-time-close-wrap" class="key-time-close-wrap" style="display:inline-flex;align-items:center;gap:4px;font-size:.85rem;color:#9aa">
<label style="display:inline-flex;align-items:center;gap:4px;margin:0;cursor:pointer">
<input type="checkbox" name="time_close_enabled" value="1" id="key-time-close-cb"> 时间平仓
</label>
<select name="time_close_hours" id="key-time-close-hours" title="持仓满该时长后自动平仓">
<option value="1">1h</option>
<option value="2">2h</option>
<option value="4" selected>4h</option>
</select>
</label>
</span>
<button type="submit">添加</button>
</form>
<details class="tip-collapse key-rule-collapse">
+26
View File
@@ -245,3 +245,29 @@ def test_resolve_archive_chart_history_uses_trade_span_not_200_bars():
assert out["ok"] is True
assert out["range_mode"] == "history"
assert out["bar_count"] > 200
def test_upsert_forces_sync_exchange_key():
with tempfile.TemporaryDirectory() as td:
db = Path(td) / "archive.db"
init_db(db)
upsert_trades_cache(
"gate_bot",
[
{
"id": 77,
"exchange_key": "gate",
"account_exchange_key": "gate",
"symbol": "ETH/USDT",
"result": "止损",
"pnl_amount": -1,
"opened_at_ms": 1_700_000_000_000,
"closed_at_ms": 1_700_007_200_000,
}
],
db_path=db,
)
rows = load_symbol_trades("gate_bot", "ETH/USDT", db_path=db)
assert len(rows) == 1
assert rows[0]["exchange_key"] == "gate_bot"
assert "account_exchange_key" not in rows[0]