🔒

量化实验室 · 工程实战

输入专属密码解锁《Python 工业级开源量化系统全流程实操指南》

⚠️ 密码错误,请重新输入
root / stock / python-quant-stack
⚙️ 赛博炒股 · 工业级量化工程实战 2026-09-01 · 约 45 分钟阅读 · 完整源码与架构落地

Python 工业级开源量化系统全流程实战

从零搭建“DuckDB 本地数据底座 → Qlib AI 因子挖掘 → Backtrader 精细回测 → miniQMT 实盘全自动调仓”高可用生产闭环
🛠️ 核心使命:搭建一套 0 成本、超低延迟、解耦高可用的个人量化交易系统
本篇为 《AI 辅助 A 股交易完全指南》硬核工程实战篇
我们将彻底打通目前国内个人量化与中小型私募中最成熟、最高效的开源技术栈:利用 DuckDB 替代繁重笨重的数据库,通过 BaoStock / AkShare 增量同步 A 股全市场量价与杜邦财务指标;将数据编译为 Microsoft Qlib 二进制特征流训练 LightGBM 模型;将预测得分回写 DuckDB 预测库,驱动 Backtrader 进行事件驱动等权轮动回测;最终无缝对接券商官方 XtQuant (miniQMT) 配合 APScheduler 实现每个交易日 09:35 自动下单调仓。

1. 架构总览与技术选型清单

一套成熟的工业级量化系统必须解决“数据吞吐、特征算力、撮合仿真、实盘安全”四大核心矛盾。整体选型与分工如下:

工程环节 核心选型 职责与核心价值 费用情况
数据底座 DuckDB + BaoStock / AkShare 单文件列存时序数据库,支持百亿级切片毫秒级查询,增量 Upsert 保证幂等性 完全免费
因子挖掘与建模 Microsoft Qlib + LightGBM 内置 Alpha158/Alpha360,利用内存映射(mmap)与 GBDT 决策树进行横截面超额 Alpha 挖掘 完全开源免费
事件驱动精细回测 Backtrader + ExtendedPandasData 模拟真实 A 股环境(印花税、双边佣金、滑点、涨跌停不可撮合、等权 1/K 调仓) 完全开源免费
实盘执行与调度 XtQuant (miniQMT) + APScheduler 直连券商本地 QMT 客户端,100 股向下取整下单,交易日 09:35 定时执行 软件 0 元 (满足券商开户资金门槛)
监控与告警 飞书 / 微信 Webhook 机器人 异步监听成交回报,盘后发送收益归因报告,滑点与网络异常秒级告警 完全免费

2. 数据资产底座:DuckDB 本地数据库搭建与增量同步

为什么选用 DuckDB?金融时序数据具有“写少读多、按列聚合、大批量范围扫描”的典型特征。DuckDB 是嵌入式列式数据库,压缩比高达 3~5 倍,支持直接零拷贝导出为 Pandas / Arrow,性能比 SQLite 和原生 Pandas CSV 快 10~50 倍。

STEP 1 表结构设计与前视偏差(未来函数)防范

日线行情表 (daily_bars):复合主键为 (symbol, trade_date),严格存储前复权量价与估值指标;
财务指标表 (financial_metrics):复合主键为 (symbol, stat_date, pub_date),必须保留实际公告发布日期 (pub_date),杜绝在财报公布前调用数据的“前视偏差”。

🐍 DuckDB 数据表初始化与 A 股日线/财务增量同步完整脚本
Python 3.9+ · 88 lines
import baostock as bs
import duckdb
import pandas as pd
from datetime import datetime, timedelta

DB_PATH = "quant_data.duckdb"

def init_db(db_path: str = DB_PATH):
    """初始化 DuckDB 表结构与主键索引"""
    con = duckdb.connect(db_path)
    
    # 1. 创建前复权日线行情表
    con.execute("""
        CREATE TABLE IF NOT EXISTS daily_bars (
            symbol VARCHAR,
            trade_date DATE,
            open DOUBLE,
            high DOUBLE,
            low DOUBLE,
            close DOUBLE,
            volume BIGINT,
            amount DOUBLE,
            turnover_rate DOUBLE,
            pe_ttm DOUBLE,
            pb_mrq DOUBLE,
            PRIMARY KEY (symbol, trade_date)
        )
    """)
    
    # 2. 创建杜邦财务指标表 (含发布日期以防止前视偏差)
    con.execute("""
        CREATE TABLE IF NOT EXISTS financial_metrics (
            symbol VARCHAR,
            pub_date DATE,      -- 实际公告披露日 (严格用于回测时间对齐)
            stat_date DATE,     -- 财务报告期 (一季报/中报/三季报/年报)
            roe DOUBLE,
            net_profit_margin DOUBLE,
            asset_turnover DOUBLE,
            PRIMARY KEY (symbol, stat_date, pub_date)
        )
    """)
    con.close()
    print("DuckDB 数据库初始化完成。")

def get_latest_trade_date(con, symbol: str) -> str:
    """查询本地数据库中指定标的的最新交易日"""
    res = con.execute("SELECT MAX(trade_date) FROM daily_bars WHERE symbol = ?", [symbol]).fetchone()[0]
    if res is None:
        return "2020-01-01"  # 默认初始回溯起点
    next_date = res + timedelta(days=1)
    return next_date.strftime("%Y-%m-%d")

def fetch_and_update_daily_bars(symbols: list, db_path: str = DB_PATH):
    """增量同步全市场前复权日 K 线数据"""
    bs.login()
    con = duckdb.connect(db_path)
    today = datetime.today().strftime("%Y-%m-%d")
    fields = "date,code,open,high,low,close,volume,amount,turn,peTTM,pbMRQ"
    
    for symbol in symbols:
        start_date = get_latest_trade_date(con, symbol)
        if start_date > today:
            continue
            
        rs = bs.query_history_k_data_plus(
            symbol, fields, start_date=start_date, end_date=today,
            frequency="d", adjustflag="2"  # 2 表示前复权
        )
        
        data_list = []
        while (rs.error_code == '0') & rs.next():
            data_list.append(rs.get_row_data())
            
        if not data_list:
            continue
            
        df = pd.DataFrame(data_list, columns=rs.fields)
        df.rename(columns={
            'code': 'symbol', 'date': 'trade_date', 'turn': 'turnover_rate',
            'peTTM': 'pe_ttm', 'pbMRQ': 'pb_mrq'
        }, inplace=True)
        
        df.replace('', None, inplace=True)
        numeric_cols = ['open', 'high', 'low', 'close', 'volume', 'amount', 'turnover_rate', 'pe_ttm', 'pb_mrq']
        for col in numeric_cols:
            df[col] = pd.to_numeric(df[col], errors='coerce')
        df['trade_date'] = pd.to_datetime(df['trade_date']).dt.date

        # 使用 DuckDB 临时表批量 Upsert (插入或替换)
        con.register("df_temp", df)
        con.execute("INSERT OR REPLACE INTO daily_bars SELECT * FROM df_temp")
        con.unregister("df_temp")
        print(f"[{symbol}] 增量同步完成: {start_date} -> {today} ({len(df)} 条)")

    con.close()
    bs.logout()

3. 数据格式转换:DuckDB 直连 Backtrader 与 Qlib 二进制编译

Backtrader 是逐根 K 线的事件驱动引擎,需要内存 DataFrame;而 Qlib 是面向矩阵向量化运算的 AI 平台,需要紧凑的二进制 .bin 格式以支持底层 C++ 内存映射(mmap)秒级加载。

🐍 DuckDB 直连 Backtrader 与批量导出 Qlib CSV 完整源码
Python 3.9+ · 50 lines
import os
import duckdb
import backtrader as bt
import pandas as pd

# ================= 方案 A: DuckDB 直连 Backtrader (毫秒级注入) =================

class ExtendedPandasData(bt.feeds.PandasData):
    """扩展 Backtrader 数据源,支持 PE、PB 等多因子列"""
    lines = ('pe_ttm', 'pb_mrq', 'turnover_rate')
    params = (('pe_ttm', -1), ('pb_mrq', -1), ('turnover_rate', -1))

def load_feed_from_duckdb(db_path: str, symbol: str, start_date: str, end_date: str) -> ExtendedPandasData:
    con = duckdb.connect(db_path, read_only=True)
    query = """
        SELECT trade_date AS datetime, open, high, low, close, volume, pe_ttm, pb_mrq, turnover_rate
        FROM daily_bars WHERE symbol = ? AND trade_date BETWEEN ? AND ? ORDER BY trade_date ASC
    """
    df = con.execute(query, [symbol, start_date, end_date]).fetchdf()
    con.close()
    
    df['datetime'] = pd.to_datetime(df['datetime'])
    df.set_index('datetime', inplace=True)
    return ExtendedPandasData(dataname=df, name=symbol)

# ================= 方案 B: 批量导出并编译为 Qlib 二进制数据集 =================

def export_duckdb_to_qlib_csv(db_path: str, output_csv_dir: str):
    """利用 DuckDB 高性能分区导出为 Qlib 兼容的标准 CSV 目录"""
    os.makedirs(output_csv_dir, exist_ok=True)
    con = duckdb.connect(db_path, read_only=True)
    symbols = [row[0] for row in con.execute("SELECT DISTINCT symbol FROM daily_bars").fetchall()]
    
    print(f"开始导出 {len(symbols)} 只标的数据至 Qlib CSV 目录...")
    for sym in symbols:
        file_name = f"{sym.replace('.', '')}.csv"  # sh.600519 -> sh600519.csv
        file_path = os.path.join(output_csv_dir, file_name)
        query = f"""
            COPY (
                SELECT trade_date AS date, open, high, low, close, volume, turnover_rate, pe_ttm
                FROM daily_bars WHERE symbol = '{sym}' ORDER BY trade_date ASC
            ) TO '{file_path}' (HEADER, DELIMITER ',')
        """
        con.execute(query)
    con.close()
    print("Qlib CSV 导出完成。")

# 导出后在终端中执行编译(或在 Python 中调用 os.system):
# python -m qlib.dump_bin --csv_path ./qlib_csv_dump --qlib_dir ~/.qlib/qlib_data/cn_data_custom --include_fields open,high,low,close,volume,turnover_rate,pe_ttm --symbol_field_name symbol --date_field_name date

4. AI 模型训练与预测打分持久化 (Qlib + LightGBM)

在工业实践中,模型研究与交易执行必须彻底解耦。我们将训练完成的 Qlib 预测打分(Prediction Score)与每日横截面排名批量写回 DuckDB 的 qlib_predictions 表中,为下游回测与实盘提供极简 SQL 访问接口。

🐍 Qlib 模型预测打分提取与 DuckDB 批量 Upsert 持久化脚本
Python 3.9+ · 48 lines
import duckdb
import pandas as pd
import qlib
from qlib.constant import REG_CN
from qlib.utils import init_instance_by_config

DB_PATH = "quant_data.duckdb"

def init_prediction_table(db_path: str = DB_PATH):
    """初始化预测结果存储表"""
    con = duckdb.connect(db_path)
    con.execute("""
        CREATE TABLE IF NOT EXISTS qlib_predictions (
            model_name VARCHAR,
            trade_date DATE,
            symbol VARCHAR,
            score DOUBLE,
            score_rank BIGINT,      -- 当日横截面得分排名 (1 为最优)
            PRIMARY KEY (model_name, trade_date, symbol)
        )
    """)
    con.close()

def save_qlib_scores_to_duckdb(model, dataset, model_name: str, db_path: str = DB_PATH, segment: str = "test"):
    """执行模型推理并将结果 Upsert 写回 DuckDB"""
    pred_series = model.predict(dataset, segment=segment)
    pred_df = pred_series.to_frame(name="score") if isinstance(pred_series, pd.Series) else pred_series.copy()
    pred_df.columns = ["score"]
    
    pred_df.reset_index(inplace=True)
    pred_df.rename(columns={'datetime': 'trade_date', 'instrument': 'symbol'}, inplace=True)
    pred_df['trade_date'] = pd.to_datetime(pred_df['trade_date']).dt.date
    
    # 格式对齐: sh600519 -> sh.600519
    pred_df['symbol'] = pred_df['symbol'].apply(lambda x: f"{x[:2]}.{x[2:]}" if not '.' in x else x)
    
    # 计算当日横截面得分排名
    pred_df['score_rank'] = pred_df.groupby('trade_date')['score'].rank(ascending=False, method='min').astype(int)
    pred_df['model_name'] = model_name
    pred_df = pred_df[['model_name', 'trade_date', 'symbol', 'score', 'score_rank']]
    
    con = duckdb.connect(db_path)
    con.register("df_pred_temp", pred_df)
    con.execute("INSERT OR REPLACE INTO qlib_predictions SELECT * FROM df_pred_temp")
    con.unregister("df_pred_temp")
    con.close()
    print(f"[{model_name}] 写入完成: 共写入 {len(pred_df)} 条预测评分记录到 DuckDB。")

5. Backtrader Top-K 等权轮动精细回测

为了避免在回测主循环中频繁产生数据库 IO 瓶颈,我们在策略初始化阶段一次性预加载所有日期的 Top-K 打分,缓存在内存哈希表中。策略每个调仓周期(如每周一)卖出调出标的、等权买入新入选标的,并注入真实印花税与滑点。

🐍 Backtrader Top-K 等权轮动回测策略核心代码
Python 3.9+ · 52 lines
import backtrader as bt
import duckdb
import pandas as pd

DB_PATH = "quant_data.duckdb"

class QlibTopKRotationStrategy(bt.Strategy):
    params = (
        ('top_k', 5),                      # 目标持仓股票数量
        ('rebalance_weekday', 1),          # 每周几调仓 (1=周一; None 表示每日调仓)
        ('model_name', 'LGBM_Alpha158_v1'),# 对应 DuckDB 中的模型标签
        ('target_weight_ratio', 0.95),     # 总仓位控制 (保留 5% 现金防滑点)
        ('scores_dict', None),             # 预加载的打分哈希字典
    )

    def __init__(self):
        self.scores_dict = self.p.scores_dict or {}
        self.last_rebalance_date = None
        self.target_symbols = []

    def next(self):
        dt = self.data0.datetime.date(0)
        dt_str = dt.strftime("%Y-%m-%d")

        # 调仓周期控制 (避免同周重复触发)
        if self.p.rebalance_weekday is not None:
            if dt.weekday() != (self.p.rebalance_weekday - 1) and self.last_rebalance_date is not None:
                return
            if self.last_rebalance_date and (dt - self.last_rebalance_date).days < 4:
                return

        daily_top = self.scores_dict.get(dt_str, [])
        if not daily_top:
            return

        self.target_symbols = daily_top[:self.p.top_k]
        self.last_rebalance_date = dt
        target_weight = self.p.target_weight_ratio / len(self.target_symbols)

        # 1. 卖出已掉出 Top-K 的持仓标的
        for data in self.datas:
            symbol = data._name
            pos = self.getposition(data).size
            if pos > 0 and symbol not in self.target_symbols:
                self.order_target_percent(data, target=0.0)

        # 2. 买入或调整入选标的至目标等权
        for data in self.datas:
            symbol = data._name
            if symbol in self.target_symbols:
                self.order_target_percent(data, target=target_weight)

6. miniQMT (XtQuant) 实盘自动化下单与定时调仓引擎

实盘与回测最大的区别在于资金管理、100 股向下取整、A 股 T+1 可用股数控制以及涨跌停避险机制。脚本通过 APScheduler 在交易日 09:35 自动触发。

🐍 miniQMT (XtQuant) 实盘自动化下单与 APScheduler 定时调仓完整脚本
Python 3.9+ · 118 lines
import math
import time
from datetime import datetime
import duckdb
from apscheduler.schedulers.blocking import BlockingScheduler
from xtquant import xtconstant, xtdata
from xtquant.xttrader import XtQuantTrader, XtQuantTraderCallback
from xtquant.xttype import StockAccount

MINI_QMT_PATH = r"D:\国金证券QMT交易端\userdata_mini"  # miniQMT 本地安装路径
ACCOUNT_ID = "12345678"                             # 实盘资金账号
DB_PATH = "quant_data.duckdb"
MODEL_NAME = "LGBM_Alpha158_v1"
TOP_K = 5
TARGET_POSITION_RATIO = 0.95

class CustomTraderCallback(XtQuantTraderCallback):
    """订单与成交回报异步监听器"""
    def on_stock_order(self, order):
        print(f"[订单回报] {order.stock_code} | 状态: {order.order_status} | 报单: {order.order_volume} | 成交: {order.traded_volume}")

    def on_stock_trade(self, trade):
        print(f"[成交确认] {trade.stock_code} | 价格: {trade.traded_price:.2f} | 数量: {trade.traded_volume}")

def init_trader() -> tuple[XtQuantTrader, StockAccount]:
    """建立与本地 miniQMT 客户端的 IPC 通信通道"""
    session_id = int(time.time())
    xt_trader = XtQuantTrader(MINI_QMT_PATH, session_id)
    account = StockAccount(ACCOUNT_ID, 'STOCK')
    
    xt_trader.register_callback(CustomTraderCallback())
    xt_trader.start()
    if xt_trader.connect() != 0:
        raise ConnectionError("miniQMT 连接失败,请确保本地 QMT 客户端已处于登录状态!")
    
    xt_trader.subscribe(account)
    return xt_trader, account

def execute_daily_rebalance():
    """每日/每周定时调仓任务 (09:35 避开集合竞价剧烈跳空)"""
    today_str = datetime.today().strftime("%Y-%m-%d")
    print(f"\n[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 开始执行调仓任务...")

    xt_trader, account = init_trader()

    # 1. 从 DuckDB 读取当日截面 Top-K 标的
    con = duckdb.connect(DB_PATH, read_only=True)
    df_top = con.execute(
        "SELECT symbol FROM qlib_predictions WHERE model_name = ? AND trade_date = ? ORDER BY score_rank ASC LIMIT ?",
        [MODEL_NAME, today_str, TOP_K]
    ).fetchdf()
    con.close()

    if df_top.empty:
        print("未获取到当日预测信号,跳过本次调仓。")
        return

    # 代码格式转换: sh.600519 -> 600519.SH
    target_stocks = [f"{s.split('.')[1]}.{s.split('.')[0].upper()}" for s in df_top['symbol']]
    print(f"今日目标持仓 (Top-{TOP_K}): {target_stocks}")

    # 2. 查询当前资金与持仓
    asset = xt_trader.query_stock_asset(account)
    positions = xt_trader.query_stock_positions(account)
    current_pos_map = {p.stock_code: p for p in positions} if positions else {}

    # 3. 获取实时行情与涨跌停价
    all_needed_stocks = list(set(target_stocks + list(current_pos_map.keys())))
    full_ticks = xtdata.get_full_tick(all_needed_stocks)

    # ---------------- 步骤 A: 卖出调出持仓 ----------------
    for stock_code, pos in current_pos_map.items():
        if stock_code not in target_stocks and pos.can_use_volume > 0:
            tick = full_ticks.get(stock_code, {})
            if tick.get('lastPrice', 0.0) <= tick.get('downLimit', 0.0) and tick.get('downLimit', 0.0) > 0:
                print(f"[风控提示] {stock_code} 处于跌停状态,暂无法平仓。")
                continue
            
            xt_trader.order_stock_async(
                account, stock_code, xtconstant.STOCK_SELL,
                pos.can_use_volume, xtconstant.LATEST_PRICE, 0.0,
                "TopK_Rotation", "Rebalance_Sell"
            )
            print(f"发出卖单: {stock_code} | 数量: {pos.can_use_volume}")

    time.sleep(3)  # 等待卖单处理释放可用资金

    # ---------------- 步骤 B: 买入/配平目标持仓 ----------------
    asset_after = xt_trader.query_stock_asset(account)
    target_value_per_stock = (asset_after.total_asset * TARGET_POSITION_RATIO) / len(target_stocks)

    for stock_code in target_stocks:
        tick = full_ticks.get(stock_code, {})
        last_price = tick.get('lastPrice', 0.0)
        up_limit = tick.get('upLimit', 0.0)

        if last_price <= 0 or (last_price >= up_limit and up_limit > 0):
            print(f"[风控提示] {stock_code} 封死涨停或行情缺失,跳过买入。")
            continue

        # 按 100 股向下取整
        target_vol = math.floor((target_value_per_stock / last_price) / 100) * 100
        current_vol = current_pos_map[stock_code].volume if stock_code in current_pos_map else 0
        diff_vol = target_vol - current_vol

        if diff_vol >= 100:
            xt_trader.order_stock_async(
                account, stock_code, xtconstant.STOCK_BUY,
                int(diff_vol), xtconstant.LATEST_PRICE, 0.0,
                "TopK_Rotation", "Rebalance_Buy"
            )
            print(f"发出买单: {stock_code} | 增持: {diff_vol} 股")

if __name__ == "__main__":
    scheduler = BlockingScheduler(timezone="Asia/Shanghai")
    scheduler.add_job(execute_daily_rebalance, 'cron', day_of_week='mon-fri', hour=9, minute=35)
    print("实盘自动调仓引擎已就绪,等待交易日 09:35 触发...")
    scheduler.start()

7. 进阶生产化工具链与风控避坑

进阶扩展模块 推荐工具 生产化痛点解决方案
单因子快速检验 Alphalens-reloaded 在用 Qlib 训练大模型前,先检验单因子的 IC/IR 值、换手率衰减与 5 层收益单调性
全市场矩阵粗筛 VectorBT (vbt) Backtrader 逐根 K 线模拟 5000 只股票极其耗时,VectorBT 可在数秒内完成大样本参数寻优
专业绩效报表 Pyfolio-reloaded 生成包含夏普比率、卡玛比率、水下回撤图、VaR 风险价值的专业机构级评估报表
换手率缓冲平滑 自定义换手过滤器 已有持仓掉出 Top 15 时才卖出,新买入只选 Top 5,降低 50% 交易摩擦成本
⚠️ 实盘生产化四大铁律:
  • 避开开盘头 5 分钟(09:30~09:35):开盘瞬间部分股票未完成连续撮合,且价格跳空剧烈,调仓建议放在 09:35 之后。
  • 防重复下单锁:在异步发单前生成内存 UUID 锁,防止网络波动触发重试导致重单打满仓位。
  • 前置数据就绪检查:09:00 前通过自动化监控检查 DuckDB 数据库完整性与当日 Qlib 预测得分是否正常写入。