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 @@
-
+