tradingSystem/scripts/repair_ledger_history.py

96 lines
5.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
"""155 容器:python scripts/repair_ledger_history.py backup --codes ... > 主机备份.json
预演:python scripts/repair_ledger_history.py preview --codes ... < 备份.json
写入:停止 worker/beat 后,apply --backup-sha256 <文件哈希> --codes ... < 同一备份
回退:rollback --run-id HIST_xxx,仅原值未再变化时可回退。审计和原证据均保留。
脚本无模型调用、无交易下发,所有 SQL 经单表守卫;不得在开发机连接数据库。
"""
import argparse
import hashlib
import json
import sys
from datetime import datetime
from pathlib import Path
sys.path.insert(0,str(Path(__file__).resolve().parents[1]))
from app.core.ledger_repair import compile_repair, canonical
def backup(codes):
from app.db.session import fetch_all, TABLE_PREFIX
from app.repo import qmt_repo as q, pms_repo as p, downstream_repo as ds
from app.services import param_store as ps
if TABLE_PREFIX:
raise ValueError('仅允许模拟仓命名空间')
out = dict(schema=1,at=datetime.now().isoformat()+'+08:00',instance='155-sim',tables={})
for table in ['pms_position','pms_lot','pms_instruction','pms_plan','pms_command','pms_qmt_order',
'pms_cash_flow','pms_strategy','pms_proposal','pms_daily_report','pms_nav_daily']:
out['tables'][table] = fetch_all('SELECT * FROM '+table+' ORDER BY '+('ymd' if table=='pms_nav_daily' else 'id'))
params={f'c{i}':c for i,c in enumerate(codes)}; where='ts_code IN ('+','.join(':'+k for k in params)+')'
for table in ['pms_action_ledger','pms_op_log']:
out['tables'][table]=fetch_all('SELECT * FROM '+table+' WHERE '+where+' ORDER BY id',params)
rows=[];seq=0
while True:
page=fetch_all("SELECT * FROM pms_qmt_inbox WHERE msg_type='trade' AND seq>:s ORDER BY seq LIMIT 1000",{'s':seq})
if not page:break
rows.extend(page);seq=page[-1]['seq']
out['tables']['pms_qmt_inbox']=rows
out['position_snapshot']=q.latest_snapshot('positions')
out['funds_snapshot']=q.latest_snapshot('funds')
out['table_fills']=ds.fetch_filled_orders(since_time='2000-01-01 00:00:00',limit=100000)
if len(out['table_fills'])>=100000:raise ValueError('成交表截断,拒绝备份')
out['replay_cursor']=p.get_param('PMS_REPLAY_CURSOR')
out['runtime_rows']=fetch_all("SELECT * FROM pms_runtime_param WHERE param_key IN ('PMS_TOTAL_SCALE','PMS_CUSHION_SOLID','PMS_DISPATCH_MODE','PMS_PLAN_VERSION_RECHECK','PMS_REPLAY_CURSOR') ORDER BY param_key")
out['effective']={k:ps.get(k) for k in ['PMS_TOTAL_SCALE','PMS_CUSHION_SOLID','PMS_DISPATCH_MODE','PMS_PLAN_VERSION_RECHECK']}
return out
def guards_for(b,codes):
guards=[]
params={f'c{i}':c for i,c in enumerate(codes)};where='ts_code IN ('+','.join(':'+k for k in params)+')'
for table in ['pms_position','pms_lot','pms_instruction','pms_qmt_order','pms_strategy','pms_plan']:
guards.append(dict(name=table,sql='SELECT * FROM '+table+' WHERE '+where+' ORDER BY id FOR UPDATE',
params=params,expected=[x for x in b['tables'][table] if x['ts_code'] in codes]))
guards.append(dict(name='全部逐笔证据',sql="SELECT * FROM pms_qmt_inbox WHERE msg_type='trade' ORDER BY seq FOR UPDATE",
expected=b['tables']['pms_qmt_inbox']))
guards.append(dict(name='运行参数及回放游标',sql="SELECT * FROM pms_runtime_param WHERE param_key IN ('PMS_TOTAL_SCALE','PMS_CUSHION_SOLID','PMS_DISPATCH_MODE','PMS_PLAN_VERSION_RECHECK','PMS_REPLAY_CURSOR') ORDER BY param_key FOR UPDATE", expected=b['runtime_rows']))
return guards
def main():
ap=argparse.ArgumentParser(description=__doc__)
ap.add_argument('mode',choices=['backup','preview','apply','rollback'])
ap.add_argument('--codes',nargs='+');ap.add_argument('--backup-sha256');ap.add_argument('--run-id')
args=ap.parse_args()
if args.mode=='backup':
if not args.codes:raise ValueError('缺股票范围')
print(canonical(backup(args.codes)));return
if args.mode=='rollback':
from app.db.session import fetch_all, apply_audited_repair, TABLE_PREFIX
if TABLE_PREFIX or not args.run_id:raise ValueError('回退实例或批号错误')
rows=fetch_all("SELECT * FROM pms_op_log WHERE ref=:r AND op='history_repair_row' ORDER BY id",{'r':args.run_id})
if not rows:raise ValueError('无修复审计')
changes=[]
for row in rows:
item=json.loads(row['params_json'])
changes.append(dict(table=item['table'],key=item['key'],before=item['after'],
fields={k:item['before'][k] for k in set(item['fields'])|{'updated_at'}}))
print(canonical(apply_audited_repair(dict(run_id=args.run_id,changes=changes),[],rollback=True)));return
raw=sys.stdin.buffer.read();b=json.loads(raw)
if not args.codes:raise ValueError('缺股票范围')
effective=b.get('effective',{})
plan=compile_repair(b,args.codes,scale=float(effective.get('PMS_TOTAL_SCALE',2000000)),
solid=float(effective.get('PMS_CUSHION_SOLID',0.03)))
if args.mode=='preview':print(canonical(plan));return
if not args.backup_sha256 or hashlib.sha256(raw).hexdigest()!=args.backup_sha256:
raise ValueError('完整备份文件哈希不符')
from app.db.session import apply_audited_repair, TABLE_PREFIX
if TABLE_PREFIX:raise ValueError('拒绝真实仓')
age=(datetime.now()-datetime.fromisoformat(b['at']).replace(tzinfo=None)).total_seconds()
if not 0<=age<900:raise ValueError('备份超过15分钟,须重新备份和预演')
if not b['position_snapshot'] or b['position_snapshot']['age_sec']>900:
raise ValueError('QMT持仓快照过旧')
print(canonical(apply_audited_repair(plan,guards_for(b,args.codes))))
if __name__=='__main__':main()