ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

PowerLanguage策略开发实战:从零构建Multicharts交易系统

PowerLanguage策略开发实战:从零构建Multicharts交易系统

如果你准备用Multicharts跑一套自己的交易策略,或者已经写了一小段PowerLanguage但总觉得跑起来不对劲,这篇内容应该对你有用。我见过很多刚开始接触程序化交易的人,第一步就卡在“我到底要先写什么”这个问题上——到底先写指标?还是先写信号?是不是代码写完就能回测出漂亮的曲线?实际上,策略开发真正的难点从来不在语法,而在于你能否把交易逻辑用代码准确、稳定、不带幻觉地表达出来,并且让回测结果尽量接近实盘。

我自己在用 Multicharts 写 PowerLanguage 策略的过程中踩过不少坑,从简单的双均线到带过滤条件的趋势跟踪、从单品种到多品种组合,前后重构了好几版。这篇内容就基于我个人的实际开发经历,把从零构建一套专业级策略的过程拆开讲一遍,覆盖思路设计、代码实现、回测优化、常见问题排查,以及一些文档里不会写但实战中很有用的细节。不管你是刚接触 PowerLanguage 的新手,还是已经写了一段时间策略但总觉得不够系统的开发者,这篇内容都能给你一条比较清晰的路径。

1. 先看清整体:策略开发不是写代码,是搭一套决策系统

很多人以为策略开发就是“把想法变成代码”,实际上这只是其中一个环节。真正稳定运行的策略,背后是一套完整的决策系统:入场信号怎么生成、出场条件怎么触发、仓位怎么控制、风险怎么约束、异常行情怎么处理。代码只是把这一整套规则翻译成机器能理解的语言。如果你上来就直接写买入卖出的条件,后面会越来越难维护,改一个参数都要小心翼翼,因为你根本不知道改动会影响哪里。

1.1 从自然语言到代码逻辑:把交易规则拆成可执行的条件

我习惯的第一步,不是打开 Multicharts,而是拿一张纸把交易逻辑写成人话。举个例子,你想做一个趋势跟踪策略,你的想法可能是这样的:

“当均线形成金叉,并且当前价格站上最近一段时间的波动区间上沿时,买入做多;如果价格回撤超过某段波动幅度的百分之多少,就止损离场;如果趋势继续走强,就通过移动止损保护利润。”

这句话看着清楚,实际上没法直接写代码。你要把它继续拆:

  • “金叉”是哪个均线和哪个均线?参数是多少?
  • “波动区间上沿”用的是哪种波动度量?ATR 吗?还是最高价和最低价的某个移动窗口?
  • “回撤超过幅度”怎么计算?从哪个价格开始算?
  • “趋势继续走强”怎么定义?是持续创新高,还是均线仍然多头排列?

拆到这一步,才能开始定义 inputs 和 variables。你在纸上写下的这些条件,最终会映射到 PowerLanguage 里的每一条 if 判断、每一次仓位计算。我见过很多人绕开这一步,直接去搜别人的现成代码,结果改来改去始终不满意。原因很简单:你没有先搞清楚自己要什么,抄来的代码也不会帮你做决定。

1.2 为什么选 Multicharts 作为开发平台

在策略开发工具的选择上,市面上有商业平台、有开源框架,也有自己从头写的方案。我自己在多个平台之间切换过,最终还是把主力开发放在 Multicharts 上,几个原因比较现实:

第一,PowerLanguage 的语法结构清晰,接近自然语言,初学者花一两天就能看懂基本逻辑,不需要像很多开源框架那样先补一堆底层 API 知识。

第二,Multicharts 把回测、优化、图表展示、参数扫描、投资组合回测都放在同一个环境里,数据、策略、绩效报告一体化的程度很高,调试效率比“写代码+外部回测引擎”的组合要快很多。

第三,它能对接多种数据源和交易接口,并且支持期货、股票、外汇等多种品种类型,这对我这种需要多市场测试策略的人来说很关键。

当然,Multicharts 并不是万能的。它有自己的计算模型,有一些平台层面的限制,比如某些函数在盘中实时运行和回测时的行为会有细微差别,后面我会专门讲这些“坑”。但作为从零构建专业级策略的起点,它的性价比和易用性是很合适的。

1.3 从零构建一套策略框架,而不是写一个策略

我强烈建议你在开发第一个策略时,就按“框架”的思路去搭,而不是写一个一次性的脚本。什么意思呢?比如你的策略里会有多个出场条件:固定止损、移动止损、时间止损、趋势反转离场,这些条件不是孤立存在的,它们要能同时被管理,并且要有一个优先级关系。如果你只是在一个策略文件里从上到下写 if,后面添加逻辑时很容易冲突。

我的做法是把策略拆成三个层次:

  • 信号层:负责判断当前是否符合入场条件,输出一个方向标记。
  • 风控层:统一管理止损、移动止盈、最大持仓时间、最大回撤限制,输出平仓信号。
  • 执行层:根据信号层和风控层的输出,决定实际下单动作。

这样做的好处是,后续你想调整止损方式,只需要改风控层,不用动信号层的任何代码。策略的可维护性会大大提升,尤其是当你从单品种扩展到多品种的时候,这种结构化带来的收益非常明显。

2. 掌握 PowerLanguage 的骨架:信号、指标、函数与执行模型

开始写代码之前,你至少要搞懂 PowerLanguage 里最基本的几个代码单元——信号(Strategy)、指标(Indicator)和函数(Function)。很多人把它们混在一起用,结果代码越写越乱。

2.1 信号、指标、函数:三个代码单元各自该干什么

信号是策略的入口,里面会写买卖条件,编译后加载到图表上才会产生交易信号。指标是用来在图表上画线的可视化工具,比如均线、布林带,它们本身不会交易。函数是一段可以复用的计算逻辑,你可以把复杂计算封装成函数,然后在信号或指标里调用。

为什么要分开?因为职责清晰。举个例子,你想在策略里使用 ATR 止损,ATR 的计算逻辑可以写成一个函数。这个函数既可以在你的策略里调用,也可以在别的品种、别的策略里复用。如果直接把 ATR 公式写进策略,未来改动的时候就要逐个策略去改,很容易漏。

在 PowerLanguage 里,一个函数文件通常长这样:

// 自定义函数:计算过去N根K线的最高高点 Function: HighestHigh Input: Length(NumericSimple); HighestHigh = Highest(High, Length);

这只是个示例,实际项目中,函数可以写得非常复杂,比如计算各种形态特征、计算不同市场状态下的动态仓位。函数写好后,在策略里用value = HighestHigh(20)这样的形式调用即可。

2.2 执行模型与 Bar 的推进方式:为什么信号出现在“未来”是个大问题

PowerLanguage 策略在每个 Bar 上从第一个价格数据开始往下执行,这看起来很简单,实际上隐藏着不少细节。最核心的一点是:你的代码到底是在当前 Bar 的收盘时执行,还是在盘中实时执行?你使用的价格信息是当前 K 线的收盘价,还是已经确认的前一根 K 线的数据?

有一个非常容易踩的坑——未来函数。比如你写“如果收盘价大于开盘价,就在当前 K 线开盘价买入”,这里用到了当前 K 线的开盘价来下单,但状态是在 K 线中途运行时判断的,开盘价是已知的,收盘价是未知的。这种情况下,历史回测里它看起来没问题,因为历史 K 线已经是固定的,回测引擎可以“回头”知道这根 K 线的最终收盘价。但实盘中,收盘价还没有走完,信号就会变成闪烁信号,时有时无。

为了避免这个问题,我几乎统一采用“下一根 K 线开盘价入场”的模式。即在当前 K 线上判断条件成立,然后Buy语句设置Next Bar执行。这样当前 K 线的收盘价等数据都是确定的,程序在下一根 K 线开盘时成交,和实盘行为基本一致。

另一个相关的概念是 Bar 状态。PowerLanguage 里有BarStatus这个系统变量,它告诉你当前这根 Bar 的状态是刚开始、正在更新还是已经收盘。如果你打算做盘中实时交易,必须处理 BarStatus 的变化,确保逻辑只在 K 线收盘后触发判断,或者在盘中实时数据反复变化时不会被重复触发。这一点在日内策略中尤其重要。

2.3 用函数思维降低复杂度:可复用的不只是计算

行外人以为写好买卖信号就完事了,其实真正让一个策略从“能跑”变成“专业级”的,往往是一些不起眼的辅助逻辑:仓位管理、交易时间过滤、最大连续亏损暂停、多品种加载时的保护机制。这些逻辑如果全部硬编码在信号里,代码会变得极其臃肿。

我的做法是把“风险管理”单独做成一个函数。比如我要写一个根据账户权益、ATR 波动来决定下单手数的函数:

Function: RiskBasedSize Input: RiskPercent(NumericSimple), ATRValue(NumericSimple); var: Size(0); If ATRValue > 0 then Size = IntPortion((RiskPercent * 10000 * 0.01) / ATRValue) Else Size = 1; RiskBasedSize = Size;

代码里的数字只是演示,意思是:你愿意承受多少资金回撤,除以当前每一点波动对应的金额,就能倒推出手数。这个思路可以让你在趋势行情里自动减少仓位,在震荡行情里自动降低风险敞口。把它写成函数后,任何策略都能直接调用。

注意:PowerLanguage 函数体里的If...Then...Else和信号里的写法是一样的,但函数返回值通过函数名赋值完成,这一点和普通编程语言不太一样,我第一次写的时候也经常搞混。

3. 从零实现一个趋势跟踪策略:完整实操记录

理论说多了空泛,我把一个实际开发的策略拉出来,从需求定义到代码实现到回测设置,完整走一遍。这个策略不复杂,但它包含了多数趋势策略的基础模块:趋势判断、入场过滤、动态止损、仓位管理。

3.1 策略规格定义:先定边界,再写代码

我的目标是写一个适用于某商品期货日线级别的趋势策略。具体规则如下:

  • 使用两条均线的方向判断趋势,快线上穿慢线时启动多头关注,下穿时启动空头关注。
  • 加入一个波动过滤条件:只有当当前 K 线振幅大于过去 N 日平均振幅的一定比例时,才允许入场。这个过滤可以避免在低波动时期反复进出。
  • 止损采用 ATR 动态止损,初始止损为入场价格减去 2 倍 ATR。
  • 盈利保护采用移动止损,从最高盈利回撤 1.5 倍 ATR 时离场。

我把这些规则整理成表格:

模块参数默认值说明
趋势判断FastLen, SlowLen10, 30均线周期
波动过滤FilterATRPeriod, FilterMult14, 0.5ATR 周期与放大倍数
初始止损InitStopATR2入场价减去 2 倍 ATR
移动止损TrailATR1.5最高点回撤 1.5 倍 ATR 离场

先定规格有一个好处:你在写代码时不会被“还有更好的参数”这个念头干扰。初始参数是不是最优没关系,先让整套逻辑跑通,后面再优化。

3.2 核心代码实现与关键语句说明

下面是我实际使用的策略框架,代码做了简化,但保留了完整结构:

// 趋势跟踪策略:双均线方向 + ATR波动过滤 + 动态止损 inputs: FastLen(10), SlowLen(30), FilterATRPeriod(14), FilterMult(0.5), InitStopATR(2), TrailATR(1.5), ContractSize(1); variables: FastAvg(0), SlowAvg(0), AvgRange(0), MyStopPrice(0), MyTrailStop(0), MaxSinceEntry(0), MAUp(false), MADown(false); FastAvg = AverageFC(Close, FastLen); SlowAvg = AverageFC(Close, SlowLen); AvgRange = AvgTrueRange(FilterATRPeriod); MAUp = FastAvg > SlowAvg; MADown = FastAvg < SlowAvg; // 入场逻辑:趋势方向确认 + 波动幅度的过滤 If MarketPosition = 0 and MAUp and Close > Close[1] + AvgRange * FilterMult then Buy ContractSize contracts next bar at market; If MarketPosition = 0 and MADown and Close < Close[1] - AvgRange * FilterMult then SellShort ContractSize contracts next bar at market; // 持仓后维护止损价格 If MarketPosition > 0 then begin If MyStopPrice = 0 or EntryPrice + InitStopATR * AvgRange < MyStopPrice then begin MyStopPrice = EntryPrice - InitStopATR * AvgRange; end; If High > MaxSinceEntry then MaxSinceEntry = High; MyTrailStop = MaxSinceEntry - TrailATR * AvgRange; If MyTrailStop > MyStopPrice then MyStopPrice = MyTrailStop; Sell next bar MyStopPrice stop; end; If MarketPosition < 0 then begin If MyStopPrice = 0 or EntryPrice - InitStopATR * AvgRange > MyStopPrice then begin MyStopPrice = EntryPrice + InitStopATR * AvgRange; end; If Low < MaxSinceEntry then MaxSinceEntry = Low; MyTrailStop = MaxSinceEntry + TrailATR * AvgRange; If MyTrailStop < MyStopPrice then MyStopPrice = MyTrailStop; BuyToCover next bar MyStopPrice stop; end;

这段代码有几个关键点:

第一,AverageFC、AvgTrueRange是 PowerLanguage 内置函数,用来计算均线和真实波幅。之所以用AverageFC而不是Average,是因为前者对刚启动策略时前几根 K 线的处理方式更友好,能避免出现无效值。

第二,入场用了next bar at market,意味着所有判断都基于当前 K 线收盘后的数据,下根 K 线开盘时成交,没有未来函数问题。

第三,止损单用了Sell next bar MyStopPrice stop,这是限价止损单。它在回测中模拟的是“如果价格触及止损价就退出”,但实际成交价可能比止损价更差,比如跳空低开的情况。这个问题后面展开讲。

第四,MaxSinceEntry分别记录多头和空头持仓期间的极值,用来计算移动止损。注意这个变量必须在换仓时重置,我的代码里在入场时MyStopPrice = 0就是一种重置方式。

3.3 回测环境设置:别让默认参数掩盖问题

策略写完后,加载到图表只是第一步。真正决定回测可信度的,是数据质量和交易成本设置。

先说数据。Multicharts 展示的数据和回测数据来源必须一致,比如你加载了连续合约,但没有设置主力合约换月规则,回测结果里会出现换月跳空带来的虚假盈利或亏损。我建议使用经复权处理的连续数据,并且至少加载三年以上的历史数据,这样回测结果才具有基本的统计意义。

再说交易成本。很多人回测时把手续费和滑点设成 0,看到收益曲线很漂亮就兴奋,实盘一跑立刻原形毕露。我个人的经验是:期货策略每笔交易至少计入一个最小变动价位作为滑点,手续费按照实际标准填入。哪怕这个设置让收益曲线难看一点,它才是你真正能拿到的收益。

设置完这些,回测出的绩效报告才有分析价值。下面这张表是我用上述策略在一段历史数据上跑出的模拟结果示例:

指标数值(示例)
总收益34.2%
年化收益15.8%
最大回撤11.5%
胜率42.6%
盈亏比1.8
总交易次数36
夏普比率1.2

胜率不到一半,但盈亏比大于 1,这符合趋势跟踪的典型特征:大部分交易小亏,少数几笔大赚。如果你看到一个趋势策略胜率 70% 以上,要小心它是不是参数过度拟合了。

4. 回测和优化:数字会说谎,你要学会辨别

策略开发到了回测环节,最大的风险不是代码报错,而是数字看起来很完美。回测的目的不是找到一个历史收益最高的参数组合,而是找到一个在多种市场状态下都不会崩掉的稳健策略。

4.1 先看回撤,再看收益;先看交易逻辑,再看参数

很多新手拿到绩效报告先看总收益和年化收益,我反而先看最大回撤和回撤持续时间。原因很简单:收益是结果,回撤是过程。你在实盘里能不能拿住仓位,取决于你对回撤的承受能力。一个回撤 40% 的策略,即使年化收益 30%,绝大多数人也会在半路心态崩溃、手动干预,最终拿不到那个收益。

还要重点看交易次数。如果三年才 10 笔交易,你的策略逻辑几乎没有统计意义,任何收益都可能来自运气。一般来说,至少要有 30 笔以上的交易样本,绩效数据才稍微可信一点。

4.2 参数优化:粗扫还是细扫?如何避免过拟合

Multicharts 提供了内置的参数优化功能,可以自动扫描一组参数组合并给出最优解。优化本身不难,难的是防止过拟合。

我建议分两步走:

第一步,粗优化。每个参数用较大的步长扫描,比如均线周期从 5 到 60,步长 5。这个阶段是为了找到参数的大致有效区域。如果你发现收益最高的参数集中在某个很窄的区间,周围参数绩效急剧下降,说明这个策略对参数很敏感,稳定性差。相反,如果最优参数周围一大片区域绩效都差不多,说明策略逻辑本身是稳健的。

第二步,细优化。在粗优化选定的区域内,缩小步长扫描,找到相对更优的组合。但记住,细优化得到的“最优参数”只适合作为参考,不要迷信它的具体数值。真正要观察的是参数曲面是否平滑,有没有明显的“孤峰”。

下面是一个典型的参数稳定性对比思路:

参数区域表现特征结论
一片高原相邻参数绩效接近策略稳健,可以选用中间值
孤立尖峰只有单个参数绩效极高过拟合风险大,不应采用

4.3 样本内与样本外:唯一的诚实检验

不管你用不用的优化,有一件事是所有稳健策略都必须做的——样本外验证。具体做法是:把历史数据切成两段,前段用来调参(样本内),后段完全放着不动(样本外)。等你在样本内把参数确定好了,再在样本外跑一次,看绩效是否合理。

这种方法非常朴素,却是识别过拟合最有效的方式。我做策略时有个习惯:样本外绩效只要不低于样本内的 60%,就认为逻辑基本靠谱;如果样本外收益变成负数或者回撤离谱,我会回头审视策略逻辑,而不是继续调整参数去追样本外的曲线。

注意:很多人在回测时用“滚动优化”的方式,即每周或每月重新优化一次参数。这种做法在市场状态变化快速时有一定价值,但要小心它可能引入更多的过拟合。如果你刚接触程序化交易,我更推荐固定参数加合理过滤条件的简单做法,先把逻辑跑稳定了再考虑复杂的自适应机制。

5. 常见问题与排查技巧实录

这一部分是我在实际开发中反复遇到的坑,每一个都曾经让策略结果“看起来不对”或者“实盘和回测不一致”。我按问题类型整理了排查思路。

5.1 信号在复盘时出现,但实盘时闪烁

这是新手最常见的问题。表现是:你在历史图表上看到某个点有买入信号,信号很明确,复盘觉得很完美,但跑实盘模拟时同一个地方信号时有时无。

根本原因是使用了当前未确认的价格数据进行判断。最常见的三种写法:

  • 用Close做判断,但没有等 K 线收盘。
  • 用High、Low中的极值作为触发条件。
  • 在IntrabarPersist变量没有正确重置的情况下,盘中反复触发信号。

排查方法是逐条检查每个条件,确认使用的价格是否已经固定。如果你的策略需要在盘中实时判断,可以用BarStatus = 2来判断当前 Bar 是否已经收盘,或者干脆全部统一为下根 K 线开盘入场。

5.2 未来函数:回测结果异常好,好到让你怀疑人生

下面这些话如果在你心里出现过,大概率触发了未来函数:

  • “这个策略三年收益十倍,几乎没有回撤。”
  • “每次都能精确买在最低点,卖在最高点。”

一个经典的未来函数写法是:If Close > Highest(High, 10)[1] Then Buy at Low。这里指的是当前 K 线 close 突破前高,但买在“Current Low”——当前 K 线的最低价。在历史回测中,这根 K 线已经走完了,所以引擎知道这根 K 线的最低点在哪里,然后把成交价给你定在那个最低点。实盘里呢?当前 K 线还没有走完,你根本不知道最低点在哪,这根 K 线可能先冲高再回落到触发条件,然后你买入的位置完全不是回测显示的位置。

排查方法:检查所有 Buy 和 Sell 语句中的价格设置。只要成交价是当前 Bar 的 High、Low、Close 或 Open 中的某个值,就要非常警觉。安全的做法是在成交价格位置使用Next Bar。

5.3 同一天反复开平仓,手续费吃掉所有利润

有一个隐藏很深的问题:PowerLanguage 策略在同一个 Bar 内,如果没有额外的限制,可能会出现“开仓-平仓-再开仓”的循环。比如入场条件是金叉,出场后价格又短期触发金叉,如果你没有限制每天只能交易一次,策略就会频繁进出。

解决方式有两种:

一种是在代码里加一个EntriesToday(Date)函数判断,限制每天最多开仓一次。

另一种是加一个最短持仓时间过滤,比如刚平仓后 N 根 K 线内不允许再次开仓。

我在实际开发中通常两者结合:日内次数限制一定加,持仓时间过滤视品种而定。毕竟频繁交易本身不是问题,问题是你是否统计了相应的成本,以及这种交易逻辑是否真的有正期望。

5.4 回测止损价和实盘止损价的差距:滑点与跳空

止损单在回测中看起来没问题,但实盘中经常会出现实际亏损大于止损价的情况。原因很简单:止损限价单只能保证价格触发,不能保证成交价。如果开盘直接跳空,向着不利方向跳,止损单成交价会远差于止损价。

应对方式上,除了我在回测设置里提到的“至少计入一个最小变动价位的滑点”,还可以在策略代码里增加一个开盘跳空保护。比如:

// 如果开盘价直接低于止损价,则以开盘价立即平仓 If MarketPosition > 0 and Open < MyStopPrice then Sell this bar at Open;

这种处理在编程上多一个条件,但能让策略在极端行情下的表现更贴近实际。否则回测里的止损亏损是 2 个 ATR,实盘可能是 5 个 ATR,绩效差距会非常大。

5.5 多品种加载时,策略参数需要单独设置

当你把策略从一个品种复制到另一个品种时,有没有发现绩效差异巨大?别急着怀疑策略写错了,要先区分是“逻辑不适配”还是“参数没适配”。

不同品种的波动率、交易时间、最小变动价位、涨跌停规则完全不同。同一个 ATR 止损参数,在农产品上可能是合理的一日波动,在股指上可能是半根 K 线的噪音。我的做法是:每个品种单独做一次参数粗优化,把参数范围记录下来,形成一个品种参数表,而不是所有品种用同一个参数跑天下。

另外,有些函数在不同合约上的返回值可能不稳定,比如成交量类的函数在连续合约和指数合约上的意义完全不同。加载到新品种之前,先确认你的策略里没有用到和品种特性强相关的数据。

5.6 策略加载后没有信号?先检查计算用的 K 线数量

还有一个非常基础但容易忽略的问题:策略加载到图表后,前几十根 K 线一直没有任何信号,不是策略有问题,而是它需要“预热”。以双均线为例,慢线周期是 30,那么前 30 根 K 线根本算不出两条均线。如果策略里还有交易次数过滤、波动率统计等逻辑,预热期会更长。

在 Multicharts 的格式设置里,有一个“Maximum number of bars”的选项,它可以控制策略回测时最多使用多少根历史 K 线。如果你发现策略从某根 K 线开始突然出现密集信号,先检查是不是设置里把这个数值设得太小,导致前面的数据没有被计算。

最后再分享一个小习惯:把回测日志当成策略的一部分来维护

写代码只是第一步,真正花时间的往往是那些看不见的细节。我后来养成一个习惯:每次回测之后,都把这一次使用的参数、品种、数据区间、成本设置、绩效摘要记录下来,顺便写一句这次改了什么逻辑、为什么要改。

一开始我嫌麻烦,直接改参数就跑了,结果过了两周回头看,根本想不起来当初那个收益曲线是怎么来的,更别说对比不同版本之间的差异了。后来老老实实做日志,才发现很多“这次优化有效”的判断,其实只是市场行情变了而已。

如果你也打算长期做量化策略开发,建议从第一天就建立一个简单的表格,记录策略版本、参数、样本内绩效、样本外绩效、以及当天的市场环境。这个东西在行情好的时候看起来没价值,但等到策略遇到连续回撤、你开始怀疑要不要修改的时候,它会帮你保持冷静——你手里有据可查,而不是凭感觉做事。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进