This commit is contained in:
2026-09-11 10:44:34 +08:00
parent 4e693d773a
commit 6e554e1565
10 changed files with 140 additions and 36 deletions

View File

@@ -137,6 +137,7 @@ class State:
amount, date, deal.trade_time, deal.remark, deal.close_profit, 0),
)
self.load_deals()
def sync_state(self, positions: list[PositionItem]) -> None:
"""同步完整持仓:无状态则插入底仓,已有则保留,清仓则删除。
"""
@@ -165,20 +166,29 @@ class State:
)
self.load_state()
def archiving(self, *, base_order_prefix: str = '') -> dict[str, str]:
"""将未归档成交累加到当前底仓和加仓;失败证券保留记录供重试。"""
errors = {}
def archiving(self) -> None:
"""将未归档成交累加到当前底仓和加仓;失败证券保留记录供重试。
归档是把成交反映到持仓状态中,再标记为已处理,不会删除成交记录。
本地委托号以 zt-base- 开头的买入计入底仓,其他买入计入补仓。
不返回结果;失败原因记录到日志,对应成交保留未归档标记供重试。
"""
with closing(self._connect()) as db, db:
# 提前取得数据库写入锁,让持仓更新和成交标记在同一事务内完成。
db.execute('BEGIN IMMEDIATE')
# 只找尚未处理的成交23、48 是买入24、49 是卖出。
codes = db.execute(
'SELECT DISTINCT stock_code FROM deals WHERE is_arch = 0 AND offset_flag IN (23, 24, 48, 49)'
'SELECT DISTINCT stock_code FROM deals WHERE is_arch = 0'
).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()
# 已有持仓就接着计算;没有记录则从底仓、补仓均为零开始。
state = dict(current) if current else asdict(StateItem(stock_code=code))
# 按成交日期、时间、记录编号依次处理,保证先买后卖等顺序正确。
deals = db.execute(
'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,)
@@ -186,34 +196,43 @@ class State:
for deal in deals:
qty = deal['volume']
if deal['offset_flag'] in (23, 48):
bucket = 'base' if base_order_prefix and deal['order_local_id'].startswith(base_order_prefix) else 'added'
# 按已有的 ZT 底仓委托号约定识别,无需调用方传入规则。
bucket = 'base' if deal['order_local_id'].startswith('zt-base-') 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[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:
total = state['base_qty'] + state['added_qty']
# 卖出不能超过本地记录的总持仓;不一致时留待排查后重试。
if qty > total:
raise ValueError(f'Sell volume {qty} exceeds recorded holdings {total}')
# 卖出优先扣补仓;补仓未卖完时只减数量,保留原均价。
if qty < state['added_qty']:
state['added_qty'] -= qty
else:
# 补仓全部卖完后,剩余卖出量扣底仓,并清空补仓信息。
state['base_qty'] = total - qty
state['added_qty'] = 0
state['added_price'] = 0.0
state['added_order_local_id'] = state['added_created_at'] = ''
if state['base_qty'] + state['added_qty'] == 0:
# 中途清仓先重置,后续若又买入,就从零重新累计。
state = asdict(StateItem(stock_code=code))
if state['base_qty'] + state['added_qty'] == 0:
# 全部成交处理完仍无持仓,则删除该股票的策略状态。
db.execute('DELETE FROM state WHERE stock_code = ?', (code,))
else:
# 更新持仓,保留策略状态及已有记录主键。
state['status'] = current['status'] if current else state['status']
state.pop('id', None)
columns = tuple(state)
# 没有该股票就新增,已有则更新仓位字段,不替换原记录主键。
db.execute(
f"INSERT INTO state ({', '.join(columns)}) "
f"VALUES ({', '.join(':' + key for key in columns)}) "
@@ -221,17 +240,19 @@ class State:
+ ', '.join(f'{key} = excluded.{key}' for key in columns if key != 'stock_code'),
state,
)
# 持仓处理成功后才标记成交,防止下次重复加仓或重复扣减。
db.execute(
'UPDATE deals SET is_arch = 1 WHERE stock_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')
errors[code] = str(exc)
log.warning('[归档] %s 失败,保留未归档成交:%s', code, exc)
finally:
# 释放当前股票的回滚点;整个事务在退出外层 with 时提交。
db.execute('RELEASE archive_stock')
# 数据库提交完成后刷新内存缓存,让策略读到最新持仓和归档标记。
self.load()
return errors