Restructure into modules/ with single-process CTP and config/ layout.
Move business code under modules/, env template to config/, PM2 single qihuo process, and _legacy shims for old imports. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,280 @@
|
||||
# Copyright (c) 2025-2026 马建军. All rights reserved.
|
||||
# 专有软件 — 未经授权禁止复制、传播、转售。
|
||||
# 严禁用于:带单/代客理财、向他人推荐期货品种或买卖建议、融资配资等业务。
|
||||
# 详见 LICENSE.zh-CN.txt 与 docs/软件购买与使用协议.md
|
||||
|
||||
"""期货合约简介:东方财富 / 新浪 / AKShare。"""
|
||||
import logging
|
||||
import re
|
||||
from typing import Any, Optional
|
||||
|
||||
import requests
|
||||
|
||||
from modules.core.contract_specs import get_contract_spec
|
||||
from modules.core.symbols import ths_to_codes, search_symbols
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
EM_LABEL_MAP = {
|
||||
"vname": "交易品种",
|
||||
"vcode": "交易代码",
|
||||
"jydw": "交易单位",
|
||||
"bjdw": "报价单位",
|
||||
"market": "交易所",
|
||||
"zxbddw": "最小变动价位",
|
||||
"zdtbfd": "涨跌停幅度",
|
||||
"hyjgyf": "合约月份",
|
||||
"jysj": "交易时间",
|
||||
"zhjyr": "最后交易日",
|
||||
"zhjgr": "交割日期",
|
||||
"jgpj": "交割品级",
|
||||
"zcjybzj": "最低交易保证金",
|
||||
"jgfs": "交割方式",
|
||||
"jgdd": "交割地点",
|
||||
"ssrq": "上市日期",
|
||||
}
|
||||
|
||||
DISPLAY_ORDER = [
|
||||
"交易品种",
|
||||
"交易代码",
|
||||
"交易单位",
|
||||
"报价单位",
|
||||
"最小变动价位",
|
||||
"最低交易保证金",
|
||||
"涨跌停幅度",
|
||||
"合约月份",
|
||||
"交易时间",
|
||||
"最后交易日",
|
||||
"交割日期",
|
||||
"交割方式",
|
||||
"交割地点",
|
||||
"交割品级",
|
||||
"上市日期",
|
||||
"交易所",
|
||||
]
|
||||
|
||||
SKIP_ITEMS = {"", "-", "None", "nan", "null"}
|
||||
|
||||
|
||||
def _normalize_ths_code(raw: str) -> Optional[str]:
|
||||
code = (raw or "").strip()
|
||||
if not code:
|
||||
return None
|
||||
# 已是完整合约
|
||||
if re.match(r"^[A-Za-z]+\d{3,4}$", code):
|
||||
return code
|
||||
# 仅品种字母时尝试匹配主力
|
||||
results = search_symbols(code)
|
||||
if results:
|
||||
return results[0].get("ths_code") or code
|
||||
codes = ths_to_codes(code)
|
||||
if codes:
|
||||
return codes["ths_code"]
|
||||
return code
|
||||
|
||||
|
||||
def _to_sina_quote_symbol(ths_code: str) -> str:
|
||||
m = re.match(r"^([A-Za-z]+)(\d+)$", ths_code.strip())
|
||||
if not m:
|
||||
return ths_code.upper()
|
||||
return m.group(1).upper() + m.group(2)
|
||||
|
||||
|
||||
def _to_em_page_symbol(ths_code: str) -> str:
|
||||
return ths_code.strip().lower() + "F"
|
||||
|
||||
|
||||
def _clean_value(val: Any) -> str:
|
||||
if val is None:
|
||||
return ""
|
||||
s = str(val).strip()
|
||||
if s in SKIP_ITEMS:
|
||||
return ""
|
||||
return s
|
||||
|
||||
|
||||
def _rows_from_dict(data: dict[str, str]) -> list[dict]:
|
||||
rows: list[dict] = []
|
||||
seen: set[str] = set()
|
||||
for label in DISPLAY_ORDER:
|
||||
val = _clean_value(data.get(label))
|
||||
if not val:
|
||||
continue
|
||||
hint = _clean_value(data.get(f"{label}_hint"))
|
||||
rows.append({"label": label, "value": val, "hint": hint})
|
||||
seen.add(label)
|
||||
for label, val in data.items():
|
||||
if label.endswith("_hint") or label in seen:
|
||||
continue
|
||||
val = _clean_value(val)
|
||||
if val:
|
||||
rows.append({"label": label, "value": val, "hint": ""})
|
||||
return rows
|
||||
|
||||
|
||||
def _add_computed_hints(ths_code: str, data: dict[str, str]) -> None:
|
||||
spec = get_contract_spec(ths_code)
|
||||
mult = spec.get("mult") or 0
|
||||
tick_raw = data.get("最小变动价位", "")
|
||||
m = re.search(r"([\d.]+)", tick_raw)
|
||||
if m and mult:
|
||||
tick = float(m.group(1))
|
||||
data["最小变动价位_hint"] = f"一手合约最小波动{round(tick * mult, 2)}元"
|
||||
|
||||
|
||||
def _fetch_em_direct(em_symbol: str) -> dict[str, str]:
|
||||
page_url = f"https://quote.eastmoney.com/qihuo/{em_symbol}.html"
|
||||
r = requests.get(page_url, timeout=12)
|
||||
r.encoding = r.apparent_encoding or "utf-8"
|
||||
inner = None
|
||||
for pat in [
|
||||
r"futures_([A-Za-z0-9_]+)",
|
||||
r"#(futures_[A-Za-z0-9_]+)",
|
||||
r"/(futures_[A-Za-z0-9_]+)",
|
||||
]:
|
||||
m = re.search(pat, r.text)
|
||||
if m:
|
||||
inner = m.group(1).replace("futures_", "")
|
||||
break
|
||||
if not inner:
|
||||
raise ValueError("无法解析东方财富合约标识")
|
||||
|
||||
info_url = f"https://futsse-static.eastmoney.com/redis?msgid={inner}_info"
|
||||
r2 = requests.get(info_url, timeout=12)
|
||||
payload = r2.json()
|
||||
if not isinstance(payload, dict):
|
||||
raise ValueError("东方财富返回数据无效")
|
||||
|
||||
out: dict[str, str] = {}
|
||||
for key, label in EM_LABEL_MAP.items():
|
||||
val = _clean_value(payload.get(key))
|
||||
if val:
|
||||
out[label] = val
|
||||
if not out:
|
||||
raise ValueError("东方财富合约字段为空")
|
||||
return out
|
||||
|
||||
|
||||
def _fetch_em_akshare(em_symbol: str) -> dict[str, str]:
|
||||
import akshare as ak
|
||||
|
||||
df = ak.futures_contract_detail_em(symbol=em_symbol)
|
||||
out: dict[str, str] = {}
|
||||
for _, row in df.iterrows():
|
||||
label = _clean_value(row.get("item"))
|
||||
val = _clean_value(row.get("value"))
|
||||
if label and val:
|
||||
if label == "跌涨停板幅度":
|
||||
label = "涨跌停幅度"
|
||||
if label == "最后交割日":
|
||||
label = "交割日期"
|
||||
if label == "上市交易所":
|
||||
label = "交易所"
|
||||
if label == "合约交割月份":
|
||||
label = "合约月份"
|
||||
if label == "最初交易保证金":
|
||||
label = "最低交易保证金"
|
||||
if label == "最小变动价格":
|
||||
label = "最小变动价位"
|
||||
out[label] = val
|
||||
return out
|
||||
|
||||
|
||||
def _fetch_sina_direct(sina_symbol: str) -> dict[str, str]:
|
||||
from io import StringIO
|
||||
|
||||
import pandas as pd
|
||||
|
||||
url = f"https://finance.sina.com.cn/futures/quotes/{sina_symbol}.shtml"
|
||||
r = requests.get(url, timeout=12, headers={"Referer": "https://finance.sina.com.cn/"})
|
||||
r.encoding = "gb2312"
|
||||
tables = pd.read_html(StringIO(r.text))
|
||||
if len(tables) < 7:
|
||||
raise ValueError("新浪页面结构变化")
|
||||
temp_df = tables[6]
|
||||
parts = []
|
||||
for ncol in [slice(0, 2), slice(2, 4), slice(4, None)]:
|
||||
part = temp_df.iloc[:, ncol]
|
||||
part.columns = ["item", "value"]
|
||||
parts.append(part)
|
||||
merged = pd.concat(parts, axis=0, ignore_index=True)
|
||||
out: dict[str, str] = {}
|
||||
for _, row in merged.iterrows():
|
||||
label = _clean_value(row["item"])
|
||||
val = _clean_value(row["value"])
|
||||
if not label or not val or len(label) > 80 or "发帖" in val:
|
||||
continue
|
||||
out[label] = val
|
||||
return out
|
||||
|
||||
|
||||
def _fetch_sina_akshare(sina_symbol: str) -> dict[str, str]:
|
||||
import akshare as ak
|
||||
|
||||
df = ak.futures_contract_detail(symbol=sina_symbol)
|
||||
out: dict[str, str] = {}
|
||||
for _, row in df.iterrows():
|
||||
label = _clean_value(row.get("item"))
|
||||
val = _clean_value(row.get("value"))
|
||||
if label and val and "发帖" not in val:
|
||||
out[label] = val
|
||||
return out
|
||||
|
||||
|
||||
def _merge_profile(primary: dict[str, str], secondary: dict[str, str]) -> dict[str, str]:
|
||||
merged = dict(secondary)
|
||||
merged.update(primary)
|
||||
return merged
|
||||
|
||||
|
||||
def get_contract_profile(raw_symbol: str) -> Optional[dict]:
|
||||
ths_code = _normalize_ths_code(raw_symbol)
|
||||
if not ths_code:
|
||||
return None
|
||||
|
||||
em_symbol = _to_em_page_symbol(ths_code)
|
||||
sina_symbol = _to_sina_quote_symbol(ths_code)
|
||||
data: dict[str, str] = {}
|
||||
source_parts: list[str] = []
|
||||
|
||||
# 东方财富(字段与看盘软件简介接近)
|
||||
try:
|
||||
try:
|
||||
data = _fetch_em_akshare(em_symbol)
|
||||
source_parts.append("东方财富")
|
||||
except ImportError:
|
||||
data = _fetch_em_direct(em_symbol)
|
||||
source_parts.append("东方财富")
|
||||
except Exception as exc:
|
||||
logger.warning("eastmoney profile failed %s: %s", em_symbol, exc)
|
||||
|
||||
# 新浪补充交割地点、上市日期等
|
||||
sina_data: dict[str, str] = {}
|
||||
try:
|
||||
try:
|
||||
sina_data = _fetch_sina_akshare(sina_symbol)
|
||||
except ImportError:
|
||||
sina_data = _fetch_sina_direct(sina_symbol)
|
||||
if sina_data:
|
||||
source_parts.append("新浪")
|
||||
except Exception as exc:
|
||||
logger.warning("sina profile failed %s: %s", sina_symbol, exc)
|
||||
|
||||
if sina_data:
|
||||
data = _merge_profile(data, sina_data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
_add_computed_hints(ths_code, data)
|
||||
rows = _rows_from_dict(data)
|
||||
if not rows:
|
||||
return None
|
||||
|
||||
return {
|
||||
"ths_code": ths_code,
|
||||
"symbol_name": data.get("交易品种", ""),
|
||||
"exchange": data.get("交易所", ""),
|
||||
"rows": rows,
|
||||
"source": " + ".join(source_parts) if source_parts else "未知",
|
||||
}
|
||||
Reference in New Issue
Block a user