fix(hub): align AI summary trades with records and restyle headings
- Query trade_records with trading-day window and reviewed fields instead of missing session_date column - Style summary headings and account names as red text with white stroke Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -446,6 +446,7 @@ def register_hub_routes(app):
|
|||||||
conn,
|
conn,
|
||||||
trading_day,
|
trading_day,
|
||||||
row_to_dict_fn=c.get("row_to_dict"),
|
row_to_dict_fn=c.get("row_to_dict"),
|
||||||
|
reset_hour=reset_hour,
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|||||||
+117
-36
@@ -4,6 +4,16 @@ from __future__ import annotations
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Any, Callable, Optional
|
from typing import Any, Callable, Optional
|
||||||
|
|
||||||
|
TRADE_COMPLETED_RESULTS = (
|
||||||
|
"止盈",
|
||||||
|
"止损",
|
||||||
|
"保本止盈",
|
||||||
|
"移动止盈",
|
||||||
|
"手动平仓",
|
||||||
|
"强制清仓",
|
||||||
|
"外部平仓",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def trading_day_from_dt(dt: datetime, reset_hour: int = 8) -> str:
|
def trading_day_from_dt(dt: datetime, reset_hour: int = 8) -> str:
|
||||||
"""与实例 get_trading_day 一致:小时 < reset_hour 归属上一日历日。"""
|
"""与实例 get_trading_day 一致:小时 < reset_hour 归属上一日历日。"""
|
||||||
@@ -16,6 +26,28 @@ def current_trading_day(*, now: datetime | None = None, reset_hour: int = 8) ->
|
|||||||
return trading_day_from_dt(now or datetime.now(), reset_hour)
|
return trading_day_from_dt(now or datetime.now(), reset_hour)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_dt_for_trading_day(raw: Any) -> datetime | None:
|
||||||
|
if raw is None:
|
||||||
|
return None
|
||||||
|
s = str(raw).strip().replace("Z", "").replace("T", " ")
|
||||||
|
if not s:
|
||||||
|
return None
|
||||||
|
for fmt, ln in (("%Y-%m-%d %H:%M:%S", 19), ("%Y-%m-%d %H:%M", 16), ("%Y-%m-%d", 10)):
|
||||||
|
try:
|
||||||
|
return datetime.strptime(s[:ln], fmt)
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def trading_day_window_bounds(trading_day: str, reset_hour: int = 8) -> tuple[str, str]:
|
||||||
|
"""交易日 [reset_hour, 次日 reset_hour) 对应的北京时间字符串区间(闭区间)。"""
|
||||||
|
day = datetime.strptime((trading_day or "").strip()[:10], "%Y-%m-%d")
|
||||||
|
start = day.replace(hour=reset_hour, minute=0, second=0, microsecond=0)
|
||||||
|
end = start + timedelta(days=1) - timedelta(seconds=1)
|
||||||
|
return start.strftime("%Y-%m-%d %H:%M:%S"), end.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
|
||||||
def _row_dict(row, row_to_dict: Optional[Callable] = None) -> dict:
|
def _row_dict(row, row_to_dict: Optional[Callable] = None) -> dict:
|
||||||
if row is None:
|
if row is None:
|
||||||
return {}
|
return {}
|
||||||
@@ -36,62 +68,111 @@ def _row_dict(row, row_to_dict: Optional[Callable] = None) -> dict:
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _effective_field(d: dict, reviewed_key: str, base_key: str, default: Any = None) -> Any:
|
||||||
|
rv = d.get(reviewed_key)
|
||||||
|
if rv is not None and str(rv).strip() != "":
|
||||||
|
return rv
|
||||||
|
bv = d.get(base_key)
|
||||||
|
if bv is not None and str(bv).strip() != "":
|
||||||
|
return bv
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def _effective_pnl(d: dict) -> float:
|
||||||
|
reviewed = d.get("reviewed_pnl_amount")
|
||||||
|
if reviewed is not None and str(reviewed).strip() != "":
|
||||||
|
try:
|
||||||
|
return float(reviewed)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
ex = d.get("exchange_realized_pnl")
|
||||||
|
if ex is not None and str(ex).strip() != "":
|
||||||
|
try:
|
||||||
|
return float(ex)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
return float(d.get("pnl_amount") or 0)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def _trade_close_dt(d: dict) -> datetime | None:
|
||||||
|
raw = _effective_field(d, "reviewed_closed_at", "closed_at")
|
||||||
|
if raw is None or str(raw).strip() == "":
|
||||||
|
raw = d.get("created_at") or d.get("opened_at")
|
||||||
|
return parse_dt_for_trading_day(raw)
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_trade_row(
|
||||||
|
d: dict,
|
||||||
|
*,
|
||||||
|
trading_day: str,
|
||||||
|
reset_hour: int,
|
||||||
|
) -> dict[str, Any] | None:
|
||||||
|
effective_result = str(_effective_field(d, "reviewed_result", "result") or "").strip()
|
||||||
|
if effective_result not in TRADE_COMPLETED_RESULTS:
|
||||||
|
return None
|
||||||
|
close_dt = _trade_close_dt(d)
|
||||||
|
if not close_dt:
|
||||||
|
return None
|
||||||
|
if trading_day_from_dt(close_dt, reset_hour) != trading_day:
|
||||||
|
return None
|
||||||
|
pnl = _effective_pnl(d)
|
||||||
|
closed_at = _effective_field(d, "reviewed_closed_at", "closed_at")
|
||||||
|
opened_at = _effective_field(d, "reviewed_opened_at", "opened_at")
|
||||||
|
return {
|
||||||
|
"symbol": d.get("symbol"),
|
||||||
|
"direction": d.get("direction"),
|
||||||
|
"result": effective_result,
|
||||||
|
"pnl_amount": round(pnl, 4),
|
||||||
|
"closed_at": closed_at,
|
||||||
|
"opened_at": opened_at,
|
||||||
|
"monitor_type": d.get("monitor_type"),
|
||||||
|
"actual_rr": d.get("actual_rr"),
|
||||||
|
"planned_rr": d.get("planned_rr"),
|
||||||
|
"trade_style": d.get("trade_style"),
|
||||||
|
"entry_reason": d.get("entry_reason"),
|
||||||
|
"reviewed": bool(d.get("reviewed_at") or d.get("reviewed_result")),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def fetch_trades_for_trading_day(
|
def fetch_trades_for_trading_day(
|
||||||
conn,
|
conn,
|
||||||
trading_day: str,
|
trading_day: str,
|
||||||
*,
|
*,
|
||||||
row_to_dict_fn: Optional[Callable] = None,
|
row_to_dict_fn: Optional[Callable] = None,
|
||||||
|
reset_hour: int = 8,
|
||||||
limit: int = 200,
|
limit: int = 200,
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
"""返回指定交易日的已平仓记录(优先 session_date,否则 closed_at 日期)。"""
|
"""返回指定交易日的已平仓记录(与 /records 交易记录一致,复盘字段优先)。"""
|
||||||
day = (trading_day or "").strip()[:10]
|
day = (trading_day or "").strip()[:10]
|
||||||
if not day:
|
if not day:
|
||||||
return []
|
return []
|
||||||
lim = max(1, min(int(limit or 200), 500))
|
lim = max(1, min(int(limit or 200), 500))
|
||||||
|
start_bj, end_bj = trading_day_window_bounds(day, reset_hour)
|
||||||
|
ts_expr = "REPLACE(COALESCE(reviewed_closed_at, closed_at, created_at, opened_at), 'T', ' ')"
|
||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
f"""
|
f"""
|
||||||
SELECT symbol, exchange_symbol, direction, result, pnl_amount,
|
SELECT symbol, direction, result, reviewed_result, pnl_amount, reviewed_pnl_amount,
|
||||||
closed_at, opened_at, session_date, monitor_type,
|
exchange_realized_pnl, closed_at, reviewed_closed_at, opened_at, reviewed_opened_at,
|
||||||
actual_rr, planned_rr, trade_style, entry_reason
|
created_at, monitor_type, actual_rr, planned_rr, trade_style, entry_reason,
|
||||||
|
reviewed_at
|
||||||
FROM trade_records
|
FROM trade_records
|
||||||
WHERE (
|
WHERE {ts_expr} >= ? AND {ts_expr} <= ?
|
||||||
(session_date IS NOT NULL AND TRIM(session_date) != '' AND session_date = ?)
|
ORDER BY {ts_expr} ASC
|
||||||
OR (
|
|
||||||
(session_date IS NULL OR TRIM(session_date) = '')
|
|
||||||
AND closed_at IS NOT NULL AND TRIM(closed_at) != ''
|
|
||||||
AND substr(closed_at, 1, 10) = ?
|
|
||||||
)
|
|
||||||
)
|
|
||||||
AND result IS NOT NULL AND TRIM(result) != ''
|
|
||||||
ORDER BY COALESCE(closed_at, opened_at) ASC
|
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
""",
|
""",
|
||||||
(day, day, lim),
|
(start_bj, end_bj, lim * 3),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
out: list[dict[str, Any]] = []
|
out: list[dict[str, Any]] = []
|
||||||
for row in rows:
|
for row in rows:
|
||||||
d = _row_dict(row, row_to_dict_fn)
|
d = _row_dict(row, row_to_dict_fn)
|
||||||
try:
|
norm = _normalize_trade_row(d, trading_day=day, reset_hour=reset_hour)
|
||||||
pnl = float(d.get("pnl_amount") or 0)
|
if norm:
|
||||||
except (TypeError, ValueError):
|
out.append(norm)
|
||||||
pnl = 0.0
|
if len(out) >= lim:
|
||||||
out.append(
|
break
|
||||||
{
|
|
||||||
"symbol": d.get("symbol"),
|
|
||||||
"exchange_symbol": d.get("exchange_symbol"),
|
|
||||||
"direction": d.get("direction"),
|
|
||||||
"result": d.get("result"),
|
|
||||||
"pnl_amount": round(pnl, 4),
|
|
||||||
"closed_at": d.get("closed_at"),
|
|
||||||
"opened_at": d.get("opened_at"),
|
|
||||||
"session_date": d.get("session_date"),
|
|
||||||
"monitor_type": d.get("monitor_type"),
|
|
||||||
"actual_rr": d.get("actual_rr"),
|
|
||||||
"planned_rr": d.get("planned_rr"),
|
|
||||||
"trade_style": d.get("trade_style"),
|
|
||||||
"entry_reason": d.get("entry_reason"),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3534,27 +3534,66 @@ body.hub-page-ai #page-ai {
|
|||||||
color: var(--text);
|
color: var(--text);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
.ai-result-md h2 {
|
.ai-md-body.ai-result-md h2 {
|
||||||
font-size: 1.02rem;
|
font-size: 1.02rem;
|
||||||
color: var(--accent-2, var(--accent));
|
color: var(--red);
|
||||||
margin: 14px 0 8px;
|
margin: 14px 0 8px;
|
||||||
padding-bottom: 4px;
|
padding-bottom: 4px;
|
||||||
border-bottom: 1px solid var(--border-soft);
|
border-bottom: 1px solid var(--border-soft);
|
||||||
|
-webkit-text-stroke: 0.45px #fff;
|
||||||
|
paint-order: stroke fill;
|
||||||
|
text-shadow:
|
||||||
|
0 0 1px #fff,
|
||||||
|
0 0 2px rgba(255, 255, 255, 0.9),
|
||||||
|
1px 0 0 #fff,
|
||||||
|
-1px 0 0 #fff,
|
||||||
|
0 1px 0 #fff,
|
||||||
|
0 -1px 0 #fff;
|
||||||
}
|
}
|
||||||
.ai-md-body.ai-result-md h2:first-child {
|
.ai-md-body.ai-result-md h2:first-child {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
.ai-md-body.ai-result-md h3 {
|
.ai-md-body.ai-result-md h3 {
|
||||||
font-size: 0.92rem;
|
font-size: 0.92rem;
|
||||||
color: var(--accent-2, var(--accent));
|
color: var(--red);
|
||||||
margin: 16px 0 8px;
|
margin: 16px 0 8px;
|
||||||
padding-bottom: 4px;
|
padding-bottom: 4px;
|
||||||
border-bottom: 1px solid var(--border-soft);
|
border-bottom: 1px solid var(--border-soft);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
-webkit-text-stroke: 0.45px #fff;
|
||||||
|
paint-order: stroke fill;
|
||||||
|
text-shadow:
|
||||||
|
0 0 1px #fff,
|
||||||
|
0 0 2px rgba(255, 255, 255, 0.9),
|
||||||
|
1px 0 0 #fff,
|
||||||
|
-1px 0 0 #fff,
|
||||||
|
0 1px 0 #fff,
|
||||||
|
0 -1px 0 #fff;
|
||||||
}
|
}
|
||||||
.ai-md-body.ai-result-md h3:first-of-type {
|
.ai-md-body.ai-result-md h3:first-of-type {
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
.ai-md-body.ai-result-md h4 {
|
||||||
|
font-size: 0.92rem;
|
||||||
|
color: var(--red);
|
||||||
|
margin: 10px 0 6px;
|
||||||
|
-webkit-text-stroke: 0.45px #fff;
|
||||||
|
paint-order: stroke fill;
|
||||||
|
text-shadow:
|
||||||
|
0 0 1px #fff,
|
||||||
|
0 0 2px rgba(255, 255, 255, 0.9),
|
||||||
|
1px 0 0 #fff,
|
||||||
|
-1px 0 0 #fff,
|
||||||
|
0 1px 0 #fff,
|
||||||
|
0 -1px 0 #fff;
|
||||||
|
}
|
||||||
|
.ai-result-md h2 {
|
||||||
|
font-size: 1.02rem;
|
||||||
|
color: var(--accent-2, var(--accent));
|
||||||
|
margin: 14px 0 8px;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
border-bottom: 1px solid var(--border-soft);
|
||||||
|
}
|
||||||
.ai-result-md h3,
|
.ai-result-md h3,
|
||||||
.ai-result-md h4 {
|
.ai-result-md h4 {
|
||||||
font-size: 0.92rem;
|
font-size: 0.92rem;
|
||||||
@@ -3624,7 +3663,16 @@ body.hub-page-ai #page-ai {
|
|||||||
}
|
}
|
||||||
.ai-ac-name {
|
.ai-ac-name {
|
||||||
min-width: 9rem;
|
min-width: 9rem;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
|
color: var(--red);
|
||||||
|
-webkit-text-stroke: 0.35px #fff;
|
||||||
|
paint-order: stroke fill;
|
||||||
|
text-shadow:
|
||||||
|
0 0 1px #fff,
|
||||||
|
1px 0 0 #fff,
|
||||||
|
-1px 0 0 #fff,
|
||||||
|
0 1px 0 #fff,
|
||||||
|
0 -1px 0 #fff;
|
||||||
}
|
}
|
||||||
.ai-ac-remark {
|
.ai-ac-remark {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
@@ -3726,7 +3774,16 @@ body.hub-page-ai #page-ai {
|
|||||||
margin: 0 0 6px;
|
margin: 0 0 6px;
|
||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--accent-2, var(--accent));
|
color: var(--red);
|
||||||
|
-webkit-text-stroke: 0.45px #fff;
|
||||||
|
paint-order: stroke fill;
|
||||||
|
text-shadow:
|
||||||
|
0 0 1px #fff,
|
||||||
|
0 0 2px rgba(255, 255, 255, 0.9),
|
||||||
|
1px 0 0 #fff,
|
||||||
|
-1px 0 0 #fff,
|
||||||
|
0 1px 0 #fff,
|
||||||
|
0 -1px 0 #fff;
|
||||||
}
|
}
|
||||||
.ai-msg-attachments {
|
.ai-msg-attachments {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<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'" />
|
<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>
|
<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=20260606-hub-ai3" />
|
<link rel="stylesheet" href="/assets/app.css?v=20260607-hub-ai-v3" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="app-bg" aria-hidden="true"></div>
|
<div class="app-bg" aria-hidden="true"></div>
|
||||||
|
|||||||
@@ -3,10 +3,15 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from hub_trades_lib import fetch_trades_for_trading_day, summarize_trades, trading_day_from_dt
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
from hub_trades_lib import (
|
||||||
|
fetch_trades_for_trading_day,
|
||||||
|
summarize_trades,
|
||||||
|
trading_day_from_dt,
|
||||||
|
trading_day_window_bounds,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class HubTradesLibTest(unittest.TestCase):
|
class HubTradesLibTest(unittest.TestCase):
|
||||||
def test_trading_day_reset(self):
|
def test_trading_day_reset(self):
|
||||||
@@ -15,32 +20,44 @@ class HubTradesLibTest(unittest.TestCase):
|
|||||||
dt2 = datetime(2026, 6, 6, 8, 0, 0)
|
dt2 = datetime(2026, 6, 6, 8, 0, 0)
|
||||||
self.assertEqual(trading_day_from_dt(dt2, 8), "2026-06-06")
|
self.assertEqual(trading_day_from_dt(dt2, 8), "2026-06-06")
|
||||||
|
|
||||||
|
def test_trading_day_window_bounds(self):
|
||||||
|
start, end = trading_day_window_bounds("2026-06-06", 8)
|
||||||
|
self.assertEqual(start, "2026-06-06 08:00:00")
|
||||||
|
self.assertEqual(end, "2026-06-07 07:59:59")
|
||||||
|
|
||||||
def test_fetch_and_summarize(self):
|
def test_fetch_and_summarize(self):
|
||||||
conn = sqlite3.connect(":memory:")
|
conn = sqlite3.connect(":memory:")
|
||||||
conn.row_factory = sqlite3.Row
|
conn.row_factory = sqlite3.Row
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"""CREATE TABLE trade_records (
|
"""CREATE TABLE trade_records (
|
||||||
symbol TEXT, exchange_symbol TEXT, direction TEXT, result TEXT,
|
symbol TEXT, direction TEXT, result TEXT, reviewed_result TEXT,
|
||||||
pnl_amount REAL, closed_at TEXT, opened_at TEXT, session_date TEXT,
|
pnl_amount REAL, reviewed_pnl_amount REAL, exchange_realized_pnl REAL,
|
||||||
monitor_type TEXT, actual_rr REAL, planned_rr REAL, trade_style TEXT, entry_reason TEXT
|
closed_at TEXT, reviewed_closed_at TEXT, opened_at TEXT, reviewed_opened_at TEXT,
|
||||||
|
created_at TEXT, monitor_type TEXT, actual_rr REAL, planned_rr REAL,
|
||||||
|
trade_style TEXT, entry_reason TEXT, reviewed_at TEXT
|
||||||
)"""
|
)"""
|
||||||
)
|
)
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT INTO trade_records VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
"INSERT INTO trade_records VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||||
(
|
(
|
||||||
"ONDO/USDT",
|
"ONDO/USDT",
|
||||||
"ONDO/USDT:USDT",
|
|
||||||
"short",
|
"short",
|
||||||
"止损",
|
"止损",
|
||||||
|
None,
|
||||||
-0.5,
|
-0.5,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
"2026-06-06 10:00:00",
|
"2026-06-06 10:00:00",
|
||||||
|
None,
|
||||||
"2026-06-06 09:00:00",
|
"2026-06-06 09:00:00",
|
||||||
"2026-06-06",
|
None,
|
||||||
|
"2026-06-06 10:00:00",
|
||||||
"趋势回调",
|
"趋势回调",
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
"trend",
|
"trend",
|
||||||
"",
|
"",
|
||||||
|
None,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
@@ -52,6 +69,89 @@ class HubTradesLibTest(unittest.TestCase):
|
|||||||
self.assertAlmostEqual(stats["total_pnl_u"], -0.5)
|
self.assertAlmostEqual(stats["total_pnl_u"], -0.5)
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
def test_early_morning_belongs_prev_trading_day(self):
|
||||||
|
conn = sqlite3.connect(":memory:")
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
conn.execute(
|
||||||
|
"""CREATE TABLE trade_records (
|
||||||
|
symbol TEXT, direction TEXT, result TEXT, reviewed_result TEXT,
|
||||||
|
pnl_amount REAL, reviewed_pnl_amount REAL, exchange_realized_pnl REAL,
|
||||||
|
closed_at TEXT, reviewed_closed_at TEXT, opened_at TEXT, reviewed_opened_at TEXT,
|
||||||
|
created_at TEXT, monitor_type TEXT, actual_rr REAL, planned_rr REAL,
|
||||||
|
trade_style TEXT, entry_reason TEXT, reviewed_at TEXT
|
||||||
|
)"""
|
||||||
|
)
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO trade_records VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||||
|
(
|
||||||
|
"BTC/USDT",
|
||||||
|
"long",
|
||||||
|
"止盈",
|
||||||
|
None,
|
||||||
|
1.2,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
"2026-06-07 07:30:00",
|
||||||
|
None,
|
||||||
|
"2026-06-07 06:00:00",
|
||||||
|
None,
|
||||||
|
"2026-06-07 07:30:00",
|
||||||
|
"关键位",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
"trend",
|
||||||
|
"",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
self.assertEqual(len(fetch_trades_for_trading_day(conn, "2026-06-07")), 0)
|
||||||
|
self.assertEqual(len(fetch_trades_for_trading_day(conn, "2026-06-06")), 1)
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def test_reviewed_fields_preferred(self):
|
||||||
|
conn = sqlite3.connect(":memory:")
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
conn.execute(
|
||||||
|
"""CREATE TABLE trade_records (
|
||||||
|
symbol TEXT, direction TEXT, result TEXT, reviewed_result TEXT,
|
||||||
|
pnl_amount REAL, reviewed_pnl_amount REAL, exchange_realized_pnl REAL,
|
||||||
|
closed_at TEXT, reviewed_closed_at TEXT, opened_at TEXT, reviewed_opened_at TEXT,
|
||||||
|
created_at TEXT, monitor_type TEXT, actual_rr REAL, planned_rr REAL,
|
||||||
|
trade_style TEXT, entry_reason TEXT, reviewed_at TEXT
|
||||||
|
)"""
|
||||||
|
)
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO trade_records VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||||
|
(
|
||||||
|
"ETH/USDT",
|
||||||
|
"long",
|
||||||
|
"止损",
|
||||||
|
"止盈",
|
||||||
|
-0.5,
|
||||||
|
2.0,
|
||||||
|
None,
|
||||||
|
"2026-06-06 09:00:00",
|
||||||
|
"2026-06-06 11:00:00",
|
||||||
|
"2026-06-06 08:00:00",
|
||||||
|
None,
|
||||||
|
"2026-06-06 11:00:00",
|
||||||
|
"趋势回调",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
"trend",
|
||||||
|
"",
|
||||||
|
"2026-06-06 12:00:00",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
rows = fetch_trades_for_trading_day(conn, "2026-06-06")
|
||||||
|
self.assertEqual(len(rows), 1)
|
||||||
|
self.assertEqual(rows[0]["result"], "止盈")
|
||||||
|
self.assertAlmostEqual(rows[0]["pnl_amount"], 2.0)
|
||||||
|
self.assertTrue(rows[0]["reviewed"])
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user