feat: add structured manual trading journal

This commit is contained in:
leefer
2026-07-23 00:12:28 +08:00
parent a2a5db8f6a
commit 7d8d270e98
8 changed files with 564 additions and 1 deletions
+113
View File
@@ -297,6 +297,30 @@ class ReviewDatabase:
CREATE INDEX IF NOT EXISTS idx_alerts_user_due
ON alerts(user_id, available_date, is_read, id DESC);
CREATE TABLE IF NOT EXISTS trade_entries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
trade_date TEXT NOT NULL,
code TEXT NOT NULL,
name TEXT NOT NULL,
action TEXT NOT NULL,
price REAL NOT NULL,
quantity INTEGER NOT NULL DEFAULT 0,
position_pct REAL NOT NULL DEFAULT 0,
pnl_amount REAL,
pnl_pct REAL,
thesis TEXT NOT NULL DEFAULT '',
execution TEXT NOT NULL DEFAULT '',
emotion TEXT NOT NULL DEFAULT 'calm',
tags TEXT NOT NULL DEFAULT '[]',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_trade_entries_user_date
ON trade_entries(user_id, trade_date DESC, id DESC);
"""
)
user_columns = {
@@ -1550,6 +1574,95 @@ class ReviewDatabase:
)
return cursor.rowcount > 0
def save_trade_entry(
self,
user_id: int,
trade_date: str,
code: str,
name: str,
action: str,
price: float,
quantity: int,
position_pct: float,
pnl_amount: float | None,
pnl_pct: float | None,
thesis: str,
execution: str,
emotion: str,
tags: list[str],
trade_id: int | None = None,
) -> int:
now = datetime.now().astimezone().isoformat(timespec="seconds")
tags_json = json.dumps(tags, ensure_ascii=False, separators=(",", ":"))
with self.connect() as connection:
if trade_id:
cursor = connection.execute(
"""
UPDATE trade_entries SET
trade_date=?, code=?, name=?, action=?, price=?, quantity=?,
position_pct=?, pnl_amount=?, pnl_pct=?, thesis=?, execution=?,
emotion=?, tags=?, updated_at=?
WHERE id=? AND user_id=?
""",
(
trade_date, code, name, action, price, quantity, position_pct,
pnl_amount, pnl_pct, thesis, execution, emotion, tags_json, now,
int(trade_id), int(user_id),
),
)
if cursor.rowcount == 0:
raise ValueError("交易记录不存在或无权修改。")
return int(trade_id)
cursor = connection.execute(
"""
INSERT INTO trade_entries
(user_id, trade_date, code, name, action, price, quantity,
position_pct, pnl_amount, pnl_pct, thesis, execution, emotion,
tags, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
int(user_id), trade_date, code, name, action, price, quantity,
position_pct, pnl_amount, pnl_pct, thesis, execution, emotion,
tags_json, now, now,
),
)
return int(cursor.lastrowid)
def list_trade_entries(
self, user_id: int, start_date: str = "", end_date: str = "", code: str = "",
limit: int = 300,
) -> list[dict[str, Any]]:
clauses = ["user_id = ?"]
parameters: list[Any] = [int(user_id)]
if start_date:
clauses.append("trade_date >= ?")
parameters.append(start_date)
if end_date:
clauses.append("trade_date <= ?")
parameters.append(end_date)
if code:
clauses.append("code = ?")
parameters.append(code)
parameters.append(max(1, min(1000, int(limit))))
with self.connect() as connection:
rows = connection.execute(
f"""
SELECT * FROM trade_entries WHERE {' AND '.join(clauses)}
ORDER BY trade_date DESC, id DESC LIMIT ?
""",
parameters,
).fetchall()
return [dict(row) for row in rows]
def delete_trade_entry(self, user_id: int, trade_id: int) -> bool:
with self.connect() as connection:
cursor = connection.execute(
"DELETE FROM trade_entries WHERE id = ? AND user_id = ?",
(int(trade_id), int(user_id)),
)
return cursor.rowcount > 0
def start_sync(self, trade_date: str, source: str) -> int:
started_at = datetime.now().astimezone().isoformat(timespec="seconds")
with self.connect() as connection: