feat: archive entry type from review, prune stale trades on sync, manual delete
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+115
-8
@@ -15,7 +15,11 @@ from hub_ohlcv_lib import (
|
|||||||
aggregate_ohlcv_bars,
|
aggregate_ohlcv_bars,
|
||||||
normalize_chart_timeframe,
|
normalize_chart_timeframe,
|
||||||
)
|
)
|
||||||
from hub_trades_lib import effective_entry_type, effective_hold_minutes, format_hold_minutes
|
from hub_trades_lib import (
|
||||||
|
display_entry_type_label,
|
||||||
|
effective_hold_minutes,
|
||||||
|
format_hold_minutes,
|
||||||
|
)
|
||||||
|
|
||||||
ARCHIVE_TIMEFRAMES = frozenset({"5m", "15m", "1h", "4h"})
|
ARCHIVE_TIMEFRAMES = frozenset({"5m", "15m", "1h", "4h"})
|
||||||
ARCHIVE_DEFAULT_TIMEFRAME = "15m"
|
ARCHIVE_DEFAULT_TIMEFRAME = "15m"
|
||||||
@@ -160,18 +164,114 @@ def _parse_dt_ms(raw: Any) -> int | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _trade_entry_reason_for_cache(t: dict[str, Any]) -> str:
|
||||||
|
for key in ("entry_type", "entry_reason", "reviewed_entry_reason"):
|
||||||
|
raw = t.get(key)
|
||||||
|
if raw is not None and str(raw).strip():
|
||||||
|
return str(raw).strip()
|
||||||
|
return display_entry_type_label(t) if isinstance(t, dict) else ""
|
||||||
|
|
||||||
|
|
||||||
|
def purge_stale_trades_cache(
|
||||||
|
exchange_key: str,
|
||||||
|
active_trade_ids: list[int] | set[int],
|
||||||
|
*,
|
||||||
|
db_path: Path | None = None,
|
||||||
|
) -> int:
|
||||||
|
"""删除该所缓存中已不在复盘/交易记录里的条目。"""
|
||||||
|
ex_k = (exchange_key or "").strip().lower()
|
||||||
|
if not ex_k:
|
||||||
|
return 0
|
||||||
|
ids: list[int] = []
|
||||||
|
for raw in active_trade_ids or []:
|
||||||
|
try:
|
||||||
|
ids.append(int(raw))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
conn = _connect(db_path)
|
||||||
|
try:
|
||||||
|
if not ids:
|
||||||
|
rows = conn.execute(
|
||||||
|
"SELECT trade_id FROM archive_trade_cache WHERE exchange_key=?",
|
||||||
|
(ex_k,),
|
||||||
|
).fetchall()
|
||||||
|
stale_ids = [int(r["trade_id"]) for r in rows]
|
||||||
|
cur = conn.execute(
|
||||||
|
"DELETE FROM archive_trade_cache WHERE exchange_key=?",
|
||||||
|
(ex_k,),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
placeholders = ",".join("?" * len(ids))
|
||||||
|
rows = conn.execute(
|
||||||
|
f"""
|
||||||
|
SELECT trade_id FROM archive_trade_cache
|
||||||
|
WHERE exchange_key=? AND trade_id NOT IN ({placeholders})
|
||||||
|
""",
|
||||||
|
(ex_k, *ids),
|
||||||
|
).fetchall()
|
||||||
|
stale_ids = [int(r["trade_id"]) for r in rows]
|
||||||
|
cur = conn.execute(
|
||||||
|
f"""
|
||||||
|
DELETE FROM archive_trade_cache
|
||||||
|
WHERE exchange_key=? AND trade_id NOT IN ({placeholders})
|
||||||
|
""",
|
||||||
|
(ex_k, *ids),
|
||||||
|
)
|
||||||
|
removed = int(cur.rowcount or 0)
|
||||||
|
if stale_ids:
|
||||||
|
ph2 = ",".join("?" * len(stale_ids))
|
||||||
|
conn.execute(
|
||||||
|
f"""
|
||||||
|
DELETE FROM trade_overlay
|
||||||
|
WHERE exchange_key=? AND trade_id IN ({ph2})
|
||||||
|
""",
|
||||||
|
(ex_k, *stale_ids),
|
||||||
|
)
|
||||||
|
return removed
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def delete_trade_from_archive(
|
||||||
|
exchange_key: str,
|
||||||
|
trade_id: int,
|
||||||
|
*,
|
||||||
|
db_path: Path | None = None,
|
||||||
|
) -> bool:
|
||||||
|
ex_k = (exchange_key or "").strip().lower()
|
||||||
|
tid = int(trade_id)
|
||||||
|
conn = _connect(db_path)
|
||||||
|
try:
|
||||||
|
cur = conn.execute(
|
||||||
|
"""
|
||||||
|
DELETE FROM archive_trade_cache
|
||||||
|
WHERE exchange_key=? AND trade_id=?
|
||||||
|
""",
|
||||||
|
(ex_k, tid),
|
||||||
|
)
|
||||||
|
conn.execute(
|
||||||
|
"DELETE FROM trade_overlay WHERE exchange_key=? AND trade_id=?",
|
||||||
|
(ex_k, tid),
|
||||||
|
)
|
||||||
|
return int(cur.rowcount or 0) > 0
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
def upsert_trades_cache(
|
def upsert_trades_cache(
|
||||||
exchange_key: str,
|
exchange_key: str,
|
||||||
trades: list[dict[str, Any]],
|
trades: list[dict[str, Any]],
|
||||||
*,
|
*,
|
||||||
db_path: Path | None = None,
|
db_path: Path | None = None,
|
||||||
) -> int:
|
prune_missing: bool = True,
|
||||||
|
) -> dict[str, int]:
|
||||||
init_db(db_path)
|
init_db(db_path)
|
||||||
ex_k = (exchange_key or "").strip().lower()
|
ex_k = (exchange_key or "").strip().lower()
|
||||||
if not ex_k:
|
if not ex_k:
|
||||||
return 0
|
return {"upserted": 0, "removed": 0}
|
||||||
now = _now_ms()
|
now = _now_ms()
|
||||||
n = 0
|
n = 0
|
||||||
|
active_ids: list[int] = []
|
||||||
conn = _connect(db_path)
|
conn = _connect(db_path)
|
||||||
try:
|
try:
|
||||||
for t in trades or []:
|
for t in trades or []:
|
||||||
@@ -182,7 +282,9 @@ def upsert_trades_cache(
|
|||||||
sym = (t.get("symbol") or "").strip().upper()
|
sym = (t.get("symbol") or "").strip().upper()
|
||||||
if not sym:
|
if not sym:
|
||||||
continue
|
continue
|
||||||
|
active_ids.append(tid)
|
||||||
payload = {k: t.get(k) for k in t.keys()}
|
payload = {k: t.get(k) for k in t.keys()}
|
||||||
|
entry_label = _trade_entry_reason_for_cache(t)
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO archive_trade_cache (
|
INSERT INTO archive_trade_cache (
|
||||||
@@ -216,7 +318,7 @@ def upsert_trades_cache(
|
|||||||
t.get("opened_at_ms") or _parse_dt_ms(t.get("opened_at")),
|
t.get("opened_at_ms") or _parse_dt_ms(t.get("opened_at")),
|
||||||
t.get("closed_at_ms") or _parse_dt_ms(t.get("closed_at")),
|
t.get("closed_at_ms") or _parse_dt_ms(t.get("closed_at")),
|
||||||
t.get("monitor_type"),
|
t.get("monitor_type"),
|
||||||
t.get("entry_reason"),
|
entry_label,
|
||||||
json.dumps(payload, ensure_ascii=False, default=str),
|
json.dumps(payload, ensure_ascii=False, default=str),
|
||||||
now,
|
now,
|
||||||
),
|
),
|
||||||
@@ -224,7 +326,10 @@ def upsert_trades_cache(
|
|||||||
n += 1
|
n += 1
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
return n
|
removed = 0
|
||||||
|
if prune_missing:
|
||||||
|
removed = purge_stale_trades_cache(ex_k, active_ids, db_path=db_path)
|
||||||
|
return {"upserted": n, "removed": removed}
|
||||||
|
|
||||||
|
|
||||||
def _enrich_trade_display_fields(out: dict[str, Any]) -> dict[str, Any]:
|
def _enrich_trade_display_fields(out: dict[str, Any]) -> dict[str, Any]:
|
||||||
@@ -243,8 +348,8 @@ def _enrich_trade_display_fields(out: dict[str, Any]) -> dict[str, Any]:
|
|||||||
out["closed_at"] = datetime.fromtimestamp(int(closed_ms) / 1000).strftime(
|
out["closed_at"] = datetime.fromtimestamp(int(closed_ms) / 1000).strftime(
|
||||||
"%Y-%m-%d %H:%M:%S"
|
"%Y-%m-%d %H:%M:%S"
|
||||||
)
|
)
|
||||||
entry_type = (out.get("entry_type") or effective_entry_type(out) or "").strip()
|
entry_type = display_entry_type_label(out)
|
||||||
if entry_type:
|
if entry_type and entry_type != "—":
|
||||||
out["entry_type"] = entry_type
|
out["entry_type"] = entry_type
|
||||||
out["entry_reason"] = entry_type
|
out["entry_reason"] = entry_type
|
||||||
hold_m = out.get("hold_minutes")
|
hold_m = out.get("hold_minutes")
|
||||||
@@ -961,7 +1066,7 @@ def sync_exchange_symbol_archives(
|
|||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""同步单所:交易缓存 + 各币种 K 线种子/增量。"""
|
"""同步单所:交易缓存 + 各币种 K 线种子/增量。"""
|
||||||
ex_k = (exchange_key or "").strip().lower()
|
ex_k = (exchange_key or "").strip().lower()
|
||||||
upsert_trades_cache(ex_k, trades, db_path=db_path)
|
cache_stats = upsert_trades_cache(ex_k, trades, db_path=db_path, prune_missing=True)
|
||||||
|
|
||||||
by_sym: dict[str, int] = {}
|
by_sym: dict[str, int] = {}
|
||||||
for t in trades or []:
|
for t in trades or []:
|
||||||
@@ -997,6 +1102,8 @@ def sync_exchange_symbol_archives(
|
|||||||
"ok": True,
|
"ok": True,
|
||||||
"exchange_key": ex_k,
|
"exchange_key": ex_k,
|
||||||
"symbols": len(by_sym),
|
"symbols": len(by_sym),
|
||||||
|
"trades_upserted": int(cache_stats.get("upserted") or 0),
|
||||||
|
"trades_removed": int(cache_stats.get("removed") or 0),
|
||||||
"seed_bars": seeded,
|
"seed_bars": seeded,
|
||||||
"appended_bars": appended,
|
"appended_bars": appended,
|
||||||
"trades": len(trades or []),
|
"trades": len(trades or []),
|
||||||
|
|||||||
+12
-1
@@ -119,9 +119,20 @@ def effective_entry_type(d: dict) -> str:
|
|||||||
kst = str(d.get("key_signal_type") or "").strip()
|
kst = str(d.get("key_signal_type") or "").strip()
|
||||||
if kst:
|
if kst:
|
||||||
return kst
|
return kst
|
||||||
|
legacy = str(d.get("entry_type") or "").strip()
|
||||||
|
if legacy and legacy not in ("trend_pullback", "roll", "trend"):
|
||||||
|
return _normalize_monitor_type_label(legacy) or legacy
|
||||||
return mt
|
return mt
|
||||||
|
|
||||||
|
|
||||||
|
def display_entry_type_label(d: dict) -> str:
|
||||||
|
"""档案/列表展示用开仓类型(不回落为「下单监控」若已有复盘或建档类型)。"""
|
||||||
|
label = effective_entry_type(d).strip()
|
||||||
|
if not label:
|
||||||
|
return "—"
|
||||||
|
return _normalize_monitor_type_label(label) or label
|
||||||
|
|
||||||
|
|
||||||
def effective_hold_minutes(
|
def effective_hold_minutes(
|
||||||
d: dict,
|
d: dict,
|
||||||
*,
|
*,
|
||||||
@@ -281,7 +292,7 @@ def _normalize_archive_trade_row(
|
|||||||
opened_ms_i = int(opened_ms) if opened_ms else None
|
opened_ms_i = int(opened_ms) if opened_ms else None
|
||||||
closed_ms_i = int(closed_ms) if closed_ms else None
|
closed_ms_i = int(closed_ms) if closed_ms else None
|
||||||
hold_m = effective_hold_minutes(d, opened_ms=opened_ms_i, closed_ms=closed_ms_i)
|
hold_m = effective_hold_minutes(d, opened_ms=opened_ms_i, closed_ms=closed_ms_i)
|
||||||
entry_type = effective_entry_type(d)
|
entry_type = display_entry_type_label(d)
|
||||||
reviewed = bool(
|
reviewed = bool(
|
||||||
d.get("reviewed_at")
|
d.get("reviewed_at")
|
||||||
or d.get("reviewed_result")
|
or d.get("reviewed_result")
|
||||||
|
|||||||
@@ -1835,6 +1835,20 @@ def api_archive_trade_overlay(
|
|||||||
return {"ok": True, "overlay": out}
|
return {"ok": True, "overlay": out}
|
||||||
|
|
||||||
|
|
||||||
|
@app.delete("/api/archive/trade/{exchange_key}/{trade_id}")
|
||||||
|
def api_archive_trade_delete(exchange_key: str, trade_id: int):
|
||||||
|
from hub_symbol_archive_lib import delete_trade_from_archive
|
||||||
|
|
||||||
|
ex_k = (exchange_key or "").strip().lower()
|
||||||
|
if not ex_k:
|
||||||
|
raise HTTPException(status_code=400, detail="缺少 exchange_key")
|
||||||
|
init_archive_db()
|
||||||
|
removed = delete_trade_from_archive(ex_k, int(trade_id))
|
||||||
|
if not removed:
|
||||||
|
raise HTTPException(status_code=404, detail="档案中无该笔交易")
|
||||||
|
return {"ok": True, "exchange_key": ex_k, "trade_id": int(trade_id)}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/archive/sync")
|
@app.post("/api/archive/sync")
|
||||||
async def api_archive_sync():
|
async def api_archive_sync():
|
||||||
body = await _run_archive_sync_once()
|
body = await _run_archive_sync_once()
|
||||||
|
|||||||
@@ -4063,6 +4063,18 @@ body.hub-page-ai #page-ai {
|
|||||||
.archive-trades-table td.neg {
|
.archive-trades-table td.neg {
|
||||||
color: #ef4444;
|
color: #ef4444;
|
||||||
}
|
}
|
||||||
|
.archive-del-btn {
|
||||||
|
padding: 3px 8px;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid rgba(239, 68, 68, 0.35);
|
||||||
|
background: rgba(239, 68, 68, 0.08);
|
||||||
|
color: #f87171;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.archive-del-btn:hover {
|
||||||
|
background: rgba(239, 68, 68, 0.16);
|
||||||
|
}
|
||||||
.archive-tag-select,
|
.archive-tag-select,
|
||||||
.archive-note-input {
|
.archive-note-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -109,14 +109,25 @@
|
|||||||
return mins + "分钟";
|
return mins + "分钟";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ENTRY_TYPE_LABELS = {
|
||||||
|
trend_pullback: "趋势回调",
|
||||||
|
roll: "顺势加仓",
|
||||||
|
trend: "趋势回调",
|
||||||
|
};
|
||||||
|
|
||||||
function fmtEntryType(tr) {
|
function fmtEntryType(tr) {
|
||||||
if (!tr) return "—";
|
if (!tr) return "—";
|
||||||
return (
|
const raw = String(
|
||||||
tr.entry_type ||
|
tr.entry_type || tr.entry_reason || tr.reviewed_entry_reason || ""
|
||||||
tr.entry_reason ||
|
).trim();
|
||||||
tr.monitor_type ||
|
if (raw) {
|
||||||
"—"
|
return ENTRY_TYPE_LABELS[raw] || raw;
|
||||||
);
|
}
|
||||||
|
const mt = String(tr.monitor_type || "").trim();
|
||||||
|
if (mt && mt !== "下单监控") {
|
||||||
|
return ENTRY_TYPE_LABELS[mt] || mt;
|
||||||
|
}
|
||||||
|
return "—";
|
||||||
}
|
}
|
||||||
|
|
||||||
function reviewMark(tr) {
|
function reviewMark(tr) {
|
||||||
@@ -551,7 +562,7 @@
|
|||||||
elTrades.innerHTML =
|
elTrades.innerHTML =
|
||||||
'<table class="archive-trades-table"><thead><tr>' +
|
'<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><th>备注</th><th>操作</th>" +
|
||||||
"</tr></thead><tbody>" +
|
"</tr></thead><tbody>" +
|
||||||
trades
|
trades
|
||||||
.map(function (t) {
|
.map(function (t) {
|
||||||
@@ -618,15 +629,30 @@
|
|||||||
'" value="' +
|
'" value="' +
|
||||||
String(t.note || "").replace(/"/g, """) +
|
String(t.note || "").replace(/"/g, """) +
|
||||||
'" placeholder="备注" /></td>' +
|
'" placeholder="备注" /></td>' +
|
||||||
|
'<td><button type="button" class="archive-del-btn" data-id="' +
|
||||||
|
tid +
|
||||||
|
'" title="从档案移除(不影响实例复盘库)">删除</button></td>' +
|
||||||
"</tr>"
|
"</tr>"
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.join("") +
|
.join("") +
|
||||||
"</tbody></table>";
|
"</tbody></table>";
|
||||||
|
|
||||||
|
elTrades.querySelectorAll(".archive-del-btn").forEach(function (btn) {
|
||||||
|
btn.addEventListener("click", function (ev) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
void deleteTrade(btn.getAttribute("data-id"));
|
||||||
|
});
|
||||||
|
});
|
||||||
elTrades.querySelectorAll(".archive-trade-row").forEach(function (row) {
|
elTrades.querySelectorAll(".archive-trade-row").forEach(function (row) {
|
||||||
row.addEventListener("click", function (ev) {
|
row.addEventListener("click", function (ev) {
|
||||||
if (ev.target.closest("select") || ev.target.closest("input")) return;
|
if (
|
||||||
|
ev.target.closest("select") ||
|
||||||
|
ev.target.closest("input") ||
|
||||||
|
ev.target.closest(".archive-del-btn")
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
selectedTradeId = row.getAttribute("data-id");
|
selectedTradeId = row.getAttribute("data-id");
|
||||||
renderTrades();
|
renderTrades();
|
||||||
applyChartMarkers();
|
applyChartMarkers();
|
||||||
@@ -650,6 +676,32 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function deleteTrade(tradeId) {
|
||||||
|
if (!selected || tradeId == null) return;
|
||||||
|
if (!window.confirm("从币种档案移除该笔交易?(不影响交易所实例里的复盘记录)")) return;
|
||||||
|
const r = await apiFetch(
|
||||||
|
"/api/archive/trade/" + selected.exchange_key + "/" + tradeId,
|
||||||
|
{ method: "DELETE" }
|
||||||
|
);
|
||||||
|
if (!r.ok) {
|
||||||
|
const j = await r.json().catch(function () {
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
setStatus(j.detail || j.msg || "删除失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (String(selectedTradeId) === String(tradeId)) {
|
||||||
|
selectedTradeId = null;
|
||||||
|
}
|
||||||
|
trades = trades.filter(function (t) {
|
||||||
|
return String(t.trade_id || t.id) !== String(tradeId);
|
||||||
|
});
|
||||||
|
renderTrades();
|
||||||
|
applyChartMarkers();
|
||||||
|
await loadList();
|
||||||
|
setStatus("已移除 1 笔档案记录");
|
||||||
|
}
|
||||||
|
|
||||||
async function saveOverlay(tradeId, tag, note) {
|
async function saveOverlay(tradeId, tag, note) {
|
||||||
if (!selected) return;
|
if (!selected) return;
|
||||||
const body = { behavior_tag: tag || "", note: note != null ? note : undefined };
|
const body = { behavior_tag: tag || "", note: note != null ? note : undefined };
|
||||||
@@ -742,7 +794,12 @@
|
|||||||
if (row.ok === false) {
|
if (row.ok === false) {
|
||||||
parts.push(label + " 失败: " + (row.msg || "未知错误"));
|
parts.push(label + " 失败: " + (row.msg || "未知错误"));
|
||||||
} else {
|
} else {
|
||||||
parts.push(label + " " + (row.trade_count != null ? row.trade_count : row.trades || 0) + " 笔");
|
let line =
|
||||||
|
label + " " + (row.trade_count != null ? row.trade_count : row.trades || 0) + " 笔";
|
||||||
|
if (row.trades_removed > 0) {
|
||||||
|
line += " 清" + row.trades_removed;
|
||||||
|
}
|
||||||
|
parts.push(line);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return parts.join(" · ");
|
return parts.join(" · ");
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from hub_ohlcv_lib import aggregate_ohlcv_bars
|
|||||||
from hub_symbol_archive_lib import (
|
from hub_symbol_archive_lib import (
|
||||||
_fill_missing_bars,
|
_fill_missing_bars,
|
||||||
init_db,
|
init_db,
|
||||||
|
load_symbol_trades,
|
||||||
resolve_archive_chart,
|
resolve_archive_chart,
|
||||||
upsert_bars_5m,
|
upsert_bars_5m,
|
||||||
upsert_trade_overlay,
|
upsert_trade_overlay,
|
||||||
@@ -134,6 +135,31 @@ def test_resolve_archive_chart_history_range():
|
|||||||
assert len(out["candles"]) >= 40
|
assert len(out["candles"]) >= 40
|
||||||
|
|
||||||
|
|
||||||
|
def test_sync_prunes_missing_trades():
|
||||||
|
with tempfile.TemporaryDirectory() as td:
|
||||||
|
db = Path(td) / "archive.db"
|
||||||
|
init_db(db)
|
||||||
|
upsert_trades_cache(
|
||||||
|
"gate",
|
||||||
|
[
|
||||||
|
{"id": 1, "symbol": "BNB/USDT", "result": "止损", "pnl_amount": -1},
|
||||||
|
{"id": 2, "symbol": "BNB/USDT", "result": "止盈", "pnl_amount": 1},
|
||||||
|
],
|
||||||
|
db_path=db,
|
||||||
|
prune_missing=False,
|
||||||
|
)
|
||||||
|
stats = upsert_trades_cache(
|
||||||
|
"gate",
|
||||||
|
[{"id": 1, "symbol": "BNB/USDT", "result": "止损", "pnl_amount": -1}],
|
||||||
|
db_path=db,
|
||||||
|
prune_missing=True,
|
||||||
|
)
|
||||||
|
rows = load_symbol_trades("gate", "BNB/USDT", db_path=db)
|
||||||
|
assert len(rows) == 1
|
||||||
|
assert rows[0]["trade_id"] == 1
|
||||||
|
assert stats["removed"] == 1
|
||||||
|
|
||||||
|
|
||||||
def test_list_with_overlay_filters():
|
def test_list_with_overlay_filters():
|
||||||
with tempfile.TemporaryDirectory() as td:
|
with tempfile.TemporaryDirectory() as td:
|
||||||
db = Path(td) / "archive.db"
|
db = Path(td) / "archive.db"
|
||||||
|
|||||||
@@ -8,10 +8,24 @@ from datetime import datetime, timedelta
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from hub_symbol_archive_lib import init_db, load_symbol_trades, upsert_trades_cache
|
from hub_symbol_archive_lib import init_db, load_symbol_trades, upsert_trades_cache
|
||||||
from hub_trades_lib import _normalize_archive_trade_row, effective_entry_type, effective_hold_minutes
|
from hub_trades_lib import (
|
||||||
|
_normalize_archive_trade_row,
|
||||||
|
display_entry_type_label,
|
||||||
|
effective_entry_type,
|
||||||
|
effective_hold_minutes,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestHubTradesReviewFields(unittest.TestCase):
|
class TestHubTradesReviewFields(unittest.TestCase):
|
||||||
|
def test_display_entry_type_for_manual_monitor_review(self):
|
||||||
|
d = {
|
||||||
|
"monitor_type": "下单监控",
|
||||||
|
"entry_reason": "",
|
||||||
|
"reviewed_entry_reason": "突破回踩",
|
||||||
|
"reviewed_at": "2026-06-08 10:00:00",
|
||||||
|
}
|
||||||
|
self.assertEqual(display_entry_type_label(d), "突破回踩")
|
||||||
|
|
||||||
def test_effective_entry_type_prefers_reviewed(self):
|
def test_effective_entry_type_prefers_reviewed(self):
|
||||||
d = {
|
d = {
|
||||||
"entry_reason": "突破回踩",
|
"entry_reason": "突破回踩",
|
||||||
|
|||||||
Reference in New Issue
Block a user