#!/usr/bin/env python3
"""Python 3.10+ / standard library only. Demo-only; default is public market data.
Never paste keys into a webpage or chat. Uses OKX_DEMO_KEY, OKX_DEMO_SECRET,
OKX_DEMO_PASSPHRASE from your own environment. No real-money mode exists.
"""
import argparse,base64,datetime,decimal,hashlib,hmac,json,os,time,urllib.request,uuid
BASE='https://openapi.okx.com'
def signed_headers(method,path,body,timestamp,secret,key,passphrase):
    signature=base64.b64encode(hmac.new(secret.encode(),(timestamp+method.upper()+path+body).encode(),hashlib.sha256).digest()).decode()
    return {'OK-ACCESS-KEY':key,'OK-ACCESS-SIGN':signature,'OK-ACCESS-TIMESTAMP':timestamp,'OK-ACCESS-PASSPHRASE':passphrase,'x-simulated-trading':'1','Content-Type':'application/json'}
def request(method,path,payload=None,private=False):
    body=json.dumps(payload,separators=(',',':')) if payload is not None else ''
    headers={'Content-Type':'application/json','x-simulated-trading':'1','User-Agent':'MarketDepthLab-demo-example/1.0'}
    if private:
        key,secret,phrase=[os.environ.get('OKX_DEMO_'+x,'') for x in ('KEY','SECRET','PASSPHRASE')]
        if not all((key,secret,phrase)):raise RuntimeError('Set the three OKX_DEMO_* variables locally. Do not share them.')
        stamp=datetime.datetime.now(datetime.timezone.utc).isoformat(timespec='milliseconds').replace('+00:00','Z')
        headers=signed_headers(method,path,body,stamp,secret,key,phrase)
    req=urllib.request.Request(BASE+path,data=body.encode() if body else None,headers=headers,method=method)
    with urllib.request.urlopen(req,timeout=15) as r:result=json.load(r)
    if result.get('code')!='0':raise RuntimeError('OKX code '+str(result.get('code'))+': '+str(result.get('msg')))
    for row in result.get('data',[]):
        if row.get('sCode','0')!='0':raise RuntimeError('Order code '+row['sCode']+': '+str(row.get('sMsg')))
    return result['data']
def main():
    p=argparse.ArgumentParser(description=__doc__)
    p.add_argument('--balance',action='store_true');p.add_argument('--demo-order',action='store_true');p.add_argument('--price');p.add_argument('--size');args=p.parse_args()
    instrument='BTC-USDT' # Spot: size is BTC, not contracts. Demo only.
    spec=request('GET','/api/v5/public/instruments?instType=SPOT&instId='+instrument)[0]
    print(json.dumps({k:spec.get(k) for k in ('instId','minSz','lotSz','tickSz')},indent=2))
    if args.balance or args.demo_order:
        rows=request('GET','/api/v5/account/balance?ccy=USDT',private=True)
        print('Demo balance request succeeded. Account data is intentionally not printed.')
    if not args.demo_order:return
    D=decimal.Decimal
    if not args.price or not args.size:p.error('--demo-order requires --price and --size')
    price,size=D(args.price),D(args.size)
    if price<=0 or size<D(spec['minSz']) or size%D(spec['lotSz']) or price%D(spec['tickSz']):p.error('Price / size violates the instrument minimum or tick / lot size.')
    client_id='mdl'+uuid.uuid4().hex[:24]
    print('DEMO post-only order client ID:',client_id)
    payload={'instId':instrument,'tdMode':'cash','side':'buy','ordType':'post_only','px':str(price),'sz':str(size),'clOrdId':client_id}
    # No automatic retry of POST: a timeout may have happened after acceptance.
    try:
        order=request('POST','/api/v5/trade/order',payload,True)[0]
        print('Demo order accepted:',order['ordId'])
    finally:
        # Look up by client ID even if POST timed out; avoid an accidental duplicate.
        try:
            row=request('GET','/api/v5/trade/order?instId='+instrument+'&clOrdId='+client_id,private=True)[0]
            print('State before cleanup:',row['state'])
            if row['state'] in ('live','partially_filled'):
                request('POST','/api/v5/trade/cancel-order',{'instId':instrument,'clOrdId':client_id},True)
                for _ in range(5):
                    time.sleep(.4)
                    row=request('GET','/api/v5/trade/order?instId='+instrument+'&clOrdId='+client_id,private=True)[0]
                    if row['state'] not in ('live','partially_filled'):break
            print('Final demo state:',row['state'],'filled BTC:',row.get('accFillSz','?'))
            if row['state'] in ('live','partially_filled'):print('Cancellation not yet confirmed: check the Demo Trading open orders screen.')
        except Exception as e:
            print('Cleanup could not be confirmed. Check Demo Trading by client ID',client_id,'Error:',str(e))
            raise
if __name__=='__main__':main()
