This commit is contained in:
qq564020069
2026-07-18 23:44:50 +08:00
parent 380a4f9268
commit 9c42ca0b39
12 changed files with 360 additions and 63 deletions
+43
View File
@@ -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)} 行")
+33
View File
@@ -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)
+14 -1
View File
@@ -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,
+95
View File
@@ -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下载完成!")
+124
View File
@@ -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())
+26 -41
View File
@@ -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
+17 -14
View File
@@ -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 后不足
+1 -2
View File
@@ -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] = {}
+2 -2
View File
@@ -24,8 +24,8 @@ _DEFAULT = {
"feishu_webhook_url": "",
"feishu_secret": "",
# tqsdk 天勤量化账号(国内期货实时数据源)
"tqsdk_user": "",
"tqsdk_password": "",
"tqsdk_user": "七斗居士",
"tqsdk_password": "ghhkphs8",
}
+2
View File
@@ -1942,6 +1942,8 @@ async function initRealtimeOnce() {
sel.innerHTML = rtSources
.map((s) => `<option value="${s.id}">${escHtml(s.label)}${s.available ? "" : " · 未就绪"}</option>`)
.join("");
// 默认选第一个可用数据源
if (rtSources.length) sel.value = rtSources[0].id;
}
if (data.min_exposure != null && $("rtThresholdHint")) {
$("rtThresholdHint").textContent = `无信号阈值 |tanh(因子)| < ${data.min_exposure}`;
+1 -1
View File
@@ -567,6 +567,6 @@
</div>
<script src="/static/bg.js?v=1"></script>
<script src="/static/app.js?v=60"></script>
<script src="/static/app.js?v=61"></script>
</body>
</html>
+2 -2
View File
@@ -10,6 +10,6 @@
"feishu_enabled": false,
"feishu_webhook_url": "",
"feishu_secret": "",
"tqsdk_user": "",
"tqsdk_password": ""
"tqsdk_user": "七斗居士",
"tqsdk_password": "ghhkphs8"
}