Download scripts/export_holding_lab.py from Ypeng12/quant-ai: direct link, hf CLI and curl.
- Browser
- Download file 4.78 kB
-
https://huggingface.co/spaces/Ypeng12/quant-ai/resolve/main/scripts/export_holding_lab.py
- Command line
-
hf download hf://spaces/Ypeng12/quant-ai/scripts/export_holding_lab.py
-
curl -L -o export_holding_lab.py https://huggingface.co/spaces/Ypeng12/quant-ai/resolve/main/scripts/export_holding_lab.py
4.78 kB
| #!/usr/bin/env python3 | |
| """Publish all declared holding experiments with reconciled ledgers and hashes.""" | |
| import argparse | |
| import hashlib | |
| import json | |
| from pathlib import Path | |
| import shutil | |
| import sys | |
| import pandas as pd | |
| ROOT=Path(__file__).resolve().parents[1] | |
| sys.path.insert(0,str(ROOT/'backend')) | |
| from app.research.attribution import ledger_attribution | |
| from app.research.artifacts import load_platform_results | |
| def digest(path): | |
| return hashlib.sha256(Path(path).read_bytes()).hexdigest() | |
| def export(inputs, output): | |
| out=Path(output).resolve();out.mkdir(parents=True,exist_ok=False) | |
| trials=[];seen=set();first=None;sources={} | |
| for number, source in enumerate(map(Path,inputs)): | |
| registry=json.loads((source/'registry.json').read_text()) | |
| if registry['status']!='complete':raise ValueError('Incomplete source experiment') | |
| if first is None: | |
| first=registry;shutil.copytree(source/'bars',out/'bars') | |
| if any(digest(source/'bars'/f'{s}.parquet')!=digest(out/'bars'/f'{s}.parquet') for s in first['inputs']): | |
| raise ValueError('Candidate input snapshots differ') | |
| archive=out/'experiments'/str(number);archive.mkdir(parents=True) | |
| shutil.copy2(source/'registry.json',archive/'registry.json') | |
| for relative, expected in registry['sources'].items(): | |
| path=source/'source'/relative | |
| if digest(path)!=expected:raise ValueError('Missing exact experimental source snapshot') | |
| destination=archive/'source'/relative;destination.parent.mkdir(parents=True,exist_ok=True) | |
| shutil.copy2(path,destination);sources[str(destination.relative_to(ROOT))]=expected | |
| for name, summary in registry['results'].items(): | |
| if name in seen:continue | |
| seen.add(name);folder=out/name;shutil.copytree(source/name,folder) | |
| model=source/'models'/f'{name}.json' | |
| if model.exists(): | |
| (out/'models').mkdir(exist_ok=True);shutil.copy2(model,out/'models'/model.name) | |
| marks=pd.read_csv(folder/'marks.csv');fills=pd.read_csv(folder/'fills.csv') | |
| attr,detail=ledger_attribution(marks,fills,registry['symbols'],registry['capital']) | |
| detail.to_csv(folder/'attribution.csv',index=False) | |
| if abs(attr['net_pnl']-summary['net_pnl'])>1e-6:raise ValueError('Experiment ledger mismatch') | |
| candidate=next((c for c in registry['candidates'] if c['name']==name),None) | |
| trials.append(dict(candidate=dict(name=name,features=candidate['family'] if candidate else 'benchmark', | |
| model='Ridge with prior calibration' if name.startswith('calibrated') else 'Ridge' if candidate else 'No ML', | |
| portfolio='costed return curve' if candidate and candidate['planning'] else 'single interval or benchmark'), | |
| status='complete',cost_bps=5,summary=summary,attribution=attr, | |
| per_symbol={s:{k:v for k,v in r.items() if k!='symbol'} for r in attr['by_symbol'] for s in [r['symbol']]}, | |
| experiment_registry=str((archive/'registry.json').relative_to(out)),policy_cost_bps=candidate['cost_bps'] if candidate else None)) | |
| days=pd.read_csv(out/trials[0]['candidate']['name']/'daily.csv')['date'].tolist() | |
| report=dict(status='complete',registered_at=first['created_at'],evaluation_dates=days, | |
| symbols=first['symbols']+first['references'],traded_symbols=first['symbols'],starting_equity=first['capital'], | |
| cost_bps_per_side=[5],cost_description='成交账本统一按单边 5 bps 模拟。默认候选内部规划成本为 2 bps;标注“规划 5 bps”的候选同时按 5 bps 规划。两者均非券商实际手续费。', | |
| trials=trials,selected_for_live=None,performance_verified=False,default_display_candidate='calibrated_legacy_curve', | |
| unverified_assumptions=first['limitations']+[first['evaluation']],sources=sources, | |
| data_paths={s:str((out/'bars'/f'{s}.parquet').relative_to(ROOT)) for s in first['inputs']}, | |
| data_hashes={s:digest(out/'bars'/f'{s}.parquet') for s in first['inputs']}) | |
| report['artifact_hashes']={str(p.relative_to(out)):digest(p) for p in out.rglob('*') if p.is_file()} | |
| (out/'registry.json').write_text(json.dumps(report,indent=2,allow_nan=False)+'\n') | |
| (out/'registry.sha256').write_text(digest(out/'registry.json')+'\n') | |
| checked=load_platform_results(out) | |
| if not checked['success']:raise ValueError(checked['reason']) | |
| print(json.dumps(dict(output=str(out),trials=len(trials),verified=True))) | |
| if __name__=='__main__': | |
| p=argparse.ArgumentParser(description=__doc__);p.add_argument('--inputs',nargs='+',required=True);p.add_argument('--output',required=True) | |
| a=p.parse_args();export(a.inputs,a.output) | |