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)