看海回测系统回测过程
我是Mr.看海,我在尝试用信号处理的知识积累和思考方式做量化交易,应用深度学习和AI实现股票自动交易,目的是实现财务自由~
目前我正在开发基于miniQMT的量化交易系统——看海量化交易系统(KhQuant 是其核心框架)。
一、引言:为何选择"打板策略"进行深度对比?
在之前的分享中,我们曾通过一个相对简单的双均线策略,对KhQuant回测引擎的基础准确性进行了初步的验证。相信那篇文章让一些朋友对KhQuant有了初步的认识。这一次,我选择了"打板策略"——一种在市场上更具挑战性、对系统要求也更高的策略类型,来进行更深层次的对比检验。
为什么是打板策略呢?因为它追求极致的入场时机和高频的决策特性,对回测平台的每一个细节都提出了严苛的考验:
- 撮合机制的真实性:能否准确模拟涨停板附近的价格行为和成交可能性?
- 滑点处理的精度:微小的滑点差异是否会导致成交结果的显著不同?
- 交易时序的精确性:事件触发和订单执行的顺序是否严格符合逻辑?
这些都是我在设计KhQuant时重点关注和打磨的核心环节。因此,通过打板策略与Supermind这样的成熟商业平台进行对比,不仅能更直观地展现KhQuant在处理复杂交易行为时的能力,也能帮助我们发现潜在的差异点。
看海回测系统回测过程
二、策略简介:探秘"打板"
在我们开始对比之前,我想先简单介绍一下什么是"打板"策略,方便不太熟悉的朋友理解我们后续的讨论。
所谓"打板",通常指的是在股票价格即将触及当日涨停板(在中国A股市场,主板股票的日涨幅上限通常是10%)的瞬间或极短时间内迅速买入。采用这种策略的投资者,期望的是股票在封死涨停后,次日能够继续高开高走,从而在短期内获取价差收益。简单来说,这是一种捕捉市场短期内最强劲上涨动能的交易方法。
提及"打板",很多资深股民可能会联想到市场上一些传奇的游资手法,比如曾经名噪一时的"北京炒家"。他们以其果敢、凌厉的打板风格著称,常常在关键时刻重仓出击,利用资金优势和对市场情绪的精准把握,在热门板块龙头股上反复操作,创造了4年1亿的惊人战绩。他们的策略往往复杂且多变,既有对基本面、题材、资金流向的深入研究,也包含对盘口语言、对手盘行为的精妙解读,更强调纪律性和执行力。
需要说明的是,我们本次用于对比的打板策略,在设计上远比那些传奇游资的手法简化得多。我们的策略更侧重于一个核心的、可量化的标准——即"当日涨幅超过特定阈值(如9%)",以此来触发交易。这样做的主要目的,并非追求复刻那些高深的游资技巧,也不是为了构建一个实战中收益最大化的策略。更重要的是,我们希望通过这样一个相对纯粹和清晰的规则,来检验和对比KhQuant与Supermind这两个平台在行情响应速度、订单执行精确度、交易成本计算真实性等核心功能上的表现。因此,读者朋友们在理解后续对比时,请主要关注两个平台在执行这套既定规则时的差异,而非策略本身的优劣。
2.1. "打板"策略的核心逻辑
打板策略能够吸引众多市场参与者,其背后主要基于以下几点逻辑:
- 市场焦点效应:一只即将涨停的股票,往往是当前市场的绝对热点,能够吸引大量的眼球和资金的追捧。
- 动能溢出效应:部分强势封板的股票,其上涨动能并不会在当天完全释放,次日往往仍有惯性上冲的可能,这可能源于市场情绪的持续发酵,或是背后主力资金的进一步运作。
- 博弈心理驱动:打板本身也是一种市场博弈行为。参与者期望通过在关键的涨停点位抢先买入,从而在次日的开盘竞价中占据有利地位,获得更高的卖出溢价。
当然,高收益往往伴随着高风险。打板策略同样面临诸如封板失败(即"炸板",股价在涨停后又打开)、次日大幅低开导致亏损等风险。这对策略执行的时效性和准确性要求极高。
2.2. 本次对比策略的关键参数与条件
khquant参数设置
为了让大家对我们接下来要对比的策略有一个清晰的蓝图,我梳理了这个打板策略的核心参数和运作条件:
- 监控范围(股票池):策略会设定一个特定的股票池进行监控。在本次对比中,为了与Supermind的示例对齐,我们假设其监控的是沪深300这样的宽基指数成分股。
- 买入决策逻辑:
- 涨幅信号:当监控范围内的某只股票,其当日盘中的实时价格,相对于它前一个交易日的收盘价,涨幅超过预设的一个阈值时(例如,我们设定为9%),这只股票就进入了我们的重点关注名单。这里选择9%而不是直接使用10%(即涨停价)作为触发条件,是基于以下几点考虑:
- 实盘成交的挑战:在真实的A股市场中,当一只股票封死涨停板时,买一队列往往堆积了大量的买单。此时,除非有巨量卖单砸开涨停,否则后续的普通买单很难成交。直接以10%的涨停价作为买入信号,在回测中如果简单地认为能够买入,会与实盘情况严重脱节,导致回测结果过于乐观。KhQuant虽然在设计上可以模拟"排队"或设置在特定条件下允许涨停价买入,但为了更贴近多数情况下的实盘限制,并公平对比,我们选择了一个略低于涨停的价格作为观察点。
- 平台机制的统一性:不同的回测平台对于涨停价买入的处理机制可能存在差异。Supermind这类平台通常对涨停封死后的买入行为有较严格的限制。为了在两个平台上尽可能采用相似的逻辑进行对比,避免因平台对极端价格处理方式的不同而引入额外变量,设定一个略有缓冲的9%涨幅作为触发点,可以使得策略逻辑在两个平台上的可实现性更趋一致。
捕捉"临门一脚":设置9%的阈值,也是希望捕捉那些股价快速拉升、即将触及涨停板但尚未完全封死的"临门一脚"时刻。这在一定程度上模拟了部分打板策略试图在涨停前夕介入的思路。
- "临界触发价"计算:为了精确捕捉"打板"时机,策略通常会在每日开盘前,根据股票池内每只股票的前一日收盘价,预先计算出一个当日的"临界触发价"(例如:前收盘价 * 1.09)。
- 实时价格监控与执行:进入交易时段后,策略会密切关注那些已发出涨幅信号的股票。一旦某只股票的当前市场价格达到或超过了我们盘前计算的"临界触发价",策略就会考虑执行买入操作。需要特别说明的是,在本次对比中,两个平台的策略都设定为按分钟级别获取行情数据并进行判断。这意味着,策略会在每个交易分钟结束时,获取最新的分钟线数据(通常是该分钟的收盘价,或者在一些平台可能是最新价),并基于此价格与预设的"临界触发价"进行比较,以决定是否执行买入。
- 仓位控制:为了分散风险,策略会设定一个最大允许同时持有的股票数量。
- 避免当日重复交易:对于同一只股票,如果当日已经执行过买入操作,策略通常会避免在同一天内再次买入。
- 卖出决策逻辑:
- 次日开盘即卖:这是打板策略中一种常见的卖出方式。在本策略设定中,凡是策略在T日(今日)买入的股票,统一在T+1日(次日)的开盘集合竞价阶段或开盘后极短时间内执行卖出操作。
- 资金调配:
- 均等化资金分配:在不超过设定的最大持仓股票数量的前提下,策略会尝试为每只符合买入条件的股票分配大致相等的资金量。
三、 回测环境与参数设置
要客观地对比两个回测平台,首先要尽可能地统一回测环境和核心参数。下面这张表列出了我在设置KhQuant和尝试理解Supermind回测时,力求对齐的关键参数项:
参数项 | KhQuant (我的系统) | Supermind (同花顺) | 对比结果 |
---|---|---|---|
运行模式 | 回测 | 回测 | ✅ |
基准合约 | sh.000300 | 000300.SH | ✅ |
初始资金 | 1,000,000 | 1,000,000 | ✅ |
回测区间 | 开始: 2024-09-10, 结束: 2024-09-13 | 开始: 2024-09-10, 结束: 2024-09-13 | ✅ |
回测K线类型 | 分钟线 | 分钟线 | ✅ |
监控股票池 | 沪深300成分股 | 沪深300成分股 | ✅ |
手续费-买入 | 配置文件(佣金比例0.0001,最低5元) | PerShare(cost=0.0001, min_cost=5) | ✅ |
手续费-卖出 | 配置文件(佣金比例、最低消费、印花税) | PerShare(cost=0.0001, min_cost=5, tax_rate=0.001) | ✅ |
滑点设置 | 配置文件(例如按金额比例0.5%) | PriceSlippage(0.005) | ✅ |
最大持仓数量 | 全局变量 g_max_positions = 5 | 5 | ✅ |
涨停触发阈值 | 前日收盘价* 1.09 | 前日收盘价* 1.09 | ✅ |
卖出逻辑 | khHandlebar首根Bar或khPostMarket后次日开盘 | 次日开盘卖出 | ✅ |
从上表可以看出,虽然KhQuant和Supermind在参数的配置方式和展示细节上有所不同(例如KhQuant倾向于通过配置文件和GUI进行更细致的暴露,而Supermind则更多依赖API和平台默认设置),但在本次对比中涉及的核心策略参数,如回测区间、初始资金、涨跌幅触发阈值、手续费率、滑点的大致水平以及核心买卖逻辑等方面,我都已尽力确保两者在数值和目标效果上是基本对齐的。这样做的目的是为了尽可能地消除因参数设置差异带来的干扰,从而更纯粹地聚焦于两个平台在交易执行、数据处理等核心机制上的不同表现。
四、短期回测结果对比与分析
接下来,我们将聚焦于一个为期三天的具体回测片段,通过对比双方的交易记录,深入挖掘两个平台在实际运行中的细微差异。正是这些细节,往往决定了回测结果的真实性与可靠性。
4.1. 交易记录的直接对比
KhQuant 交易记录
交易时间 | 证券代码 | 交易方向 | 成交价格 | 成交数量 | 成交金额 | 手续费 |
---|---|---|---|---|---|---|
2024-09-11 10:03:00 | 002466.SZ | 买入 | 27.25 | 7300 | 198,925.00 | 19.89 |
2024-09-11 10:39:00 | 002460.SZ | 买入 | 27.82 | 5700 | 158,574.00 | 15.86 |
2024-09-11 13:08:00 | 002709.SZ | 买入 | 15.06 | 8500 | 128,010.00 | 12.80 |
2024-09-12 09:30:00 | 002460.SZ | 卖出 | 27.69 | 5700 | 157,833.00 | 173.62 |
2024-09-12 09:30:00 | 002466.SZ | 卖出 | 27.93 | 7300 | 203,889.00 | 224.28 |
2024-09-12 09:30:00 | 002709.SZ | 卖出 | 14.53 | 8500 | 123,505.00 | 135.86 |
2024-09-13 11:22:00 | 300502.SZ | 买入 | 104.25 | 1900 | 198,075.00 | 19.81 |
同花顺 Supermind 交易记录
日期 | 时间 | 代码/名称 | 买卖操作 | 成交价 | 成交数量 | 成交金额 | 费率 | 平仓盈亏 |
---|---|---|---|---|---|---|---|---|
2024-09-11 | 10:03:00 | 002466.SZ 天齐锂业 | 买入 | 27.258 | 7300 | 198,983.22 | 19.90 | -- |
2024-09-11 | 10:39:00 | 002460.SZ 赣锋锂业 | 买入 | 27.809 | 5700 | 158,513.29 | 15.85 | -- |
2024-09-12 | 09:31:00 | 002460.SZ 赣锋锂业 | 卖出 | 27.581 | -5700 | -157,210.99 | 172.93 | -1,318.16 |
2024-09-12 | 09:31:00 | 002466.SZ 天齐锂业 | 卖出 | 28.130 | -7300 | -205,345.35 | 225.88 | 6,342.23 |
4.2. 关键差异点剖析
通过仔细比对上述交易流水,我们可以发现几个值得深入探讨的差异点:
4.2.1. 成交价格的精度问题:失之毫厘,差之千里?
- 现象观察: 最显眼的差异在于成交价格的显示精度。根据我们观察到的交易记录,在Supermind的记录中,成交价格是精确到小数点后三位的(例如,002466.SZ的买入价是27.258)。而在KhQuant的交易记录中,我们看到的成交价格则保留到小数点后两位(例如,002466.SZ的买入价是27.25)。
我的分析与KhQuant的设计考量: - A股市场的真实报价:我们知道,在中国A股市场,股票的最小报价变动单位是0.01元。这意味着,最终在交易所层面撮合成交的价格,理论上会精确到小数点后两位。
- Supermind为何可能显示三位小数? Supermind展示三位小数的成交价格,这通常意味着其内部在考虑了滑点(Slippage)之后,记录的是一个更精细的理论成交价。例如,如果设定的滑点是按固定比例或特定跳数计算,那么计算出的成交价完全可能不是两位小数。Supermind选择展示这个包含了滑点影响的原始计算价格,可能是为了让用户更清晰地看到滑点对单笔交易的精确影响。
- KhQuant的显示与处理:KhQuant采取的是更贴近交易所实际回报的习惯做法。在KhQuant内部,即使计算滑点时可能产生更多位数的小数,但在最终记录成交或进行资金清算时,会按照交易所的实际报价单位(0.01元)进行圆整(通常是四舍五入或向更不利于策略的方向取整,具体取决于配置)。这种处理方式旨在使回测结果更贴近最终的实盘交割单。
潜在影响不容忽视: - 单笔成交额的微差:小数位数的不同,必然导致单笔成交总金额的细微出入。以上述002466.SZ的买入为例,Supermind的计算是
27.258 * 7300 = 198983.40
元,而KhQuant的计算是27.25 * 7300 = 198925.00
元。这两者之间因小数位不同导致的初始计算差异为58.40元。 - 手续费的连锁反应:由于交易手续费(尤其是佣金)通常是按成交金额的一定比例来收取的,成交金额上的这些微小差异会直接传递到手续费的计算上。比如,Supermind对002466.SZ的买入手续费记录为19.90元(基于198983.40元计算),KhQuant则为19.89元(基于198925.00元计算)。这细微的0.01元差异,正是源于两者成交总金额计算基数的不同。
- 累积效应:虽然单笔差异看似不大,但在频繁交易或长期回测中,这些微小的差异可能会累积,对最终的策略收益率和各项绩效指标产生可见的影响。这也是我在设计KhQuant时,坚持细节精确和过程透明的原因之一:确保回测的每一步都尽可能地接近真实交易,为策略迭代提供可靠依据。
4.2.2. Supermind未能按预期执行买入:回测的"黑箱"与KhQuant的透明
在深入对比Supermind和KhQuant的交易记录时,我发现了一个对策略表现至关重要的差异:Supermind似乎未能严格按照我们设定的打板策略逻辑执行所有预期的买入操作。这直接关系到回测结果的有效性和我们对策略潜力的评估。下面我将具体分析:
- 002709.SZ (天赐材料) - 2024-09-11:
-
- 查看历史数据中的9月11日股票涨跌幅,达到了9.15%。
- 策略执行预期:按照我们设定的"当日涨幅超过9%"的买入条件,这只股票在盘中价格触及9%的时刻,就应该被策略捕捉到并执行买入。
- Supermind表现:然而,从Supermind的交易记录来看,并没有在2024-09-11买入002709.SZ的操作。这是一个明显的遗漏。
- KhQuant表现:与此形成对比的是,KhQuant的交易记录清晰地显示,在2024-09-11 13:08:00,买入了8500股002709.SZ。
- 300502.SZ (新易盛) - 2024-09-13:
-
- 行情回顾与我的判断:300502.SZ在2024-09-12的收盘价为95.22元。在2024-09-13当天,该股票的最高价达到了104.90元。
- 涨幅计算:当日最高价相对于前一日收盘价的涨幅为
(104.90 - 95.22) / 95.22 * 100% ≈ 10.16%
。 - 策略执行预期:这个10.16%的涨幅显著超过了我们设定的9%的买入阈值,因此,300502.SZ在当天是明确的买入标的。
- Supermind表现:同样地,Supermind的交易记录中并未显示在2024-09-13买入300502.SZ的操作。这再次暴露了其执行逻辑上的问题。
- KhQuant表现:KhQuant则在2024-09-13 11:22:00,以104.25元的价格买入了1900股300502.SZ。成交价104.25元对应的实际涨幅约为
(104.25 - 95.22) / 95.22 * 100% ≈ 9.48%
,同样符合策略的买入逻辑,并且成交时间点也合理。
Supermind未能准确执行买入,揭示了什么问题?
Supermind未能捕捉到这两次明确的交易机会,这对于一个以精确复现为目标的回测系统来说,无疑是一个严重的问题。作为KhQuant的开发者,我分析这背后可能的原因有以下几点:
1.内部撮合机制的"黑箱":
- Supermind的潜在问题:即便获取到的行情数据是准确的,Supermind内部的订单撮合逻辑是如何运作的?这往往是一个"黑箱"。例如,当策略发出一个接近涨停板的买单时,它是如何判断这笔订单能否成交的?其模拟成交的算法是否过于保守?或者,它在模拟交易所"价格优先、时间优先"的撮合原则时,是否存在某些简化或偏差?特别是对于涨停板附近"无法买入"的情况,它的模拟阈值和判断条件是什么?这些不透明的机制,都可能导致策略的预期行为与实际回测结果不符。
2.策略执行逻辑的细微差异:
- Supermind策略代码中handle_bar函数的执行频率(例如每分钟一次)以及其内部get_current等API获取数据的具体方式,是否保证了策略总能基于最新的、最准确的价格信息进行判断?策略中关于可用资金判断、最大持仓限制等辅助逻辑,如果其实现方式与KhQuant存在细微但关键的差异,也可能间接导致某些预期的买入操作无法执行。
这两次看似不起眼的"漏单",对于依赖精确入场时机的打板策略而言,其影响可能是致命的。这不仅仅是少了几笔交易那么简单,它直接揭示了Supermind的回测结果可能未能真实反映策略的全部潜力,甚至可能误导开发者。
- 低估策略的真实表现:错失了本应成功(或至少应被尝试)的交易机会,必然导致回测报告中的整体策略收益率、交易胜率、盈利因子等关键绩效指标低于策略在理想执行状态下的应有水平。
- 误导策略的优化方向:如果策略开发者基于这样不准确、甚至可以说是"打了折扣"的回测结果去进行参数调优或模型迭代,很可能会做出错误的决策。例如,可能会过早地否定一个本应有价值的策略思路,或者在错误的参数空间上浪费大量时间。
五、关于回测绩效指标的说明
在分析了Supermind的交易记录,并发现了其未能按预期执行关键买入操作(如前述4.2.2章节所讨论的002709.SZ和300502.SZ的案例)之后,我们再来看Supermind提供的整体回测绩效指标时,就需要带着审慎的眼光了。
我们当前的关注焦点,或者说本次对比更有价值的发现,已经从"比较两个平台谁的最终收益高",转向了"揭示并理解两个平台在核心交易撮合与指令执行层面可能存在的根本性差异"。只有确保了底层交易行为模拟的真实性和准确性,上层的绩效指标对比才具有坚实的逻辑基础和实际的参考意义。
六、总结与思考:细节决定成败,透明助力优化
通过这次围绕"打板策略"对KhQuant和Supermind进行的深度对比,特别是对具体交易记录的细致剖析,我相信大家能和我一样,对两个平台在设计理念和实际表现上的差异有了更深刻的认识。
- 追求极致的仿真度与透明度:回测的终极目标是无限逼近真实交易环境。KhQuant将持续在交易撮合机制的模拟、交易成本模型的精细化、历史数据与实时数据处理的准确性等核心环节进行投入和打磨。我希望为用户提供的不仅仅是一个回测结果,更是一个清晰可查、有据可依的完整模拟过程,包括详尽的过程数据记录和日志系统。
- 赋予开发者最大的灵活性与掌控力:无论是参数设置的细致程度、策略逻辑编写的自由度(例如可以方便地使用Python强大的数据分析和AI建模库进行集成),还是对系统内部机制的理解与调整能力,KhQuant都致力于为专业的量化策略开发者和研究者提供一个能够让他们充分施展才华、不受过多黑箱限制的平台。
- 拥抱开源精神,持续迭代优化:量化交易是一个快速发展的领域,没有一劳永逸的完美解决方案。KhQuant从诞生之初就带有开源的基因。我会密切关注用户的反馈,积极吸收社区的智慧,并结合业界最新的技术进展,对KhQuant进行持续的功能迭代和性能优化。
七、 当前进展、挑战与展望
通过前面对比Supermind和聚宽的详细分析,我对KhQuant在核心交易逻辑的准确性、参数设置的灵活性以及对细节处理的考究等方面,都建立了更强的信心。这无疑是一个令人鼓舞的阶段性成果。
然而,打造一个全面成熟、稳定高效的量化交易系统,依然任重道远。在当前的开发和测试过程中,我也注意到了一些KhQuant尚需打磨和提升的方面。其中一个比较突出的问题是,在进行长周期、大规模数据回测时,KhQuant的整体运行效率还有较大的提升空间。虽然对于短周期、小范围的策略验证,目前的效率尚可接受,但要支撑更复杂、更长时间跨度的研究,效率优化将是我近期的重点工作之一。
我的下一步计划主要围绕以下几点展开:
- 核心性能优化:我会投入精力分析KhQuant中可能存在的性能瓶颈,特别是在数据加载、事件处理、和计算密集型策略的执行效率方面,力求在保证准确性的前提下,大幅提升长周期回测的速度。
- 功能迭代与易用性提升: 结合目前已发现问题以及未来内测用户的意见,持续改进KhQuant的功能细节和用户交互体验,使其更加便捷易用。
我预计在下一阶段的分享中,会向大家展示KhQuant在长周期回测效率优化方面取得的进展,或者可能会带来一些其他令人期待的新功能和特性。敬请期待!
关于内测:
我正在加紧优化程序运行效率,后续将尽快计划启动"看海量化交易系统 (KhQuant)"的小范围 Beta 内测。
- 优先通道: 为了感谢一直以来支持我的朋友,特别是通过我公众号"看海的城堡"推荐渠道开通 MiniQMT 账户的朋友们,我会优先邀请你们参与内测,提前体验并反馈宝贵意见。
- 最终会公开: 也请其他朋友放心,内测只是为了打磨产品。最终软件会公开发布,核心框架代码也计划开源,让所有人都能用上并参与进来。
我的目标是做一款真正好用、开放的工具,严格的测试和大家的反馈是成功的关键。
八、 关于开通 MiniQMT
MiniQMT 是什么?
简单说,它是迅投(QMT)交易系统提供的一个编程接口(API),很多券商都在用 QMT。通过 MiniQMT,我们可以用 Python 代码连接到券商服务器,查行情、跑策略、下单交易。
对于量化交易者来说,它是一个常用且通常免费(可能需要满足券商一定的资产要求)的实盘交易接口,稳定性和速度都不错。
KhQuant 和 MiniQMT 的关系
我开发的"看海量化交易系统 (KhQuant)"就是基于 MiniQMT 接口来连接真实券商账户进行交易的。所以,如果你想用 KhQuant 进行实盘交易(或者未来可能的回测数据获取),就需要一个 MiniQMT 账户。
如何开通?
如果你还没有 MiniQMT 账户,又对 KhQuant 感兴趣,或者想支持一下我的开发工作,可以关注我的公众号"看海的城堡",在公众号页面下方有开通 MiniQMT 的指引。
走推荐渠道开户不是必须的,但对我来说是一种鼓励和支持,也能确保你开的账户类型和 KhQuant 是兼容的。再次感谢大家的关注!
九、 免责声明
本文所有内容仅供学习和技术交流使用,不构成任何投资建议。所述策略及回测结果仅为历史数据模拟,不代表未来实际表现。平台特性分析基于当前观察,可能随平台更新而变化。投资者据此操作,风险自担。
十、 附录:完整策略代码
附录A:KhQuant 打板策略
# 涨幅监控策略(中证500版本)
# 当股票当日涨幅超过9%时买入,第二天开盘卖出from xtquant import xtdata
from khQTTools import generate_signal
import logging
import os# 配置日志系统
LOGS_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'logs')
os.makedirs(LOGS_DIR, exist_ok=True)logging.basicConfig(filename=os.path.join(LOGS_DIR, 'strategy.log'),level=logging.INFO,format='%(asctime)s - %(levelname)s - %(message)s',filemode='a'
)# 全局变量
g_today_bought = set() # 今日买入的股票
g_yesterday_bought = set() # 昨日买入的股票,今日开盘卖出
g_is_first_bar = True # 是否是当天第一个bar
g_trigger_prices = {} # 存储触发价格的字典
g_max_positions = 5 # 最大持仓数量def init(stock_list, context):"""策略初始化函数,系统启动时会调用此函数"""global g_today_bought, g_yesterday_bought, g_is_first_bar, g_trigger_prices, g_max_positions# 记录日志logging.info('策略开始运行,初始化函数全局只运行一次')# 获取框架实例framework = context.get("__framework__")if framework and hasattr(framework, 'trader_callback') and hasattr(framework.trader_callback, 'gui'):framework.trader_callback.gui.log_message(f'策略开始运行,初始化函数全局只运行一次', "INFO")framework.trader_callback.gui.log_message(f'设置监控股票数量: {len(stock_list)} 只', "INFO")# 初始化全局变量g_today_bought = set()g_yesterday_bought = set()g_is_first_bar = Trueg_trigger_prices = {}# 预加载关键历史数据try:batch_size = 50# 每次处理50只股票for i in range(0, len(stock_list), batch_size):batch_stocks = stock_list[i:i+batch_size]# 批量获取历史数据以提高效率xtdata.get_market_data(field_list=['open', 'close'], stock_list=batch_stocks, period='1d',count=5)except Exception as e:logging.error(f"预加载历史数据时出错: {str(e)}")logging.info("初始化完成")def khPreMarket(data):"""盘前回调函数,计算当日触发价格"""global g_today_bought, g_yesterday_bought, g_is_first_bar, g_trigger_prices# 获取当前日期并转换为API所需格式current_time = data["__current_time__"]date = current_time["date"]date_yyyymmdd = date.replace('-', '')# 获取框架实例framework = data.get("__framework__")gui = Noneif framework and hasattr(framework, 'trader_callback') and hasattr(framework.trader_callback, 'gui'):gui = framework.trader_callback.guigui.log_message(f'=== 交易日 {date} 盘前运行 ===', "INFO")# 重置每日状态g_today_bought = set()g_is_first_bar = Trueg_trigger_prices = {}# 获取股票列表stock_list = []try:# 尝试从配置获取股票列表if framework and hasattr(framework, 'config'):stock_list_file = framework.config.config_dict.get("data", {}).get("stock_list_file", "")if stock_list_file and os.path.exists(stock_list_file):with open(stock_list_file, 'r', encoding='utf-8') as f:stock_list = [line.strip() for line in f if line.strip()]# 如果无法从配置获取,尝试从持仓中获取ifnot stock_list:positions = data.get("__positions__", {})stock_list = list(positions.keys())except Exception as e:logging.error(f"获取股票列表失败: {str(e)}")# 计算所有股票的触发价格if stock_list:try:# 批量获取前一日收盘价并计算触发价hist_data = xtdata.get_market_data_ex(field_list=['close'],stock_list=stock_list,period='1d',start_time=str(int(date_yyyymmdd) - 1),end_time=str(int(date_yyyymmdd) - 1),dividend_type='none')# 处理每只股票的数据for stock in stock_list:if stock in hist_data and len(hist_data[stock]['close']) > 0:prev_close = hist_data[stock]['close'].iloc[0]if prev_close > 0:g_trigger_prices[stock] = prev_close * 1.09except Exception as e:logging.error(f"批量计算触发价格时出错: {str(e)}")# 确保昨日买入的股票在今日卖出positions = data.get("__positions__", {})g_yesterday_bought.update(positions.keys())return []def khHandlebar(data):"""策略主函数,处理实时行情并生成交易信号"""global g_today_bought, g_yesterday_bought, g_is_first_bar, g_trigger_prices, g_max_positionssignals = []# 获取当前时间和股票代码列表current_time = data["__current_time__"]date = current_time["date"]date_yyyymmdd = date.replace('-', '')# 获取有效的股票代码列表(过滤掉特殊键)stock_codes = [key for key in data.keys() ifnot key.startswith('__')]# 获取账户和持仓信息account = data.get("__account__", {})positions = data.get("__positions__", {})available_cash = account.get("cash", 0)current_positions_count = len(positions)# 如果是当天第一个bar,先处理卖出昨日买入的股票if g_is_first_bar:for stock in g_yesterday_bought:if stock in positions:# 获取当前价格current_price = positions[stock].get("close", 0)if current_price <= 0and stock in data:current_price = data[stock].get("close", 0)# 生成卖出信号if current_price > 0:sell_signals = generate_signal(data, stock, current_price, 1.0, "sell", "涨停后第二天卖出")signals.extend(sell_signals)g_is_first_bar = False# 如果有卖出信号,先返回执行if signals:return signals# 如果持仓已达到最大限制,不执行买入操作if current_positions_count >= g_max_positions:return signals# 计算单股可用资金(平均分配)position_value = available_cash / g_max_positions if g_max_positions > 0else0# 遍历所有股票检查是否满足买入条件for stock_code in stock_codes:# 跳过已买入或已持有的股票if stock_code in g_today_bought or stock_code in positions:continue# 获取当前价格current_price = data[stock_code].get("close", 0)if current_price <= 0:continue# 检查是否已有触发价或需要计算trigger_price = Noneif stock_code in g_trigger_prices:trigger_price = g_trigger_prices[stock_code]else:# 补充计算触发价格try:hist_data = xtdata.get_market_data(field_list=['open'],stock_list=[stock_code],period='1d',start_time=date_yyyymmdd,end_time=date_yyyymmdd,dividend_type='none')if stock_code in hist_data and len(hist_data[stock_code]['open']) > 0:open_price = hist_data[stock_code]['open'].iloc[0]if open_price > 0:trigger_price = open_price * 1.09g_trigger_prices[stock_code] = trigger_priceexcept Exception as e:logging.error(f"补充计算 {stock_code} 触发价格时出错: {str(e)}")# 如果当前价格达到或超过触发价,执行买入if trigger_price and current_price >= trigger_price:# 计算可买股数(整数手)shares_to_buy = int(position_value / current_price / 100) * 100# 确保至少买一手(100股)if shares_to_buy >= 100:# 生成买入信号buy_signals = generate_signal(data, stock_code, current_price, shares_to_buy, "buy", "当日涨幅超过9%买入")signals.extend(buy_signals)# 添加到今日买入记录g_today_bought.add(stock_code)# 如果已达到最大持仓数,立即返回if len(g_today_bought) + current_positions_count >= g_max_positions:breakreturn signalsdef khPostMarket(data):"""盘后回调函数,记录今日买入的股票"""global g_yesterday_bought, g_today_bought# 获取当前时间current_time = data["__current_time__"]date_time = current_time["datetime"]# 更新昨日买入记录,用于明天开盘卖出g_yesterday_bought = g_today_bought.copy()# 记录日志if g_today_bought:logging.info(f"今日买入股票: {list(g_today_bought)}")logging.info(f"明日开盘将卖出股票: {list(g_yesterday_bought)}")else:logging.info(f"今日未买入股票")# 获取账户信息account = data.get("__account__", {})available_cash = account.get("cash", 0)total_asset = account.get("total_asset", 0)logging.info(f"=== 交易日 {date_time} 盘后运行 ===")logging.info(f"当前资金: {available_cash:.2f}, 总资产: {total_asset:.2f}")logging.info(f"====================")return []
附录B:Supermind 打板策略
# 涨幅监控策略
# 当股票当日涨幅超过9%时买入,第二天开盘卖出# 全局变量
g_today_bought = set() # 今日买入的股票
g_yesterday_bought = set() # 昨日买入的股票,今日开盘卖出
g_last_log_time = None # 上次记录日志的时间
g_is_first_bar = True # 是否是当天第一个bar# 初始化函数,全局只运行一次
def init(context):# 设置基准收益set_benchmark('000300.SH')# 设置要操作的默认股票context.security = '000300.SH'# 打印日志log.info('策略开始运行,初始化函数全局只运行一次')# 设置股票每笔交易的手续费为万分之三(买)/万分之十三(卖)set_commission(PerShare(type='stock', cost=0.0001, min_cost=5, tax_rate=0.001))# 设置股票交易滑点0.1%set_slippage(PriceSlippage(0.005))# 设置日级最大成交比例25%,分钟级最大成交比例50%set_volume_limit(0.25, 0.5)# 获取中证500成分股try:context.stocks = get_index_stocks('000300.SH')log.info('获取中证500成分股 {} 只'.format(len(context.stocks)))except Exception as e:log.error('获取中证500成分股失败: {}, 使用默认股票池'.format(str(e)))log.info('设置监控股票数量: {} 只'.format(len(context.stocks)))# 初始化存储触发价格的字典context.trigger_prices = {}# 设置最大持仓数量context.max_positions = 5# 每日开盘前被调用一次
def before_trading(context):global g_today_bought, g_yesterday_bought, g_is_first_bar# 获取日期date = get_datetime().strftime('%Y-%m-%d')log.info('=== 交易日 {} 盘前运行 ==='.format(date))# 清空今日购买记录g_today_bought = set()# 重置第一个bar标志g_is_first_bar = True# 清空并重新计算触发价格context.trigger_prices = {}# 由于股票数量可能很多,输出一下计算进度total_stocks = len(context.stocks)log.info("开始计算 {} 只股票的触发价格...".format(total_stocks))# 使用分批处理方式获取开盘价数据batch_size = 50# 每次处理50只股票for i in range(0, total_stocks, batch_size):batch_stocks = context.stocks[i:i+batch_size]log.info("处理第 {}-{}/{} 只股票".format(i+1, min(i+batch_size, total_stocks), total_stocks))for stock in batch_stocks:try:# 获取股票信息try:stock_info = get_security_info(stock)stock_name = stock_info.display_name if stock_info else stockexcept:stock_name = stock# 获取前一日收盘价数据hist_data = history(stock, ['close'], 1, '1d', False, 'pre', is_panel=1)ifnot hist_data.empty:open_price = hist_data['close'].iloc[0]# 计算触发价格(涨幅9%)if open_price > 0:trigger_price = open_price * 1.09context.trigger_prices[stock] = trigger_price# 打印每只股票的开盘价和触发价格log.info("股票 {}({}): 开盘价 {:.2f}, 触发价格 {:.2f}".format(stock, stock_name, open_price, trigger_price))except Exception as e:log.error("计算 {} 触发价格时出错: {}".format(stock, str(e)))log.info("已计算 {} 只股票的9%涨幅触发价格".format(len(context.trigger_prices)))log.info("盘前处理结束")# 开盘时运行函数
def handle_bar(context, bar_dict):global g_last_log_time, g_today_bought, g_yesterday_bought, g_is_first_bar# 获取当前时间current_dt = get_datetime()#log.info("-----------------------------------------------------")#log.info("执行 handle_bar @ {}".format(current_dt.strftime('%Y-%m-%d %H:%M:%S')))# 如果是每天的第一个bar,先卖出昨日买入的股票if g_is_first_bar:#log.info("执行每日第一个bar的操作,卖出昨日买入的股票")# 卖出昨日买入的股票for stock in g_yesterday_bought:if stock in context.portfolio.stock_account.positions:log.info("卖出昨日买入的股票: {}".format(stock))order_target(stock, 0)g_is_first_bar = False# 使用get_current()获取当前行情数据try:# 由于股票数量很多,我们可能需要分批获取行情数据batch_size = 100# 每次处理100只股票all_current_data = {}# 筛选尚未持有且未买入的股票,作为待检查股票stocks_to_check = [stock for stock in context.trigger_prices.keys() if stock notin context.portfolio.stock_account.positions and stock notin g_today_bought]#log.info("今日待检查的股票: {} 只".format(len(stocks_to_check)))# 移除随机抽样,直接检查所有股票sampled_stocks = stocks_to_check# 分批获取行情数据for i in range(0, len(sampled_stocks), batch_size):batch_stocks = sampled_stocks[i:i+batch_size]batch_data = get_current(batch_stocks)if batch_data:all_current_data.update(batch_data)#log.info("获取到 {} 只股票的当前行情数据".format(len(all_current_data)))ifnot all_current_data:log.info("未获取到行情数据")returnexcept Exception as e:log.error("获取行情数据出错: {}".format(str(e)))return# 检查当前持仓数量current_positions = len(context.portfolio.stock_account.positions)if current_positions >= context.max_positions:log.info("当前持仓数量 ({}) 已达或超过最大持仓限制 ({}), 不执行买入操作".format(current_positions, context.max_positions))return# 计算可用于买入的资金available_cash = context.portfolio.cashposition_value = available_cash / context.max_positions#log.info("当前可用资金: {:.2f}, 单股可用资金: {:.2f}".format(available_cash, position_value))# 记录符合条件的股票potential_stocks = []# 检查每只股票for stock in sampled_stocks:if stock notin all_current_data:continuetry:# 获取股票信息try:stock_info = get_security_info(stock)stock_name = stock_info.display_name if stock_info else stockexcept:stock_name = stock# 获取当前价格和触发价格current_price = all_current_data[stock].closetrigger_price = context.trigger_prices[stock]# 输出比较信息#log.info("检查股票 {}({}): 当前价 {:.2f} vs 触发价 {:.2f}, 结果: {}".format(# stock, stock_name, current_price, trigger_price, # "符合条件" if current_price >= trigger_price else "不符合条件"))# 如果当前价格超过触发价格,记录下来if current_price >= trigger_price:potential_stocks.append((stock, stock_name, 0, current_price, trigger_price))except Exception as e:log.error("处理股票 {} 时出错: {}".format(stock, str(e)))# 买入股票stocks_bought = 0for stock, stock_name, _, current_price, trigger_price in potential_stocks:# 计算可买股数(整数手)shares_to_buy = int(position_value / current_price / 100) * 100log.info("准备买入 {}({}): 单股资金 {:.2f}, 当前价 {:.2f}, 可买股数 {}".format(stock, stock_name, position_value, current_price, shares_to_buy))if shares_to_buy >= 100: # 至少买一手log.info("执行买入: {}({}), 价格: {:.2f} > 触发价: {:.2f}, 数量: {}股".format(stock, stock_name, current_price, trigger_price, shares_to_buy))try:# 使用order函数下单order(stock, shares_to_buy)log.info("下单成功!")g_today_bought.add(stock)stocks_bought += 1except Exception as e:log.error("下单失败: {}".format(str(e)))# 如果已达到最大持仓数,直接退出if stocks_bought + current_positions >= context.max_positions:log.info("已达到最大持仓数({}/{}),停止买入".format(stocks_bought + current_positions, context.max_positions))breakelse:log.info("资金不足买入一手 {}({}), 当前价: {:.2f}, 需要资金: {:.2f}".format(stock, stock_name, current_price, current_price*100))#log.info("handle_bar执行完毕,本次买入: {} 只股票".format(stocks_bought))#log.info("-----------------------------------------------------")# 收盘后运行函数
def after_trading(context):global g_yesterday_bought, g_today_bought# 获取时间time = get_datetime().strftime('%Y-%m-%d %H:%M:%S')log.info('=== 交易日 {} 盘后运行 ==='.format(time))# 更新昨日买入记录,用于明天开盘卖出g_yesterday_bought = g_today_bought.copy()if g_today_bought:log.info("今日买入股票: {}".format(list(g_today_bought)))log.info("明日开盘将卖出股票: {}".format(list(g_yesterday_bought)))else:log.info("今日未买入股票")log.info("当前资金: {:.2f}, 总资产: {:.2f}".format(context.portfolio.cash, context.portfolio.total_value))log.info("====================")log.info('一天结束')
相关文章
【深度学习量化交易1】一个金融小白尝试量化交易的设想、畅享和遐想
【深度学习量化交易2】财务自由第一步,三个多月的尝试,找到了最合适我的量化交易路径
【深度学习量化交易3】为了轻松免费地下载股票历史数据,我开发完成了可视化的数据下载模块
【深度学习量化交易4】 量化交易历史数据清洗——为后续分析扫清障碍
【深度学习量化交易5】 量化交易历史数据可视化模块
【深度学习量化交易6】优化改造基于miniQMT的量化交易软件,已开放下载~(已完成数据下载、数据清洗、可视化模块)
【深度学习量化交易7】miniQMT快速上手教程案例集——使用xtQuant进行历史数据下载篇
【深度学习量化交易8】miniQMT快速上手教程案例集——使用xtQuant进行获取实时行情数据篇
【深度学习量化交易9】miniQMT快速上手教程案例集——使用xtQuant获取基本面数据篇
【深度学习量化交易10】miniQMT快速上手教程案例集——使用xtQuant获取板块及成分股数据篇
【深度学习量化交易11】miniQMT快速上手教程——使用XtQuant进行实盘交易篇(八千字超详细版本)
【深度学习量化交易12】基于miniQMT的量化交易框架总体构建思路——回测、模拟、实盘通吃的系统架构
【深度学习量化交易13】继续优化改造基于miniQMT的量化交易软件,增加补充数据功能,优化免费下载数据模块体验!
【深度学习量化交易14】正式开源!看海量化交易系统——基于miniQMT的量化交易软件
【深度学习量化交易15】基于miniQMT的量化交易回测系统已基本构建完成!AI炒股的框架初步实现
【深度学习量化交易16】韭菜进阶指南:A股交易成本全解析
【深度学习量化交易17】触发机制设置——基于miniQMT的量化交易回测系统开发实记
【深度学习量化交易18】盘前盘后回调机制设计与实现——基于miniQMT的量化交易回测系统开发实记
【深度学习量化交易19】行情数据获取方式比测(1)——基于miniQMT的量化交易回测系统开发实记
【深度学习量化交易20】量化交易策略评价指标全解析——基于miniQMT的量化交易回测系统开发实记
【深度学习量化交易21】行情数据获取方式比测(2)——基于miniQMT的量化交易回测系统开发实记
【AI量化第22篇】如何轻松看懂回测结果——基于miniQMT的量化交易回测系统开发实记
【AI量化第23篇】数据下载/补充模块升级,并与回测系统正式集成——基于miniQMT的量化交易回测系统开发实记
【AI量化第24篇】KhQuant 策略框架深度解析:让策略开发回归本质——基于miniQMT的量化交易回测系统开发实记
【AI量化第25篇】看海量化交易系统日志系统详解
【AI量化第26篇】以配置为核心的工程化研究管理——基于miniQMT的量化交易回测系统开发实记
【AI量化第27篇】看海量化 vs. 同花顺 回测横评!以“双移动均线”策略为例的量化回测结果深度对比分析
大量化平台也有坑?khQuant回测横评第二弹,一次“排雷”实录【AI量化第28篇】