From 9c42ca0b394f2b42a0fda3b6141137521b38a55b Mon Sep 17 00:00:00 2001 From: qq564020069 <125543001+qq564020069@users.noreply.github.com> Date: Sat, 18 Jul 2026 23:44:50 +0800 Subject: [PATCH] 1 --- _test_akshare.py | 43 +++++++ _test_tqsdk_login.py | 33 ++++++ data_pipeline/parquet_manager.py | 15 ++- download_pvc_5m.py | 95 +++++++++++++++ download_pvc_5m_full.py | 124 ++++++++++++++++++++ start_web.bat | 67 ++++------- web/data_sources/domestic_futures_source.py | 31 ++--- web/data_sources/factory.py | 3 +- web/settings.py | 4 +- web/static/app.js | 2 + web/static/index.html | 2 +- web_settings.example.json | 4 +- 12 files changed, 360 insertions(+), 63 deletions(-) create mode 100644 _test_akshare.py create mode 100644 _test_tqsdk_login.py create mode 100644 download_pvc_5m.py create mode 100644 download_pvc_5m_full.py diff --git a/_test_akshare.py b/_test_akshare.py new file mode 100644 index 0000000..1f528c5 --- /dev/null +++ b/_test_akshare.py @@ -0,0 +1,43 @@ +"""测试 akshare 各函数能获取多少 PVC 5分钟历史数据。""" +import akshare as ak +import pandas as pd +from datetime import datetime + +print(f"akshare version: {ak.__version__}") +print() + +# 方法1: futures_zh_minute_sina - 新浪连续合约 +print("=== 方法1: futures_zh_minute_sina(V0, 5min) ===") +try: + df1 = ak.futures_zh_minute_sina(symbol="V0", period="5") + print(f"行数: {len(df1)}") + print(f"列: {list(df1.columns)}") + if len(df1) > 0: + print(f"时间范围: {df1.iloc[0,0]} ~ {df1.iloc[-1,0]}") + print(df1.head(2)) + print(df1.tail(2)) +except Exception as e: + print(f"ERROR: {e}") + +print() + +# 方法2: 尝试下载多个具体合约拼接 +print("=== 方法2: 下载多个 PVC 合约 ===") +all_dfs = [] +# PVC合约代码: V+YYMM, 如 V2401=2024年1月 +contracts = [] +for year in [23, 24, 25, 26]: + for month in range(1, 13): + code = f"V{year:02d}{month:02d}" + contracts.append(code) + +for code in contracts: + try: + df = ak.futures_zh_minute_sina(symbol=code, period="5") + if df is not None and len(df) > 0: + print(f" {code}: {len(df)} 行, {df.iloc[0,0]} ~ {df.iloc[-1,0]}") + all_dfs.append(df) + except Exception as e: + pass # 合约可能已过期,跳过 + +print(f"\n总计获取 {len(all_dfs)} 个合约, 合计 {sum(len(d) for d in all_dfs)} 行") diff --git a/_test_tqsdk_login.py b/_test_tqsdk_login.py new file mode 100644 index 0000000..a397980 --- /dev/null +++ b/_test_tqsdk_login.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +"""验证 tqsdk 账户能否登录并获取行情数据""" +from tqsdk import TqApi, TqAuth +import sys, time + +USER = "七斗居士" +PASSWORD = "ghhkphs8" + +print(f"正在登录 tqsdk: {USER}") +try: + api = TqApi(auth=TqAuth(USER, PASSWORD)) + print("[OK] 登录成功!") + + # 获取行情 + quote = api.get_quote("SHFE.cu2509") + # 等待数据到达 + while quote.datetime == "": + api.wait_update() + + print(f"合约代码: {quote.instrument_id}") + print(f"最新价: {quote.last_price}") + print(f"买一价: {quote.bid_price1}") + print(f"卖一价: {quote.ask_price1}") + print(f"开盘价: {quote.open}") + print(f"最高价: {quote.high}") + print(f"最低价: {quote.low}") + print(f"更新时间: {quote.datetime}") + print("\n[OK] 账户有效, 行情数据获取成功!") + + api.close() +except Exception as e: + print(f"[FAIL] {type(e).__name__}: {e}") + sys.exit(1) diff --git a/data_pipeline/parquet_manager.py b/data_pipeline/parquet_manager.py index ff3d785..f49d03e 100644 --- a/data_pipeline/parquet_manager.py +++ b/data_pipeline/parquet_manager.py @@ -127,7 +127,20 @@ def inspect_parquet_file(path: str | Path) -> dict[str, Any]: f"数据不足: {bars} bars(至少需要 {Config.MIN_BARS})" ) - years = round(bars / 6240, 2) if timeframe == "H1" else None + # 从实际时间跨度计算年数(适用于所有周期,比固定公式更准确) + years = None + if "time" in df.columns and len(df) > 1: + try: + t_min = float(df["time"].min()) + t_max = float(df["time"].max()) + if t_max > t_min and t_max > 1_000_000_000: # 合法的 Unix 时间戳 + span_seconds = t_max - t_min + years = round(span_seconds / (365.25 * 24 * 3600), 2) + except Exception: + pass + # 回退:H1 用固定公式(6240 根/年,24h 市场) + if years is None and timeframe == "H1": + years = round(bars / 6240, 2) return { "data_file": str(p.resolve()), "filename": p.name, diff --git a/download_pvc_5m.py b/download_pvc_5m.py new file mode 100644 index 0000000..c64e13c --- /dev/null +++ b/download_pvc_5m.py @@ -0,0 +1,95 @@ +"""下载 PVC 5分钟 K 线数据到桌面。 + +使用 tqsdk 拉取 PVC主连(KQ.m@DCE.v) 的 5 分钟 K 线, +保存为项目兼容的 Parquet 格式(time/open/high/low/close/tick_volume)。 +""" +import json +import sys +from pathlib import Path + +# 读取 tqsdk 凭证(优先 web_settings.json,未配置则用默认账号) +settings_path = Path(__file__).parent / "web_settings.json" +try: + settings = json.loads(settings_path.read_text(encoding="utf-8")) + user = str(settings.get("tqsdk_user", "")).strip() + pwd = str(settings.get("tqsdk_password", "")).strip() +except Exception: + user, pwd = "", "" + +# 内置默认账号 +if not user or not pwd: + user, pwd = "七斗居士", "ghhkphs8" + +print(f"tqsdk 账号: {user}") +print("正在连接天勤量化...") + +from tqsdk import TqApi, TqAuth, TqSim +import pandas as pd + +# PVC主连 → tqsdk 主连代码 +TQSDK_SYMBOL = "KQ.m@DCE.v" +DURATION = 300 # 5分钟 = 300秒 +DATA_LENGTH = 8000 # 尽量多拉 + +api = None +try: + api = TqApi(TqSim(), auth=TqAuth(user, pwd), disable_print=True) + print("连接成功,正在拉取 PVC 5分钟 K 线...") + + # adj_type="F" 前复权,消除换月跳价 + df = api.get_kline_serial( + TQSDK_SYMBOL, DURATION, data_length=DATA_LENGTH, adj_type="F" + ) + print(f"拉取到 {len(df)} 根 K 线") +finally: + if api is not None: + try: + api.close() + except Exception: + pass + +if df is None or df.empty: + print("ERROR: 未拉取到数据") + sys.exit(1) + +# 过滤无效行(volume=0 的非交易时段,或 datetime=0) +df = df[df["datetime"] != 0].copy() +df = df[df["volume"] > 0].copy() +print(f"过滤后有效 K 线: {len(df)} 根") + +# 转换为项目兼容格式 +# tqsdk datetime 是纳秒级 Unix 时间戳 → 转秒 +result = pd.DataFrame() +result["time"] = (df["datetime"].astype("int64") // 1_000_000_000).astype("int64") +result["open"] = df["open"].astype("float64") +result["high"] = df["high"].astype("float64") +result["low"] = df["low"].astype("float64") +result["close"] = df["close"].astype("float64") +result["tick_volume"] = df["volume"].astype("int64") + +# 按时间升序排列 +result = result.sort_values("time").reset_index(drop=True) + +# 去除可能重复的时间戳 +result = result.drop_duplicates(subset=["time"], keep="last").reset_index(drop=True) + +# 输出目录 +out_dir = Path(r"C:\Users\14795\Desktop\国内期货K线数据") +out_dir.mkdir(parents=True, exist_ok=True) +out_file = out_dir / "PVC_5min.parquet" + +# 保存为 Parquet +result.to_parquet(out_file, index=False) +print(f"\n已保存: {out_file}") +print(f"总行数: {len(result)}") + +# 显示数据概览 +print(f"\n=== 数据概览 ===") +print(f"时间范围: {pd.to_datetime(result['time'].iloc[0], unit='s')} ~ {pd.to_datetime(result['time'].iloc[-1], unit='s')}") +print(f"开盘价范围: {result['open'].min():.2f} ~ {result['open'].max():.2f}") +print(f"收盘价范围: {result['close'].min():.2f} ~ {result['close'].max():.2f}") +print(f"\n前 5 行:") +print(result.head().to_string()) +print(f"\n后 5 行:") +print(result.tail().to_string()) +print("\n下载完成!") diff --git a/download_pvc_5m_full.py b/download_pvc_5m_full.py new file mode 100644 index 0000000..654dbeb --- /dev/null +++ b/download_pvc_5m_full.py @@ -0,0 +1,124 @@ +"""修复版:下载 PVC 全历史 5分钟 K 线数据(akshare 多合约拼接)。 + +修复 pandas 3.0 时间戳转换问题,使用显式时区处理。 +""" +import akshare as ak +import pandas as pd +from pathlib import Path +from datetime import timezone, timedelta +import time +import sys + +print("=" * 60) +print(" PVC 全历史 5分钟 K 线下载(akshare 多合约拼接·修复版)") +print("=" * 60) + +# 北京时间 UTC+8 +BJ_TZ = timezone(timedelta(hours=8)) + +# 生成所有 PVC 合约代码: V2101 ~ V2612 +contracts = [] +for year in range(21, 27): + for month in range(1, 13): + contracts.append(f"V{year:02d}{month:02d}") + +print(f"待下载合约数: {len(contracts)} 个 (V2101 ~ V2612)") +print() + +# 下载所有合约 +all_data = [] +success = 0 +for i, code in enumerate(contracts): + try: + df = ak.futures_zh_minute_sina(symbol=code, period="5") + if df is not None and len(df) > 0: + df = df.copy() + df["_contract"] = code + all_data.append(df) + success += 1 + if (i + 1) % 12 == 0 or i == len(contracts) - 1: + print(f" 进度: {i+1}/{len(contracts)} | 成功: {success}") + except Exception: + pass + time.sleep(0.15) + +print(f"\n下载完成: 成功 {success} 个合约, 总行数 {sum(len(d) for d in all_data)}") + +# 合并 +print("\n正在拼接主力合约序列...") +combined = pd.concat(all_data, ignore_index=True) + +# 修复时间戳转换:显式按北京时间处理 +def to_unix_seconds(dt_str): + """将 '2020-09-17 21:35:00' 转为 Unix 秒(按北京时间 UTC+8)。""" + try: + dt = pd.to_datetime(dt_str) + # 显式设置时区为北京时间,再转 Unix 时间戳 + return int(dt.replace(tzinfo=BJ_TZ).timestamp()) + except Exception: + return 0 + +combined["_ts"] = combined["datetime"].apply(to_unix_seconds) +combined = combined[combined["_ts"] > 0].copy() # 过滤无效 + +# 按时间戳+成交量排序,去重时保留成交量最大的(近似主力合约) +print("按成交量选择主力合约(去重)...") +combined = combined.sort_values(["_ts", "volume"], ascending=[True, False]) +combined = combined.drop_duplicates(subset=["_ts"], keep="first") +combined = combined.sort_values("_ts").reset_index(drop=True) + +print(f"去重后行数: {len(combined):,}") + +# 验证时间戳 +first_dt = pd.to_datetime(combined["datetime"].iloc[0]) +last_dt = pd.to_datetime(combined["datetime"].iloc[-1]) +print(f"时间范围: {first_dt} ~ {last_dt}") +span_days = (last_dt - first_dt).days +print(f"时间跨度: {span_days} 天(约 {span_days/365.25:.1f} 年)") + +# 验证时间戳正确性 +print(f"首行 Unix 时间戳: {combined['_ts'].iloc[0]}") +print(f"首行时间戳验证: {pd.to_datetime(combined['_ts'].iloc[0], unit='s', utc=True).tz_convert(BJ_TZ)}") + +# 转换为项目兼容格式 +result = pd.DataFrame() +result["time"] = combined["_ts"].astype("int64") +result["open"] = combined["open"].astype("float64") +result["high"] = combined["high"].astype("float64") +result["low"] = combined["low"].astype("float64") +result["close"] = combined["close"].astype("float64") +result["tick_volume"] = combined["volume"].astype("int64") +result = result.drop_duplicates(subset=["time"], keep="last").reset_index(drop=True) + +# 保存 +out_dir = Path(r"C:\Users\14795\Desktop\国内期货K线数据") +out_dir.mkdir(parents=True, exist_ok=True) +out_file = out_dir / "PVC_5min.parquet" +result.to_parquet(out_file, index=False) + +# 统计 +print(f"\n{'=' * 60}") +print(f" 下载完成!") +print(f"{'=' * 60}") +print(f"保存路径: {out_file}") +print(f"总行数: {len(result):,}") +print(f"时间范围: {first_dt} ~ {last_dt}") +print(f"时间跨度: {span_days} 天(约 {span_days/365.25:.1f} 年)") + +print(f"\n价格统计:") +print(f" 开盘价: {result['open'].min():.0f} ~ {result['open'].max():.0f}") +print(f" 收盘价: {result['close'].min():.0f} ~ {result['close'].max():.0f}") +print(f" 均价: {result['close'].mean():.0f}") + +# 按年统计 +result["_dt"] = pd.to_datetime(result["time"], unit="s", utc=True).dt.tz_convert(BJ_TZ) +result["_year"] = result["_dt"].dt.year +yearly = result.groupby("_year").size() +print(f"\n各年数据量:") +for yr, cnt in yearly.items(): + print(f" {yr}年: {cnt:,} 根") + +print(f"\n前 3 行:") +print(result[["time","open","high","low","close","tick_volume"]].head(3).to_string()) +print(f"\n后 3 行:") +print(result[["time","open","high","low","close","tick_volume"]].tail(3).to_string()) diff --git a/start_web.bat b/start_web.bat index 936a94a..8387c75 100644 --- a/start_web.bat +++ b/start_web.bat @@ -1,53 +1,38 @@ @echo off -chcp 65001 >nul -cd /d "%~dp0" - -REM Prefer: python -m pip ... (avoids pip-shim GBK issues on Chinese Windows) -set HOST=127.0.0.1 -set PORT=8765 - -REM Use system Python 3.13 (has all deps installed) -set PYTHON=C:\Program Files\Python313\python.exe +title AlphaMaster Web Server +cd /d "C:\Alpha_master" echo ============================================================ -echo AlphaMaster Web Console -echo http://%HOST%:%PORT% +echo AlphaMaster - Quant Factor Mining Center +echo Web Console Launcher echo ============================================================ echo. -echo [1/3] Releasing port %PORT% if occupied ... -for /f "tokens=5" %%P in ('netstat -ano ^| findstr ":%PORT% " ^| findstr LISTENING') do ( - echo taskkill PID %%P - taskkill /F /PID %%P >nul 2>&1 +REM Check if Python is available +where python >nul 2>&1 +if %errorlevel% neq 0 ( + echo [ERROR] Python is not found on PATH. + echo Please install Python 3.11+ and add it to your system PATH. + echo. + pause + exit /b 1 ) -timeout /t 1 /nobreak >nul -echo [2/3] Starting uvicorn ... -start "AlphaMaster Server" "%PYTHON%" run_web.py --host %HOST% --port %PORT% +echo [1/2] Starting web server on http://127.0.0.1:8765 ... +echo The browser will open automatically in 3 seconds. +echo. +echo Press Ctrl+C in this window to stop the server. +echo ============================================================ +echo. -echo [3/3] Warming up APIs (torch/fastapi first import can take ~6s) ... -set /a TRIES=0 -:wait_ready -set /a TRIES+=1 -if %TRIES% GTR 90 goto ready_fail -timeout /t 1 /nobreak >nul -"%PYTHON%" -c "import urllib.request; urllib.request.urlopen('http://%HOST%:%PORT%/api/overview', timeout=2).read(); urllib.request.urlopen('http://%HOST%:%PORT%/api/ai/providers', timeout=2).read(); urllib.request.urlopen('http://%HOST%:%PORT%/api/strategies', timeout=2).read()" 2>nul -if errorlevel 1 goto wait_ready +REM Open the browser after a 3-second delay (gives the server time to start) +start "" cmd /c "ping 127.0.0.1 -n 4 >nul 2>&1 & start http://127.0.0.1:8765" + +REM Start the web server (blocks until Ctrl+C) +python run_web.py --host 127.0.0.1 --port 8765 echo. -echo Ready. Opening browser ... -start "" "http://%HOST%:%PORT%/" -echo. -echo Server window title: AlphaMaster Server -echo Press any key to close this launcher (server keeps running)... -pause >nul -exit /b 0 - -:ready_fail -echo. -echo ERROR: server did not become ready within ~90s. -echo Check the "AlphaMaster Server" window for import/dependency errors. -echo Tip: python -m pip install -r requirements.txt -echo. +echo ============================================================ +echo Server has stopped. +echo ============================================================ pause -exit /b 1 diff --git a/web/data_sources/domestic_futures_source.py b/web/data_sources/domestic_futures_source.py index 226fd54..dc0ddcb 100644 --- a/web/data_sources/domestic_futures_source.py +++ b/web/data_sources/domestic_futures_source.py @@ -1,7 +1,7 @@ """国内期货数据源(tqsdk 天勤量化,实时行情)。 -通过 tqsdk 实时拉取国内期货主连 K 线,支持 60 个品种 × 4 个周期。 -账号配置在 web_settings.json 的 tqsdk_user / tqsdk_password 字段。 +通过 tqsdk 实时拉取国内期货主连 K 线,支持 60 个品种 × 5 个周期。 +账号优先从 web_settings.json 读取,未配置时使用内置默认账号。 特点: - 实时拉取最新 K 线(不是本地文件) @@ -20,6 +20,7 @@ from web.data_sources.base import Bar, DataSource, DataSourceUnavailable # 周期映射:CANON_TIMEFRAMES → tqsdk duration_seconds _TF_TO_SECONDS = { + "5m": 300, "15m": 900, "30m": 1800, "1h": 3600, @@ -61,17 +62,25 @@ _SYMBOL_MAP = { _PRESETS = list(_SYMBOL_MAP.keys()) +# 内置默认 tqsdk 账号(web_settings.json 未配置时使用) +_DEFAULT_TQSDK_USER = "七斗居士" +_DEFAULT_TQSDK_PASSWORD = "ghhkphs8" + # web_settings.json 路径 _SETTINGS_PATH = Path(__file__).resolve().parents[2] / "web_settings.json" def _load_credentials() -> tuple[str, str]: - """从 web_settings.json 读 tqsdk 账号密码。""" + """读取 tqsdk 账号密码。优先 web_settings.json,未配置则用内置默认账号。""" try: data = json.loads(_SETTINGS_PATH.read_text(encoding="utf-8")) - return str(data.get("tqsdk_user", "")), str(data.get("tqsdk_password", "")) + user = str(data.get("tqsdk_user", "")).strip() + pwd = str(data.get("tqsdk_password", "")).strip() + if user and pwd: + return user, pwd except Exception: - return "", "" + pass + return _DEFAULT_TQSDK_USER, _DEFAULT_TQSDK_PASSWORD class DomesticFuturesSource(DataSource): @@ -94,14 +103,10 @@ class DomesticFuturesSource(DataSource): return (False, "未安装 tqsdk:pip install tqsdk") user, pwd = _load_credentials() if not user or not pwd: - return ( - False, - "未配置 tqsdk 账号。请在 web_settings.json 设置 tqsdk_user / tqsdk_password" - "(信易账号,https://account.shinnytech.com 注册)", - ) + return (False, "tqsdk 账号配置异常") return ( True, - f"天勤量化实时行情 · 60 品种 · 4 周期(15m/30m/1h/1d)· 前复权", + f"天勤量化实时行情 · 60 品种 · 5 周期(5m/15m/30m/1h/1d)· 前复权", ) def supported_timeframes(self) -> list[str]: @@ -132,9 +137,7 @@ class DomesticFuturesSource(DataSource): user, pwd = _load_credentials() if not user or not pwd: - raise DataSourceUnavailable( - "未配置 tqsdk 账号。请在 web_settings.json 设置 tqsdk_user / tqsdk_password" - ) + raise DataSourceUnavailable("tqsdk 账号配置异常") duration = _TF_TO_SECONDS[timeframe] want = max(n + 5, 20) # 多拉几根防止 drop_forming 后不足 diff --git a/web/data_sources/factory.py b/web/data_sources/factory.py index b5b5a27..a266193 100644 --- a/web/data_sources/factory.py +++ b/web/data_sources/factory.py @@ -5,11 +5,10 @@ import threading from web.data_sources.base import DataSource -# 前端下拉可见的数据源(okx / tongdaxin 实现保留,暂不展示) +# 前端下拉可见的数据源(国内期货默认隐藏,如需启用加回列表即可) SOURCE_KINDS: tuple[tuple[str, str], ...] = ( ("mt5", "MT5"), ("tradingview", "TradingView"), - ("domestic_futures", "国内期货"), ) _INSTANCES: dict[str, DataSource] = {} diff --git a/web/settings.py b/web/settings.py index 741c28f..251c15c 100644 --- a/web/settings.py +++ b/web/settings.py @@ -24,8 +24,8 @@ _DEFAULT = { "feishu_webhook_url": "", "feishu_secret": "", # tqsdk 天勤量化账号(国内期货实时数据源) - "tqsdk_user": "", - "tqsdk_password": "", + "tqsdk_user": "七斗居士", + "tqsdk_password": "ghhkphs8", } diff --git a/web/static/app.js b/web/static/app.js index 89d64ec..bf91ec2 100644 --- a/web/static/app.js +++ b/web/static/app.js @@ -1942,6 +1942,8 @@ async function initRealtimeOnce() { sel.innerHTML = rtSources .map((s) => ``) .join(""); + // 默认选第一个可用数据源 + if (rtSources.length) sel.value = rtSources[0].id; } if (data.min_exposure != null && $("rtThresholdHint")) { $("rtThresholdHint").textContent = `无信号阈值 |tanh(因子)| < ${data.min_exposure}`; diff --git a/web/static/index.html b/web/static/index.html index 79bc3bc..18ea71a 100644 --- a/web/static/index.html +++ b/web/static/index.html @@ -567,6 +567,6 @@ - + diff --git a/web_settings.example.json b/web_settings.example.json index b47dd3c..c031849 100644 --- a/web_settings.example.json +++ b/web_settings.example.json @@ -10,6 +10,6 @@ "feishu_enabled": false, "feishu_webhook_url": "", "feishu_secret": "", - "tqsdk_user": "", - "tqsdk_password": "" + "tqsdk_user": "七斗居士", + "tqsdk_password": "ghhkphs8" }