# -*- coding: utf-8 -*- from __future__ import annotations import json import secrets import sqlite3 import time from contextlib import contextmanager from typing import Any, Iterator, Optional from app.config import Config def _connect() -> sqlite3.Connection: Config.DATABASE_PATH.parent.mkdir(parents=True, exist_ok=True) conn = sqlite3.connect(str(Config.DATABASE_PATH), check_same_thread=False) conn.row_factory = sqlite3.Row conn.execute("PRAGMA foreign_keys = ON") return conn @contextmanager def get_db() -> Iterator[sqlite3.Connection]: conn = _connect() try: yield conn conn.commit() except Exception: conn.rollback() raise finally: conn.close() def init_db() -> None: with get_db() as conn: conn.executescript( """ CREATE TABLE IF NOT EXISTS accounts ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, broker TEXT DEFAULT '', login_id TEXT DEFAULT '', account_type TEXT DEFAULT 'demo', note TEXT DEFAULT '', enabled INTEGER DEFAULT 1, created_at REAL NOT NULL ); CREATE TABLE IF NOT EXISTS machines ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, device_fingerprint TEXT DEFAULT '', note TEXT DEFAULT '', enabled INTEGER DEFAULT 1, last_seen_at REAL, created_at REAL NOT NULL ); CREATE TABLE IF NOT EXISTS activations ( id INTEGER PRIMARY KEY AUTOINCREMENT, code TEXT NOT NULL UNIQUE, account_id INTEGER, machine_id INTEGER, token TEXT, status TEXT DEFAULT 'pending', bound_at REAL, created_at REAL NOT NULL, FOREIGN KEY(account_id) REFERENCES accounts(id), FOREIGN KEY(machine_id) REFERENCES machines(id) ); CREATE TABLE IF NOT EXISTS trades ( id INTEGER PRIMARY KEY AUTOINCREMENT, ticket TEXT, symbol TEXT NOT NULL, side TEXT NOT NULL, volume REAL, open_price REAL, close_price REAL, open_time REAL, close_time REAL, profit REAL, sl REAL, tp REAL, account_id INTEGER, machine_id INTEGER, entry_style TEXT DEFAULT '', review_note TEXT DEFAULT '', snapshot_json TEXT DEFAULT '{}', created_at REAL NOT NULL, FOREIGN KEY(account_id) REFERENCES accounts(id), FOREIGN KEY(machine_id) REFERENCES machines(id) ); CREATE TABLE IF NOT EXISTS bars ( id INTEGER PRIMARY KEY AUTOINCREMENT, symbol TEXT NOT NULL, timeframe TEXT NOT NULL, ts REAL NOT NULL, open REAL, high REAL, low REAL, close REAL, volume REAL, UNIQUE(symbol, timeframe, ts) ); """ ) def now() -> float: return time.time() def row_to_dict(row: Optional[sqlite3.Row]) -> Optional[dict]: if row is None: return None return {k: row[k] for k in row.keys()} def list_accounts(conn: sqlite3.Connection) -> list[dict]: cur = conn.execute("SELECT * FROM accounts ORDER BY id DESC") return [row_to_dict(r) for r in cur.fetchall()] def list_machines(conn: sqlite3.Connection) -> list[dict]: cur = conn.execute("SELECT * FROM machines ORDER BY id DESC") return [row_to_dict(r) for r in cur.fetchall()] def create_activation(conn: sqlite3.Connection, account_id: int, machine_id: Optional[int]) -> str: code = secrets.token_urlsafe(12) conn.execute( "INSERT INTO activations(code, account_id, machine_id, status, created_at) VALUES(?,?,?,?,?)", (code, account_id, machine_id, "pending", now()), ) return code def activate_device(conn: sqlite3.Connection, code: str, fingerprint: str, machine_name: str) -> dict[str, Any]: row = conn.execute("SELECT * FROM activations WHERE code=?", (code,)).fetchone() if row is None: raise ValueError("激活码无效") if row["status"] == "revoked": raise ValueError("激活码已作废") token = secrets.token_urlsafe(32) machine_id = row["machine_id"] if machine_id: conn.execute( "UPDATE machines SET device_fingerprint=?, last_seen_at=?, name=COALESCE(NULLIF(name,''), ?) WHERE id=?", (fingerprint, now(), machine_name or f"PC-{fingerprint[:6]}", machine_id), ) else: cur = conn.execute( "INSERT INTO machines(name, device_fingerprint, note, enabled, last_seen_at, created_at) VALUES(?,?,?,?,?,?)", (machine_name or f"PC-{fingerprint[:6]}", fingerprint, "激活自动创建", 1, now(), now()), ) machine_id = cur.lastrowid conn.execute("UPDATE activations SET machine_id=? WHERE id=?", (machine_id, row["id"])) conn.execute( "UPDATE activations SET token=?, status='active', bound_at=? WHERE id=?", (token, now(), row["id"]), ) return { "token": token, "account_id": row["account_id"], "machine_id": machine_id, "public_base": Config.PUBLIC_BASE_URL, } def auth_token(conn: sqlite3.Connection, token: str) -> Optional[dict]: if not token: return None row = conn.execute( "SELECT * FROM activations WHERE token=? AND status='active'", (token,), ).fetchone() return row_to_dict(row) def list_trades(conn: sqlite3.Connection, limit: int = 200) -> list[dict]: cur = conn.execute( """ SELECT t.*, a.name AS account_name, m.name AS machine_name FROM trades t LEFT JOIN accounts a ON a.id = t.account_id LEFT JOIN machines m ON m.id = t.machine_id ORDER BY COALESCE(t.close_time, t.open_time, t.created_at) DESC LIMIT ? """, (limit,), ) rows = [] for r in cur.fetchall(): d = row_to_dict(r) try: d["snapshot"] = json.loads(d.get("snapshot_json") or "{}") except json.JSONDecodeError: d["snapshot"] = {} rows.append(d) return rows def get_trade(conn: sqlite3.Connection, trade_id: int) -> Optional[dict]: r = conn.execute( """ SELECT t.*, a.name AS account_name, m.name AS machine_name FROM trades t LEFT JOIN accounts a ON a.id = t.account_id LEFT JOIN machines m ON m.id = t.machine_id WHERE t.id=? """, (trade_id,), ).fetchone() d = row_to_dict(r) if d: try: d["snapshot"] = json.loads(d.get("snapshot_json") or "{}") except json.JSONDecodeError: d["snapshot"] = {} return d def upsert_bars(conn: sqlite3.Connection, symbol: str, timeframe: str, bars: list[dict]) -> int: n = 0 for b in bars: conn.execute( """ INSERT INTO bars(symbol, timeframe, ts, open, high, low, close, volume) VALUES(?,?,?,?,?,?,?,?) ON CONFLICT(symbol, timeframe, ts) DO UPDATE SET open=excluded.open, high=excluded.high, low=excluded.low, close=excluded.close, volume=excluded.volume """, ( symbol, timeframe, float(b["ts"]), float(b["open"]), float(b["high"]), float(b["low"]), float(b["close"]), float(b.get("volume") or 0), ), ) n += 1 return n def get_bars(conn: sqlite3.Connection, symbol: str, timeframe: str, start_ts: float, end_ts: float) -> list[dict]: cur = conn.execute( """ SELECT ts, open, high, low, close, volume FROM bars WHERE symbol=? AND timeframe=? AND ts>=? AND ts<=? ORDER BY ts ASC """, (symbol, timeframe, start_ts, end_ts), ) return [row_to_dict(r) for r in cur.fetchall()] def seed_if_empty() -> None: with get_db() as conn: n = conn.execute("SELECT COUNT(*) AS c FROM accounts").fetchone()["c"] if n == 0: conn.execute( "INSERT INTO accounts(name, broker, login_id, account_type, note, enabled, created_at) VALUES(?,?,?,?,?,?,?)", ("模拟盘-主", "MT5", "", "demo", "默认账户,可改标注", 1, now()), ) n2 = conn.execute("SELECT COUNT(*) AS c FROM machines").fetchone()["c"] if n2 == 0: conn.execute( "INSERT INTO machines(name, device_fingerprint, note, enabled, created_at) VALUES(?,?,?,?,?)", ("未绑定机器", "", "下载通用包后用激活码绑定", 1, now()), )