"""连续有效窗口,不把缺行、非有限价格或零量前填成交易信号。""" import json from math import isfinite def generate(bars): window, output = [], [] for i, bar in enumerate(bars): reason = None if bar is None: reason = 'missing_bar' else: price, volume = bar['close'], bar['volume'] if price is None or not isfinite(price) or price <= 0: reason = 'invalid_price' elif volume is None or not isfinite(volume) or volume < 0: reason = 'invalid_volume' elif volume == 0: reason = 'zero_volume' if reason: window.clear() output.append({'i': i, 'signal': None, 'reason': reason}) continue window.append(bar['close']) window = window[-3:] signal = int(window[-1] > sum(window) / 3) if len(window) == 3 else None output.append({'i': i, 'signal': signal, 'reason': 'ready' if signal is not None else 'warmup'}) return output base = [{'close': p, 'volume': 100} for p in [10, 10, 12]] assert [r['signal'] for r in generate(base)] == [None, None, 1] assert generate([{'close': 10, 'volume': 100}] * 3)[-1] == {'i': 2, 'signal': 0, 'reason': 'ready'} gap = [base[0], None] + base assert [r['signal'] for r in generate(gap)] == [None, None, None, None, 1] cases = [] for value in [None, float('nan'), float('inf'), float('-inf'), 0, -1]: rows = generate([base[0], dict(base[1], close=value), *base]) assert rows[1]['reason'] == 'invalid_price' and rows[1]['signal'] is None assert [r['signal'] for r in rows[2:]] == [None, None, 1] cases.append({'input': repr(value), 'reason': rows[1]['reason']}) for volume in [None, float('nan'), float('inf'), float('-inf'), -1, 0]: rows = generate([base[0], dict(base[1], volume=volume), *base]) assert rows[1]['reason'] == ('zero_volume' if volume == 0 else 'invalid_volume') assert rows[1]['signal'] is None and rows[-1]['signal'] == 1 cases.append({'volume': repr(volume), 'reason': rows[1]['reason']}) assert generate([]) == [] and all(r['signal'] is None for r in generate(base[:2])) for cutoff in range(1, len(gap)): assert generate(gap[:cutoff]) == generate(gap)[:cutoff] print(json.dumps({'normal': generate(base), 'missing_bar': generate(gap), 'invalid_cases': cases, 'policy': 'zero volume breaks eligibility and window'}))