"""有限股票研究:真实隔离慢源超时、调用上限和完整性;不连接模型或行情。""" import json import math import subprocess import sys from decimal import Decimal as D from time import perf_counter def research(symbols, *, max_symbols=2, max_calls=5, attempts=2, budget_seconds=6.0, source_timeout=0.8, slow=False, empty=False): if (type(max_symbols) is not int or max_symbols < 1 or type(max_calls) is not int or max_calls < 1 or type(attempts) is not int or attempts < 1): raise ValueError("positive_integer_limits_required") if any(not math.isfinite(x) or x <= 0 for x in (budget_seconds, source_timeout)): raise ValueError("finite_positive_time_limits_required") if len(set(symbols)) != len(symbols): raise ValueError("duplicate_symbol") chosen = symbols[:max_symbols] start, calls, trace, results = perf_counter(), 0, [], [] for symbol in chosen: values = {} for source in ("price", "eps"): for attempt in range(1, attempts + 1): remaining = budget_seconds - (perf_counter() - start) if calls >= max_calls or remaining <= 0: trace.append({"symbol": symbol, "source": source, "state": "not_started_limit"}) break calls += 1 value = "20" if source == "price" else "1" payload = None if empty and source == "eps" else value program = "import json; print(json.dumps(" + repr(payload) + "))" if slow and symbol == "BBB" and source == "eps": program = "import time; time.sleep(0.4); " + program before = perf_counter() try: response = subprocess.run( [sys.executable, "-I", "-B", "-c", program], capture_output=True, text=True, check=True, timeout=min(remaining, source_timeout), ) parsed = json.loads(response.stdout) if parsed is None: state = "empty" else: number = D(parsed) if not number.is_finite() or number <= 0: raise ValueError("invalid_source_number") values[source] = number state = "received" except subprocess.TimeoutExpired: state = "timeout" trace.append({"symbol": symbol, "source": source, "attempt": attempt, "state": state, "elapsed_seconds": perf_counter() - before}) if state == "received": break complete = set(values) == {"price", "eps"} results.append({"symbol": symbol, "status": "complete" if complete else "partial", "forward_pe": str(values["price"] / values["eps"]) if complete else None, "missing": sorted({"price", "eps"} - set(values))}) return {"calls": calls, "scope_excluded": symbols[max_symbols:], "elapsed_seconds": perf_counter() - start, "results": results, "trace": trace} normal = research(["AAA", "BBB", "CCC"]) assert normal["calls"] == 4 and normal["scope_excluded"] == ["CCC"] assert [r["forward_pe"] for r in normal["results"]] == ["20", "20"] slow_case = research(["BBB"], slow=True, source_timeout=0.15) # 单独测试慢源,避免前一股票耗尽调用额度;机器慢时quote也可能超时。 assert slow_case["calls"] <= 5 assert slow_case["results"][0]["status"] == "partial" assert slow_case["results"][0]["forward_pe"] is None assert any(t["source"] == "eps" and t["state"] == "timeout" for t in slow_case["trace"]) capped = research(["AAA", "BBB"], max_calls=2) assert capped["calls"] == 2 and capped["results"][1]["missing"] == ["eps", "price"] empty_case = research(["AAA"], empty=True) assert empty_case["calls"] == 3 and empty_case["results"][0]["status"] == "partial" assert empty_case["results"][0]["forward_pe"] is None assert all(t.get("elapsed_seconds", 0) >= 0 for t in slow_case["trace"]) for bad in (0, float("nan"), float("inf")): try: research(["AAA"], budget_seconds=bad) except ValueError: pass else: raise AssertionError("非法时间预算必须拒绝") print(json.dumps({"normal": normal, "slow": slow_case, "capped": capped, "empty": empty_case}, sort_keys=True))