#!/usr/bin/env python3 """Reproduce a fictional withdrawal calendar. Standard library only; no network.""" from __future__ import annotations from datetime import date, timedelta from decimal import Decimal, ROUND_DOWN, localcontext from pathlib import Path import argparse, csv, io, json D=Decimal ATOM=D('0.00000001') ZERO=D('0') def coins(value): return format(value.quantize(ATOM),'f') def cash(value): return format(value.quantize(D('0.00000001')),'f') def csv_bytes(rows): out=io.StringIO(newline=''); writer=csv.DictWriter(out,fieldnames=list(rows[0]),lineterminator='\n') writer.writeheader();writer.writerows(rows);return out.getvalue().encode('utf-8') def model(inputs, scenario): start=date.fromisoformat(inputs['start']);end=date.fromisoformat(inputs['end_exclusive']) if end<=start:raise ValueError('Positive study window required') fee=D(scenario['fee_ltc']);minimum=D(scenario['minimum_ltc']);threshold=D(inputs['withdrawal_threshold_ltc']) if min(fee,minimum,threshold)<0 or fee%ATOM or minimum%ATOM:raise ValueError('Invalid fee/minimum') if scenario['minimum_basis'] not in ['gross','net'] or scenario['rule'] not in ['weekly','monthly','threshold']:raise ValueError('Invalid policy') prices=[D(p) for p in inputs['prices']] if not prices or any(p<=0 for p in prices):raise ValueError('Positive execution prices required') interval=int(inputs['purchase_interval_days']);budget=D(inputs['purchase_cash']) if interval<1 or budget<=0:raise ValueError('Positive purchase interval/budget required') dates=[start+timedelta(days=i*interval) for i in range(len(prices))] if dates[-1]>=end:raise ValueError('Purchases must precede end') purchases=dict(zip(dates,prices));balance=wallet=fees=acquired=spent=remainder=days=peak=ZERO successes=blocked=attempts=0;daily=[];events=[] def withdraw(day,reason): nonlocal balance,wallet,fees,successes,blocked,attempts before=balance;attempts+=1;net=balance-fee eligible=net>0 and (balance if scenario['minimum_basis']=='gross' else net)>=minimum status='success' if eligible else 'empty' if balance==0 else 'blocked' paid=received=ZERO if eligible: paid=fee;received=net;fees+=paid;wallet+=received;balance=ZERO;successes+=1 elif before>0:blocked+=1 events.append({'scenario':scenario['id'],'date':day.isoformat(),'event':'withdrawal','reason':reason,'status':status,'price_usd':'','cash_budget_usd':'','bought_ltc':coins(ZERO),'cash_remainder_usd':'','custody_before_ltc':coins(before),'fee_ltc':coins(paid),'delivered_ltc':coins(received),'custody_after_ltc':coins(balance)}) return status day=start while day=threshold if due:withdraw(day,scenario['rule']) days+=balance assert acquired==wallet+fees+balance daily.append({'scenario':scenario['id'],'date':day.isoformat(),'bought_ltc':coins(bought),'custody_closing_ltc':coins(balance),'wallet_cumulative_ltc':coins(wallet),'fee_cumulative_ltc':coins(fees),'acquired_cumulative_ltc':coins(acquired),'custody_ltc_days_cumulative':coins(days)}) day+=timedelta(days=1) before_terminal=balance;terminal_status='not-requested' if scenario['terminal_sweep']:terminal_status=withdraw(end,'terminal-sweep') assert acquired==wallet+fees+balance horizon=D(inputs['price_at_horizon']);window=(end-start).days result={'scenario':scenario['id'],'group':scenario['group'],'rule':scenario['rule'],'minimum_basis':scenario['minimum_basis'],'minimum_ltc':coins(minimum),'fee_each_ltc':coins(fee),'terminal_sweep':scenario['terminal_sweep'],'cash_contributed_usd':cash(spent),'unused_cash_usd':cash(remainder),'gross_acquired_ltc':coins(acquired),'withdrawal_attempts':attempts,'successful_withdrawals':successes,'blocked_nonempty_attempts':blocked,'total_fee_ltc':coins(fees),'wallet_received_ltc':coins(wallet),'custody_before_terminal_ltc':coins(before_terminal),'custody_final_ltc':coins(balance),'total_retained_ltc':coins(wallet+balance),'custody_ltc_days':coins(days),'mean_custodial_ltc':format(days/D(window),'.12f'),'peak_pre_withdrawal_ltc':coins(peak),'terminal_status':terminal_status,'horizon_marked_value_usd':cash((wallet+balance)*horizon+remainder),'window_days':window} return result,daily,events def outputs(inputs): summaries=[];daily=[];events=[] with localcontext() as ctx: ctx.prec=40 for scenario in inputs['scenarios']: summary,days,actions=model(inputs,scenario);summaries.append(summary);daily.extend(days);events.extend(actions) result={'format':inputs['format'],'checked_on':inputs['checked_on'],'scenarios':summaries} return {'summary.csv':csv_bytes(summaries),'daily-ledger.csv':csv_bytes(daily),'events.csv':csv_bytes(events),'results.json':(json.dumps(result,indent=2,ensure_ascii=False)+'\n').encode('utf-8')} def main(): parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('folder',nargs='?',default=str(Path(__file__).resolve().parent));parser.add_argument('--write',action='store_true',help='Write deterministic CSV/JSON; default checks existing released outputs') args=parser.parse_args();folder=Path(args.folder);inputs=json.loads((folder/'inputs.json').read_text(encoding='utf-8')) for name,data in outputs(inputs).items(): target=folder/name if args.write:target.write_bytes(data) elif target.read_bytes()!=data:raise SystemExit('Reproduction mismatch: '+name) print(json.dumps({'state':'written' if args.write else 'reproduced','scenarios':len(inputs['scenarios']),'daily_rows':len(inputs['scenarios'])*(date.fromisoformat(inputs['end_exclusive'])-date.fromisoformat(inputs['start'])).days,'network_requests':0})) if __name__=='__main__':main()