feat libs,sdk,trend
This commit is contained in:
Binary file not shown.
BIN
py-client/libs/__pycache__/order.cpython-311.pyc
Normal file
BIN
py-client/libs/__pycache__/order.cpython-311.pyc
Normal file
Binary file not shown.
BIN
py-client/libs/__pycache__/orderbook.cpython-311.pyc
Normal file
BIN
py-client/libs/__pycache__/orderbook.cpython-311.pyc
Normal file
Binary file not shown.
BIN
py-client/libs/__pycache__/overview.cpython-311.pyc
Normal file
BIN
py-client/libs/__pycache__/overview.cpython-311.pyc
Normal file
Binary file not shown.
BIN
py-client/libs/__pycache__/runtime.cpython-311.pyc
Normal file
BIN
py-client/libs/__pycache__/runtime.cpython-311.pyc
Normal file
Binary file not shown.
BIN
py-client/libs/__pycache__/watch.cpython-311.pyc
Normal file
BIN
py-client/libs/__pycache__/watch.cpython-311.pyc
Normal file
Binary file not shown.
@@ -9,6 +9,15 @@ import httpx
|
||||
COLLECTOR_URL = "http://139.224.247.176:13499/collector"
|
||||
|
||||
|
||||
def submit_trend_data() -> None:
|
||||
"""每五分钟提交趋势策略的最新缓存,尚无快照时跳过。"""
|
||||
from strategy.trend.boot import get_collector_snapshot
|
||||
|
||||
snapshot = get_collector_snapshot()
|
||||
if snapshot is not None:
|
||||
collector_push(*snapshot)
|
||||
|
||||
|
||||
def _json_value(value: Any) -> Any:
|
||||
"""Convert the QMT model values into values accepted by a JSON encoder."""
|
||||
if is_dataclass(value) and not isinstance(value, type):
|
||||
@@ -27,7 +36,7 @@ def _json_value(value: Any) -> Any:
|
||||
|
||||
|
||||
def collector_push(account_id: str, assets: Any, positions: Any) -> None:
|
||||
"""Best-effort collector upload; never propagate errors to the caller."""
|
||||
"""[暂停] 数据收集提交,太耗时,超过200毫秒."""
|
||||
try:
|
||||
payload = _json_value(
|
||||
{
|
||||
|
||||
@@ -70,29 +70,29 @@ class OrderBook:
|
||||
|
||||
for item in orders:
|
||||
# 不处理状态不对的
|
||||
if item.status not in TRACKED_STATUSES:
|
||||
if str(item.order_status) not in TRACKED_STATUSES:
|
||||
continue
|
||||
if item.status in BUSY_STATUSES:
|
||||
busy_keys.add(self._busy_key(item.side, item.code))
|
||||
if str(item.order_status) in BUSY_STATUSES:
|
||||
busy_keys.add(self._busy_key(item.side, item.stock_code))
|
||||
# 清理过期的
|
||||
if (
|
||||
item.created_at is not None
|
||||
and item.local_order_id.startswith(f"{self.order_prefix}-")
|
||||
and item.status in CANCELABLE_STATUSES
|
||||
and str(item.order_status) in CANCELABLE_STATUSES
|
||||
and current - item.created_at > self.cancel_timeout_sec
|
||||
):
|
||||
try:
|
||||
client.cancel_by_id(item.id)
|
||||
client.cancel_by_id(item.order_sys_id)
|
||||
canceled += 1
|
||||
logging.info(
|
||||
"[Order] 超时撤单,代码=%s,方向=%s,柜台订单=%s",
|
||||
item.code,
|
||||
item.stock_code,
|
||||
item.side,
|
||||
item.id,
|
||||
item.order_sys_id,
|
||||
)
|
||||
except Exception:
|
||||
logging.exception(
|
||||
"[Order] 撤单失败,保留在途状态,订单=%s", item.id
|
||||
"[Order] 撤单失败,保留在途状态,订单=%s", item.order_sys_id
|
||||
)
|
||||
|
||||
# 缓存本次有效订单
|
||||
|
||||
@@ -1,62 +1,79 @@
|
||||
"""SQLite 状态簿:每个账户/策略使用独立数据库,单个策略串行读写。"""
|
||||
"""SQLite positions and deals, aligned with SDK models; one writer per database."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import sqlite3
|
||||
from contextlib import closing
|
||||
from dataclasses import asdict
|
||||
from dataclasses import asdict, fields
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from sdk import DealItem
|
||||
from sdk import DealItem, PositionItem
|
||||
|
||||
SCHEMA = """
|
||||
CREATE TABLE IF NOT EXISTS positions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
code TEXT NOT NULL,
|
||||
base_order_id TEXT NOT NULL DEFAULT '',
|
||||
base_qty INTEGER NOT NULL DEFAULT 0 CHECK (base_qty >= 0),
|
||||
base_cost REAL NOT NULL DEFAULT 0.0 CHECK (base_cost >= 0),
|
||||
added_order_id TEXT NOT NULL DEFAULT '',
|
||||
added_num INTEGER NOT NULL DEFAULT 0 CHECK (added_num >= 0),
|
||||
added_qty INTEGER NOT NULL DEFAULT 0 CHECK (added_qty >= 0),
|
||||
added_cost REAL NOT NULL DEFAULT 0.0 CHECK (added_cost >= 0),
|
||||
status TEXT NOT NULL
|
||||
stock_code TEXT NOT NULL,
|
||||
stock_name TEXT NOT NULL DEFAULT '',
|
||||
direction INTEGER,
|
||||
volume INTEGER NOT NULL DEFAULT 0 CHECK (volume >= 0),
|
||||
open_price REAL NOT NULL DEFAULT 0,
|
||||
open_cost REAL NOT NULL DEFAULT 0,
|
||||
float_profit REAL NOT NULL DEFAULT 0,
|
||||
market_value REAL NOT NULL DEFAULT 0,
|
||||
stock_holder TEXT NOT NULL DEFAULT '',
|
||||
frozen_volume INTEGER NOT NULL DEFAULT 0,
|
||||
can_use_volume INTEGER NOT NULL DEFAULT 0,
|
||||
on_road_volume INTEGER NOT NULL DEFAULT 0,
|
||||
yesterday_volume INTEGER NOT NULL DEFAULT 0,
|
||||
last_price REAL NOT NULL DEFAULT 0,
|
||||
profit_rate REAL NOT NULL DEFAULT 0,
|
||||
future_trade_type INTEGER,
|
||||
expire_date TEXT NOT NULL DEFAULT ''
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_positions_code ON positions (code);
|
||||
CREATE INDEX IF NOT EXISTS idx_positions_base_order_id ON positions (base_order_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_positions_added_order_id ON positions (added_order_id);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_positions_stock_code ON positions (stock_code);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS deals (
|
||||
id INTEGER PRIMARY KEY,
|
||||
sys_order_id TEXT NOT NULL UNIQUE CHECK (sys_order_id <> ''),
|
||||
local_order_id TEXT NOT NULL,
|
||||
code TEXT NOT NULL,
|
||||
instrument_id TEXT NOT NULL,
|
||||
exchange_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
offset_flag TEXT NOT NULL,
|
||||
side TEXT NOT NULL CHECK (side IN ('BUY', 'SELL')),
|
||||
status TEXT NOT NULL,
|
||||
remaining_volume INTEGER NOT NULL CHECK (remaining_volume >= 0),
|
||||
traded_volume INTEGER NOT NULL CHECK (traded_volume > 0),
|
||||
order_time INTEGER NOT NULL,
|
||||
insert_date TEXT NOT NULL,
|
||||
insert_time TEXT NOT NULL,
|
||||
remark TEXT NOT NULL,
|
||||
price REAL NOT NULL,
|
||||
trade_price REAL NOT NULL CHECK (trade_price >= 0),
|
||||
trade_amount REAL NOT NULL CHECK (trade_amount > 0)
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
stock_code TEXT NOT NULL,
|
||||
order_sys_id TEXT NOT NULL CHECK (order_sys_id <> ''),
|
||||
order_local_id TEXT NOT NULL CHECK (order_local_id <> ''),
|
||||
ref INTEGER NOT NULL DEFAULT 0,
|
||||
order_ref TEXT NOT NULL DEFAULT '',
|
||||
direction INTEGER NOT NULL DEFAULT 0,
|
||||
offset_flag INTEGER NOT NULL CHECK (offset_flag IN (23, 24, 48, 49)),
|
||||
price REAL NOT NULL CHECK (price >= 0),
|
||||
volume INTEGER NOT NULL CHECK (volume > 0),
|
||||
trade_amount REAL NOT NULL CHECK (trade_amount > 0),
|
||||
trade_date TEXT NOT NULL,
|
||||
trade_time TEXT NOT NULL,
|
||||
remark TEXT NOT NULL DEFAULT '',
|
||||
close_profit REAL NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_deals_local_order_id ON deals (local_order_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_deals_code_date ON deals (code, insert_date);
|
||||
CREATE INDEX IF NOT EXISTS idx_deals_date_time ON deals (insert_date, insert_time);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_deals_order_sys_id ON deals (order_sys_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_deals_order_ref ON deals (order_local_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_deals_stock_code_date ON deals (stock_code);
|
||||
CREATE INDEX IF NOT EXISTS idx_deals_date_time ON deals (trade_date);
|
||||
"""
|
||||
|
||||
# SQL is assembled once; order_local_id is derived from the SDK remark property.
|
||||
POSITION_FIELDS = tuple(field.name for field in fields(PositionItem))
|
||||
DEAL_FIELDS = tuple(field.name for field in fields(DealItem)) + ('order_local_id',)
|
||||
POSITION_UPSERT = (
|
||||
f"INSERT INTO positions ({', '.join(POSITION_FIELDS)}) "
|
||||
f"VALUES ({', '.join(':' + key for key in POSITION_FIELDS)}) "
|
||||
"ON CONFLICT(stock_code) DO UPDATE SET "
|
||||
+ ', '.join(f'{key} = excluded.{key}' for key in POSITION_FIELDS if key != 'stock_code')
|
||||
)
|
||||
DEAL_INSERT = (
|
||||
f"INSERT INTO deals ({', '.join(DEAL_FIELDS)}) "
|
||||
f"VALUES ({', '.join(':' + key for key in DEAL_FIELDS)})"
|
||||
)
|
||||
|
||||
|
||||
class OrderBook:
|
||||
"""Persist position snapshots and append-only executions; one writer per database."""
|
||||
"""Position snapshots and append-only deals. No schema migration."""
|
||||
|
||||
def __init__(self, path: str | Path) -> None:
|
||||
self.path = Path(path)
|
||||
@@ -76,8 +93,8 @@ class OrderBook:
|
||||
def load(self) -> None:
|
||||
with closing(self._connect()) as db, db:
|
||||
db.execute('BEGIN')
|
||||
positions = {row['code']: dict(row) for row in db.execute('SELECT * FROM positions')}
|
||||
deals = {row['sys_order_id']: dict(row) for row in db.execute('SELECT * FROM deals ORDER BY id')}
|
||||
positions = {row['stock_code']: dict(row) for row in db.execute('SELECT * FROM positions')}
|
||||
deals = {row['order_sys_id']: dict(row) for row in db.execute('SELECT * FROM deals ORDER BY id')}
|
||||
self.positions = positions
|
||||
self.deals = deals
|
||||
self.deals_sys_ids = set(deals)
|
||||
@@ -85,86 +102,58 @@ class OrderBook:
|
||||
|
||||
@staticmethod
|
||||
def _insert_deals(db: sqlite3.Connection, deals: list[dict]) -> None:
|
||||
db.executemany(
|
||||
"""INSERT INTO deals
|
||||
(sys_order_id, local_order_id, code, instrument_id, exchange_id, name,
|
||||
offset_flag, side, status, remaining_volume, traded_volume, order_time,
|
||||
insert_date, insert_time, remark, price, trade_price, trade_amount)
|
||||
VALUES (:sys_order_id, :local_order_id, :code, :instrument_id, :exchange_id, :name,
|
||||
:offset_flag, :side, :status, :remaining_volume, :traded_volume, :order_time,
|
||||
:insert_date, :insert_time, :remark, :price, :trade_price, :trade_amount)""",
|
||||
deals,
|
||||
)
|
||||
db.executemany(DEAL_INSERT, deals)
|
||||
|
||||
@staticmethod
|
||||
def deal_record(deal: DealItem) -> dict:
|
||||
"""API 字段转入库记录;委托价格不用于推算成交金额。"""
|
||||
if not deal.sys_order_id or deal.traded_volume <= 0:
|
||||
raise ValueError('System order ID and positive traded volume are required')
|
||||
if not deal.order_sys_id or deal.volume <= 0:
|
||||
raise ValueError('System order ID and positive volume are required')
|
||||
if not deal.local_order_id:
|
||||
raise ValueError('Local order ID is required')
|
||||
row = asdict(deal)
|
||||
row['order_local_id'] = deal.local_order_id
|
||||
if any(isinstance(value, float) and not math.isfinite(value) for value in row.values()):
|
||||
raise ValueError('Prices and amounts must be finite')
|
||||
amount = deal.trade_amount if deal.trade_amount > 0 else deal.trade_price * deal.traded_volume
|
||||
raise ValueError('Numeric values must be finite')
|
||||
amount = deal.trade_amount if deal.trade_amount > 0 else deal.price * deal.volume
|
||||
if not math.isfinite(amount) or amount <= 0:
|
||||
raise ValueError('Execution amount must be positive and finite')
|
||||
raise ValueError('Trade amount must be positive and finite')
|
||||
row['trade_amount'] = amount
|
||||
date = deal.insert_date or datetime.now().date().isoformat()
|
||||
date = deal.trade_date or datetime.now().date().isoformat()
|
||||
if len(date) == 8 and date.isdigit():
|
||||
date = f'{date[:4]}-{date[4:6]}-{date[6:]}'
|
||||
row['insert_date'] = date
|
||||
row['trade_date'] = date
|
||||
return row
|
||||
|
||||
def sync_deals(self, deals: list[DealItem]) -> None:
|
||||
"""按系统订单号去重,批量写入已成交数据;失败时不更新缓存。"""
|
||||
new_deals = []
|
||||
seen = self.deals_sys_ids.copy()
|
||||
for deal in deals:
|
||||
if deal.sys_order_id in seen:
|
||||
if deal.order_sys_id in seen:
|
||||
continue
|
||||
new_deals.append(self.deal_record(deal))
|
||||
seen.add(deal.sys_order_id)
|
||||
seen.add(deal.order_sys_id)
|
||||
if not new_deals:
|
||||
return
|
||||
with closing(self._connect()) as db, db:
|
||||
self._insert_deals(db, new_deals)
|
||||
self.load()
|
||||
|
||||
def sync_positions(self, positions: list[PositionItem]) -> None:
|
||||
"""Replace the complete position snapshot, retaining IDs for existing stocks."""
|
||||
self.save({item.stock_code: asdict(item) for item in positions}, list(self.deals.values()))
|
||||
|
||||
def save(self, items: dict, deals: list[dict]) -> None:
|
||||
if len(deals) < self._deal_count:
|
||||
raise ValueError('Execution history is append-only')
|
||||
new_deals = deals[self._deal_count:]
|
||||
positions = [
|
||||
{
|
||||
'base_order_id': '', 'base_qty': 0, 'base_cost': 0.0,
|
||||
'added_order_id': '', 'added_num': 0, 'added_qty': 0, 'added_cost': 0.0,
|
||||
**item,
|
||||
}
|
||||
for item in items.values()
|
||||
]
|
||||
for row in [*items.values(), *new_deals]:
|
||||
positions = [{**asdict(PositionItem()), **item} for item in items.values()]
|
||||
for row in [*positions, *new_deals]:
|
||||
if any(isinstance(value, float) and not math.isfinite(value) for value in row.values()):
|
||||
raise ValueError('Quantities, prices and amounts must be finite')
|
||||
raise ValueError('Numeric values must be finite')
|
||||
with closing(self._connect()) as db, db:
|
||||
# 更新已有证券时保留其自增 ID;仅删除快照中已移除的证券。
|
||||
for row in db.execute('SELECT code FROM positions').fetchall():
|
||||
if row['code'] not in items:
|
||||
db.execute('DELETE FROM positions WHERE code = ?', (row['code'],))
|
||||
db.executemany(
|
||||
"""INSERT INTO positions
|
||||
(code, base_order_id, base_qty, base_cost,
|
||||
added_order_id, added_num, added_qty, added_cost, status)
|
||||
VALUES (:code, :base_order_id, :base_qty, :base_cost,
|
||||
:added_order_id, :added_num, :added_qty, :added_cost, :status)
|
||||
ON CONFLICT(code) DO UPDATE SET
|
||||
base_order_id = excluded.base_order_id,
|
||||
base_qty = excluded.base_qty,
|
||||
base_cost = excluded.base_cost,
|
||||
added_order_id = excluded.added_order_id,
|
||||
added_num = excluded.added_num,
|
||||
added_qty = excluded.added_qty,
|
||||
added_cost = excluded.added_cost,
|
||||
status = excluded.status""",
|
||||
positions,
|
||||
)
|
||||
for row in db.execute('SELECT stock_code FROM positions').fetchall():
|
||||
if row['stock_code'] not in items:
|
||||
db.execute('DELETE FROM positions WHERE stock_code = ?', (row['stock_code'],))
|
||||
db.executemany(POSITION_UPSERT, positions)
|
||||
self._insert_deals(db, new_deals)
|
||||
self.load()
|
||||
|
||||
@@ -26,8 +26,7 @@ def Overview(assets, positions, account_cfg=None) -> None:
|
||||
if position.volume <= 0:
|
||||
continue
|
||||
log.info(
|
||||
"[启动] %s %s %s,持仓=%d,可用=%d,成本=%.2f(%.2f),现价=%.2f,盈亏=%.2f%%",
|
||||
position.trade_id,
|
||||
"[启动] %s %s,持仓=%d,可用=%d,成本=%.2f(%.2f),现价=%.2f,盈亏=%.2f%%",
|
||||
position.stock_code,
|
||||
position.stock_name,
|
||||
position.volume,
|
||||
|
||||
Reference in New Issue
Block a user