0ee1d8be5d
When enabled, lock perp long→buy Put or short→buy Call with ITM/ATM only; off keeps ATM/ask rules. Co-authored-by: Cursor <cursoragent@cursor.com>
301 lines
9.9 KiB
Python
301 lines
9.9 KiB
Python
from __future__ import annotations
|
|
|
|
import sqlite3
|
|
import threading
|
|
import time
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from ..config import Settings, get_settings
|
|
|
|
_SCHEMA = """
|
|
CREATE TABLE IF NOT EXISTS settings (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS ledger_meta (
|
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
equity REAL NOT NULL,
|
|
available REAL NOT NULL,
|
|
reserved REAL NOT NULL DEFAULT 0,
|
|
updated_at_ms INTEGER NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS groups (
|
|
group_id TEXT PRIMARY KEY,
|
|
status TEXT NOT NULL,
|
|
bias TEXT,
|
|
option_side TEXT,
|
|
perp_side TEXT,
|
|
option_inst_id TEXT,
|
|
perp_inst_id TEXT,
|
|
strike REAL,
|
|
expiry_ymd TEXT,
|
|
entry_index_px REAL,
|
|
initial_premium REAL DEFAULT 0,
|
|
open_at_ms INTEGER,
|
|
close_at_ms INTEGER,
|
|
close_reason TEXT,
|
|
realized_pnl REAL DEFAULT 0,
|
|
fees REAL DEFAULT 0,
|
|
slip_cost REAL DEFAULT 0,
|
|
note TEXT,
|
|
exec_mode TEXT,
|
|
funding_usdt REAL,
|
|
settle_index_px REAL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS fills (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
group_id TEXT NOT NULL,
|
|
leg TEXT NOT NULL,
|
|
action TEXT NOT NULL,
|
|
side TEXT NOT NULL,
|
|
inst_id TEXT NOT NULL,
|
|
qty_eth REAL NOT NULL,
|
|
qty_contracts REAL,
|
|
base_px REAL,
|
|
fill_px REAL NOT NULL,
|
|
fee REAL NOT NULL,
|
|
slip REAL NOT NULL,
|
|
notional REAL NOT NULL,
|
|
ts_ms INTEGER NOT NULL,
|
|
exec_mode TEXT,
|
|
FOREIGN KEY(group_id) REFERENCES groups(group_id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS positions (
|
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
group_id TEXT,
|
|
perp_side TEXT,
|
|
perp_qty_eth REAL DEFAULT 0,
|
|
perp_entry_px REAL,
|
|
option_inst_id TEXT,
|
|
option_side TEXT,
|
|
option_qty_eth REAL DEFAULT 0,
|
|
option_qty_contracts REAL DEFAULT 0,
|
|
option_entry_px REAL,
|
|
entry_index_px REAL,
|
|
initial_premium REAL DEFAULT 0,
|
|
status TEXT
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS ledger_entries (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
group_id TEXT,
|
|
kind TEXT NOT NULL,
|
|
amount REAL NOT NULL,
|
|
balance_after REAL NOT NULL,
|
|
note TEXT,
|
|
ts_ms INTEGER NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS strategy_state (
|
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
running INTEGER NOT NULL DEFAULT 0,
|
|
phase TEXT NOT NULL DEFAULT 'idle',
|
|
rounds_done INTEGER NOT NULL DEFAULT 0,
|
|
window_key TEXT,
|
|
rest_until_ms INTEGER,
|
|
last_error TEXT,
|
|
updated_at_ms INTEGER NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS residual_options (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
group_id TEXT NOT NULL UNIQUE,
|
|
option_inst_id TEXT NOT NULL,
|
|
option_side TEXT NOT NULL,
|
|
option_qty_eth REAL NOT NULL,
|
|
option_qty_contracts REAL,
|
|
option_entry_px REAL NOT NULL,
|
|
strike REAL,
|
|
expiry_ymd TEXT,
|
|
expiry_ms INTEGER,
|
|
entry_index_px REAL,
|
|
initial_premium REAL DEFAULT 0,
|
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
created_at_ms INTEGER NOT NULL,
|
|
settled_at_ms INTEGER,
|
|
settle_px REAL,
|
|
settle_pnl REAL,
|
|
note TEXT,
|
|
FOREIGN KEY(group_id) REFERENCES groups(group_id)
|
|
);
|
|
CREATE TABLE IF NOT EXISTS funds_wallets (
|
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
funding_usdt REAL NOT NULL DEFAULT 0,
|
|
trading_usdt REAL NOT NULL DEFAULT 0,
|
|
options_funding_usdc REAL NOT NULL DEFAULT 0,
|
|
options_trading_usdc REAL NOT NULL DEFAULT 0,
|
|
options_funding_usdt REAL NOT NULL DEFAULT 0,
|
|
options_trading_usdt REAL NOT NULL DEFAULT 0,
|
|
updated_at_ms INTEGER NOT NULL
|
|
);
|
|
"""
|
|
|
|
|
|
def default_db_path(settings: Settings | None = None) -> Path:
|
|
s = settings or get_settings()
|
|
if s.db_path:
|
|
return Path(s.db_path)
|
|
root = Path(__file__).resolve().parents[2] # backend/
|
|
return root / "data" / "hedge.db"
|
|
|
|
|
|
class Database:
|
|
def __init__(self, path: Path | None = None) -> None:
|
|
self.path = path or default_db_path()
|
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
self._lock = threading.RLock()
|
|
self._conn = sqlite3.connect(str(self.path), check_same_thread=False)
|
|
self._conn.row_factory = sqlite3.Row
|
|
self._conn.execute("PRAGMA journal_mode=WAL;")
|
|
self._conn.executescript(_SCHEMA)
|
|
self._conn.commit()
|
|
self._migrate_columns()
|
|
self._ensure_seed()
|
|
|
|
def _migrate_columns(self) -> None:
|
|
"""幂等补列:exec_mode。"""
|
|
with self._lock:
|
|
for table, col, decl in (
|
|
("groups", "exec_mode", "TEXT"),
|
|
("groups", "funding_usdt", "REAL"),
|
|
("groups", "settle_index_px", "REAL"),
|
|
("fills", "exec_mode", "TEXT"),
|
|
("fills", "fee_ccy", "TEXT"),
|
|
):
|
|
cols = {
|
|
str(r[1])
|
|
for r in self._conn.execute(f"PRAGMA table_info({table})").fetchall()
|
|
}
|
|
if col not in cols:
|
|
self._conn.execute(
|
|
f"ALTER TABLE {table} ADD COLUMN {col} {decl}"
|
|
)
|
|
self._conn.commit()
|
|
|
|
def close(self) -> None:
|
|
with self._lock:
|
|
self._conn.close()
|
|
|
|
def _ensure_seed(self) -> None:
|
|
s = get_settings()
|
|
now = int(time.time() * 1000)
|
|
with self._lock:
|
|
row = self._conn.execute("SELECT id FROM ledger_meta WHERE id=1").fetchone()
|
|
if row is None:
|
|
self._conn.execute(
|
|
"INSERT INTO ledger_meta(id, equity, available, reserved, updated_at_ms) VALUES (1,?,?,0,?)",
|
|
(s.initial_equity, s.initial_equity, now),
|
|
)
|
|
pos = self._conn.execute("SELECT id FROM positions WHERE id=1").fetchone()
|
|
if pos is None:
|
|
self._conn.execute(
|
|
"INSERT INTO positions(id, status) VALUES (1, 'flat')"
|
|
)
|
|
st = self._conn.execute("SELECT id FROM strategy_state WHERE id=1").fetchone()
|
|
if st is None:
|
|
self._conn.execute(
|
|
"INSERT INTO strategy_state(id, running, phase, rounds_done, updated_at_ms) VALUES (1,0,'idle',0,?)",
|
|
(now,),
|
|
)
|
|
fw = self._conn.execute("SELECT id FROM funds_wallets WHERE id=1").fetchone()
|
|
if fw is None:
|
|
led = self._conn.execute(
|
|
"SELECT equity FROM ledger_meta WHERE id=1"
|
|
).fetchone()
|
|
eq = float(led["equity"]) if led else float(s.initial_equity)
|
|
self._conn.execute(
|
|
"""INSERT INTO funds_wallets(
|
|
id, funding_usdt, trading_usdt, options_funding_usdc, options_trading_usdc,
|
|
options_funding_usdt, options_trading_usdt, updated_at_ms
|
|
) VALUES (1,?,0,0,0,0,0,?)""",
|
|
(eq, now),
|
|
)
|
|
defaults = {
|
|
"fee_rate": str(s.fee_rate),
|
|
"initial_equity": str(s.initial_equity),
|
|
"exit_move_points": str(s.exit_move_points),
|
|
"exit_move_pct": str(s.exit_move_pct),
|
|
"exit_mode": str(s.exit_mode),
|
|
"net_profit_target": str(s.net_profit_target),
|
|
"premium_exit_multiple": str(s.premium_exit_multiple),
|
|
"rest_seconds": str(s.rest_seconds),
|
|
"live_order_interval_sec": str(s.live_order_interval_sec),
|
|
"skip_weekends": str(s.skip_weekends),
|
|
"max_rounds": str(s.max_rounds),
|
|
"leverage": str(s.leverage),
|
|
"min_option_hours": str(s.min_option_hours),
|
|
"min_option_leverage": str(s.min_option_leverage),
|
|
"atm_open_offset_enabled": str(s.atm_open_offset_enabled),
|
|
"max_atm_open_offset": str(s.max_atm_open_offset),
|
|
"fixed_direction_enabled": str(s.fixed_direction_enabled),
|
|
"fixed_perp_side": str(s.fixed_perp_side),
|
|
"close_bid_mark_max_pct": str(s.close_bid_mark_max_pct),
|
|
"perp_qty_eth": str(s.perp_qty_eth),
|
|
"option_qty_eth": str(s.option_qty_eth),
|
|
"exchange": str(s.exchange),
|
|
"perp_inst_id": str(s.perp_inst_id),
|
|
"option_inst_family": str(s.option_inst_family),
|
|
"index_inst_id": str(s.index_inst_id),
|
|
"option_ct_mult_default": str(s.option_ct_mult_default),
|
|
}
|
|
for k, v in defaults.items():
|
|
exists = self._conn.execute(
|
|
"SELECT key FROM settings WHERE key=?", (k,)
|
|
).fetchone()
|
|
if exists is None:
|
|
self._conn.execute(
|
|
"INSERT INTO settings(key, value) VALUES (?,?)", (k, v)
|
|
)
|
|
self._conn.commit()
|
|
|
|
def execute(self, sql: str, params: tuple[Any, ...] | list[Any] = ()) -> sqlite3.Cursor:
|
|
with self._lock:
|
|
cur = self._conn.execute(sql, params)
|
|
self._conn.commit()
|
|
return cur
|
|
|
|
def executemany(self, sql: str, seq: list[tuple[Any, ...]]) -> None:
|
|
with self._lock:
|
|
self._conn.executemany(sql, seq)
|
|
self._conn.commit()
|
|
|
|
def fetchone(self, sql: str, params: tuple[Any, ...] | list[Any] = ()) -> sqlite3.Row | None:
|
|
with self._lock:
|
|
return self._conn.execute(sql, params).fetchone()
|
|
|
|
def fetchall(self, sql: str, params: tuple[Any, ...] | list[Any] = ()) -> list[sqlite3.Row]:
|
|
with self._lock:
|
|
return list(self._conn.execute(sql, params).fetchall())
|
|
|
|
def get_setting(self, key: str, default: str | None = None) -> str | None:
|
|
row = self.fetchone("SELECT value FROM settings WHERE key=?", (key,))
|
|
if row is None:
|
|
return default
|
|
return str(row["value"])
|
|
|
|
def set_setting(self, key: str, value: str) -> None:
|
|
self.execute(
|
|
"INSERT INTO settings(key, value) VALUES(?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value",
|
|
(key, value),
|
|
)
|
|
|
|
|
|
_db: Database | None = None
|
|
|
|
|
|
def get_db() -> Database:
|
|
global _db
|
|
if _db is None:
|
|
_db = Database()
|
|
return _db
|
|
|
|
|
|
def set_db(db: Database | None) -> None:
|
|
global _db
|
|
_db = db
|