"""Reproduce the new 5.4 teaching cases from saved Bid OHLC. Python standard library only. No network, terminal, trading or account calls. Default verifies bundled excerpts; --sources DIRECTORY also repeats the selection over the verified complete source files. """ from pathlib import Path from decimal import Decimal from fractions import Fraction from datetime import datetime,timedelta import argparse,csv,hashlib,json FIELDS=('open','high','low','close') SOURCES={ 'GBPUSD-H1.csv':('5289cd58e0fb935e0c65f0f57ac5047b4f27db5f8e6ed29fe937c559baba9df9',5), 'EURUSD-H1.csv':('0f58687cc1be679669de57ce3c128d708a51e415f8a2a7c614909fae97a49fef',5), 'USDJPY-H1.csv':('143ed1ae9c90c1da7890bbf858911822e8590999180ff4a765689a4718d3530d',3) } def sha(b):return hashlib.sha256(b).hexdigest() def load(path,digits): rows=[] with Path(path).open(encoding='utf-8-sig',newline='') as f: for raw in csv.DictReader(f): q={'time':raw['time_mt5'],'epoch':int(raw['time_raw_epoch'])} for k in FIELDS: v=Decimal(raw[k])*(10**digits);q[k]=int(v.to_integral_value()) assert abs(v-q[k])max(x['high'] for x in rows[i-2:i]) and q['high']>=max(x['high'] for x in rows[i+1:i+3]) l=q['low']t:break if not out or out[-1]['kind']!=q['kind']:out.append(q.copy()) elif (q['kind']=='H' and q['price']>out[-1]['price']) or (q['kind']=='L' and q['price']hi[0] and lo[1]>lo[0] def first_candidate(rows): raw=pivots(rows) for h in range(12,len(rows)-10): t=h+1 if not window(rows[t]['time']):continue q=alternating(raw,h-1) if len(q)<5:continue a=q[-5:] if ''.join(x['kind'] for x in a)!='LHLHL' or not rising(a[:4]):continue lo0,h0,lo1,h1,neck=a if h-lo0['i']>42 or h-neck['i']<2:continue if not(rows[h]['high']>max(x['high'] for x in rows[h-2:h]) and rows[h+1]['high']<=rows[h]['high']):continue if abs(rows[h]['high']-h1['price'])>100 or min(rows[h]['high'],h1['price'])-neck['price']<100:continue if rows[t]['close']<=neck['price']:continue return {'id':'ta25-pattern-candidate','source':'GBPUSD-H1.csv','digits':5,'kind':'candidate','start':max(0,lo0['i']-3),'assessment':t,'end':t+8,'anchors':a,'candidate':{'i':h,'price':rows[h]['high'],'kind':'H','earliest_known':h+2},'boundary':neck['price'],'tolerance_points':100} raise AssertionError('No candidate; do not relax rules') def first_triangle(rows): raw=pivots(rows) for t in range(20,len(rows)-8): if not window(rows[t]['time']):continue q=alternating(raw,t-1) if len(q)<8:continue a=q[-4:];context=q[-8:-4] hi=[x for x in a if x['kind']=='H'];lo=[x for x in a if x['kind']=='L'] if len(hi)!=2 or len(lo)!=2:continue if not(hi[1]['price']lo[0]['price']):continue if rising(context) or t-context[0]['i']>52:continue k=max(x['known'] for x in a) if not(kline(*hi,t)):continue return {'id':'ta25-pattern-context','source':'USDJPY-H1.csv','digits':3,'kind':'triangle-context','start':max(0,context[0]['i']-3),'assessment':t,'end':t+8,'anchors':a,'context_anchors':context,'upper_anchors':hi,'lower_anchors':lo,'apex_record':str(apex),'uptrend_context':False} raise AssertionError('No triangle; do not relax rules') def first_failed_double(rows): raw=pivots(rows);eligible=[] for t in range(20,len(rows)-8): if not window(rows[t]['time']):continue q=alternating(raw,t) if len(q)<6:continue a=q[-6:] if ''.join(x['kind'] for x in a)!='LHLHLH':continue lo0,h0,lo1,h1,neck,h2=a if not rising(a[:4]) or t-lo0['i']>44 or abs(h1['price']-h2['price'])>100:continue if min(h1['price'],h2['price'])-neck['price']<100 or h2['known']>t:continue if not(rows[t]['close']=neck['price']):continue if any(rows[j]['close']neck['price']),None) if invalid is None:continue return {'id':'ta25-pattern-invalidation','source':'EURUSD-H1.csv','digits':5,'kind':'double-top-failure','start':max(0,lo0['i']-3),'assessment':t,'end':t+8,'anchors':a,'boundary':neck['price'],'tolerance_points':100,'invalidation':invalid,'selection_uses_future_outcome':True} raise AssertionError('No failed double top; do not relax rules') def select(folder): data={} for name,(expected,digits) in SOURCES.items(): p=Path(folder)/name assert sha(p.read_bytes())==expected,('Source checksum',name) data[name]=load(p,digits) return [first_candidate(data['GBPUSD-H1.csv']),first_triangle(data['USDJPY-H1.csv']),first_failed_double(data['EURUSD-H1.csv'])],data def verify(folder): folder=Path(folder);m=json.loads((folder/'cases.json').read_text('utf8')) for c in m['cases']: before=(folder/c['before_csv']).read_bytes();after=(folder/c['after_csv']).read_bytes() assert sha(before)==c['before_sha256'] and sha(after)==c['after_sha256'] assert after.startswith(before), 'Before data must be the unedited prefix of the answer data' b=load(folder/c['before_csv'],c['digits']) a=load(folder/c['after_csv'],c['digits']) assert len(b)==c['assessment']-c['start']+1 and len(a)==c['end']-c['start']+1 assert b[-1]['time']==c['as_of_bar_open'] assert len(a)-len(b)==8 offset=c['start'];raw=pivots(b);known={(x['i']+offset,x['kind'],x['price']) for x in raw} for x in c['anchors']+c.get('context_anchors',[]): assert (x['i'],x['kind'],x['price']) in known assert x['known']<=c['assessment'] if c['kind']=='candidate': x=c['candidate'];i=x['i']-offset assert i==len(b)-2 and x['earliest_known']==c['assessment']+1 assert b[i]['high']>max(q['high'] for q in b[i-2:i]) and b[i+1]['high']<=b[i]['high'] assert (x['i'],'H',x['price']) not in known assert b[-1]['close']>c['boundary'] elif c['kind']=='triangle-context': assert not rising(c['context_anchors']) assert b[-1]['close']>line(*c['upper_anchors'],c['assessment']) assert Fraction(c['assessment'])=level j=c['invalidation']-offset assert a[j]['close']>level and all(a[k]['close']<=level for k in range(t+1,j)) return m def main(): p=argparse.ArgumentParser(description=__doc__);p.add_argument('--sources',type=Path);args=p.parse_args() here=Path(__file__).parent;m=verify(here) if args.sources: cases,_=select(args.sources) for selected,recorded in zip(cases,m['cases']): for k,v in selected.items():assert recorded[k]==v,('Selection mismatch',k) print(json.dumps({'status':'PASS','cases':len(m['cases']),'exact_prefixes':True,'source_selection_repeated':bool(args.sources),'trading_calls':0})) if __name__=='__main__':main()