修复主力合约识别:按持仓量判定并移除当月占位

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-15 18:28:51 +08:00
parent 404872007f
commit 9c0e5d9c57
4 changed files with 109 additions and 46 deletions
+53 -23
View File
@@ -71,6 +71,7 @@ _CACHE_TTL = 300
_main_index_lock = threading.Lock()
_main_index: dict[str, dict] = {}
_main_index_ts = 0.0
_index_refresh_lock = threading.Lock()
def build_ths_code(product: dict, year: int, month: int) -> str:
@@ -210,7 +211,20 @@ def is_near_expiry_main(ths_code: str) -> bool:
return months_ahead <= 1
def _make_symbol_item(product: dict, year: int, month: int, volume: float) -> dict:
def _main_contract_score(raw: dict) -> float:
"""主力判定:优先持仓量,其次成交量。"""
oi = float(raw.get("open_interest") or 0)
vol = float(raw.get("volume") or 0)
return oi if oi > 0 else vol
def _make_symbol_item(
product: dict,
year: int,
month: int,
volume: float,
open_interest: float = 0,
) -> dict:
ths = build_ths_code(product, year, month)
name = product["name"]
return {
@@ -223,6 +237,7 @@ def _make_symbol_item(product: dict, year: int, month: int, volume: float) -> di
"display": f"{name} 主力 {ths}",
"input_label": f"{name} {ths}",
"volume": volume,
"open_interest": open_interest,
}
@@ -236,6 +251,7 @@ def resolve_main_contract(product: dict) -> Optional[dict]:
today = date.today()
y, m = today.year, today.month
best = None
best_score = 0.0
for i in range(14):
cy, cm = y, m + i
@@ -244,10 +260,17 @@ def resolve_main_contract(product: dict) -> Optional[dict]:
cy += 1
sina = build_sina_code(product, cy, cm)
raw = fetch_raw_for_volume(sina)
if raw and raw["volume"] > 0:
item = _make_symbol_item(product, cy, cm, raw["volume"])
if best is None or raw["volume"] > best["volume"]:
best = item
if not raw:
continue
score = _main_contract_score(raw)
if score <= 0:
continue
item = _make_symbol_item(
product, cy, cm, raw["volume"], raw.get("open_interest", 0),
)
if score > best_score:
best_score = score
best = item
if best is None:
sina_main = build_sina_main_code(product)
@@ -288,20 +311,21 @@ def _enrich_item(item: dict) -> dict:
def refresh_main_index():
"""后台预热全部品种主力合约,搜索时只读本地缓存。"""
global _main_index, _main_index_ts
new_idx: dict[str, dict] = {}
with ThreadPoolExecutor(max_workers=10) as pool:
futures = {pool.submit(resolve_main_contract, p): p for p in PRODUCTS}
for fut in as_completed(futures):
product = futures[fut]
try:
main = fut.result()
if main:
new_idx[product["sina"]] = _enrich_item(main)
except Exception:
pass
with _main_index_lock:
_main_index = new_idx
_main_index_ts = time.time()
with _index_refresh_lock:
new_idx: dict[str, dict] = {}
with ThreadPoolExecutor(max_workers=10) as pool:
futures = {pool.submit(resolve_main_contract, p): p for p in PRODUCTS}
for fut in as_completed(futures):
product = futures[fut]
try:
main = fut.result()
if main:
new_idx[product["sina"]] = _enrich_item(main)
except Exception:
pass
with _main_index_lock:
_main_index = new_idx
_main_index_ts = time.time()
def _warm_loop():
@@ -399,15 +423,21 @@ def list_main_contracts_grouped() -> list[dict]:
"""按交易所分类返回全部品种主力合约(行情页下拉用)。"""
with _main_index_lock:
index = dict(_main_index)
index_ready = bool(index)
if len(index) < len(PRODUCTS) // 2:
refresh_main_index()
with _main_index_lock:
index = dict(_main_index)
buckets: dict[str, list] = defaultdict(list)
for p in PRODUCTS:
main = index.get(p["sina"])
if not main and not index_ready:
main = _stub_main_contract(p)
if not main:
resolved = resolve_main_contract(p)
if resolved:
main = _enrich_item(resolved)
if main:
buckets[p["exchange"]].append(_enrich_item(main))
buckets[p["exchange"]].append(main)
groups: list[dict] = []
for cat in EXCHANGE_ORDER: