"""63期收益低波动20股、陈旧报价排除、月度替换与固定末期清仓。""" from copy import deepcopy from decimal import Decimal as D import json from math import isfinite from statistics import pstdev def make_data(): result = {} for i in range(1, 23): amplitude = .0001 if i == 21 else (0 if i == 22 else i/1000) prices = [10.0] for j in range(1, 87): prices.append(prices[-1] * (1 + (amplitude if j % 2 else -amplitude))) prices[85:] = [10.0, 10.0] if i == 21: prices[0] = None result[f'S{i:02}'] = {'close': prices, 'open': [10] * 87, 'volume': [0 if i == 22 else 2000] * 87, 'price_age': [1 if i == 22 else 0] * 87} return result def select(data, index): scores, exclusions = {}, {} for symbol, bars in data.items(): prices = bars['close'][index-63:index+1] if index >= 63 else [] volumes = bars['volume'][index-62:index+1] if index >= 63 else [] ages = bars['price_age'][index-63:index+1] if index >= 63 else [] if len(prices) != 64 or any(p is None or not isfinite(p) or p <= 0 for p in prices): exclusions[symbol] = 'incomplete_prices' elif len(ages) != 64 or any(age != 0 for age in ages): exclusions[symbol] = 'stale_price' elif len(volumes) != 63 or any(v is None or not isfinite(v) or v <= 0 for v in volumes) or sum(volumes)/63 < 1000: exclusions[symbol] = 'liquidity' else: returns = [prices[j]/prices[j-1]-1 for j in range(1, 64)] scores[symbol] = pstdev(returns) return sorted(scores, key=lambda s: (scores[s], s))[:20], scores, exclusions def run(data): cash, holdings, fees, ledger = D(10000), {}, D(0), [] def execution_price(symbol, session): raw = data[symbol]['open'][session] if raw is None or not isfinite(raw) or raw <= 0: raise ValueError('invalid_open') return D(str(raw)) for i in range(len(next(iter(data.values()))['open'])): orders, selected = [], None if i - 1 in (63, 84): selected, _, _ = select(data, i-1) target = set(selected) for symbol in sorted(set(holdings)-target): price = execution_price(symbol, i) cash += holdings.pop(symbol) * price - 1 fees += 1 orders.append([symbol, 'sell', 40, str(price), 1, 'monthly_exit']) for symbol in sorted(target-set(holdings)): price = execution_price(symbol, i) if 40*price+1 > cash: raise ValueError('cash') cash -= 40*price+1 holdings[symbol] = 40 fees += 1 orders.append([symbol, 'buy', 40, str(price), 1, 'monthly_entry']) if i == 86: # 冻结的教学结算时点,不依赖传入数据末行 for symbol in sorted(list(holdings)): price = execution_price(symbol, i) cash += holdings.pop(symbol)*price-1 fees += 1 orders.append([symbol, 'sell', 40, str(price), 1, 'declared_terminal_exit']) equity = cash + sum(q * D(str(data[s]['close'][i])) for s, q in holdings.items()) ledger.append({'i': i, 'selected': selected, 'orders': orders, 'holdings': dict(holdings), 'cash': str(cash), 'fees': str(fees), 'equity': str(equity)}) return ledger data = make_data() first, first_scores, excluded = select(data, 63) second, _, _ = select(data, 84) assert first == [f'S{i:02}' for i in range(1, 21)] assert second == ['S21'] + [f'S{i:02}' for i in range(1, 20)] assert excluded == {'S21': 'incomplete_prices', 'S22': 'stale_price'} assert first_scores['S01'] < first_scores['S20'] rows = run(data) assert rows[64]['orders'] == [[f'S{i:02}', 'buy', 40, '10', 1, 'monthly_entry'] for i in range(1, 21)] assert rows[64]['cash'] == '1980' and rows[64]['fees'] == '20' first_open_equity = D(rows[64]['cash']) + sum( quantity * D(str(data[symbol]['open'][64])) for symbol, quantity in rows[64]['holdings'].items()) assert first_open_equity == D(10000) - D(rows[64]['fees']) == D(9980) assert rows[85]['orders'] == [['S20', 'sell', 40, '10', 1, 'monthly_exit'], ['S21', 'buy', 40, '10', 1, 'monthly_entry']] assert rows[85]['cash'] == '1978' and rows[85]['equity'] == '9978.0' assert rows[85]['fees'] == '22' and len(rows[85]['holdings']) == 20 assert rows[86]['cash'] == rows[86]['equity'] == '9958' assert rows[86]['fees'] == '42' and rows[86]['holdings'] == {} assert len(rows[86]['orders']) == 20 assert rows[86]['orders'] == [[symbol, 'sell', 40, '10', 1, 'declared_terminal_exit'] for symbol in sorted(second)] prefix = {s: {field: xs[:65] for field, xs in bars.items()} for s, bars in data.items()} assert run(prefix) == rows[:65] future = deepcopy(data) for bars in future.values(): bars['close'][64:] = [10000.0] * 23 assert select(future, 63) == select(data, 63) fresh_zero = deepcopy(data) fresh_zero['S22']['volume'] = [2000]*87 fresh_zero['S22']['price_age'] = [0]*87 assert select(fresh_zero, 63)[0][0] == 'S22' # 真正新鲜且有交易的平价不等于陈旧报价 inactive = deepcopy(data) inactive['S22']['price_age'] = [0]*87 assert select(inactive, 63)[2]['S22'] == 'liquidity' stale_liquid = deepcopy(data) stale_liquid['S22']['volume'] = [2000]*87 assert 'S22' not in select(stale_liquid, 63)[0] assert select(stale_liquid, 63)[2]['S22'] == 'stale_price' for invalid in [None, float('nan'), float('inf'), float('-inf'), 0, -1]: missing = deepcopy(data) missing['S01']['close'][63] = invalid names, _, reasons = select(missing, 63) assert 'S01' not in names and reasons['S01'] == 'incomplete_prices' print(json.dumps({'first': first, 'second': second, 'exclusions_first': excluded, 'first_open_equity': str(first_open_equity), 'first_entry': rows[64], 'rebalance': rows[85], 'terminal': rows[86], 'prefix_equal': True, 'future_price_mutation_selection_equal': True, 'fresh_liquid_flat_price_can_qualify': True, 'fresh_zero_volume_excluded': True, 'invalid_price_cases_checked': 6}))