"""R625:合成引擎配置语义迁移、未知键拒绝、兼容回放与可执行回滚包。""" from decimal import Decimal as D, InvalidOperation from hashlib import sha256 import json from pathlib import Path import subprocess import sys from tempfile import TemporaryDirectory from zipfile import ZipFile OLD={'period':3,'commission_pct':.1,'trade_on_close':False} DATA=[100,101,102,100,104,105] def finite_fee(value): try: result=D(str(value)) except (InvalidOperation,ValueError) as error: raise ValueError('invalid decimal fee') from error if not result.is_finite() or result<0: raise ValueError('fee must be finite and nonnegative') return result def resolve_old(raw): if set(raw)-set(OLD):raise ValueError('unknown old key') cfg={**OLD,**raw} if type(cfg['period']) is not int or cfg['period']<2:raise ValueError('bad lookback') if cfg['trade_on_close'] is not False:raise ValueError('same-close semantics unsupported') cfg['commission_pct']=finite_fee(cfg['commission_pct']) return cfg def migrate(raw): old=resolve_old(raw) return {'lookback':old['period'],'fee_bps':str(D(str(old['commission_pct']))*100), 'execution':'next_open'} def kernel(prices,lookback,rate,execution): if execution!='next_open':raise ValueError('unsupported execution') cash=D(1000);shares=0;signals=[];fills=[] for t in range(lookback-1,len(prices)-1): target=int(D(prices[t])>sum(D(v) for v in prices[t-lookback+1:t+1])/lookback) signals.append([t,target]);qty=target-shares if qty: price=D(prices[t+1]);fee=abs(qty)*price*rate cash-=qty*price+fee;fills.append([t,t+1,qty,str(price),str(fee)]) shares=target return {'effective':{'lookback':lookback,'fee_rate':str(rate),'execution':execution}, 'signals':signals,'fills':fills,'ending':str(cash+shares*D(prices[-1]))} def old_run(raw,prices): cfg=resolve_old(raw) return kernel(prices,cfg['period'],D(str(cfg['commission_pct']))/100,'next_open') def new_run(raw,prices): if set(raw)-{'lookback','fee_bps','execution'}:raise ValueError('unknown new key') cfg={'lookback':3,'fee_bps':'0','execution':'next_open',**raw} if type(cfg['lookback']) is not int or cfg['lookback']<2:raise ValueError('bad new lookback') rate=finite_fee(cfg['fee_bps'])/10000 return kernel(prices,cfg['lookback'],rate,cfg['execution']) def main(): converted=migrate({});old=old_run({},DATA);new=new_run(converted,DATA) assert old==new and D(old['ending'])==D('1003.691') assert converted=={'lookback':3,'fee_bps':'10.0','execution':'next_open'} assert new_run({'lookback':3},DATA)['ending']=='1004' try:new_run({'fee_bp':'10'},DATA) except ValueError:pass else:raise AssertionError('unknown new key accepted') for bad in [{'commision_pct':.1},{'trade_on_close':True}]: try:migrate(bad) except ValueError:pass else:raise AssertionError('ambiguous migration accepted') # 审阅回归:旧解析、迁移和新解析均拒绝NaN及正负无穷。 rejected=[] for label,call in [('old',lambda x:resolve_old({'commission_pct':x})), ('migration',lambda x:migrate({'commission_pct':x})), ('new',lambda x:new_run({'fee_bps':x},DATA))]: for invalid in [float('nan'),float('inf'),float('-inf'),'NaN','Infinity','-Infinity']: try:call(invalid) except ValueError:pass except Exception as error:rejected.append((label,str(invalid),'wrong_exception:'+type(error).__name__)) else:rejected.append((label,str(invalid),'accepted')) assert not rejected,rejected with TemporaryDirectory(prefix='r625-compat-') as temp: folder=Path(temp);restored=folder/'restored';restored.mkdir() blobs={'runner.py':Path(__file__).read_bytes(),'old_config.json':json.dumps(OLD).encode(), 'data.json':json.dumps(DATA).encode(),'expected.json':json.dumps(old,sort_keys=True).encode()} manifest={name:sha256(blob).hexdigest() for name,blob in blobs.items()} archive=folder/'rollback.zip' with ZipFile(archive,'w') as out: for name,blob in blobs.items():out.writestr(name,blob) out.writestr('manifest.json',json.dumps(manifest)) with ZipFile(archive) as source: for name,expected in manifest.items(): blob=source.read(name);assert sha256(blob).hexdigest()==expected (restored/name).write_bytes(blob) replay=subprocess.run([sys.executable,'-I','-B',str(restored/'runner.py'),'--replay-v1',str(restored)], cwd=folder,capture_output=True,text=True,check=True,timeout=5) assert json.loads(replay.stdout)==old print('mapped='+str(converted)+' effective_rate=0.001') print('signals_equal=True fills_equal=True ending_old=1003.691 ending_new=1003.691') print('unknown_key=rejected unsupported_close=rejected omitted_new_fee=incompatible') print('rollback_archive=4_hashed_files independent_old_replay=matched') if __name__=='__main__': if len(sys.argv)>1 and sys.argv[1]=='--replay-v1': directory=Path(sys.argv[2]) print(json.dumps(old_run(json.loads((directory/'old_config.json').read_text()),json.loads((directory/'data.json').read_text())))) else:main()