@@ -17,7 +17,7 @@ from flask import (
|
|||||||
)
|
)
|
||||||
from werkzeug.security import check_password_hash, generate_password_hash
|
from werkzeug.security import check_password_hash, generate_password_hash
|
||||||
|
|
||||||
from symbols import search_symbols, ths_to_codes, list_main_contracts_grouped
|
from symbols import search_symbols, ths_to_codes, list_main_contracts_grouped, refresh_main_index
|
||||||
from contract_specs import calc_position_metrics
|
from contract_specs import calc_position_metrics
|
||||||
from fee_specs import (
|
from fee_specs import (
|
||||||
calc_fee_breakdown,
|
calc_fee_breakdown,
|
||||||
@@ -580,6 +580,7 @@ def start_background_threads():
|
|||||||
target=lambda: kline_hub.worker_loop(DB_PATH, build_market_quote_payload),
|
target=lambda: kline_hub.worker_loop(DB_PATH, build_market_quote_payload),
|
||||||
daemon=True,
|
daemon=True,
|
||||||
).start()
|
).start()
|
||||||
|
threading.Thread(target=refresh_main_index, daemon=True).start()
|
||||||
|
|
||||||
|
|
||||||
start_background_threads()
|
start_background_threads()
|
||||||
|
|||||||
@@ -51,6 +51,52 @@ def _sina_headers() -> dict:
|
|||||||
return {"Referer": "https://finance.sina.com.cn"}
|
return {"Referer": "https://finance.sina.com.cn"}
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_sina_futures_quote(parts: list) -> Optional[dict]:
|
||||||
|
"""解析新浪 nf_/CFF_RE_ 期货行情字段。"""
|
||||||
|
if len(parts) < 9:
|
||||||
|
return None
|
||||||
|
price = None
|
||||||
|
for idx in (8, 7, 6, 5):
|
||||||
|
if len(parts) > idx and parts[idx]:
|
||||||
|
try:
|
||||||
|
val = float(parts[idx])
|
||||||
|
if val > 0:
|
||||||
|
price = val
|
||||||
|
break
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
if price is None:
|
||||||
|
price = 0.0
|
||||||
|
|
||||||
|
open_interest = 0.0
|
||||||
|
volume = 0.0
|
||||||
|
if len(parts) > 13 and parts[13]:
|
||||||
|
try:
|
||||||
|
open_interest = float(parts[13])
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
if len(parts) > 14 and parts[14]:
|
||||||
|
try:
|
||||||
|
volume = float(parts[14])
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
prev_close = None
|
||||||
|
if len(parts) > 9 and parts[9]:
|
||||||
|
try:
|
||||||
|
prev_close = float(parts[9])
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return {
|
||||||
|
"name": parts[0],
|
||||||
|
"price": price,
|
||||||
|
"volume": volume,
|
||||||
|
"open_interest": open_interest,
|
||||||
|
"prev_close": prev_close,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _fetch_sina_raw(sina_code: str) -> Optional[dict]:
|
def _fetch_sina_raw(sina_code: str) -> Optional[dict]:
|
||||||
try:
|
try:
|
||||||
url = f"https://hq.sinajs.cn/list={sina_code}"
|
url = f"https://hq.sinajs.cn/list={sina_code}"
|
||||||
@@ -62,27 +108,7 @@ def _fetch_sina_raw(sina_code: str) -> Optional[dict]:
|
|||||||
if not body:
|
if not body:
|
||||||
return None
|
return None
|
||||||
parts = body.split(",")
|
parts = body.split(",")
|
||||||
if len(parts) < 9:
|
return _parse_sina_futures_quote(parts)
|
||||||
return None
|
|
||||||
price = float(parts[8])
|
|
||||||
volume = float(parts[14]) if len(parts) > 14 and parts[14] else 0
|
|
||||||
prev_close = None
|
|
||||||
if len(parts) > 9 and parts[9]:
|
|
||||||
try:
|
|
||||||
prev_close = float(parts[9])
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
if prev_close is None and len(parts) > 2 and parts[2]:
|
|
||||||
try:
|
|
||||||
prev_close = float(parts[2])
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
return {
|
|
||||||
"name": parts[0],
|
|
||||||
"price": price,
|
|
||||||
"volume": volume,
|
|
||||||
"prev_close": prev_close,
|
|
||||||
}
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.debug("sina fetch failed %s: %s", sina_code, exc)
|
logger.debug("sina fetch failed %s: %s", sina_code, exc)
|
||||||
return None
|
return None
|
||||||
|
|||||||
+7
-1
@@ -112,8 +112,14 @@
|
|||||||
renderGrouped(mainsCache, filterQ);
|
renderGrouped(mainsCache, filterQ);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (mainsLoading) return;
|
if (mainsLoading) {
|
||||||
|
dropdown.innerHTML = '<div class="symbol-option">正在识别主力合约…</div>';
|
||||||
|
dropdown.classList.add('show');
|
||||||
|
return;
|
||||||
|
}
|
||||||
mainsLoading = true;
|
mainsLoading = true;
|
||||||
|
dropdown.innerHTML = '<div class="symbol-option">正在识别主力合约…</div>';
|
||||||
|
dropdown.classList.add('show');
|
||||||
fetch('/api/symbols/mains')
|
fetch('/api/symbols/mains')
|
||||||
.then(function (r) { return r.json(); })
|
.then(function (r) { return r.json(); })
|
||||||
.then(function (groups) {
|
.then(function (groups) {
|
||||||
|
|||||||
+53
-23
@@ -71,6 +71,7 @@ _CACHE_TTL = 300
|
|||||||
_main_index_lock = threading.Lock()
|
_main_index_lock = threading.Lock()
|
||||||
_main_index: dict[str, dict] = {}
|
_main_index: dict[str, dict] = {}
|
||||||
_main_index_ts = 0.0
|
_main_index_ts = 0.0
|
||||||
|
_index_refresh_lock = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
def build_ths_code(product: dict, year: int, month: int) -> str:
|
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
|
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)
|
ths = build_ths_code(product, year, month)
|
||||||
name = product["name"]
|
name = product["name"]
|
||||||
return {
|
return {
|
||||||
@@ -223,6 +237,7 @@ def _make_symbol_item(product: dict, year: int, month: int, volume: float) -> di
|
|||||||
"display": f"{name} 主力 {ths}",
|
"display": f"{name} 主力 {ths}",
|
||||||
"input_label": f"{name} {ths}",
|
"input_label": f"{name} {ths}",
|
||||||
"volume": volume,
|
"volume": volume,
|
||||||
|
"open_interest": open_interest,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -236,6 +251,7 @@ def resolve_main_contract(product: dict) -> Optional[dict]:
|
|||||||
today = date.today()
|
today = date.today()
|
||||||
y, m = today.year, today.month
|
y, m = today.year, today.month
|
||||||
best = None
|
best = None
|
||||||
|
best_score = 0.0
|
||||||
|
|
||||||
for i in range(14):
|
for i in range(14):
|
||||||
cy, cm = y, m + i
|
cy, cm = y, m + i
|
||||||
@@ -244,10 +260,17 @@ def resolve_main_contract(product: dict) -> Optional[dict]:
|
|||||||
cy += 1
|
cy += 1
|
||||||
sina = build_sina_code(product, cy, cm)
|
sina = build_sina_code(product, cy, cm)
|
||||||
raw = fetch_raw_for_volume(sina)
|
raw = fetch_raw_for_volume(sina)
|
||||||
if raw and raw["volume"] > 0:
|
if not raw:
|
||||||
item = _make_symbol_item(product, cy, cm, raw["volume"])
|
continue
|
||||||
if best is None or raw["volume"] > best["volume"]:
|
score = _main_contract_score(raw)
|
||||||
best = item
|
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:
|
if best is None:
|
||||||
sina_main = build_sina_main_code(product)
|
sina_main = build_sina_main_code(product)
|
||||||
@@ -288,20 +311,21 @@ def _enrich_item(item: dict) -> dict:
|
|||||||
def refresh_main_index():
|
def refresh_main_index():
|
||||||
"""后台预热全部品种主力合约,搜索时只读本地缓存。"""
|
"""后台预热全部品种主力合约,搜索时只读本地缓存。"""
|
||||||
global _main_index, _main_index_ts
|
global _main_index, _main_index_ts
|
||||||
new_idx: dict[str, dict] = {}
|
with _index_refresh_lock:
|
||||||
with ThreadPoolExecutor(max_workers=10) as pool:
|
new_idx: dict[str, dict] = {}
|
||||||
futures = {pool.submit(resolve_main_contract, p): p for p in PRODUCTS}
|
with ThreadPoolExecutor(max_workers=10) as pool:
|
||||||
for fut in as_completed(futures):
|
futures = {pool.submit(resolve_main_contract, p): p for p in PRODUCTS}
|
||||||
product = futures[fut]
|
for fut in as_completed(futures):
|
||||||
try:
|
product = futures[fut]
|
||||||
main = fut.result()
|
try:
|
||||||
if main:
|
main = fut.result()
|
||||||
new_idx[product["sina"]] = _enrich_item(main)
|
if main:
|
||||||
except Exception:
|
new_idx[product["sina"]] = _enrich_item(main)
|
||||||
pass
|
except Exception:
|
||||||
with _main_index_lock:
|
pass
|
||||||
_main_index = new_idx
|
with _main_index_lock:
|
||||||
_main_index_ts = time.time()
|
_main_index = new_idx
|
||||||
|
_main_index_ts = time.time()
|
||||||
|
|
||||||
|
|
||||||
def _warm_loop():
|
def _warm_loop():
|
||||||
@@ -399,15 +423,21 @@ def list_main_contracts_grouped() -> list[dict]:
|
|||||||
"""按交易所分类返回全部品种主力合约(行情页下拉用)。"""
|
"""按交易所分类返回全部品种主力合约(行情页下拉用)。"""
|
||||||
with _main_index_lock:
|
with _main_index_lock:
|
||||||
index = dict(_main_index)
|
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)
|
buckets: dict[str, list] = defaultdict(list)
|
||||||
for p in PRODUCTS:
|
for p in PRODUCTS:
|
||||||
main = index.get(p["sina"])
|
main = index.get(p["sina"])
|
||||||
if not main and not index_ready:
|
if not main:
|
||||||
main = _stub_main_contract(p)
|
resolved = resolve_main_contract(p)
|
||||||
|
if resolved:
|
||||||
|
main = _enrich_item(resolved)
|
||||||
if main:
|
if main:
|
||||||
buckets[p["exchange"]].append(_enrich_item(main))
|
buckets[p["exchange"]].append(main)
|
||||||
|
|
||||||
groups: list[dict] = []
|
groups: list[dict] = []
|
||||||
for cat in EXCHANGE_ORDER:
|
for cat in EXCHANGE_ORDER:
|
||||||
|
|||||||
Reference in New Issue
Block a user