307 lines
12 KiB
Python
307 lines
12 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import sqlite3
|
|
|
|
|
|
class ReviewRepository:
|
|
def watchlist(self, connection: sqlite3.Connection, user_id: int) -> tuple[sqlite3.Row, ...]:
|
|
return tuple(
|
|
connection.execute(
|
|
"""SELECT identifier, name, sector, remark, created_at FROM watchlist_entries
|
|
WHERE user_id = ? ORDER BY created_at, identifier""",
|
|
(user_id,),
|
|
).fetchall()
|
|
)
|
|
|
|
def save_watch(
|
|
self, connection: sqlite3.Connection, user_id: int, item: dict, now: str
|
|
) -> None:
|
|
connection.execute(
|
|
"""INSERT INTO watchlist_entries (user_id, identifier, name, sector, remark, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
ON CONFLICT(user_id, identifier) DO UPDATE SET
|
|
name=excluded.name, sector=excluded.sector,
|
|
remark=CASE
|
|
WHEN excluded.remark = '' THEN watchlist_entries.remark
|
|
ELSE excluded.remark
|
|
END""",
|
|
(
|
|
user_id,
|
|
item["identifier"],
|
|
item["name"],
|
|
item.get("sector"),
|
|
item.get("remark") or "",
|
|
now,
|
|
),
|
|
)
|
|
|
|
def delete_watch(self, connection: sqlite3.Connection, user_id: int, identifier: str) -> bool:
|
|
cursor = connection.execute(
|
|
"DELETE FROM watchlist_entries WHERE user_id = ? AND identifier = ?",
|
|
(user_id, identifier),
|
|
)
|
|
return cursor.rowcount > 0
|
|
|
|
def save_watch_remark(
|
|
self, connection: sqlite3.Connection, user_id: int, identifier: str, remark: str
|
|
) -> bool:
|
|
cursor = connection.execute(
|
|
"UPDATE watchlist_entries SET remark=? WHERE user_id=? AND identifier=?",
|
|
(remark, user_id, identifier),
|
|
)
|
|
return cursor.rowcount > 0
|
|
|
|
def latest_factors(self, connection: sqlite3.Connection, through: str) -> dict[str, dict]:
|
|
snapshot = connection.execute(
|
|
"""SELECT id, trade_date FROM screener_factor_snapshots
|
|
WHERE trade_date <= ? ORDER BY trade_date DESC, id DESC LIMIT 1""",
|
|
(through,),
|
|
).fetchone()
|
|
if snapshot is None:
|
|
return {}
|
|
return {
|
|
str(row["identifier"]): json.loads(str(row["payload_json"]))
|
|
for row in connection.execute(
|
|
"SELECT identifier, payload_json FROM screener_factor_values WHERE snapshot_id = ?",
|
|
(int(snapshot["id"]),),
|
|
).fetchall()
|
|
}
|
|
|
|
def latest_auction_scores(
|
|
self, connection: sqlite3.Connection, through: str
|
|
) -> dict[str, float]:
|
|
row = connection.execute(
|
|
"""SELECT payload_json FROM market_insight_snapshots
|
|
WHERE kind = 'auction' AND trade_date <= ? AND entity_key = ''
|
|
ORDER BY trade_date DESC LIMIT 1""",
|
|
(through,),
|
|
).fetchone()
|
|
if row is None:
|
|
return {}
|
|
payload = json.loads(str(row["payload_json"]))
|
|
scores = {}
|
|
for key in ("_market_rows", "rows", "one_price_rows"):
|
|
for item in payload.get(key) or []:
|
|
identifier = str(item.get("identifier") or item.get("ts_code") or "")
|
|
score = item.get("attention_score")
|
|
if identifier and isinstance(score, (int, float)):
|
|
scores[identifier] = float(score)
|
|
return scores
|
|
|
|
def note(
|
|
self, connection: sqlite3.Connection, user_id: int, code: str, trade_date: str
|
|
) -> sqlite3.Row | None:
|
|
return connection.execute(
|
|
"SELECT * FROM review_notes WHERE user_id = ? AND code = ? AND trade_date = ?",
|
|
(user_id, code, trade_date),
|
|
).fetchone()
|
|
|
|
def notes(
|
|
self, connection: sqlite3.Connection, user_id: int, code: str = "", limit: int = 60
|
|
) -> tuple[sqlite3.Row, ...]:
|
|
return tuple(
|
|
connection.execute(
|
|
"""SELECT * FROM review_notes WHERE user_id = ? AND code = ?
|
|
ORDER BY trade_date DESC, id DESC LIMIT ?""",
|
|
(user_id, code, limit),
|
|
).fetchall()
|
|
)
|
|
|
|
def save_note(self, connection: sqlite3.Connection, user_id: int, data: dict, now: str) -> int:
|
|
connection.execute(
|
|
"""INSERT INTO review_notes (
|
|
user_id, code, stock_name, trade_date, summary, content, plan,
|
|
created_at, updated_at
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
ON CONFLICT(user_id, code, trade_date) DO UPDATE SET
|
|
stock_name=excluded.stock_name, summary=excluded.summary,
|
|
content=excluded.content, plan=excluded.plan, updated_at=excluded.updated_at""",
|
|
(
|
|
user_id,
|
|
data["code"],
|
|
data["stock_name"],
|
|
data["trade_date"],
|
|
data["summary"],
|
|
data["content"],
|
|
data["plan"],
|
|
now,
|
|
now,
|
|
),
|
|
)
|
|
row = self.note(connection, user_id, data["code"], data["trade_date"])
|
|
if row is None:
|
|
raise RuntimeError("复盘记录写入失败")
|
|
return int(row["id"])
|
|
|
|
def delete_note(self, connection: sqlite3.Connection, user_id: int, note_id: int) -> bool:
|
|
cursor = connection.execute(
|
|
"DELETE FROM review_notes WHERE user_id = ? AND id = ?", (user_id, note_id)
|
|
)
|
|
return cursor.rowcount > 0
|
|
|
|
def trades(
|
|
self, connection: sqlite3.Connection, user_id: int, limit: int = 300
|
|
) -> tuple[sqlite3.Row, ...]:
|
|
return tuple(
|
|
connection.execute(
|
|
"""SELECT * FROM trade_entries WHERE user_id = ?
|
|
ORDER BY trade_date DESC, id DESC LIMIT ?""",
|
|
(user_id, limit),
|
|
).fetchall()
|
|
)
|
|
|
|
def trade(
|
|
self, connection: sqlite3.Connection, user_id: int, trade_id: int
|
|
) -> sqlite3.Row | None:
|
|
return connection.execute(
|
|
"SELECT * FROM trade_entries WHERE user_id = ? AND id = ?", (user_id, trade_id)
|
|
).fetchone()
|
|
|
|
def save_trade(self, connection: sqlite3.Connection, user_id: int, data: dict, now: str) -> int:
|
|
values = (
|
|
data["trade_date"],
|
|
data["code"],
|
|
data["name"],
|
|
data["action"],
|
|
data["price"],
|
|
data["quantity"],
|
|
data["position_pct"],
|
|
data["pnl_amount"],
|
|
data["pnl_pct"],
|
|
data["emotion"],
|
|
json.dumps(data["tags"], ensure_ascii=False),
|
|
data["thesis"],
|
|
data["execution"],
|
|
now,
|
|
)
|
|
if data.get("id"):
|
|
cursor = connection.execute(
|
|
"""UPDATE trade_entries SET trade_date=?, code=?, name=?, action=?, price=?,
|
|
quantity=?, position_pct=?, pnl_amount=?, pnl_pct=?, emotion=?, tags_json=?,
|
|
thesis=?, execution=?, updated_at=? WHERE user_id=? AND id=?""",
|
|
(*values, user_id, int(data["id"])),
|
|
)
|
|
if cursor.rowcount == 0:
|
|
raise ValueError("交易记录不存在或无权修改。")
|
|
return int(data["id"])
|
|
cursor = connection.execute(
|
|
"""INSERT INTO trade_entries (
|
|
trade_date, code, name, action, price, quantity, position_pct, pnl_amount,
|
|
pnl_pct, emotion, tags_json, thesis, execution, created_at, updated_at, user_id
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
(*values, now, user_id),
|
|
)
|
|
return int(cursor.lastrowid)
|
|
|
|
def delete_trade(self, connection: sqlite3.Connection, user_id: int, trade_id: int) -> bool:
|
|
cursor = connection.execute(
|
|
"DELETE FROM trade_entries WHERE user_id = ? AND id = ?", (user_id, trade_id)
|
|
)
|
|
return cursor.rowcount > 0
|
|
|
|
def save_alert(self, connection: sqlite3.Connection, user_id: int, data: dict, now: str) -> int:
|
|
connection.execute(
|
|
"""INSERT INTO alerts (
|
|
user_id, kind, title, content, available_date, code, dedupe_key,
|
|
created_at, updated_at
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
ON CONFLICT(user_id, dedupe_key) DO NOTHING""",
|
|
(
|
|
user_id,
|
|
data["kind"],
|
|
data["title"],
|
|
data["content"],
|
|
data["available_date"],
|
|
data["code"],
|
|
data["dedupe_key"],
|
|
now,
|
|
now,
|
|
),
|
|
)
|
|
row = connection.execute(
|
|
"SELECT id FROM alerts WHERE user_id = ? AND dedupe_key = ?",
|
|
(user_id, data["dedupe_key"]),
|
|
).fetchone()
|
|
return int(row["id"])
|
|
|
|
def alerts(
|
|
self, connection: sqlite3.Connection, user_id: int, unread: bool
|
|
) -> tuple[sqlite3.Row, ...]:
|
|
clause = "AND is_read = 0" if unread else ""
|
|
return tuple(
|
|
connection.execute(
|
|
f"""SELECT * FROM alerts WHERE user_id = ? {clause}
|
|
ORDER BY available_date > date('now'), is_read,
|
|
available_date DESC, id DESC LIMIT 100""",
|
|
(user_id,),
|
|
).fetchall()
|
|
)
|
|
|
|
def unread_count(self, connection: sqlite3.Connection, user_id: int, today: str) -> int:
|
|
row = connection.execute(
|
|
"""SELECT COUNT(*) total FROM alerts
|
|
WHERE user_id = ? AND available_date <= ? AND is_read = 0""",
|
|
(user_id, today),
|
|
).fetchone()
|
|
return int(row["total"])
|
|
|
|
def mark_alert(
|
|
self, connection: sqlite3.Connection, user_id: int, alert_id: int, now: str
|
|
) -> bool:
|
|
cursor = connection.execute(
|
|
"UPDATE alerts SET is_read=1, read_at=?, updated_at=? WHERE user_id=? AND id=?",
|
|
(now, now, user_id, alert_id),
|
|
)
|
|
return cursor.rowcount > 0
|
|
|
|
def mark_all_alerts(
|
|
self, connection: sqlite3.Connection, user_id: int, today: str, now: str
|
|
) -> int:
|
|
cursor = connection.execute(
|
|
"""UPDATE alerts SET is_read=1, read_at=?, updated_at=?
|
|
WHERE user_id=? AND available_date <= ? AND is_read=0""",
|
|
(now, now, user_id, today),
|
|
)
|
|
return int(cursor.rowcount)
|
|
|
|
def delete_alert(self, connection: sqlite3.Connection, user_id: int, alert_id: int) -> bool:
|
|
cursor = connection.execute(
|
|
"DELETE FROM alerts WHERE user_id=? AND id=?", (user_id, alert_id)
|
|
)
|
|
return cursor.rowcount > 0
|
|
|
|
def messages(
|
|
self, connection: sqlite3.Connection, user_id: int, limit: int = 100
|
|
) -> tuple[sqlite3.Row, ...]:
|
|
rows = connection.execute(
|
|
"""SELECT * FROM review_assistant_messages WHERE user_id=?
|
|
ORDER BY id DESC LIMIT ?""",
|
|
(user_id, limit),
|
|
).fetchall()
|
|
return tuple(reversed(rows))
|
|
|
|
def add_message(
|
|
self, connection: sqlite3.Connection, user_id: int, data: dict, now: str
|
|
) -> None:
|
|
connection.execute(
|
|
"""INSERT INTO review_assistant_messages
|
|
(user_id, role, content, context_date, request_id, status, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)""",
|
|
(
|
|
user_id,
|
|
data["role"],
|
|
data["content"],
|
|
data["context_date"],
|
|
data.get("request_id"),
|
|
data["status"],
|
|
now,
|
|
),
|
|
)
|
|
|
|
def clear_messages(self, connection: sqlite3.Connection, user_id: int) -> int:
|
|
cursor = connection.execute(
|
|
"DELETE FROM review_assistant_messages WHERE user_id=?", (user_id,)
|
|
)
|
|
return int(cursor.rowcount)
|