feat: scaffold gold_monitor web hub, API, PM2 deploy and docs
This commit is contained in:
@@ -0,0 +1,275 @@
|
||||
# -*- 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()),
|
||||
)
|
||||
Reference in New Issue
Block a user