"""计划消费与提议版本核验的纯函数;不改变准入、仓位或自然语言判断。""" import hashlib import json def fingerprint(value): return hashlib.sha256(json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(',', ':'), default=str).encode()).hexdigest() def basis(row): # 排名与审计时间不是资格/风险/数量依据。完整评析引用变化需要重新研判。 return {k: row.get(k) for k in ('score', 'tier', 'upside', 'heat', 'theme', 'verdict', 'risk', 'missing', 'reasons', 'logic_state', 'company_gate', 'company_review', 'company_review_text', 'events', 'pricing_state', 'news')} def portfolio_context(view, commitments, stock=None): """持仓/资金/在途承诺变化使旧数量依据失效;价格漂移仍由执行终检管理。""" totals = view.get('totals') or {} inflight = {'names': sorted(commitments.get('names') or [])} # 实时报价只改变预估占用,不能使同一份确认每次失效。 # 旧调用方缺少稳定依据时保留原比较;旧提议指纹变化仍须重核一次。 if isinstance(commitments.get('basis'), list): inflight['basis'] = sorted(commitments['basis']) else: inflight['amount'] = commitments.get('amount') return fingerprint({'held': sorted((r['ts_code'], r.get('total_qty'), r.get('sector')) for r in view.get('held') or []), 'cash_source': totals.get('cash_source'), 'cash_avail': totals.get('cash_avail'), 'sector_ready': view.get('sector_ready'), 'stock': stock or {}, 'inflight': inflight}) def compare(old, current, code): """返回 unavailable/material/unchanged。资格失效由调用方通过缺候选表达。""" if not current or not (current.get('decision') or {}).get('id'): return 'unavailable' state = current.get('refresh_status') or {} if code in (state.get('pending_codes') or []) or current.get('consumption_error'): return 'unavailable' if old is None or fingerprint(old) != fingerprint(basis(current)): return 'material' return 'unchanged' def needs_new_judgement(hard, previous): """决策系统缓存按股票/动作,缺同依据的既有研判就必须要求重新回答。""" keys = ('plan_basis', 'scan_params_hash', 'portfolio_context_hash') return (not previous or any(previous.get(k) is None for k in keys) or any(fingerprint(hard.get(k)) != fingerprint(previous.get(k)) for k in keys))