dev zt
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
"""SQLite 策略状态与成交存储;每个数据库仅使用一个写入者,不做数据迁移。"""
|
||||
|
||||
import math
|
||||
import json
|
||||
import logging as log
|
||||
import sqlite3
|
||||
from contextlib import closing
|
||||
@@ -29,12 +28,6 @@ CREATE TABLE IF NOT EXISTS state (
|
||||
-- 每个证券仅保留一条策略状态。
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_state_stock_code ON state (stock_code);
|
||||
|
||||
-- 首次归档前的持仓基准,清仓后仍保留,供迟到成交按时间重算。
|
||||
CREATE TABLE IF NOT EXISTS state_origin (
|
||||
stock_code TEXT PRIMARY KEY, -- 证券代码
|
||||
snapshot TEXT NOT NULL -- 初始持仓字段的 JSON 快照
|
||||
);
|
||||
|
||||
-- 成交记录独立保存,不随状态删除。
|
||||
CREATE TABLE IF NOT EXISTS deals (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@@ -104,8 +97,8 @@ class State:
|
||||
self.deals = deals
|
||||
self.deals_sys_ids = set(deals)
|
||||
|
||||
def sync_deals(self, deals: list[DealItem]) -> None:
|
||||
"""按系统成交编号去重,整批写入成功后刷新缓存。"""
|
||||
def sync_deals(self, deals: list[DealItem], *, archived: bool = False) -> None:
|
||||
"""按成交编号去重;初始化底仓时,已包含在快照内的成交可直接标记归档。"""
|
||||
new_deals = {}
|
||||
for deal in deals:
|
||||
if deal.order_sys_id not in self.deals_sys_ids and deal.order_sys_id not in new_deals:
|
||||
@@ -127,62 +120,44 @@ class State:
|
||||
db.execute(
|
||||
'INSERT INTO deals (stock_code, order_sys_id, order_local_id, ref, '
|
||||
'order_ref, direction, offset_flag, price, volume, trade_amount, '
|
||||
'trade_date, trade_time, remark, close_profit) '
|
||||
'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
'trade_date, trade_time, remark, close_profit, is_arch) '
|
||||
'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
(deal.stock_code, deal.order_sys_id, order_id, deal.ref,
|
||||
deal.order_ref, deal.direction, deal.offset_flag, deal.price, deal.volume,
|
||||
amount, date, deal.trade_time, deal.remark, deal.close_profit),
|
||||
amount, date, deal.trade_time, deal.remark, deal.close_profit, int(archived)),
|
||||
)
|
||||
self.load()
|
||||
|
||||
def archiving(self) -> dict[str, str]:
|
||||
"""按证券从持仓基准重放成交;失败证券保留未归档记录并返回原因。"""
|
||||
def archiving(self, *, base_order_prefix: str = '') -> dict[str, str]:
|
||||
"""将未归档成交累加到当前底仓和加仓;失败证券保留记录供重试。"""
|
||||
errors = {}
|
||||
with closing(self._connect()) as db, db:
|
||||
db.execute('BEGIN IMMEDIATE')
|
||||
codes = db.execute(
|
||||
'SELECT DISTINCT stock_code FROM deals WHERE is_arch = 0 AND offset_flag IN (48, 49)'
|
||||
'SELECT DISTINCT stock_code FROM deals WHERE is_arch = 0 AND offset_flag IN (23, 24, 48, 49)'
|
||||
).fetchall()
|
||||
for entry in codes:
|
||||
code = entry['stock_code']
|
||||
db.execute('SAVEPOINT archive_stock')
|
||||
try:
|
||||
current = db.execute('SELECT * FROM state WHERE stock_code = ?', (code,)).fetchone()
|
||||
origin = db.execute('SELECT snapshot FROM state_origin WHERE stock_code = ?', (code,)).fetchone()
|
||||
if origin is None:
|
||||
# 没有旧基准时不能用已归档后的持仓反推历史,不做数据迁移。
|
||||
if db.execute(
|
||||
'SELECT 1 FROM deals WHERE stock_code = ? AND is_arch = 1 LIMIT 1', (code,)
|
||||
).fetchone():
|
||||
raise ValueError('Missing holding baseline for archived history')
|
||||
state = dict(current) if current else asdict(StateItem(stock_code=code))
|
||||
db.execute('INSERT INTO state_origin VALUES (?, ?)', (code, json.dumps(state)))
|
||||
else:
|
||||
state = json.loads(origin['snapshot'])
|
||||
# 数量相等的初始买入视为已包含在快照中,只匹配一次。
|
||||
snapshot_qty = state['base_qty'] + state['added_qty']
|
||||
covered = False
|
||||
state = dict(current) if current else asdict(StateItem(stock_code=code))
|
||||
deals = db.execute(
|
||||
'SELECT * FROM deals WHERE stock_code = ? AND offset_flag IN (48, 49) '
|
||||
'SELECT * FROM deals WHERE stock_code = ? AND is_arch = 0 AND offset_flag IN (23, 24, 48, 49) '
|
||||
"ORDER BY trade_date, REPLACE(trade_time, ':', ''), id", (code,)
|
||||
).fetchall()
|
||||
for deal in deals:
|
||||
qty = deal['volume']
|
||||
if deal['offset_flag'] == 48:
|
||||
if not covered and snapshot_qty == qty:
|
||||
covered = True
|
||||
continue
|
||||
covered = True
|
||||
total = state['added_qty'] + qty
|
||||
state['added_price'] = (
|
||||
state['added_qty'] * state['added_price'] + deal['trade_amount']
|
||||
if deal['offset_flag'] in (23, 48):
|
||||
bucket = 'base' if base_order_prefix and deal['order_local_id'].startswith(base_order_prefix) else 'added'
|
||||
total = state[f'{bucket}_qty'] + qty
|
||||
state[f'{bucket}_price'] = (
|
||||
state[f'{bucket}_qty'] * state[f'{bucket}_price'] + deal['trade_amount']
|
||||
) / total
|
||||
state['added_qty'] = total
|
||||
state['added_order_local_id'] = deal['order_local_id']
|
||||
state['added_created_at'] = f"{deal['trade_date']} {deal['trade_time']}".strip()
|
||||
state[f'{bucket}_qty'] = total
|
||||
state[f'{bucket}_order_local_id'] = deal['order_local_id']
|
||||
state[f'{bucket}_created_at'] = f"{deal['trade_date']} {deal['trade_time']}".strip()
|
||||
else:
|
||||
# 卖出之后的买入属于新交易,不再与初始快照匹配。
|
||||
covered = True
|
||||
total = state['base_qty'] + state['added_qty']
|
||||
if qty > total:
|
||||
raise ValueError(f'Sell volume {qty} exceeds recorded holdings {total}')
|
||||
@@ -198,7 +173,7 @@ class State:
|
||||
if state['base_qty'] + state['added_qty'] == 0:
|
||||
db.execute('DELETE FROM state WHERE stock_code = ?', (code,))
|
||||
else:
|
||||
# 重算数量和成本,保留调用方当前设置的 status 及已有记录主键。
|
||||
# 更新持仓,保留策略状态及已有记录主键。
|
||||
state['status'] = current['status'] if current else state['status']
|
||||
state.pop('id', None)
|
||||
columns = tuple(state)
|
||||
@@ -211,7 +186,7 @@ class State:
|
||||
)
|
||||
db.execute(
|
||||
'UPDATE deals SET is_arch = 1 WHERE stock_code = ? '
|
||||
'AND is_arch = 0 AND offset_flag IN (48, 49)', (code,)
|
||||
'AND is_arch = 0 AND offset_flag IN (23, 24, 48, 49)', (code,)
|
||||
)
|
||||
except (ValueError, sqlite3.IntegrityError) as exc:
|
||||
db.execute('ROLLBACK TO archive_stock')
|
||||
@@ -222,28 +197,24 @@ class State:
|
||||
self.load()
|
||||
return errors
|
||||
|
||||
def sync_state(self, positions: list[PositionItem]) -> None:
|
||||
def sync_state(self, positions: list[PositionItem], *, remove_missing: bool = True) -> None:
|
||||
"""同步完整持仓:无状态则插入底仓,已有则保留,清仓则删除。
|
||||
|
||||
数量为零或未出现在完整持仓列表中的证券视为已清仓;空列表清空状态。
|
||||
底仓已包含的历史成交不应再次归档;后续成交须先归档,再同步持仓。
|
||||
remove_missing=False 时仅接纳新底仓,减仓由成交归档处理。
|
||||
"""
|
||||
# 传入完整账户持仓;同步时间作为新增底仓的创建时间。
|
||||
created_at = datetime.now().isoformat(timespec='seconds')
|
||||
holdings = {item.stock_code: item for item in positions if item.volume > 0}
|
||||
with closing(self._connect()) as db, db:
|
||||
db.execute('BEGIN')
|
||||
existing = {row['stock_code']: dict(row) for row in db.execute('SELECT * FROM state')}
|
||||
# 先保留基准,再删除清仓状态,卖出成交仍可据此归档。
|
||||
for code, row in existing.items():
|
||||
db.execute(
|
||||
'INSERT OR IGNORE INTO state_origin SELECT ?, ? WHERE NOT EXISTS '
|
||||
'(SELECT 1 FROM deals WHERE stock_code = ? AND is_arch = 1)',
|
||||
(code, json.dumps(row), code),
|
||||
existing = {row['stock_code'] for row in db.execute('SELECT stock_code FROM state')}
|
||||
if remove_missing:
|
||||
db.executemany(
|
||||
'DELETE FROM state WHERE stock_code = ?',
|
||||
[(code,) for code in existing if code not in holdings],
|
||||
)
|
||||
db.executemany(
|
||||
'DELETE FROM state WHERE stock_code = ?',
|
||||
[(code,) for code in existing if code not in holdings],
|
||||
)
|
||||
for code, item in holdings.items():
|
||||
if code in existing:
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user