"""冻结已核Form4事件表;P代码不自动等于普通股公开市场购买。""" from datetime import datetime,timedelta,timezone from decimal import Decimal as D,InvalidOperation import json def qualified(row): try: shares,price=D(str(row['shares'])),D(str(row['price'])) except (InvalidOperation,ValueError): return False return (row['canonical_reviewed'] is True and row['code']=='P' and row['table']=='I' and row['security']=='common_shares' and row['acquired']=='A' and row['open_market_confirmed'] is True and bool(row['independent_person_group']) and shares.is_finite() and price.is_finite() and shares>0 and price>0) def instant(value): parsed=datetime.fromisoformat(value) if parsed.tzinfo is None or parsed.utcoffset() is None: raise ValueError('timezone_required') return parsed.astimezone(timezone.utc) def cluster(events): known,triggers={},[] ordered=sorted([(instant(r['available']),r) for r in events], key=lambda pair:(pair[0],pair[1]['issuer'],pair[1]['event_id'])) for now,row in ordered: issuer=row['issuer'] key=(issuer,row['event_id']) if key in known: if known[key]!=row: raise ValueError('修订冲突需先冻结正确版本') continue # 按当前事件时刻重新计算前态;时间流逝本身也可能使旧组过期。 prior_groups={r['independent_person_group'] for r in known.values() if r['issuer']==issuer and qualified(r) and now-timedelta(days=10)<=instant(r['available'])<=now} known[key]=row valid=[r for r in known.values() if r['issuer']==issuer and qualified(r) and now-timedelta(days=10)<=instant(r['available'])<=now] groups={r['independent_person_group'] for r in valid} if len(groups)>=2 and len(prior_groups)<2: triggers.append({'issuer':issuer,'available':row['available'],'available_utc':now.isoformat(),'groups':sorted(groups), 'events':sorted(r['event_id'] for r in valid)}) return triggers def event(key,person,day,**updates): row={'issuer':'ALFA','event_id':key,'independent_person_group':person, 'available':f'2026-01-{day:02}T16:30:00-05:00','transaction_date':'2026-01-02', 'table':'I','security':'common_shares','code':'P','acquired':'A', 'shares':100,'price':10,'open_market_confirmed':True,'canonical_reviewed':True} row.update(updates);return row e1=event('E1','P1',5) rows=[e1,dict(e1),event('E2','P1',6),event('E3','P2',7,open_market_confirmed=False), event('E4','P2',8),event('GRANT','P3',8,code='A'), event('DERIVATIVE','P4',8,table='II'),event('UNKNOWN','P5',8,open_market_confirmed=None)] assert cluster(rows[:3])==[] and cluster(rows[:4])==[] triggers=cluster(rows) assert len(triggers)==1 and triggers[0]['available']=='2026-01-08T16:30:00-05:00' assert triggers[0]['groups']==['P1','P2'] and triggers[0]['events']==['E1','E2','E4'] assert not qualified(rows[3]) and not qualified(rows[-1]) assert not qualified(event('EXERCISE','P6',8,code='M')) assert not qualified(event('TAX','P7',8,code='F')) assert not qualified(event('BAD','P8',8,price=float('inf'))) assert cluster([e1,event('OTHER','P2',6,issuer='BETA')])==[] days=['2026-01-05','2026-01-06','2026-01-07','2026-01-08','2026-01-09', '2026-01-12','2026-01-13','2026-01-14','2026-01-15','2026-01-16', '2026-01-20','2026-01-21','2026-01-22','2026-01-23','2026-01-26', '2026-01-27','2026-01-28','2026-01-29','2026-01-30','2026-02-02', '2026-02-03','2026-02-04','2026-02-05','2026-02-06','2026-02-09'] opens=[d+'T09:30:00-05:00' for d in days] public=instant(triggers[0]['available']) entry=min(i for i,t in enumerate(opens) if instant(t)>public) due=entry+20 assert entry==4 and due==24 and days[entry]=='2026-01-09' and days[due]=='2026-02-09' cash,quantity,fees,orders=D(1000),0,0,[] for i,time in enumerate(opens): if i==entry: cash-=401;quantity=40;fees+=1;orders.append([time,'buy',40,'10',1]) assert cash==599 if i==due: cash+=D('9.5')*quantity-1;quantity=0;fees+=1 orders.append([time,'sell',40,'9.5',1]) assert orders==[[opens[4],'buy',40,'10',1],[opens[24],'sell',40,'9.5',1]] assert cash==978 and quantity==0 and fees==2 assert cluster(list(reversed(rows)))==triggers assert cluster(rows[:4]+[event('LATE','P2',25)])==[] # 1月5/6的P1购买已超10日 renewed=cluster([event('E1','P1',5),event('E4','P2',8),event('E5','P1',17)]) assert [r['available'] for r in renewed]==['2026-01-08T16:30:00-05:00','2026-01-17T16:30:00-05:00'] assert renewed[1]['events']==['E4','E5'] # 旧P1已过期,新P1恢复两人组 try: cluster([e1,dict(e1,shares=200)]) except ValueError: pass else: raise AssertionError('同事件内容冲突必须先解决版本') assert cluster(rows[:4])==[r for r in triggers if r['available']<=rows[3]['available']] mixed=[event('TZ1','P1',5,available='2026-01-05T22:00:00+00:00'), event('TZ2','P2',5,available='2026-01-05T17:30:00-05:00')] mixed_result=cluster(mixed) assert len(mixed_result)==1 and mixed_result[0]['available_utc']=='2026-01-05T22:30:00+00:00' assert mixed_result[0]['groups']==['P1','P2'] and mixed_result[0]['events']==['TZ1','TZ2'] assert cluster(list(reversed(mixed)))==mixed_result utc_rows=[dict(r,available=instant(r['available']).isoformat()) for r in mixed] logical=lambda ts:[{k:r[k] for k in ['issuer','available_utc','groups','events']} for r in ts] assert logical(cluster(utc_rows))==logical(mixed_result) at_boundary=[mixed[0],dict(mixed[1],available='2026-01-15T17:00:00-05:00')] assert len(cluster(at_boundary))==1 # 精确240小时,包含边界 assert cluster([at_boundary[0],dict(at_boundary[1],available='2026-01-15T17:00:00.000001-05:00')])==[] assert len(cluster([mixed[0],dict(mixed[1],available='2026-01-05T17:00:00-05:00')]))==1 try: cluster([dict(e1,available='2026-01-05T16:30:00')]) except ValueError as error: assert str(error)=='timezone_required' else: raise AssertionError('无时区时间不得猜测UTC或交易所时区') print(json.dumps({'trigger':triggers,'entry_time':opens[entry],'due_time':opens[due], 'orders':orders,'ending_cash':str(cash),'fees':fees,'quantity':quantity, 'mixed_offset_control':mixed_result,'inclusive_240h_boundary':True,'naive_rejected':True, 'scope':'externally reviewed synthetic canonical Form4 rows; no raw-filing parser or real insider performance'}))