"""Deterministic teaching model. No APIs, accounts or real orders.
Run: python order-model.py /new/output/dir
Prices, quantities, fees, queue and later events are explicitly synthetic.
"""
from decimal import Decimal as D
from pathlib import Path
import csv,json,sys
ASKS=[(D('60.00'),D('0.50')),(D('60.10'),D('1.00')),(D('60.40'),D('1.50'))]
MAKER=D('.001');TAKER=D('.002')
def serialize(x):
    if isinstance(x,D):return format(x,'f')
    if isinstance(x,dict):return {k:serialize(v) for k,v in x.items()}
    if isinstance(x,list):return [serialize(v) for v in x]
    return x
def candidate(case,limit=None,tif='GTC',post=False,quantity=D('2')):
    if limit is not None:limit=D(limit)
    eligible=[(p,q) for p,q in ASKS if limit is None or p<=limit]
    available=sum((q for p,q in eligible),D(0));remaining=quantity;fills=[]
    if post and available>0:status='rejected_post_only'
    elif tif=='FOK' and available<quantity:status='cancelled_fok'
    else:
        for price,size in eligible:
            got=min(remaining,size)
            if got>0:fills.append({'price':price,'quantity':got,'role':'taker','fee':price*got*TAKER});remaining-=got
        if remaining==0:status='filled'
        elif limit is None:status='depth_exhausted'
        elif tif in ['IOC','FOK']:status='cancelled_remainder'
        else:status='open_remainder'
    filled=sum((f['quantity'] for f in fills),D(0));notional=sum((f['price']*f['quantity'] for f in fills),D(0))
    fee=sum((f['fee'] for f in fills),D(0));open_qty=remaining if status=='open_remainder' else D(0)
    cancelled=quantity-filled-open_qty
    return {'case':case,'quantity_requested':quantity,'limit':limit,'time_in_force':tif,'post_only':post,'status':status,'fills':fills,
            'quantity_filled':filled,'quantity_open':open_qty,'quantity_not_filled_or_open':cancelled,'notional_eur':notional,'fee_eur':fee,
            'cash_debit_eur':notional+fee,'average_fill_price':notional/filled if filled else None,
            'illustrative_reserve_eur':open_qty*limit*(1+TAKER) if open_qty else D(0)}
def model():
    cases=[candidate('market_two'),candidate('limit_60.10_gtc','60.10'),candidate('limit_60.10_ioc','60.10','IOC'),
           candidate('limit_60.10_fok','60.10','FOK'),candidate('post_only_60.10','60.10',post=True),candidate('post_only_59.90','59.90',post=True),
           candidate('limit_59.90_gtc','59.90'),candidate('limit_60.40_gtc','60.40'),candidate('limit_60.40_fok','60.40','FOK'),
           candidate('limit_59.90_ioc','59.90','IOC'),candidate('market_five_depth_limit',quantity=D('5'))]
    mixed=candidate('gtc_then_assumed_later_maker_fill','60.10');later_qty=mixed['quantity_open'];price=D('60.10')
    mixed['fills'].append({'price':price,'quantity':later_qty,'role':'maker','fee':price*later_qty*MAKER})
    mixed.update(status='filled_after_assumed_event',quantity_filled=D('2'),quantity_open=D(0),quantity_not_filled_or_open=D(0),illustrative_reserve_eur=D(0))
    mixed['notional_eur']=sum(f['price']*f['quantity'] for f in mixed['fills']);mixed['fee_eur']=sum(f['fee'] for f in mixed['fills'])
    mixed['cash_debit_eur']=mixed['notional_eur']+mixed['fee_eur'];mixed['average_fill_price']=mixed['notional_eur']/D(2)
    mixed['assumption']='A later incoming market seller fills the entire 0.5 LTC remainder at its resting 60.10 price. No competing order ahead at that price.';cases.append(mixed)
    prior=D('2');ours=D('2');queue=[]
    for sell in [D('1'),D('1.5')]:
        consumed_prior=min(prior,sell);prior-=consumed_prior;ours_fill=min(ours,sell-consumed_prior);ours-=ours_fill
        queue.append({'incoming_sell_ltc':sell,'prior_order_fill_ltc':consumed_prior,'our_fill_ltc':ours_fill,'prior_remaining_ltc':prior,'our_remaining_ltc':ours})
    checks=[]
    def check(name,value):assert value,name;checks.append({'name':name,'passed':True})
    for x in cases:
        check(x['case']+' quantity conservation',x['quantity_requested']==x['quantity_filled']+x['quantity_open']+x['quantity_not_filled_or_open'])
        check(x['case']+' fills reconcile',sum((f['quantity'] for f in x['fills']),D(0))==x['quantity_filled'])
        check(x['case']+' notional reconciles',sum((f['price']*f['quantity'] for f in x['fills']),D(0))==x['notional_eur'])
        check(x['case']+' fees reconcile',sum((f['price']*f['quantity']*(MAKER if f['role']=='maker' else TAKER) for f in x['fills']),D(0))==x['fee_eur'])
        check(x['case']+' debit reconciles',x['cash_debit_eur']==x['notional_eur']+x['fee_eur'])
        check(x['case']+' limit respected',x['limit'] is None or all(f['price']<=x['limit'] for f in x['fills']))
    check('Hand-calculated market debit',cases[0]['cash_debit_eur']==D('120.54060'))
    check('GTC and IOC same immediate fills, different remainder',cases[1]['fills']==cases[2]['fills'] and cases[1]['quantity_open']==D('.5') and cases[2]['quantity_open']==0)
    check('Partial FOK does not execute',cases[3]['quantity_filled']==0)
    check('Crossing post-only does not execute',cases[4]['quantity_filled']==0 and cases[4]['quantity_open']==0)
    check('Mixed roles fee hand calculation',mixed['fee_eur']==D('.21025'))
    check('FIFO queue example does not infer touch as fill',queue[0]['our_fill_ltc']==0 and queue[1]['our_fill_ltc']==D('.5') and ours==D('1.5'))
    return serialize({'schema':1,'release':'2026-10-03-v1','evidence':'synthetic_model','asks':[{'price_eur':p,'quantity_ltc':q} for p,q in ASKS],
                      'maker_fee_fraction':MAKER,'taker_fee_fraction':TAKER,'scenarios':cases,'fifo_queue_events':queue,'assertions':checks,
                      'scope':'Synthetic three-level book, declared maker/taker fees, no exchange engine, order precision, hidden liquidity, latency, funding or withdrawal. Quote-currency fees and a conservative illustrative reserve. Market-five row only walks bounded depth; not an exchange order outcome.'})
def main():
    out=Path(sys.argv[1]).resolve();out.mkdir(parents=True,exist_ok=True);data=model()
    (out/'order-results.json').write_text(json.dumps(data,indent=2)+'\n',encoding='utf-8')
    fields=['case','quantity_requested','limit','time_in_force','post_only','status','quantity_filled','quantity_open','quantity_not_filled_or_open','notional_eur','fee_eur','cash_debit_eur','average_fill_price','illustrative_reserve_eur']
    with (out/'order-cases.csv').open('w',newline='',encoding='utf-8') as f:
        w=csv.DictWriter(f,fieldnames=fields,extrasaction='ignore');w.writeheader();w.writerows(data['scenarios'])
    print(json.dumps({'scenarios':len(data['scenarios']),'queue_events':len(data['fifo_queue_events']),'assertions':len(data['assertions'])}))
if __name__=='__main__':main()
