commit a8b79df953f3348000f80b5280b83bf34f84a8d8 Author: Janecjn Date: Mon Jun 8 20:55:08 2026 +0800 Initial fundamental dashboard diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0910681 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +__pycache__/ +*.pyc +node_modules/ +logs/ +outputs/ +缓存/ +备份/ +.DS_Store +Thumbs.db diff --git a/README.md b/README.md new file mode 100644 index 0000000..d9bd77d --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +# 商品期货基本面评分面板 + +本仓库用于根据本地 Excel 数据生成商品期货基本面评分 HTML 面板。 + +## 文件说明 + +- `基本面评分面板.html`:当前已生成的可视化面板,可直接打开查看。 +- `generate_fundamental_dashboard.py`:底层计算与 HTML 生成脚本。 +- `refresh_dashboard_server.py`:本地刷新服务,支持在页面点击“刷新数据”后重新运行生成脚本。 +- `wind数据汇总.xlsx`:Wind 导出的原始数据。 +- `钢联数据汇总.xlsx`:钢联导出的原始数据。 +- `基本面指标确认.xlsx`:各品种各维度使用的指标与公式。 +- `月差拼接数据.xlsx`、`月差/`:月差相关资料。 + +## 更新数据后重新生成 + +将新的 Excel 数据覆盖到同名文件后,在仓库根目录运行: + +```powershell +python generate_fundamental_dashboard.py +``` + +运行完成后打开 `基本面评分面板.html` 即可查看更新后的面板。 + +## 使用页面内“刷新数据”按钮 + +如果希望在浏览器页面中点击“刷新数据”自动重新计算,请先启动本地服务: + +```powershell +python refresh_dashboard_server.py +``` + +然后用浏览器打开终端输出的地址,通常是: + +```text +http://127.0.0.1:8765/基本面评分面板.html +``` + +通过该地址打开时,页面内“刷新数据”按钮会调用 `generate_fundamental_dashboard.py` 并刷新页面。 + +## Python 依赖 + +脚本依赖: + +```text +pandas +numpy +openpyxl +``` + +如本机缺少依赖,可安装: + +```powershell +pip install pandas numpy openpyxl +``` + diff --git a/generate_fundamental_dashboard.py b/generate_fundamental_dashboard.py new file mode 100644 index 0000000..d4bec09 --- /dev/null +++ b/generate_fundamental_dashboard.py @@ -0,0 +1,931 @@ +from __future__ import annotations + +import html +import json +import math +import re +from dataclasses import dataclass +from datetime import datetime +from pathlib import Path +from typing import Any + +import numpy as np +import pandas as pd + + +BASE_DIR = Path(__file__).resolve().parent +CONFIRM_FILE = BASE_DIR / "基本面指标确认.xlsx" +WIND_FILE = BASE_DIR / "wind数据汇总.xlsx" +STEEL_FILE = BASE_DIR / "钢联数据汇总.xlsx" +OUTPUT_FILE = BASE_DIR / "基本面评分面板.html" + +DIMENSIONS = ["利润", "产量", "库存", "需求"] +INVERSE_DIMS = {"库存"} +DIM_COLORS = { + "利润": "#2563eb", + "产量": "#dc2626", + "库存": "#1d4ed8", + "需求": "#0f9f6e", + "月差": "#64748b", +} +DEFAULT_BAND_WINDOW = 15 +DEFAULT_BAND_K = 3.0 +DEFAULT_BAND_TRIM = 0.10 +HISTORY_START_DEFAULT = "2026-01-01" +BOARD_MAP = { + "黑色": ["焦煤", "焦炭", "铁矿", "螺纹", "热卷", "硅铁", "锰硅", "不锈钢"], + "建材": ["玻璃", "纯碱", "PVC", "原木"], + "有色": ["沪铜", "沪铝", "沪锌", "沪铅", "沪镍", "沪锡", "氧化铝", "铝合金", "工业硅", "多晶硅", "碳酸锂", "国际铜"], + "贵金属": ["沪金", "沪银", "铂", "钯"], + "能源": ["原油", "液化气", "燃油", "LU燃油", "沥青", "动力煤", "欧线"], + "煤化工": ["尿素", "甲醇"], + "油化工": ["塑料", "聚丙烯", "丙烯", "纯苯", "苯乙烯", "对二甲苯", "PTA", "乙二醇", "短纤", "瓶片", "烧碱", "丁二烯"], + "农产品": ["玉米", "淀粉", "豆一", "豆二", "豆粕", "豆油", "菜籽油", "菜籽粕", "棕榈油", "鸡蛋", "生猪", "棉花", "白糖", "苹果", "红枣", "花生", "棉纱"], + "软商品": ["纸浆", "双胶纸", "橡胶", "20号胶", "BR橡胶"], + "金融": ["IF", "IC", "IM", "IH", "二年债", "五年债", "十年债", "三十年债"], +} + + +@dataclass +class Metric: + name: str + source: str + unit: str + freq: str + series: pd.Series + + +def clean_name(value: Any) -> str: + if pd.isna(value): + return "" + text = str(value).strip() + text = text.replace("(", "(").replace(")", ")") + text = text.replace(";", ";").replace(",", ",") + return re.sub(r"\s+", "", text) + + +def read_metrics(path: Path, sheet: str, name_row: int, unit_row: int, freq_row: int, data_row: int, source: str) -> list[Metric]: + raw = pd.read_excel(path, sheet_name=sheet, header=None) + dates = pd.to_datetime(raw.iloc[data_row:, 0], errors="coerce") + metrics: list[Metric] = [] + for col in range(1, raw.shape[1]): + name = clean_name(raw.iat[name_row, col]) + if not name: + continue + values = pd.to_numeric(raw.iloc[data_row:, col], errors="coerce") + frame = pd.DataFrame({"date": dates, "value": values}).dropna(subset=["date", "value"]) + if frame.empty: + continue + series = frame.drop_duplicates("date").set_index("date")["value"].sort_index() + if series.empty: + continue + metrics.append( + Metric( + name=name, + source=source, + unit=str(raw.iat[unit_row, col]) if pd.notna(raw.iat[unit_row, col]) else "", + freq=str(raw.iat[freq_row, col]) if pd.notna(raw.iat[freq_row, col]) else "", + series=series, + ) + ) + return metrics + + +def build_metric_index(metrics: list[Metric]) -> tuple[dict[str, Metric], list[str]]: + grouped: dict[str, list[Metric]] = {} + for metric in metrics: + grouped.setdefault(metric.name, []).append(metric) + selected: dict[str, Metric] = {} + for name, items in grouped.items(): + selected[name] = max(items, key=lambda m: (m.series.index.max(), m.series.notna().sum())) + return selected, sorted(selected.keys(), key=len, reverse=True) + + +def normalize_expr(expr: Any) -> str: + if pd.isna(expr): + return "" + text = str(expr).strip() + text = text.replace("(", "(").replace(")", ")") + text = text.replace(";", ";").replace(",", ",") + text = re.sub(r"\s+", "", text) + text = re.sub(r"(?i)\bSum\s*\(", "(", text) + text = text.replace(";", "+") + return text + + +def parse_expression(expr: Any, metric_index: dict[str, Metric], metric_names: list[str]) -> tuple[str, dict[str, Metric], list[str]]: + text = normalize_expr(expr) + if not text: + return "", {}, [] + used: dict[str, Metric] = {} + token_expr = text + for idx, name in enumerate(metric_names): + if name and name in token_expr: + token = f"m{idx}" + token_expr = token_expr.replace(name, token) + used[token] = metric_index[name] + leftovers = re.sub(r"m\d+", " ", token_expr) + leftovers = re.sub(r"[0-9eE\.\+\-\*/\(\)\s,]+", " ", leftovers) + missing = [x.strip() for x in re.split(r"\s+", leftovers) if x.strip()] + return token_expr, used, missing + + +def eval_expression(expr: Any, metric_index: dict[str, Metric], metric_names: list[str]) -> tuple[pd.Series | None, list[str], list[Metric]]: + token_expr, used, missing = parse_expression(expr, metric_index, metric_names) + if not token_expr or not used: + return None, missing, [] + local_env = {token: metric.series for token, metric in used.items()} + try: + value = eval(token_expr, {"__builtins__": {}}, local_env) + except Exception: + return None, missing or [token_expr], list(used.values()) + if isinstance(value, (int, float, np.number)): + return None, missing, list(used.values()) + series = pd.to_numeric(value, errors="coerce").dropna().sort_index() + return (series if not series.empty else None), missing, list(used.values()) + + +def circular_day_diff(day_values: np.ndarray, target_day: int) -> np.ndarray: + diff = np.abs(day_values - target_day) + return np.minimum(diff, 366 - diff) + + +def trimmed_values(values: pd.Series, trim: float = DEFAULT_BAND_TRIM) -> pd.Series: + values = pd.to_numeric(values, errors="coerce").dropna().sort_values() + if values.empty or trim <= 0: + return values + cut = int(len(values) * trim) + if len(values) - cut * 2 < 4: + return values + return values.iloc[cut : len(values) - cut] + + +def seasonal_band_sample(history: pd.Series, day: int, window: int = DEFAULT_BAND_WINDOW) -> tuple[pd.Series, int]: + history = pd.to_numeric(history, errors="coerce").dropna().sort_index() + if history.empty: + return history, window + days = history.index.dayofyear.to_numpy() + sample = history[circular_day_diff(days, day) <= window] + used_window = window + if len(sample) < 8: + sample = history[circular_day_diff(days, day) <= 30] + used_window = 30 + if len(sample) < 8: + sample = history + used_window = 366 + return sample, used_window + + +def band_score(latest_value: float, history: pd.Series, day: int, inverse: bool = False) -> dict[str, Any] | None: + sample, used_window = seasonal_band_sample(history, day) + sample = trimmed_values(sample) + if len(sample) < 4: + return None + mean = float(sample.mean()) + std = float(sample.std(ddof=0)) + if not math.isfinite(std): + std = 0.0 + low = mean - DEFAULT_BAND_K * std + high = mean + DEFAULT_BAND_K * std + if not math.isfinite(low) or not math.isfinite(high): + return None + if high == low: + raw_score = 100.0 if latest_value >= high else 0.0 + elif latest_value >= high: + raw_score = 100.0 + elif latest_value <= low: + raw_score = 0.0 + else: + raw_score = 100.0 * (latest_value - low) / (high - low) + score = 100.0 - raw_score if inverse else raw_score + return { + "score": max(0.0, min(100.0, score)), + "band_low": low, + "band_high": high, + "band_mean": mean, + "band_window": used_window, + "band_k": DEFAULT_BAND_K, + "band_trim": DEFAULT_BAND_TRIM, + } + + +def seasonal_percentile(series: pd.Series) -> dict[str, Any] | None: + series = pd.to_numeric(series, errors="coerce").dropna().sort_index() + if series.empty: + return None + latest_date = series.index.max() + latest_value = float(series.loc[latest_date]) + history = series[series.index < latest_date].dropna() + if history.empty: + return None + day = int(latest_date.dayofyear) + day_diff = np.abs(history.index.dayofyear - day) + sample = history[day_diff <= 15] + window = 15 + if len(sample) < 8: + sample = history[day_diff <= 30] + window = 30 + if len(sample) < 8: + sample = history + window = 366 + less = float((sample < latest_value).sum()) + equal = float((sample == latest_value).sum()) + raw_pct = 100.0 * (less + 0.5 * equal) / len(sample) + latest_year = int(latest_date.year) + yearly: dict[str, list[tuple[str, float]]] = {} + clipped = series[series.index.year >= latest_year - 8] + for dt, val in clipped.items(): + yearly.setdefault(str(int(dt.year)), []).append((dt.strftime("%m-%d"), float(val))) + return { + "latest_date": latest_date.strftime("%Y-%m-%d"), + "latest_value": latest_value, + "raw_percentile": round(raw_pct, 1), + "sample_size": int(len(sample)), + "window": window, + "years": yearly, + "chart_transform": "linear_z_score_in_browser", + } + + +def commodity_boards(name: str) -> list[str]: + boards = [board for board, names in BOARD_MAP.items() if name in names] + return boards or ["其他"] + + +def score_series_at(series: pd.Series, date: pd.Timestamp, inverse: bool = False) -> float | None: + series = pd.to_numeric(series, errors="coerce").dropna().sort_index() + available = series[series.index <= date] + if available.empty: + return None + value_date = available.index.max() + value = float(available.loc[value_date]) + history = series[series.index < value_date].dropna() + if history.empty: + return None + band = band_score(value, history, int(value_date.dayofyear), inverse) + if band: + return float(band["score"]) + sample, _ = seasonal_band_sample(history, int(value_date.dayofyear)) + if sample.empty: + return None + less = float((sample < value).sum()) + equal = float((sample == value).sum()) + raw = 100.0 * (less + 0.5 * equal) / len(sample) + return 100.0 - raw if inverse else raw + + +def build_score_history(dim_series: dict[str, pd.Series]) -> list[dict[str, Any]]: + available = [s.dropna().sort_index() for s in dim_series.values() if s is not None and not s.dropna().empty] + if not available: + return [] + latest = max(s.index.max() for s in available) + start = pd.Timestamp(HISTORY_START_DEFAULT) + if latest < start: + start = min(s.index.min() for s in available) + dates = pd.date_range(start=start, end=latest, freq="D") + history: list[dict[str, Any]] = [] + for date in dates: + scores: dict[str, float] = {} + for dim, series in dim_series.items(): + score = score_series_at(series, date, dim in INVERSE_DIMS) + if score is not None and math.isfinite(score): + scores[dim] = score + if not scores: + continue + dims_with_demand = [d for d in DIMENSIONS if d in scores] + dims_without_demand = [d for d in DIMENSIONS if d != "需求" and d in scores] + total = float(np.mean([scores[d] for d in dims_with_demand])) if dims_with_demand else None + total_no_demand = float(np.mean([scores[d] for d in dims_without_demand])) if dims_without_demand else None + history.append( + { + "date": date.strftime("%Y-%m-%d"), + "total": round(total, 1) if total is not None else None, + "totalNoDemand": round(total_no_demand, 1) if total_no_demand is not None else None, + } + ) + return history + + +def make_records() -> dict[str, Any]: + metrics = [] + metrics.extend(read_metrics(WIND_FILE, "wind", 1, 3, 2, 8, "Wind")) + metrics.extend(read_metrics(STEEL_FILE, "钢联", 1, 2, 5, 10, "钢联")) + metric_index, metric_names = build_metric_index(metrics) + confirm = pd.read_excel(CONFIRM_FILE).fillna("") + commodities: list[dict[str, Any]] = [] + for idx, row in confirm.iterrows(): + name = str(row["品种"]).strip() + if not name: + continue + dims: dict[str, Any] = {} + dim_series: dict[str, pd.Series] = {} + valid_scores: list[float] = [] + for dim in DIMENSIONS: + series, missing, used_metrics = eval_expression(row.get(dim, ""), metric_index, metric_names) + if series is not None: + dim_series[dim] = series + stat = seasonal_percentile(series) if series is not None else None + if stat: + latest_dt = pd.Timestamp(stat["latest_date"]) + history = series[series.index < latest_dt].dropna() + band = band_score(stat["latest_value"], history, int(latest_dt.dayofyear), dim in INVERSE_DIMS) + if band: + score = band["score"] + stat.update(band) + else: + raw = stat["raw_percentile"] + score = 100.0 - raw if dim in INVERSE_DIMS else raw + stat["score"] = round(score, 1) + stat["direction"] = "低库存更优,按正常区间反向计分" if dim in INVERSE_DIMS else "高于正常区间更优" + stat["expr"] = str(row.get(dim, "")).strip() + stat["metrics"] = [ + {"name": m.name, "source": m.source, "unit": m.unit, "freq": m.freq} + for m in used_metrics + ] + valid_scores.append(float(score)) + dims[dim] = stat or { + "score": None, + "raw_percentile": None, + "direction": "缺数据", + "expr": str(row.get(dim, "")).strip(), + "missing": missing, + "metrics": [{"name": m.name, "source": m.source, "unit": m.unit, "freq": m.freq} for m in used_metrics], + } + total = round(float(np.mean(valid_scores)), 1) if valid_scores else None + boards = commodity_boards(name) + commodities.append( + { + "rank": None, + "name": name, + "board": " / ".join(boards), + "boards": boards, + "total": total, + "valid_count": len(valid_scores), + "dims": dims, + "scoreHistory": build_score_history(dim_series), + } + ) + ranked = sorted([c for c in commodities if c["total"] is not None], key=lambda x: x["total"], reverse=True) + for i, item in enumerate(ranked, 1): + item["rank"] = i + no_score = [c for c in commodities if c["total"] is None] + commodities = ranked + no_score + return { + "generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "source_files": { + "wind": WIND_FILE.name, + "steel": STEEL_FILE.name, + "confirm": CONFIRM_FILE.name, + }, + "dimensions": DIMENSIONS + ["月差"], + "boards": sorted(set(board for c in commodities for board in c.get("boards", [c["board"]]))), + "commodities": commodities, + } + + +def html_template(data: dict[str, Any]) -> str: + payload = json.dumps(data, ensure_ascii=False, allow_nan=False) + return f""" + + + + +NO.5 可更新数据 - 基本面评分面板 + + + +
+
NO.5 可更新数据
+ + + + + + + + + + + +
核心修正:库存反向计分 生成时间:
+
+
+
+
基本面总分
+
--
+
当前筛选覆盖 -- 个有可用数据的品种;月差当前暂无映射,未计入总分。
+
+
+
基本面较强
+
+
+
+
基本面较弱
+
+
+
+
维度得分按同期正常波动区间计算:正向指标高于上沿为 100,低于下沿为 0,区间内线性映射;库存反向。阴影带参数只影响图表展示,总分使用默认 ±15 天、3 倍标准差、去尾 10%。
+
+
+
+ 各品种基本面得分变化 + + +
+
+
+ +
+
+
+ + +""" + + +def main() -> None: + data = make_records() + OUTPUT_FILE.write_text(html_template(data), encoding="utf-8") + scored = [c for c in data["commodities"] if c["total"] is not None] + print(f"Wrote {OUTPUT_FILE}") + print(f"Scored commodities: {len(scored)} / {len(data['commodities'])}") + if scored: + print("Top 5:", ", ".join(f"{c['name']} {c['total']}" for c in scored[:5])) + print("Bottom 5:", ", ".join(f"{c['name']} {c['total']}" for c in scored[-5:])) + + +if __name__ == "__main__": + main() diff --git a/refresh_dashboard_server.py b/refresh_dashboard_server.py new file mode 100644 index 0000000..65f38fb --- /dev/null +++ b/refresh_dashboard_server.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +import http.server +import subprocess +import sys +from pathlib import Path + + +BASE_DIR = Path(__file__).resolve().parent +PORT = 8765 + + +class Handler(http.server.SimpleHTTPRequestHandler): + def __init__(self, *args, **kwargs): + super().__init__(*args, directory=str(BASE_DIR), **kwargs) + + def do_POST(self): + if self.path != "/refresh": + self.send_error(404) + return + proc = subprocess.run( + [sys.executable, str(BASE_DIR / "generate_fundamental_dashboard.py")], + cwd=str(BASE_DIR), + text=True, + capture_output=True, + ) + body = (proc.stdout or "") + (proc.stderr or "") + self.send_response(200 if proc.returncode == 0 else 500) + self.send_header("Access-Control-Allow-Origin", "*") + self.send_header("Content-Type", "text/plain; charset=utf-8") + self.end_headers() + self.wfile.write(body.encode("utf-8", errors="replace")) + + def end_headers(self): + self.send_header("Access-Control-Allow-Origin", "*") + super().end_headers() + + +if __name__ == "__main__": + server = http.server.ThreadingHTTPServer(("127.0.0.1", PORT), Handler) + print(f"http://127.0.0.1:{PORT}/基本面评分面板.html") + server.serve_forever() diff --git a/wind数据汇总.xlsx b/wind数据汇总.xlsx new file mode 100644 index 0000000..46b9610 Binary files /dev/null and b/wind数据汇总.xlsx differ diff --git a/基本面指标确认.xlsx b/基本面指标确认.xlsx new file mode 100644 index 0000000..c76478a Binary files /dev/null and b/基本面指标确认.xlsx differ diff --git a/基本面评分面板.html b/基本面评分面板.html new file mode 100644 index 0000000..989380b --- /dev/null +++ b/基本面评分面板.html @@ -0,0 +1,529 @@ + + + + + +NO.5 可更新数据 - 基本面评分面板 + + + +
+
NO.5 可更新数据
+ + + + + + + + + + + +
核心修正:库存反向计分 生成时间:
+
+
+
+
基本面总分
+
--
+
当前筛选覆盖 -- 个有可用数据的品种;月差当前暂无映射,未计入总分。
+
+
+
基本面较强
+
+
+
+
基本面较弱
+
+
+
+
维度得分按同期正常波动区间计算:正向指标高于上沿为 100,低于下沿为 0,区间内线性映射;库存反向。阴影带参数只影响图表展示,总分使用默认 ±15 天、3 倍标准差、去尾 10%。
+
+
+
+ 各品种基本面得分变化 + + +
+
+
+ +
+
+
+ + + \ No newline at end of file diff --git a/月差/A-期货日级数据/SKILL.md b/月差/A-期货日级数据/SKILL.md new file mode 100644 index 0000000..d8ed6ce --- /dev/null +++ b/月差/A-期货日级数据/SKILL.md @@ -0,0 +1,132 @@ +--- +name: A-期货日级数据 +description: 通过 mysql_dll2.dll 连接 MySQL 数据库并执行 SQL 查询。当用户需要查询 MySQL 数据库、执行 SQL 语句、获取数据库中的数据时触发。 触发关键词包括:查询数据库、查合约、查日线、查K线、查结算价、查持仓量、查成交量、查保证金、 查品种数据、读取数据库、SQL查询、查某品种某合约、查期货数据、从数据库取数据、db查询、查纯碱、查螺纹等。 注意:此 Skill 依赖 mysql_dll2.dll 文件,仅限 Windows 平台使用。数据库编号固定为 1。 +disable: true +allowed-tools: +--- + +.dll 连接 MySQL 数据库并执行 SQL 查询。当用户需要查询 MySQL 数据库、执行 SQL 语句、获取数据库中的数据时触发。 + 触发关键词包括:查询数据库、查合约、查日线、查K线、查结算价、查持仓量、查成交量、查保证金、 + 查品种数据、读取数据库、SQL查询、查某品种某合约、查期货数据、从数据库取数据、db查询、查纯碱、查螺纹等。 + 注意:此 Skill 依赖 mysql_dll2.dll 文件,仅限 Windows 平台使用。数据库编号固定为 1。 +allowed-tools: +disable: true +--- + +# A-期货日级数据 + +## 用途 + +把自然语言翻译成 SQL 执行,返回原始数据。不生成汇总、不分析、不联想。 + +--- + +## 数据库 + +- **db_index:固定为 1**,无需询问用户 + +--- + +## 调用方式 + +```python +import sys +sys.path.insert(0, r"") +from mysql_client import MysqlDLLClient + +db = MysqlDLLClient() +result = db.query(1, "SELECT * FROM `contract_day` WHERE p_code = ?", ["sa"]) +print(result) +db.close() +``` + +--- + +## 字段说明(references/field_mapping.md) + +| 英文字段名 | 中文含义 | 字段类型 | +|-----------|---------|---------| +| contract | 合约 | varchar(10) | +| open | 开盘价 | float | +| high | 高(最高价) | float | +| low | 低(最低价) | float | +| close | 收(收盘价) | float | +| volume | 成交(成交量) | float | +| oi | 持仓(持仓量) | float | +| p_code | 品种 | varchar(4) | +| jys | 交易所 | varchar(6) | +| times | 时间 | datetime | + +### 品种 vs 合约 + +| 概念 | 字段 | 说明 | 示例 | +|------|------|------|------| +| 品种 | `p_code` | 品种代码,拼音小写 | `sa`(纯碱)、`rb`(螺纹钢) | +| 合约 | `contract` | 品种 + 交割月份 | `sa605`(纯碱2026年5月交割) | + +> 郑商所(CZCE)合约月份为3位,如 `SR501`、`MA509`。 + +--- + +## 查询示例 + +**查所有品种某时间段数据(直接一条SQL搞定,不需要先查品种)** + +```sql +SELECT * FROM contract_day WHERE times >= '2026-01-01' AND times <= '2026-12-31' +``` + +**查纯碱26年所有合约数据** + +``` +p_code = 'SA' +时间范围: times >= '2026-01-01' AND times <= '2026-12-31' +``` + +**查纯碱605合约数据** + +``` +contract = 'SA605' +``` + +**查螺纹钢26年所有数据** + +``` +p_code = 'RB' +时间范围: times >= '2026-01-01' AND times <= '2026-12-31' +``` + +--- + +## 执行规则 + +1. **直接执行,不询问确认。** +2. **只返回数据,不生成汇总、不分析。** +3. **db_index 固定为 1。** +4. **参数化查询**,所有条件用 `?` 占位符传参。 +5. **查所有品种时直接一条 SQL**,不需要先 `SELECT DISTINCT p_code` 再逐条查。 + +--- + +## 依赖文件 + +``` +scripts/ +└── mysql_client.py +lib/ +└── mysql_dll/ + ├── mysql_dll2.dll + ├── jsoncpp.dll + ├── libcrypto-1_1-x64.dll + ├── libmysql.dll + ├── libssl-1_1-x64.dll + └── mysqlcppconn-9-vs14.dll +``` + +DLL 由 `mysql_client.py` 自动加载,无需手动配置。 + +--- + +## 品种代码对照(references/variety_mapping.md) + +详见 `references/variety_mapping.md`,包含所有交易所品种代码。 diff --git a/月差/A-期货日级数据/lib/mysql_dll/jsoncpp.dll b/月差/A-期货日级数据/lib/mysql_dll/jsoncpp.dll new file mode 100644 index 0000000..a06e6d9 Binary files /dev/null and b/月差/A-期货日级数据/lib/mysql_dll/jsoncpp.dll differ diff --git a/月差/A-期货日级数据/lib/mysql_dll/libcrypto-1_1-x64.dll b/月差/A-期货日级数据/lib/mysql_dll/libcrypto-1_1-x64.dll new file mode 100644 index 0000000..4f4a8a9 Binary files /dev/null and b/月差/A-期货日级数据/lib/mysql_dll/libcrypto-1_1-x64.dll differ diff --git a/月差/A-期货日级数据/lib/mysql_dll/libmysql.dll b/月差/A-期货日级数据/lib/mysql_dll/libmysql.dll new file mode 100644 index 0000000..d9a2c29 Binary files /dev/null and b/月差/A-期货日级数据/lib/mysql_dll/libmysql.dll differ diff --git a/月差/A-期货日级数据/lib/mysql_dll/libssl-1_1-x64.dll b/月差/A-期货日级数据/lib/mysql_dll/libssl-1_1-x64.dll new file mode 100644 index 0000000..9f53ee1 Binary files /dev/null and b/月差/A-期货日级数据/lib/mysql_dll/libssl-1_1-x64.dll differ diff --git a/月差/A-期货日级数据/lib/mysql_dll/mysql_dll2.dll b/月差/A-期货日级数据/lib/mysql_dll/mysql_dll2.dll new file mode 100644 index 0000000..3b3d386 Binary files /dev/null and b/月差/A-期货日级数据/lib/mysql_dll/mysql_dll2.dll differ diff --git a/月差/A-期货日级数据/lib/mysql_dll/mysqlcppconn-9-vs14.dll b/月差/A-期货日级数据/lib/mysql_dll/mysqlcppconn-9-vs14.dll new file mode 100644 index 0000000..8b9b41a Binary files /dev/null and b/月差/A-期货日级数据/lib/mysql_dll/mysqlcppconn-9-vs14.dll differ diff --git a/月差/A-期货日级数据/references/field_mapping.md b/月差/A-期货日级数据/references/field_mapping.md new file mode 100644 index 0000000..b932fc5 --- /dev/null +++ b/月差/A-期货日级数据/references/field_mapping.md @@ -0,0 +1,29 @@ +# 数据库字段中英文对照表 + +## 品种 vs 合约 + +| 概念 | 字段 | 说明 | 示例 | +|------|------|------|------| +| **品种** | `p_code` | 品种代码,拼音小写,代表品种分类 | `rb`(螺纹钢)、`cu`(铜) | +| **合约** | `contract` | 具体合约代码 = 品种 + 交割月份 | `rb2506`(螺纹钢2025年6月交割) | + +> 郑商所(CZCE)合约月份为3位,如 `SR501`、`MA509`。 + + +## 字段说明 + +| 英文字段名 | 中文注释 | 字段类型 | +|-----------|---------|---------| +| contract | 合约 | varchar(10) | +| open | 开盘价 | float | +| high | 高(最高价) | float | +| low | 低(最低价) | float | +| close | 收(收盘价) | float | +| volume | 成交(成交量) | float | +| oi | 持仓(持仓量) | float | +| p_code | 品种 | varchar(4) | +| jys | 交易所 | varchar(6) | +| times | 时间 | datetime | + + + diff --git a/月差/A-期货日级数据/references/variety_mapping.md b/月差/A-期货日级数据/references/variety_mapping.md new file mode 100644 index 0000000..0321a78 --- /dev/null +++ b/月差/A-期货日级数据/references/variety_mapping.md @@ -0,0 +1,119 @@ +# 期货品种中英文对照表 + +## 中金所(CFFEX) + +| p_code(查询用) | 中文名称 | 合约位数 | 备注 | +|-----------------|---------|---------|------| +| if / IF | 沪深300股指期货 | 4位 | 如 IF2506 | +| ih / IH | 上证50股指期货 | 4位 | 如 IH2506 | +| im / IM | 中证1000股指期货 | 4位 | 如 IM2506 | +| ic / IC | 中证500股指期货 | 4位 | 如 IC2506 | +| tf / TF | 5年国债期货 | 4位 | 如 TF2506 | +| t / T | 10年国债期货 | 4位 | 如 T2506 | +| ts / TS | 2年国债期货 | 4位 | 如 TS2506 | +| tl / TL | 30年国债期货 | 4位 | 如 TL2506 | + +--- + +## 上海期货交易所(SHFE) + +| p_code | 中文名称 | 合约位数 | 示例 | +|--------|---------|---------|------| +| cu | 铜 | 4位 | cu2506 | +| al | 铝 | 4位 | al2506 | +| zn | 锌 | 4位 | zn2506 | +| pb | 铅 | 4位 | pb2506 | +| ni | 镍 | 4位 | ni2506 | +| sn | 锡 | 4位 | sn2506 | +| au | 黄金 | 4位 | au2506 | +| ag | 白银 | 4位 | ag2506 | +| rb | 螺纹钢 | 4位 | rb2506 | +| wr | 线材 | 4位 | wr2506 | +| hc | 热轧卷板 | 4位 | hc2506 | +| ss | 不锈钢 | 4位 | ss2506 | +| fu | 燃料油 | 4位 | fu2506 | +| bu | 沥青 | 4位 | bu2506 | +| ru | 天然橡胶 | 4位 | ru2506 | +| sp | 纸浆 | 4位 | sp2506 | +| br | 丁二烯橡胶 | 4位 | br2506 | + +--- + +## 大连商品交易所(DCE) + +| p_code | 中文名称 | 合约位数 | 示例 | +|--------|---------|---------|------| +| a | 豆一 | 4位 | a2506 | +| b | 豆二 | 4位 | b2506 | +| m | 豆粕 | 4位 | m2506 | +| y | 豆油 | 4位 | y2506 | +| p | 棕榈油 | 4位 | p2506 | +| c | 玉米 | 4位 | c2506 | +| cs | 玉米淀粉 | 4位 | cs2506 | +| jd | 鸡蛋 | 4位 | jd2506 | +| l | 聚乙烯 | 4位 | l2506 | +| v | 聚氯乙烯 | 4位 | v2506 | +| pp | 聚丙烯 | 4位 | pp2506 | +| j | 焦炭 | 4位 | j2506 | +| jm | 焦煤 | 4位 | jm2506 | +| i | 铁矿石 | 4位 | i2506 | +| eg | 乙二醇 | 4位 | eg2506 | +| rr | 粳米 | 4位 | rr2506 | +| eb | 苯乙烯 | 4位 | eb2506 | +| pg | 液化石油气 | 4位 | pg2506 | +| lh | 生猪 | 4位 | lh2506 | + +--- + +## 郑州商品交易所(CZCE) + +> ⚠️ **郑商所合约特殊规则:合约月份只有 3 位数字** +> 例如:SR501(白糖2501年1月),MA509(甲醇2509年9月) +> 年份只取个位数,与其他交易所4位不同! + +| p_code | 中文名称 | 合约格式 | 示例 | +|--------|---------|---------|------| +| SR | 白糖 | 品种+3位 | SR501 | +| CF | 棉花 | 品种+3位 | CF501 | +| MA | 甲醇 | 品种+3位 | MA509 | +| TA | PTA | 品种+3位 | TA509 | +| OI | 菜籽油 | 品种+3位 | OI509 | +| RM | 菜籽粕 | 品种+3位 | RM509 | +| RS | 菜籽 | 品种+3位 | RS509 | +| WH | 强麦 | 品种+3位 | WH509 | +| PM | 普麦 | 品种+3位 | PM509 | +| RI | 早籼稻 | 品种+3位 | RI509 | +| JR | 粳稻 | 品种+3位 | JR509 | +| LR | 晚籼稻 | 品种+3位 | LR509 | +| AP | 苹果 | 品种+3位 | AP509 | +| CJ | 红枣 | 品种+3位 | CJ509 | +| SF | 硅铁 | 品种+3位 | SF509 | +| SM | 锰硅 | 品种+3位 | SM509 | +| ZC | 动力煤 | 品种+3位 | ZC509 | +| FG | 玻璃 | 品种+3位 | FG509 | +| SA | 纯碱 | 品种+3位 | SA509 | +| UR | 尿素 | 品种+3位 | UR509 | +| PF | 短纤 | 品种+3位 | PF509 | +| PX | 对二甲苯 | 品种+3位 | PX509 | +| PR | 丙烯 | 品种+3位 | PR509 | +| SH | 烧碱 | 品种+3位 | SH509 | + +--- + +## 上海国际能源交易中心(INE) + +| p_code | 中文名称 | 合约位数 | 示例 | +|--------|---------|---------|------| +| sc | 原油 | 4位 | sc2506 | +| lu | 低硫燃料油 | 4位 | lu2506 | +| nr | 20号胶 | 4位 | nr2506 | +| bc | 国际铜 | 4位 | bc2506 | + +--- + +## 广州期货交易所(GFEX) + +| p_code | 中文名称 | 合约位数 | 示例 | +|--------|---------|---------|------| +| si | 工业硅 | 4位 | si2506 | +| lc | 碳酸锂 | 4位 | lc2506 | diff --git a/月差/A-期货日级数据/scripts/mysql_client.py b/月差/A-期货日级数据/scripts/mysql_client.py new file mode 100644 index 0000000..a55c93a --- /dev/null +++ b/月差/A-期货日级数据/scripts/mysql_client.py @@ -0,0 +1,172 @@ +import ctypes +import json +from pathlib import Path +import time + + +class MysqlDLLClient: + """ + MySQL DLL 调用客户端 + 用于调用 mysql_dll2.dll 执行 SQL + + Example + ------- + db = MysqlDLLClient() + + data = db.query( + 7, + "select * from min15_a_jq where times>=? and times<=?", + ["2025-12-24 00:00:00", "2025-12-26 00:00:00"] + ) + + print(data) + """ + + def __init__(self, dll_path=None): + """ + 初始化 DLL + + Parameters + ---------- + dll_path : str | Path + dll路径,默认当前目录 mysql_dll2.dll + """ + + if dll_path is None: + root = Path(__file__).resolve().parent.parent + dll_path = root / "lib/mysql_dll/mysql_dll2.dll" + + self.dll = ctypes.WinDLL(str(dll_path)) + + self._init_functions() + + time.sleep(1) + + ret = self.dll.InitDBConfig() + if ret != 1: + raise RuntimeError("服务器授权失败") + + # --------------------------------------------------------- + # 初始化 DLL 函数声明 + # --------------------------------------------------------- + def _init_functions(self): + + # 初始化数据库配置 + self.dll.InitDBConfig.argtypes = [] + self.dll.InitDBConfig.restype = ctypes.c_int + + # 关闭 DLL + self.dll.ShutdownDLL.argtypes = [] + self.dll.ShutdownDLL.restype = None + + # QuerySQL_Auto + self.dll.QuerySQL_Auto.argtypes = [ + ctypes.c_int, # db_index + ctypes.c_char_p, # sql + ctypes.c_char_p, # params + ctypes.POINTER(ctypes.c_void_p), # out_buf + ctypes.POINTER(ctypes.c_int) # out_size + ] + + self.dll.QuerySQL_Auto.restype = ctypes.c_int + + # FreeBuffer + self.dll.FreeBuffer.argtypes = [ctypes.c_void_p] + self.dll.FreeBuffer.restype = None + + # --------------------------------------------------------- + # 参数转换 + # --------------------------------------------------------- + def _build_params(self, params): + """ + 参数列表转 DLL 参数格式 + + ["2025-01-01","2025-01-02"] + + -> + + b"s:2025-01-01|s:2025-01-02" + """ + + if not params: + return b"" + + arr = [] + + for p in params: + if isinstance(p, str): + arr.append(f"s:{p}") + elif isinstance(p, int): + arr.append(f"i:{p}") + elif isinstance(p, float): + arr.append(f"f:{p}") + else: + raise TypeError(f"不支持的参数类型: {type(p)}") + + return "|".join(arr).encode() + + # --------------------------------------------------------- + # 查询 + # --------------------------------------------------------- + def query(self, db_index, sql, params=None): + """ + 执行 SQL 查询 + + Parameters + ---------- + db_index : int + 数据库编号 + + sql : str + SQL语句 + + params : list + SQL参数 + + Returns + ------- + str + 查询结果(JSON 或文本) + """ + + if isinstance(sql, str): + sql = sql.encode() + + params_bytes = self._build_params(params) + + out_buf = ctypes.c_void_p() + out_size = ctypes.c_int() + + ret = self.dll.QuerySQL_Auto( + db_index, + sql, + params_bytes, + ctypes.byref(out_buf), + ctypes.byref(out_size) + ) + + if ret != 0: + raise RuntimeError("SQL执行失败") + + try: + data = ctypes.string_at(out_buf.value, out_size.value) + return json.loads(data.decode()) + finally: + self.dll.FreeBuffer(out_buf) + + # --------------------------------------------------------- + # 关闭 + # --------------------------------------------------------- + def close(self): + """关闭 DLL""" + if self.dll: + self.dll.ShutdownDLL() + + # --------------------------------------------------------- + # with 支持 + # --------------------------------------------------------- + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() \ No newline at end of file diff --git a/月差拼接数据.xlsx b/月差拼接数据.xlsx new file mode 100644 index 0000000..fdee812 Binary files /dev/null and b/月差拼接数据.xlsx differ diff --git a/钢联数据汇总.xlsx b/钢联数据汇总.xlsx new file mode 100644 index 0000000..2c2d641 Binary files /dev/null and b/钢联数据汇总.xlsx differ