QMT 量化策略开发

Python 量化交易
学习手册

只学写 QMT 策略真正用得上的 Python,零基础起步,学完就能看懂并修改你的量化代码。

基于迅投 QMT 实盘策略代码提取 · 2026 年 7 月

本书覆盖的 Python 知识点

  • 变量、数字运算、布尔判断
  • 字符串操作(提取、拼接、格式化)
  • 列表与字典(QMT 策略中使用频率最高的数据结构)
  • if / elif / else 条件判断
  • for 循环、列表推导式、lambda 表达式
  • 函数定义与调用
  • try / except 异常处理
  • datetime 日期时间处理
  • import 模块导入、hasattr 动态属性检查
  • QMT 策略代码逐行精读
Chapter 01

开始写 Python

Python 是什么、怎么运行、最基本的代码长什么样

1.1 Python 是什么

Python 是一种编程语言,特点是语法简洁、接近人类语言。在量化交易领域,它是 QMT 策略编写的唯一语言。

你不需要成为 Python 专家。QMT 策略用到的 Python 只是它的一个很小的子集——变量、列表、字典、if 判断、for 循环、函数定义,掌握了这些就足够读懂和修改策略代码。

1.2 如何运行 Python 代码

QMT 内置了 Python 环境,你不需要单独安装。在 QMT 的"模型研究"或"策略编写"界面中,直接写代码、点击运行即可。

如果你想在 QMT 之外练习,可以使用终端:

终端
python

这会进入交互模式,输入一行代码按回车就立即看到结果,按 Ctrl+Z 退出。

1.3 第一行代码

Python 最基本的操作就是 print——在屏幕上显示文字:

Python
print("你好,QMT!")
print("策略开始运行")
print("=" * 80)
你好,QMT! 策略开始运行 ================================================================================

你的策略代码里大量使用了 print 来输出调试信息。比如 print("=" * 80) 用来输出一条分隔线,这在查看日志时很常用。

1.4 注释

注释是给人看的说明文字,Python 不会执行它:

Python
# 这是单行注释,# 后面的内容不会被执行

"""
这是多行注释(文档字符串)
可以写多行说明
常用于函数开头的功能描述
"""

你的策略代码中 # ============================================================================ 就是用来分隔不同功能区块的注释。

练习

在 QMT 的策略编辑器中输入 print("我的第一个Python程序") 并运行,观察输出结果。

Chapter 02

变量与数据类型

存数据的盒子:数字、字符串、布尔值,以及它们之间的运算

2.1 什么是变量

变量就是一个名字,指向一个。你可以把它想象成一个贴了标签的盒子。

Python
# 把 100 赋值给变量 stock_num
stock_num = 100

# 把文字赋值给变量 name
name = "贵州茅台"

# 使用变量
print(name, stock_num)
贵州茅台 100

Python 中不需要声明变量类型,直接赋值就行。变量名可以用英文、数字、下划线,但不能以数字开头。

QMT 中的变量

你的策略代码中,Context 就是 QMT 框架提供的一个全局变量(对象),里面存了账户ID、参数配置等所有信息。Context.stock_num = 10 就是往这个"盒子"里放一个值。

2.2 数字类型与运算

Python 有两种数字:整数int)和浮点数float,带小数点)。

Python
# 整数
a = 10
b = 3

# 浮点数
price = 25.68
weight = 1.0

# 四则运算
print(a + b)    # 13    加法
print(a - b)    # 7     减法
print(a * b)    # 30    乘法
print(a / b)    # 3.333...  除法(结果永远是浮点数)
print(a // b)   # 3     整除(只保留整数部分)
print(a % b)    # 1     取余数

# 其他常用运算
print(abs(-5))        # 5       绝对值
print(round(3.1416, 2))  # 3.14  四舍五入
print(max(10, 20, 5))    # 20    最大值
print(min(10, 20, 5))    # 5     最小值

对应策略代码

round(pre_close * 1.20, 2) — 计算涨停价,保留两位小数

abs(current_price - high_limit) <= 0.01 — 判断价格偏差是否在 0.01 以内

max(tr1, tr2, tr3) — 取三个值中的最大值,用于计算 ATR

2.3 比较运算

比较运算的结果是 TrueFalse(布尔值):

运算符含义示例结果
==等于10 == 10True
!=不等于10 != 5True
>大于10 > 5True
<小于10 < 5False
>=大于等于10 >= 10True
<=小于等于5 <= 3False

2.4 逻辑运算

andornot 组合多个条件:

Python
# and:两个条件都为 True 才是 True
price = 15.5
pre_close = 14.0
print(price > 0 and pre_close > 0)   # True

# or:只要有一个为 True 就是 True
code = "300001.SZ"
print(code.startswith("68") or code.startswith("30"))  # True

# not:取反
is_st = False
print(not is_st)   # True

对应策略代码

if pre_close <= 0 or current_price <= 0: — 任一条件成立则返回 False

if code_part.startswith("68") or code_part.startswith("30"): — 判断是否为科创板或创业板

2.5 字符串基础

字符串是用引号包裹的文字。单引号、双引号都可以:

Python
stock = "000001.SZ"
name = '平安银行'

# 字符串拼接
full = stock + " " + name
print(full)   # 000001.SZ 平安银行

# 字符串重复(策略中的分隔线就是这么做的)
print("=" * 40)   # ========================================

# f-string:在字符串中嵌入变量(Python 3.6+)
price = 15.68
print(f"{stock} 当前价格: {price}")   # 000001.SZ 当前价格: 15.68
print(f"{stock} 涨停价: {price * 1.1:.2f}")  # 000001.SZ 涨停价: 17.25

重点掌握 f-string

你的策略代码中大量使用了 f-string 来输出调试信息,比如 f"获取{stock}最新价时出错:{str(e)}"{} 里面可以放任何表达式,:.2f 表示保留两位小数。

练习

用 f-string 输出:假设股票代码 "600519.SH",昨收 1800.50,计算并输出涨停价(主板 10%)和跌停价,保留两位小数。

Chapter 03

字符串操作

处理股票代码、股票名称、日期字符串的必备技巧

3.1 字符串方法速查

字符串自带很多实用的"方法"(功能),用 字符串.方法名() 的格式调用:

方法作用示例结果
.split(分隔符)按分隔符拆分为列表"300001.SZ".split(".")['300001', 'SZ']
.startswith(前缀)是否以某字符串开头"300001".startswith("30")True
.endswith(后缀)是否以某字符串结尾"file.py".endswith(".py")True
.replace(旧, 新)替换"a,b".replace(",", "-")"a-b"
.upper()转大写"abc".upper()"ABC"
.lower()转小写"ABC".lower()"abc"
.strip()去掉两端空白" hi ".strip()"hi"
"x" in 字符串是否包含"ST" in "*ST股票"True
len(字符串)长度len("600519")6
str(数字)数字转字符串str(3.14)"3.14"
float(字符串)字符串转浮点数float("15.68")15.68
int(字符串)字符串转整数int("100")100

3.2 QMT 中的典型应用

你的策略代码中,字符串操作用得最多的是处理股票代码

来自你的策略
# 股票代码格式:"300001.SZ" 或 "600519.SH"
stock = "300001.SZ"

# 提取纯数字部分
code_part = stock.split('.')[0]    # "300001"

# 判断板块
if code_part.startswith("68"):     # 科创板
    print("科创板,涨跌幅20%")
elif code_part.startswith("30"):   # 创业板
    print("创业板,涨跌幅20%")
elif code_part.startswith("4") or code_part.startswith("8"):  # 北交所
    print("北交所,过滤掉")
else:                               # 主板
    print("主板,涨跌幅10%")

# 检查股票名称中是否包含 ST
stock_name = "*ST大控"
if "ST" in stock_name or "退" in stock_name:
    print("ST或退市股,过滤掉")

3.3 字符串切片

[起始:结束] 提取子字符串(左闭右开——包含起始位置,不包含结束位置):

Python
date_str = "20260718"

year = date_str[0:4]      # "2026"
month = date_str[4:6]     # "07"
day = date_str[6:8]       # "18"

# 负数索引:从末尾开始
code = "600519.SH"
exchange = code[-2:]      # "SH"  (最后两个字符)
code_only = code[:-3]     # "600519" (去掉最后三个字符)

常见陷阱

stock.split('.')[0]stock[:-3] 在你的代码中等价,但 split 更安全——它不依赖代码长度,而 [:-3] 假设后缀一定是 3 个字符。优先使用 split

练习

给定 stock = "688981.SH",请写出判断逻辑:如果是科创板(68开头)输出"科创板20%涨跌幅",如果是北交所(4或8开头)输出"过滤",否则输出"主板10%涨跌幅"。

Chapter 04

列表

QMT 策略中使用频率最高的数据结构,用来存放一批数据

4.1 什么是列表

列表就是一组有序的数据,用方括号 [] 包裹,逗号分隔:

Python
# 创建列表
stocks = ["600519.SH", "000858.SZ", "300750.SZ"]
numbers = [10, 20, 30, 40, 50]
mixed = ["茅台", 1800.5, True]   # 可以混合类型

# 通过索引访问(从 0 开始)
print(stocks[0])    # "600519.SH"  第一个
print(stocks[-1])   # "300750.SZ"  最后一个

# 获取列表长度
print(len(stocks))  # 3

4.2 修改列表

Python
holdings = ["600519.SH", "000858.SZ", "300750.SZ"]

# 添加元素
holdings.append("002475.SZ")       # 末尾添加
# holdings: ["600519.SH", "000858.SZ", "300750.SZ", "002475.SZ"]

# 删除元素
holdings.remove("000858.SZ")       # 按值删除(只删第一个匹配的)
holdings.pop(0)                    # 按索引删除并返回
# holdings: ["300750.SZ", "002475.SZ"]

# 检查是否包含
if "600519.SH" in holdings:
    print("已持有茅台")

4.3 列表排序

排序是选股策略中的核心操作。你的策略代码中多次用到:

Python
# 按价格升序排序(小到大)
stock_prices = [("600519.SH", 1800.5), ("000001.SZ", 15.68), ("300750.SZ", 220.0)]
stock_prices.sort(key=lambda x: x[1])
# 结果: [("000001.SZ", 15.68), ("300750.SZ", 220.0), ("600519.SH", 1800.5)]

# 按市值升序(小到大)
stock_cap_list = [("A", 50e8), ("B", 20e8), ("C", 100e8)]
stock_cap_list.sort(key=lambda x: x[1], reverse=False)
# 结果: [("B", 20e8), ("A", 50e8), ("C", 100e8)]

# reverse=True 则降序(大到小)
stock_cap_list.sort(key=lambda x: x[1], reverse=True)

对应策略代码

stock_prices.sort(key=lambda x: x[1]) — 按股价升序排列,取最低的 10%

stock_cap_list.sort(key=lambda x: x[1], reverse=False) — 按市值从小到大排序

4.4 列表切片

Python
nums = [10, 20, 30, 40, 50, 60, 70]

nums[0:3]     # [10, 20, 30]   前三个
nums[-3:]     # [50, 60, 70]   最后三个
nums[2:5]     # [30, 40, 50]   中间一段
nums[:3]      # [10, 20, 30]   从头到第3个
nums[3:]      # [40, 50, 60, 70]  第4个到末尾

对应策略代码

tr_values[-period:] — 取最后 period 个 TR 值计算 ATR

low_price_stocks = [stock for stock, _ in stock_prices[:low_price_count]] — 取前 N 个低价股

4.5 列表推导式

从已有列表快速创建新列表的简写方式:

Python
# 基本语法:[表达式 for 变量 in 列表 if 条件]

stocks = ["600519.SH", "000858.SZ", "300750.SZ", "002475.SZ"]

# 筛选创业板股票(30开头)
kcb = [s for s in stocks if s.startswith("30")]
# ["300750.SZ"]

# 从元组列表中提取代码
pairs = [("600519.SH", 1800.5), ("000001.SZ", 15.68)]
codes = [code for code, price in pairs]
# ["600519.SH", "000001.SZ"]

# 筛选增长率大于0的股票
positive = [stock for stock, growth in revenue_growth_dict.items() if growth > 0]

什么时候用列表推导式

当你需要"从一组数据中筛选/转换出新的列表"时,列表推导式比写 for 循环 + append 更简洁。你的策略代码中 [stock for stock, _ in stock_prices[:low_price_count]] 就是一个典型例子——从 (股票, 价格) 列表中只提取股票代码。

4.6 列表转其他类型

Python
# 列表转字典
pairs = [("A", 100), ("B", 200)]
d = dict(pairs)
# d = {"A": 100, "B": 200}

# 列表转字符串
codes = ["600519", "000858", "300750"]
result = ",".join(codes)
# "600519,000858,300750"
练习

给定 data = [("A", 50), ("B", 20), ("C", 80), ("D", 10)],请:(1) 按数值升序排序;(2) 取数值最小的前 3 个;(3) 用列表推导式只提取字母部分。

Chapter 05

字典

键值对存储,QMT 中用于存放持仓、市值、行业分类等数据

5.1 什么是字典

字典是键值对的集合,用花括号 {} 表示。每个键(key)对应一个值(value):

Python
# 创建字典
target_assets = {
    "600519.SH": 0.1,
    "000858.SZ": 0.1,
    "300750.SZ": 0.1
}

# 访问值
print(target_assets["600519.SH"])   # 0.1

# 添加/修改
target_assets["002475.SZ"] = 0.1    # 添加新键值对
target_assets["600519.SH"] = 0.2    # 修改已有值

# 删除
del target_assets["000858.SZ"]

# 安全访问(键不存在时不报错,返回默认值)
price = target_assets.get("999999.SH", 0)   # 0(键不存在,返回默认值)

对应策略代码

target_assets[stock] = weight — 设置目标持仓权重

if stock not in target_assets: — 检查股票是否在目标中

revenue_growth_dict[stock] = revenue_growth — 存储营收增长率

5.2 遍历字典

Python
holdings = {"600519.SH": 5, "000858.SZ": 10, "300750.SZ": 3}

# 遍历键和值
for stock, hand in holdings.items():
    print(f"{stock}: {hand}手")
# 输出:
# 600519.SH: 5手
# 000858.SZ: 10手
# 300750.SZ: 3手

# 只遍历键
for stock in holdings.keys():
    print(stock)

# 只遍历值
for hand in holdings.values():
    print(hand)

# 检查键是否存在
if "600519.SH" in holdings:
    print("持有茅台")

# 获取字典长度
print(len(holdings))   # 3

5.3 字典的嵌套取值

你的策略代码中经常遇到"字典套字典"的结构:

模拟 QMT 返回数据
# QMT 的 get_market_data_ex 返回的数据结构类似这样:
hist_data = {
    "600519.SH": {
        "close": [1800.0, 1815.5, 1830.2],
        "high":  [1820.0, 1830.0, 1845.0],
        "low":   [1790.0, 1800.0, 1815.0]
    }
}

# 取某只股票的收盘价数据
close_data = hist_data["600519.SH"]["close"]
# [1800.0, 1815.5, 1830.2]

# 判断是否有 tolist 方法(QMT 返回的可能是 numpy 数组)
if hasattr(close_data, 'tolist'):
    close_list = close_data.tolist()
else:
    close_list = close_data

5.4 字典推导式

Python
# 从一个字典筛选出满足条件的键值对
all_growth = {"A": 5.2, "B": -3.1, "C": 8.7, "D": -1.0}

positive = {k: v for k, v in all_growth.items() if v > 0}
# {"A": 5.2, "C": 8.7}

5.5 字典计数模式

你的策略中"行业分散"用到了典型的字典计数逻辑:

来自你的策略 — 行业分散
count_dict = {}       # 行业 -> 已选数量
final_stocks = []      # 最终选中的股票

for stock in stock_list:
    industry = Context.code2industry_dict[stock]   # 查字典获取行业

    if industry not in count_dict:
        count_dict[industry] = 1       # 第一次出现
        final_stocks.append(stock)
    elif count_dict[industry] < 3:     # 每行业最多3只
        count_dict[industry] += 1
        final_stocks.append(stock)
    # 超过3只的跳过
练习

创建一个字典 positions = {"600519.SH": 5, "000858.SZ": 0, "300750.SZ": 3},遍历并卖出(删除)持仓为 0 的股票,打印最终持仓。

Chapter 06

条件判断与循环

让程序做判断和重复操作,这是策略逻辑的核心

6.1 if / elif / else

条件判断让程序根据不同情况执行不同代码。你的策略中"风格判断规则矩阵"就是一个经典的多分支判断:

来自你的策略 — 简化版
# 风格判断规则矩阵
if small_ret >= -4:
    print("小盘偏强 → 执行小市值选股")
    target_stocks = get_target_stocks(ContextInfo, current_date)

elif small_ret < -4 and big_ret > 0:
    print("大盘偏强 → 红利ETF+银行ETF")
    # 配置防御性资产

elif small_ret < -4 and big_ret < 0:
    print("全市场弱 → 黄金ETF+国债ETF")
    # 配置避险资产

else:
    print("边界情况,不调仓")
    return

if 判断要点

冒号不能少缩进必须一致(4个空格),elif 可以有多个,else 最多一个。Python 用缩进来表示代码块,这是它和其他语言最大的区别。

6.2 for 循环

for 循环用来遍历列表、字典等可迭代对象:

Python
# 遍历列表
stocks = ["600519.SH", "000858.SZ", "300750.SZ"]
for stock in stocks:
    print(f"处理 {stock}")

# 遍历字典
holdings = {"600519.SH": 5, "000858.SZ": 10}
for stock, hand in holdings.items():
    print(f"{stock} 持有 {hand} 手")

# 带索引的遍历
for i, stock in enumerate(stocks):
    print(f"第{i+1}只: {stock}")

# range 生成数字序列
for i in range(5):       # 0, 1, 2, 3, 4
    print(i)
for i in range(1, 6):    # 1, 2, 3, 4, 5
    print(i)

6.3 循环中的控制

来自你的策略 — 过滤股票
filtered = []
for stock in stock_list:
    try:
        # 停牌过滤
        if ContextInfo.is_suspended_stock(stock):
            continue    # 跳过这只,继续下一只

        # ST过滤
        stock_name = ContextInfo.get_stock_name(stock)
        if "ST" in stock_name or "退" in stock_name:
            continue    # 跳过

        # 通过所有过滤条件
        filtered.append(stock)

    except Exception as e:
        print(f"过滤{stock}时出错:{str(e)}")
        continue        # 出错也跳过,不中断整个循环
关键字作用
continue跳过本次循环,直接进入下一次
break立即退出整个循环
return退出整个函数

6.4 lambda 表达式

lambda 是一种"匿名函数"的简写,主要配合 sort 使用:

Python
# lambda 参数: 返回值
# 等价于:def func(x): return x[1]

data = [("A", 50), ("B", 20), ("C", 80)]

# 按第二个元素升序排列
data.sort(key=lambda x: x[1])
# [("B", 20), ("A", 50), ("C", 80)]

# 按第二个元素降序排列
data.sort(key=lambda x: x[1], reverse=True)
# [("C", 80), ("A", 50), ("B", 20)]

不需要深入理解 lambda

在你的策略代码中,lambda 只出现在 .sort(key=lambda x: x[1]) 这一种场景。你只需要记住:当需要对列表中的元组/对象按某个字段排序时,用 lambda x: x[字段位置或属性名] 即可。

练习

模拟你的策略中的涨停价判断逻辑:给定 stocks = [("300001.SZ", 15.0, 13.6), ("600519.SH", 1800.0, 1700.0), ("688001.SH", 50.0, 42.0)](代码、当前价、昨收),用 for 循环判断每只股票是否涨停(主板10%、创业板/科创板20%)。

Chapter 07

函数

把代码封装成可复用的模块,让你的策略结构清晰

7.1 定义和调用函数

Python
# 定义函数:def 函数名(参数):
def calculate_high_limit(pre_close, code_part):
    """计算涨停价"""
    if pre_close <= 0:
        return 0

    if code_part.startswith("68") or code_part.startswith("30"):
        return round(pre_close * 1.20, 2)   # 20%涨跌幅
    else:
        return round(pre_close * 1.10, 2)   # 10%涨跌幅

# 调用函数
price1 = calculate_high_limit(14.0, "300001")   # 16.8
price2 = calculate_high_limit(1700.0, "600519") # 1870.0

7.2 函数的返回值

return 语句将结果返回给调用者。没有 return 的函数默认返回 None

Python
# 返回单个值
def get_pre_close(stock):
    # ... 获取前收盘价的逻辑
    return 15.68

# 返回多个值(实际上是返回元组)
def get_style_returns(ContextInfo, current_date):
    big_ret = 3.5
    small_ret = -5.2
    return (big_ret, small_ret)

# 调用时用多个变量接收
big_ret, small_ret = get_style_returns(ContextInfo, current_date)

# 不返回值,只执行操作
def check_limit_up(ContextInfo):
    # 检查涨停状态,只打印信息
    print("涨停炸板检查...")
    # 没有 return,默认返回 None

7.3 函数参数详解

Python
# 必需参数:调用时必须传入
def filter_st(stock_list, ContextInfo):
    ...

# 带默认值的参数:调用时可以不传,使用默认值
def calc_atr(stock, ContextInfo, period=20):
    ...

# 调用
calc_atr("600519.SH", ContextInfo)           # period 使用默认值 20
calc_atr("600519.SH", ContextInfo, period=10) # period 使用传入值 10

7.4 你的策略中的函数结构

你的策略代码组织得非常好,每个函数负责一个明确的功能:

函数名功能调用时机
handlebar(ContextInfo)核心执行函数,每根K线调用QMT 自动调用
get_current_holdings()获取当前持仓调仓时
get_available_cash()获取可用资金调仓时
calculate_high_limit()计算涨停价风控检查时
is_limit_up()判断是否涨停风控检查时
get_style_returns()风格判断每周调仓时
get_target_stocks()小市值选股每周调仓时
weekly_adjustment()调仓主逻辑每周第一个交易日
check_limit_up()涨停炸板检查每日
check_atr_stop_loss()ATR 止损检查每日

写函数的原则

每个函数只做一件事,函数名用动词开头(get_check_filter_calculate_),这样代码读起来就像自然语言。你的策略代码在这方面做得很好,值得保持。

7.5 嵌套函数

你的策略中 get_style_returns 里面定义了一个内部函数 calc_index_return

来自你的策略
def get_style_returns(ContextInfo, current_date):
    """风格判断:计算大盘和小盘的N日涨幅"""

    # 内部函数:只在 get_style_returns 内部使用
    def calc_index_return(index_code):
        try:
            hist_data = ContextInfo.get_market_data_ex(...)
            # ... 计算逻辑
            return 涨幅
        except:
            return 0.0

    # 调用内部函数
    big_ret = calc_index_return("000300.SH")    # 沪深300
    small_ret = calc_index_return("399101.SZ")  # 中小板综
    return (big_ret, small_ret)

内部函数的好处是:它只在父函数内部有意义,不会"污染"全局命名空间。当一段逻辑只在某个函数内使用时,就可以定义为内部函数。

练习

写一个函数 filter_by_price(stock_list, max_price),接收股票列表和最高价格,返回价格低于 max_price 的股票列表。

Chapter 08

异常处理与日期时间

让程序出错时不崩溃,以及处理 QMT 中大量出现的日期字符串

8.1 try / except

你的策略代码中几乎每个与 QMT API 交互的地方都用了 try / except。这是因为获取行情数据、财务数据等操作随时可能失败(网络问题、数据缺失等),你不能让一个错误导致整个策略崩溃。

Python
# 基本结构
try:
    # 可能出错的代码
    result = 10 / 0
except Exception as e:
    # 出错时执行
    print(f"出错了:{str(e)}")
# 输出:出错了:division by zero
来自你的策略 — 典型模式
def get_last_price(ContextInfo, stock):
    """获取最新价"""
    try:
        tick_data = ContextInfo.get_full_tick([stock])
        if tick_data and tick_data[stock] and tick_data[stock]["lastPrice"] > 0:
            return tick_data[stock]["lastPrice"]
    except Exception as e:
        print(f"获取{stock}最新价时出错:{str(e)}")

    return 0   # 出错时返回默认值

策略中的异常处理模式

记住这个模式:try 里放可能失败的操作,except 里打印错误信息,最后返回一个安全的默认值。这样即使某只股票的数据获取失败,也不会影响其他股票的处理。

8.2 裸 except(不推荐但常见)

你的策略代码中有一些地方用了 except:(不指定异常类型):

来自你的策略
try:
    detail = ContextInfo.get_instrument_detail(stock)
    # ... 处理逻辑
except:
    pass    # 出错时什么都不做,直接跳过

关于 except: pass

这种写法会吞掉所有错误(包括键盘中断等),在正式项目中不推荐。但在 QMT 策略中,对于非关键的辅助检查(如次新股过滤),用它来"出错就跳过"是可以接受的。关键路径(下单、获取持仓)建议用 except Exception as e 并打印日志。

8.3 datetime 日期时间

QMT 策略中大量涉及日期处理。Python 内置的 datetime 模块是你唯一需要掌握的日期工具:

Python
import datetime

# 字符串 → 日期对象
date_str = "20260718"
date_obj = datetime.datetime.strptime(date_str, "%Y%m%d")
# datetime.datetime(2026, 7, 18, 0, 0)

# 日期对象 → 字符串
formatted = date_obj.strftime("%Y%m%d")
# "20260718"

# 日期计算
from datetime import datetime, timedelta
today = datetime(2026, 7, 18)
yesterday = today - timedelta(days=1)     # 2026-07-17
one_year_ago = today - timedelta(days=365)  # 2025-07-19

# 计算两个日期之间的天数差
open_date = datetime(2025, 3, 15)
days_since_listing = (today - open_date).days   # 490

# 获取星期几(0=周一,6=周日)
weekday = today.weekday()    # 5(周六)
is_monday = (weekday == 0)   # False

# 获取周数
week_num = today.isocalendar()[1]   # 29(第29周)

对应策略代码

datetime.datetime.strptime(current_date_str, "%Y%m%d") — 把 QMT 的日期字符串转为日期对象

(current_date_obj - prev_date_obj).days > 3 — 判断是否跨周

current_date_obj.weekday() == 0 — 判断是否周一

current_date_obj.isocalendar()[1] — 获取当前是第几周

days_diff = (current_dt - open_date).days — 计算上市天数,过滤次新股

8.4 日期格式符号

符号含义示例
%Y四位年份2026
%m两位月份07
%d两位日期18
%H小时(24小时制)14
%M分钟30
%S45

在 QMT 中,最常见的日期格式就是 "%Y%m%d"(如 "20260718"),记住这一个就够用了。

8.5 import 导入模块

Python
# 导入整个模块,使用时需要写模块名
import datetime
datetime.datetime.strptime(...)

# 导入模块中的特定内容,使用时不需要写模块名
from datetime import datetime, timedelta
datetime.strptime(...)      # 不需要写 datetime.datetime
timedelta(days=1)           # 直接使用

# 导入并取别名(numpy 常用)
import numpy as np
np.mean([1, 2, 3])   # 2.0

8.6 hasattr 动态属性检查

你的策略代码中频繁使用 hasattr 来检查对象是否有某个属性。这是因为 QMT 不同版本返回的数据结构可能不同:

来自你的策略
# 检查对象是否有某个属性
if hasattr(pos, 'm_strInstrumentID') and hasattr(pos, 'm_strExchangeID'):
    stock = f"{pos.m_strInstrumentID}.{pos.m_strExchangeID}"
elif hasattr(pos, 'code'):
    stock = pos.code

# 检查 numpy 数组是否有 tolist 方法
if hasattr(close_data, 'tolist'):
    close_list = close_data.tolist()
else:
    close_list = close_data

hasattr 的作用

hasattr(对象, "属性名") 返回 True/False。在 QMT 开发中,API 返回的数据格式可能因版本或数据源而异,用 hasattr 做兼容性检查是一种常见做法。你不需要理解它底层原理,只需要知道这个模式可以安全地检查"这个对象有没有某个属性"。

练习

给定上市日期 "20250315" 和当前日期 "20260718",计算上市天数,判断是否超过 365 天(是则"可交易",否则"次新股,过滤")。

Chapter 09

QMT 策略代码精读

用前 8 章学到的知识,逐段读懂你的风格轮动小市值策略

9.1 策略全局变量(初始化)

策略代码解读
# === 这些都是全局变量,在策略启动时执行一次 ===

# Context 是 QMT 框架提供的全局对象
Context.accountID = "你的资金账号"
Context.stock_num = 10           # 持仓股票数量
Context.style_lookback_days = 20 # 风格判断回看天数
Context.small_ret_threshold = -4 # 小盘涨幅阈值
Context.atr_period = 20          # ATR 周期
Context.atr_multiple = 2         # ATR 倍数
Context.enable_atr_stop_loss = True  # 是否启用ATR止损
Context.max_industry_count = 3   # 单行业最大持仓数

# 运行时状态变量
Context.last_check_date = ""     # 上次检查日期
Context.last_week = 0            # 上次周数
Context.hold_list = []           # 当前持仓列表
Context.high_limit_list = []     # 昨日涨停列表

# 防御性资产配置(字典 + 列表)
Context.defensive_assets = {
    "大盘偏强": [
        {"code": "510050.SH", "weight": 0.4},  # 上证50ETF
        {"code": "510880.SH", "weight": 0.4},  # 红利ETF
        {"code": "600900.SH", "weight": 0.2},  # 长江电力
    ],
    "全市场弱": [
        {"code": "518880.SH", "weight": 0.5},  # 黄金ETF
        {"code": "511010.SH", "weight": 0.5},  # 国债ETF
    ]
}

# 行业分类字典(股票代码 → 行业名称),数据量很大
Context.code2industry_dict = {
    '000001.SZ': 'SW1银行',
    '000002.SZ': 'SW1房地产',
    # ... 几千条 ...
}

这段代码用到了:变量赋值(字符串、数字、布尔值)、列表字典(嵌套字典、字典值是列表)。

9.2 核心执行函数 handlebar

策略代码解读
def handlebar(ContextInfo):
    """核心执行函数,每根K线执行一次"""

    # 获取当前K线位置
    d = ContextInfo.barpos
    if d < Context.style_lookback_days + 5:  # 数据不足,跳过
        return

    # 获取当前日期 → 字符串操作
    current_timetag = ContextInfo.get_bar_timetag(d)
    current_date_str = timetag_to_datetime(current_timetag, "%Y%m%d")

    # 判断是否新交易日 → 字符串比较
    is_new_day = current_date_str != Context.last_check_date
    if not is_new_day:
        return

    Context.last_check_date = current_date_str

    # 判断是否新的一周 → datetime 日期计算
    if prev_date_str:
        current_date_obj = datetime.datetime.strptime(current_date_str, "%Y%m%d")
        prev_date_obj = datetime.datetime.strptime(prev_date_str, "%Y%m%d")

        is_new_week = (current_date_obj.weekday() == 0) or \
                      (current_date_obj - prev_date_obj).days > 3

        current_week = current_date_obj.isocalendar()[1]
        if current_week != Context.last_week:
            is_new_week = True
            Context.last_week = current_week

    # 每日执行:风控
    check_limit_up(ContextInfo)       # 涨停炸板检查
    check_atr_stop_loss(ContextInfo)  # ATR止损检查

    # 每周执行:调仓
    if is_new_week:
        weekly_adjustment(ContextInfo)

这段代码用到了:函数定义和调用if 条件判断比较运算datetime 日期处理字符串比较

9.3 过滤函数

策略代码解读 — ST 过滤
def filter_st(stock_list, ContextInfo):
    """ST过滤"""
    filtered = []                          # 空列表
    for stock in stock_list:               # for 循环遍历
        try:                               # 异常处理
            stock_name = ContextInfo.get_stock_name(stock)
            if stock_name:                 # 非空判断
                if "ST" not in stock_name and "*ST" not in stock_name \
                   and "退" not in stock_name:   # 字符串 in 判断
                    filtered.append(stock)       # 列表添加
            else:
                filtered.append(stock)           # 名称为空,保留
        except:
            filtered.append(stock)               # 出错保留
    return filtered
策略代码解读 — 次新股过滤
def filter_new(stock_list, ContextInfo, current_date):
    """次新股过滤(上市不足365天)"""
    filtered = []
    current_dt = datetime.datetime.strptime(current_date, "%Y%m%d")  # 日期解析

    for stock in stock_list:
        try:
            detail = ContextInfo.get_instrument_detail(stock)   # QMT API
            if detail and "OpenDate" in detail:                 # 字典键检查
                open_date_str = str(detail["OpenDate"])         # 转字符串
                if open_date_str.isdigit() and len(open_date_str) == 8:
                    open_date = datetime.datetime.strptime(open_date_str, "%Y%m%d")
                    days_diff = (current_dt - open_date).days    # 日期差
                    if days_diff >= 365:                         # 比较
                        filtered.append(stock)
                else:
                    filtered.append(stock)
            else:
                filtered.append(stock)
        except:
            filtered.append(stock)
    return filtered

9.4 调仓逻辑

策略代码解读 — 卖出逻辑
# 卖出不在目标中的持仓
for stock, hand in holdings.items():          # 遍历字典
    if hand <= 0:
        continue                              # 持仓为0跳过

    if stock not in target_assets:            # 字典键检查
        # 涨停暂不卖出
        if stock in Context.high_limit_list:   # 列表包含检查
            print(f"{stock} 昨日涨停,暂不卖出")
            continue

        # 卖出
        price = get_last_price(ContextInfo, stock)  # 调用函数
        if price > 0:
            volume = -hand * 100                     # 算术运算
            order_shares(stock, volume, "latest", price,
                        ContextInfo, Context.accountID)  # QMT 下单API
策略代码解读 — 买入逻辑
# 买入目标资产
for stock, target_weight in target_assets.items():
    # 计算目标市值
    target_value = total_asset * target_weight

    price = get_last_price(ContextInfo, stock)
    if price <= 0:
        continue

    # 非ETF需要额外检查
    if not is_etf(stock):
        if ContextInfo.is_suspended_stock(stock):
            continue

        pre_close = get_pre_close(ContextInfo, stock, current_date)
        if pre_close > 0 and is_limit_up(stock, price, pre_close):
            print(f"{stock} 涨停,跳过买入")
            continue

    # 计算买入股数(100的整数倍)
    volume = int(target_value / price / 100) * 100   # 整除

    if volume >= 100:
        order_shares(stock, volume, "latest", price,
                    ContextInfo, Context.accountID)

9.5 ATR 止损计算

策略代码解读
def calc_atr(stock, ContextInfo):
    """计算ATR(Average True Range)"""
    period = Context.atr_period

    # 获取历史数据(QMT API)
    hist_data = ContextInfo.get_market_data_ex(
        ["high", "low", "close"], [stock],
        period="1d", count=period + 1
    )

    # 提取数据(处理 numpy 数组)
    high_data = hist_data[stock]["high"]
    if hasattr(high_data, 'tolist'):       # hasattr 属性检查
        highs = high_data.tolist()
    else:
        highs = high_data

    # 计算 TR(True Range)
    tr_values = []
    for i in range(1, len(highs)):          # for + range 循环
        high = float(highs[i])
        low = float(lows[i])
        prev_close = float(closes[i - 1])

        tr1 = high - low                    # 当日振幅
        tr2 = abs(high - prev_close)        # 当日最高-昨收
        tr3 = abs(low - prev_close)         # 当日最低-昨收

        tr = max(tr1, tr2, tr3)             # 取最大值
        tr_values.append(tr)

    # 计算ATR(最近period个TR的平均值)
    atr = np.mean(tr_values[-period:])      # numpy求均值 + 列表切片
    return atr

9.6 知识点总结

回顾整个策略,用到的 Python 知识点如下:

知识点出现频率典型位置
变量赋值极高全局参数配置
数字运算(+-*/、round、abs、max)极高涨停价计算、ATR 计算
字符串(split、startswith、in、f-string)股票代码处理、日志输出
列表(append、remove、sort、切片、推导式)极高过滤、排序、持仓管理
字典(创建、访问、.items()、.get()、嵌套)极高持仓数据、行业分类、QMT 返回值
if / elif / else极高风格判断、过滤条件、买入卖出
for 循环极高遍历股票列表、遍历持仓
函数定义与调用整个策略的模块化结构
try / except所有 QMT API 调用
datetime日期判断、次新股过滤
lambdasort 排序
hasattrQMT 返回值兼容处理
importdatetime、numpy

下一步建议

学完这本手册后,建议你:
1. 把你的策略代码在 QMT 中完整通读一遍,用本手册作为参考
2. 尝试修改参数(如 Context.stock_num = 20Context.small_ret_threshold = -3)观察回测变化
3. 尝试在选股逻辑中添加新的过滤条件(如换手率过滤)
4. 参考 QMT 官方文档了解更多 API(如 get_market_data_ex 的更多字段)