"""含费用的整篮共同缩放;标准库合成账户,不运行产品撮合器。""" from decimal import Decimal as D from fractions import Fraction as F from itertools import permutations import json def fit(plans,cash): cash=D(str(cash)) if not cash.is_finite() or cash<0: raise ValueError('invalid_available_cash') if len({p['symbol'] for p in plans})!=len(plans): raise ValueError('duplicate_symbol') for p in plans: if type(p['quantity']) is not int or p['quantity']<=0 or type(p['lot']) is not int or p['lot']<=0: raise ValueError('invalid_quantity_or_lot') if p['quantity']%p['lot']!=0:raise ValueError('plan_not_board_lot_multiple') if not D(str(p['price'])).is_finite() or D(str(p['price']))<=0: raise ValueError('invalid_price') def at(scale): rows=[] for p in sorted(plans,key=lambda p:p['symbol']): q=(p['quantity']*scale//p['lot'])*p['lot'] if q: rows.append({'symbol':p['symbol'],'quantity':q,'price':str(p['price']), 'fee':'1','cost':str(q*D(str(p['price']))+1)}) return rows full=at(F(1));low,high=F(0),F(1);trials=0 if sum(D(r['cost']) for r in full)<=cash: chosen=full;low=F(1) else: chosen=[] for _ in range(50): trials+=1;mid=(low+high)/2;candidate=at(mid) if sum(D(r['cost']) for r in candidate)<=cash: low,chosen=mid,candidate else: high=mid filled={r['symbol']:r['quantity'] for r in chosen} shortfalls=[{'symbol':p['symbol'],'requested':p['quantity'],'filled':filled.get(p['symbol'],0), 'unfilled':p['quantity']-filled.get(p['symbol'],0)} for p in sorted(plans,key=lambda p:p['symbol'])] rejected=[{'symbol':r['symbol'],'reason':'insufficient_capital'} for r in full if r['symbol'] not in filled] return {'full_cost':str(sum(D(r['cost']) for r in full)),'scale':str(low),'trials':trials, 'orders':chosen,'shortfalls':shortfalls,'rejected':rejected, 'cash_after':str(cash-sum(D(r['cost']) for r in chosen))} plans=[{'symbol':'A','quantity':8,'price':10,'lot':1}, {'symbol':'B','quantity':8,'price':10,'lot':1}, {'symbol':'C','quantity':1,'price':20,'lot':1}] cash,fees=D(50),D(0);holdings={'OLD':10};orders=[] cash+=holdings.pop('OLD')*D(10)-1;fees+=1;orders.append(['OLD','sell',10,'10','1']) assert cash==149 and fees==1 and not holdings result=fit(plans,cash) assert result['full_cost']=='183' and F(7,8)<=F(result['scale'])<1 and result['trials']==50 assert result['orders']==[{'symbol':s,'quantity':7,'price':'10','fee':'1','cost':'71'} for s in ['A','B']] assert result['rejected']==[{'symbol':'C','reason':'insufficient_capital'}] assert result['shortfalls']==[{'symbol':'A','requested':8,'filled':7,'unfilled':1}, {'symbol':'B','requested':8,'filled':7,'unfilled':1}, {'symbol':'C','requested':1,'filled':0,'unfilled':1}] for r in result['orders']: cash-=D(r['cost']);fees+=D(r['fee']);holdings[r['symbol']]=r['quantity'] orders.append([r['symbol'],'buy',r['quantity'],r['price'],r['fee']]) assert cash==7 and fees==3 and holdings=={'A':7,'B':7} assert cash+sum(holdings.values())*10==150-fees for ordering in permutations(plans): assert fit(list(ordering),149)==result empty=fit(plans,0) assert empty['orders']==[] and empty['cash_after']=='0' and len(empty['rejected'])==3 assert fit(plans,183)['scale']=='1' and len(fit(plans,183)['orders'])==3 for requested in [1,150]: try:fit([{'symbol':'LOT','quantity':requested,'price':10,'lot':100}],1000) except ValueError as error:assert str(error)=='plan_not_board_lot_multiple' else:raise AssertionError('完整计划必须已在lot网格;非法计划不是资金不足') # 顺序现金截断是另一机制:首个名字获得8股,第二个仅6股。 def greedy(ordering): remaining,quantities=D(149),{} for p in ordering: q=min(p['quantity'],max(0,int((remaining-1)//D(str(p['price']))))) if q: remaining-=q*D(str(p['price']))+1 quantities[p['symbol']]=q return quantities assert greedy(plans)=={'A':8,'B':6,'C':0} assert greedy(list(reversed(plans)))!=greedy(plans) for symbol,price in [('A',D(11)),('B',D(9))]: q=holdings.pop(symbol);cash+=q*price-1;fees+=1;orders.append([symbol,'sell',q,str(price),'1']) assert orders==[['OLD','sell',10,'10','1'],['A','buy',7,'10','1'],['B','buy',7,'10','1'], ['A','sell',7,'11','1'],['B','sell',7,'9','1']] assert cash==145 and fees==5 and not holdings and cash==150-fees print(json.dumps({'initial_cash':'50','initial_OLD_market_value':'100','sale_released_cash':'99', 'available_after_sale':'149','fit':result,'completed_orders':orders, 'ending_cash':str(cash),'fees':str(fees),'holdings':holdings, 'scope':'synthetic common-factor planning and cash; not real broker partial fills or live Vibe'}))