概述
开发指标、智能交易系统和脚本时,开发人员往往需要创建大量与交易策略没有直接关系的各种代码片段。 例如,这样的代码也许关注的是智能交易系统的操作时间表:每天、每周或每月。 如是结果,我们将创建一个独立的项目,该项目可以借助简单的界面与交易逻辑和其他组件进行交互。 只需花费少许精力,此规程即可在未来的智能交易系统和指标的定制开发中复用,且无需每次都进行严谨的修改。在本文中,我们尝试将此复用模块系统方法应用于搭建智能交易系统。 我们还将考察一种有趣的可能性,它会伴随我们的工作而出现。 本文适用于初学者。
惯例是什么样的呢?
我们尝试理解一款智能交易系统的模样,以及它可以包含哪些部件/组件/模块。 我们在哪里可以取得这些组件? 答案很简单明了 — 在应用程序开发过程中,程序员必须创建各种组件,而这些组件通常具有相似或相同的功能。
显然,无需每次都从头实现相同的功能,例如尾随停止功能。 一般情况下,尾随停止可以执行功能类似,且不同的智能交易系统确拥有相似的输入。 所以,程序员创建一次尾随功能代码,以后只需少量代价便可将其插入所需的 EA 中。 这同样适用于许多其他组件,包括交易时间表,各种新闻过滤器和包含交易功能的模块,等等。
因此,我们即拥有了一套构造集合,有基于此,我们能够复用单独的模型/模块搭建智能交易系统。 各模型之间以及智能交易系统的“核心”(即制定决策的“策略”)能够彼此交换信息。 我们来展示一下各模型之间的可能关系:
由此产生的方案相当令人困惑。 尽管它仅展示了三个模型和两个 EA 响应程序的交互:OnStart 和 OnTick。 在更复杂的智能交易系统中,内部绑定将更加复杂。 这样的智能系统难于管理。 甚至于,如果需要剔除任何一个模块,或需要添加一个额外的模块,都会导致极大的困难。 进而,初始调试和故障排除也不轻松。 造成这种困难的实际原因之一,即没有运用恰当的系统方法来设计绑定。 在必要时,禁止模块之间以及与 EA 响应程序进行相互通信,且会出现某种顺序:
- 所有模块都在 OnInit 中创建。
- EA逻辑包含在 OnTick 当中。
- 模块仅与 OnTick 交换信息。
- 如有必要,将在 OnDeinit 中删除模块。
这种简单的解决方案可以迅速产生积极的效果。 单独的模块更易于连接/断连、调试和修改。 如果在一个响应程序中实现绑定,来替代在整个 EA 代码中的不同位置添加绑定,则 OnTick 中的逻辑将变得更加易于维护和改进。
略微的设计更改提供了更清晰的 EA 结构,使之变得更加直观。 新结构类似于“观察者”设计范式的应用结果,尽管该结构本身与该范式不尽相同。 我们看看如何进一步改进设计。
实验 EA
我们需要一个简单的智能交易系统来检验我们的思路。 我们不需要非常复杂的 EA,因为我们现在的目的只是演示功能。 如果之前的烛条看跌,EA 将开立一笔空头卖单。 智能交易系统将使用模块化结构进行设计。 第一个模块实现交易功能:
class CTradeMod { public: double dBaseLot; double dProfit; double dStop; long lMagic; void Sell(); void Buy(); }; void CTradeMod::Sell() { CTrade Trade; Trade.SetExpertMagicNumber(lMagic); double ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK); Trade.Sell(dBaseLot,NULL,0,ask + dStop,ask - dProfit); } void CTradeMod::Buy() { CTrade Trade; Trade.SetExpertMagicNumber(lMagic); double bid = SymbolInfoDouble(Symbol(),SYMBOL_BID); Trade.Buy(dBaseLot,NULL,0,bid - dStop,bid + dProfit); }
该模块在实现之时类中含有开放的字段和方法。 迄今为止,我们在模块中不需要实现 Buy() 方法,但稍后将需要它。 各个字段的值应清楚;它们用于手数、交易价位和魔幻数字。 如何使用该模块:创建该模块,并在出现入场信号时调用 Sell() 方法。
EA 中将包含另一个模块:
class CParam { public: double dStopLevel; double dFreezeLevel; CParam() { new_day(); }//EA_PARAM() void new_day() { dStopLevel = SymbolInfoInteger(Symbol(),SYMBOL_TRADE_STOPS_LEVEL) * Point(); dFreezeLevel = SymbolInfoInteger(Symbol(),SYMBOL_TRADE_FREEZE_LEVEL) * Point(); }//void new_day() };我们更详尽地考察这个模块。 这是一个辅助模块,其中包含其他模块和 EA 响应程序用到的各种参数。 您也许遇到的代码像是这样:
... input int MaxSpread = 100; ... OnTick() { if(ask - bid > MaxSpread * Point() ) return; .... }
显然,此片段无效。 如果将所有需要更新或转换的输入(和其他)参数添加到一个单独的模块中(在此我们处理 MaxSpread * Point()),我们将保持全局空间整洁,并可以有效地控制它们的状态,如同上述 CParam 模块中使用 Stops_level 和 Frozen_level 值所完成的那样。
也许更好的解决方案是提供特殊的取值器(getters),而不是开放模块字段。 此处,以上解决方案不过是为了简化代码。 对于真实项目,最好使用取值器。
另外,我们可以令 CParam 模块作为特例,不仅允许 OnTick() 响应程序访问该模块,还允许所有其他模块和响应程序访问此模块。
这是输入块和 EA 响应程序:
input double dlot = 0.01; input int profit = 50; input int stop = 50; input long Magic = 123456; CParam par; CTradeMod trade; int OnInit() { trade.dBaseLot = dlot; trade.dProfit = profit * _Point; trade.dStop = stop * _Point; trade.lMagic = Magic; return (INIT_SUCCEEDED); } void OnDeinit(const int reason) { } void OnTick() { int total = PositionsTotal(); ulong ticket, tsell = 0; ENUM_POSITION_TYPE type; double l, p; for (int i = total - 1; i >= 0; i--) { if ( (ticket = PositionGetTicket(i)) != 0) { if ( PositionGetString(POSITION_SYMBOL) == _Symbol) { if (PositionGetInteger(POSITION_MAGIC) == Magic) { type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); l = PositionGetDouble(POSITION_VOLUME); p = PositionGetDouble(POSITION_PRICE_OPEN); switch(type) { case POSITION_TYPE_SELL: tsell = ticket; break; }//switch(type) }//if (PositionGetInteger(POSITION_MAGIC) == lmagic) }//if (PositionGetString(POSITION_SYMBOL) == symbol) }//if ( (ticket = PositionGetTicket(i)) != 0) }//for (int i = total - 1; i >= 0; i--) if (tsell == 0) { double o = iOpen(NULL,PERIOD_CURRENT,1); double c = iClose(NULL,PERIOD_CURRENT,1); if (c < o) { trade.Sell(); } } }
EA 在 OnInit() 响应程序中初始化模块,然后仅在 OnTick() 响应程序里访问它们。 在 OnTick() 中,EA 会遍历持仓,检查在期望的价位是否已经开仓。 如果尚未开仓,则若有信号,EA 会开仓。
请注意,OnDeinit(const int reason) 响应程序当前为空。 所创建模块则因不需要而被显式删除。 另外,尚未用到 CParam,因为开仓检查尚未执行。 如果已执行了此类检查,则 CTradeMod 模块可能需要访问 CParam 模块,而开发人员则需完成上述特例,并允许访问 CParam。 不过,在我们的情况下不需要这样做。
我们更详细地观察这一刻。 CTradeMod 模块可能需要来自 CParam 的数据,以便查验止损和止盈价位,以及持仓量。 但这可在决策点执行相同的检查:如果价位和交易量不符合要求,则不要开仓。 因此,将检查移至 OnTick() 响应程序。 对于我们的示例,由于在输入参数中指定了交易价位和交易量等数值,因此可以在 OnInit() 响应程序中执行一次检查。 如果检查不成功,则整个 EA 的初始化则因错误结束。 因此,CTradeMod 和 CParam 模块可以独立运行。 这与大多数智能交易系统有关:独立模块通过 OnTick() 响应程序进行操作,彼此之间一无所知。 但是,在某些情况下无法观察到这种情况。 我们稍后会研究它们。
进行改进
要定位的第一个难题是在 OnTick() 响应程序中遍历大量持仓。 如果开发人员希望的话,则需要这段代码:
- 如果入场信号持续有效,应避免重复开仓,
- 在暂停或操作中断后,启用 EA 检测已下订单,
- 启用 EA 收集当前统计信息,例如持仓总量,或最大回撤。
如若智能交易系统正在使用网格和均摊技术,则此循环会变得更加庞大。 此外,它要求 EA 必须使用虚拟交易价位。 因此,最好根据这段代码创建一个单独的模块。在最简单的情况下,模块将检测含有特定魔幻数字的持仓,并将这些持仓通知 EA。在更复杂的情况下,该模块可以实现为包含各种简单模块的内核,譬如日志记录或统计信息收集模块。 在这种情况下,EA 将具有树型结构,其 OnInit() 和 OnTick() 响应程序位于树的底部。 这样的模块如下所示:
class CSnap { public: void CSnap() { m_lmagic = -1; m_symbol = Symbol(); } virtual void ~CSnap() {} bool CreateSnap(); long m_lmagic; string m_symbol; };所有字段再次开放。 实际上,该代码有两个字段:EA 运行时的魔幻数字,和所要处理的品种名称。 如有必要,可以在 OnInit() 响应程序中设置这些字段的值。 操作的主要部分由 CreateSnap() 方法执行:
bool CSnap::CreateSnap() { int total = PositionsTotal(); ulong ticket; ENUM_POSITION_TYPE type; double l, p; for (int i = total - 1; i >= 0; i--) { if ( (ticket = PositionGetTicket(i)) != 0) { if (StringLen(m_symbol) == 0 || PositionGetString(POSITION_SYMBOL) == m_symbol) { if (m_lmagic < 0 || PositionGetInteger(POSITION_MAGIC) == m_lmagic) { type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); l = PositionGetDouble(POSITION_VOLUME); p = PositionGetDouble(POSITION_PRICE_OPEN); switch(type) { case POSITION_TYPE_BUY: // ??????????????????????????????????????? break; case POSITION_TYPE_SELL: // ??????????????????????????????????????? break; }//switch(type) }//if (lmagic < 0 || PositionGetInteger(POSITION_MAGIC) == lmagic) }//if (StringLen(symbol) == 0 || PositionGetString(POSITION_SYMBOL) == symbol) }//if ( (ticket = PositionGetTicket(i)) != 0) }//for (int i = total - 1; i >= 0; i--) return true; }
代码很简单,但是有一些问题。 模块应如何以及在何处传递获得的信息? 在代码片段中最后带问号的一行应该写什么? 似乎看起来很简单。 在 OnTick() 响应函数中调用 CreateSnap():此方法执行所需的任务,并将结果保存在 CSnap 类的字段中。 然后,响应程序检查字段并得出结论。
好极了,解决方案可在最简单的情况下实现。 如果我们需要分别处理每笔持仓的参数,例如计算加权平均值,该怎么办? 在这种情况下,需要一种更变通的方法,其中将数据转发到后续某个对象以便进一步处理。 为此目的,必须在 CSnap 中提供指向此类对象的特殊指针:
CStrategy* m_strategy;为该文件赋值的方法:
bool SetStrategy(CStrategy* strategy) { if(CheckPointer(strategy) == POINTER_INVALID) return false; m_strategy = strategy; return true; }//bool SetStrategy(CStrategy* strategy)
之所以选择对象名称 CStrategy,是因为可以在该对象中制定入场决策,以及其他决策。 所以,对象可以定义整个 EA 的策略。
现在,CreateSnap() 方法中的 “switch” 将如下所示:
switch(type) { case POSITION_TYPE_BUY: m_strategy.OnBuyFind(ticket, p, l); break; case POSITION_TYPE_SELL: m_strategy.OnSellFind(ticket, p, l); break; }//switch(type
如有必要,可以轻松地为代码添加条件开关语句,用于挂单并调用相应的方法。 另外,可以容易地修改该方法,令其能够收集更多数据。 CreateSnap() 可以实现为虚拟方法,供 CSnap 类的衍生类实现。 但在我们的情况下不必如此,所以我们将采用更简单的代码版本。
此外,指向此类对象的指针(我们的实现将其作为 CStrategy* 指针)不仅只对当前模块有用。 EA 操作逻辑里任何执行主动计算的模块都可能需要与其连接。 所以,我们在基类中提供一个特殊的字段和一个初始化方法:
class CModule { public: CModule() {m_strategy = NULL;} ~CModule() {} virtual bool SetStrategy(CStrategy* strategy) { if(CheckPointer(strategy) == POINTER_INVALID) return false; m_strategy = strategy; return true; }//bool SetStrategy(CStrategy* strategy) protected: CStrategy* m_strategy; };此外,我们将创建从 CModule 继承的模块。 在某些情况下,我们也许会有累赘的代码。 但这一瑕疵会由那些确实需要这种指针的模块来弥补。 如果所有模块都不需要这个指针,只需在模块中不去调用 SetStrategy(...) 方法即可。 基准模块类还可用于安置尚不为我们所知的其他字段和方法。 例如,以下方法(本例中未实现)可能很有用:
public: const string GetName() const {return m_sModName;} protected: string m_sModName;
该方法返回可用于故障排除、调试或在信息面板中用到的模块名称。
现在我们看看如何实现重要的 CStrategy 类:
智能交易系统策略
如早前所述,这必须是能制定入场决策的对象。 这样的对象还可以做出离场、修改和部分平仓的决策。 此即为什么调用该对象的原因。 显然,它不能作为模块来实现:在每个智能交易系统中,决策对象都是独立的,这一点至关重要。 否则,最终的 EA 将与之前开发的 EA 雷同。 从而,我们无法像开发模块那样只开发一次此类对象,然后将其插入所有 EA 之中。 但这不是我们的目的。 我们依据已知的事实开始基类的开发:
class CStrategy { public: virtual void OnBuyFind (ulong ticket, double price, double lot) = 0; virtual void OnSellFind (ulong ticket, double price, double lot) = 0; };// class CStrategy
简单至此。 我们在 CreateSnap() 检测到所需持仓时加入了两个方法调用。 CStrategy 实现为抽象类,而方法被声明为虚函数。 这是因为不同的智能交易系统处理对象的逻辑将有所不同。 因此,基类只能用于继承,而其方法将被覆盖。
现在我们需要将 CStrategy.mqh 文件添加到 CModule.mqh 文件当中:
#include "CStrategy.mqh"
之后,可以认定 EA 框架已完成,我们继续进一步的强化和改进。
智能交易系统策略改进
使用虚方法,CSnap 对象访问 CStrategy 对象。 但是 CStrategy 对象中必须有其他方法。 策略对象必须能够做出决策。 因此,如果检测到相应的信号,它应提供入场建议,然后执行入场。 我们需要一个方法,强制 CSnap 对象调用其 CreateSnap() 方法。 我们向 CStrategy 类添加一些方法:
virtual string Name() const = 0; virtual void CreateSnap() = 0; virtual bool MayAndEnter() = 0; virtual bool MayAndContinue() = 0; virtual void MayAndClose() = 0;
这是一个非常条件性的清单,可以针对特定的 EA 进行更改或扩展。 这些方法的作用:
- Name() — 返回策略名称。
- CreateSnap() — 调用 CSnap 对象的同名方法。
- MayAndEnter() — 检查是否有入场信号,如有信号则入场。
- MayAndContinue() — 检查是否有入场信号,如有信号则再次入场。
- MayAndClose() — 检查是否有离场信号,如有信号则所有持仓平仓。
当然,这张清单还远远不够完整。 它缺少一个非常重要的方法,即策略初始化方法。 我们的目的是实现指向 CStrategy 对象中的所有模块,因此 OnTick() 和其他响应程序仅需访问 CStrategy 对象,而不必知晓其他模块的存在。 所以,需要将模块指针添加到 CStrategy 对象。 我们不能简单地提供相应的开放字段,并在 OnInit() 响应函数中对其进行初始化。 稍后将对此进行解释。
取而代之,我们添加一个初始化方法:
virtual bool Initialize(CInitializeStruct* pInit) = 0;利用初始化对象 CInitializeStruct,它包含所需的指针。 该对象在 CStrategy.mqh 中的描述如下:
class CInitializeStruct {};
它是一个空类,旨在继承,类似于 CStrategy。 我们已完成了准备工作,并可以进入真正的智能交易系统。
实际用例
我们根据一个非常简单的逻辑创建一个演示版的智能交易系统:如果前一根烛条看跌,则以固定的止盈和止损点数开立空头持仓。 在前一笔持仓未平之前,不应开新仓。
所有模块我们几乎都已准备就绪。 我们研究一下从 CStrategy 派生新类:
class CRealStrat1 : public CStrategy { public: static string m_name; CRealStrat1(){}; ~CRealStrat1(){}; virtual string Name() const {return m_name;} virtual bool Initialize(CInitializeStruct* pInit) { m_pparam = ((CInit1* )pInit).m_pparam; m_psnap = ((CInit1* )pInit).m_psnap; m_ptrade = ((CInit1* )pInit).m_ptrade; m_psnap.SetStrategy(GetPointer(this)); return true; }//Initialize(EA_InitializeStruct* pInit) virtual void CreateSnap() { m_tb = 0; m_psnap.CreateSnap(); } virtual bool MayAndEnter(); virtual bool MayAndContinue() {return false;} virtual void MayAndClose() {} virtual bool Stop() {return false;} virtual void OnBuyFind (ulong ticket, double price, double lot) {} virtual void OnBuySFind (ulong ticket, double price, double lot) {} virtual void OnBuyLFind (ulong ticket, double price, double lot) {} virtual void OnSellFind (ulong ticket, double price, double lot) {tb = ticket;} virtual void OnSellSFind(ulong ticket, double price, double lot) {} virtual void OnSellLFind(ulong ticket, double price, double lot) {} private: CParam* m_pparam; CSnap* m_psnap; CTradeMod* m_ptrade; ulong m_tb; }; static string CRealStrat1::m_name = "Real Strategy 1"; bool CRealStrat1::MayAndEnter() { if (tb != 0) return false; double o = iOpen(NULL,PERIOD_CURRENT,1); double c = iClose(NULL,PERIOD_CURRENT,1); if (c < o) { m_ptrade.Sell(); return true; } return false; }
EA 代码很简单,故无需赘言。 我们只考虑其中一部分。 CRealStrat1 类的 CreateSnap() 方法重置保存已有空头持仓票据的字段,并调用 CSnap 模块的 CreateSnap() 方法。 CSnap 模块检查开仓。 如果发现由此 EA 开立的空头持仓,则模块调用 CStrategy 类的 OnSellFind(...) 方法,该指针包含在 CSnap 模块中。 结果则是,CRealStrat1 类的 OnSellFind(...) 方法被调用。 它会再次更改 m_tb 字段值。 MayAndEnter() 方法看到一笔已开持仓,则不会再开新仓。 在我们的 EA 中未使用 CStrategy 基类的其他方法,因此其实现为空。
另一个有趣的问题涉及 Initialize(...) 方法。 此方法在 CRealStrat1 类中添加了指向其他模块的指针,这些也许要单独决策。 CStrategy 类不知道 CRealStrat1 类可能需要哪些模块,因此它使用一个空的 CInitializeStruct 类。 我们将在包含 CRealStrat1 类的文件中添加 CInit1 类(尽管这不是必需的)。 CInit1 继承自 CInitializeStruct:
class CInit1: public CInitializeStruct { public: bool Initialize(CParam* pparam, CSnap* psnap, CTradeMod* ptrade) { if (CheckPointer(pparam) == POINTER_INVALID || CheckPointer(psnap) == POINTER_INVALID) return false; m_pparam = pparam; m_psnap = psnap; m_ptrade = ptrade; return true; } CParam* m_pparam; CSnap* m_psnap; CTradeMod* m_ptrade; };
可在 OnInit 响应程序中创建和初始化类对象,并可将其传递给 CRealStrat1 类对象中的相应方法。 因此,我们已拥有一个由独立对象组成的复杂结构。 但我们可以通过 OnTick() 响应程序中的简单接口操作该结构。
OnInit() 和 OnTick() 响应程序
此为 OnInit() 响应函数,及其可能的全局对象清单:
CParam par; CSnap snap; CTradeMod trade; CStrategy* pS1; int OnInit() { ... pS1 = new CRealStrat1(); CInit1 ci; if (!ci.Initialize(GetPointer(par), GetPointer(snap), GetPointer(trade)) ) return (INIT_FAILED); pS1.Initialize(GetPointer(ci)); return (INIT_SUCCEEDED); }在 OnInit() 响应程序中仅创建一个对象:CRealStrat1 类实例。 它用 CInit1 类对象初始化。 然后在 OnDeinit() 响应程序中销毁该对象:
void OnDeinit(const int reason) { if (CheckPointer(pS1) != POINTER_INVALID) delete pS1; }产生的 OnTick() 响应程序非常简单:
void OnTick() { if (IsNewCandle() ){ pS1.CreateSnap(); pS1.MayAndEnter(); } }
在新烛条开盘时检查现有持仓,然后检查是否有入场信号。 如果有信号且 EA 尚未执行入场,则开一笔新仓。 该响应程序非常简单,因此可在其中轻易添加一些其他“全局”代码。 例如,您可以指令 EA 不要在图表上启动后立即开始交易,而是等待用户单击按钮来确认。
此处未叙述其他一些 EA 函数,但您可在随附的 zip 存档中找到它们。
因此,我们已设计完成一款由独立模块搭建的智能交易系统。 但这还不是全部。 我们看看这种编程方法提供的其他有趣机会。
下一步是什么?
进一步可实现的首件事情涉及模块的动态替换。 一个简单的时间表可被替换为更高级的时间表。 在这种情况下,我们需要将日程表替换为一个对象,而不是向现有的日程表添加属性和方法,那样会令调试和维护变得复杂。 我们可以为单独的模块提供“测试”和“发布”版本,创建一个管理器来控制模块。
但还有一个更有趣的可能性。 依照我们的编程方法,我们可以实现动态 EA 策略替换,在我们的案例中,该策略替换为 CRealStrat1 类。 产生的 EA 将有两个内核,它们执行两种策略,即趋势交易和横盘交易策略。 甚或,可以在亚洲时段增加第三种交易策略。 这意味着我们可以在一个 EA 中实现多个智能交易系统,并动态切换它们。 如何做到这一点:
- 开发一个包含决策逻辑的类,该类派生自 CStrategy 类(如 CRealStrat1)
- 开发一个类来初始化新策略,该类派生自 CInitializeStruct(CInit1)
- 将结果文件连接到项目。
- 在输入参数中添加一个新变量,其作用是在启动时设置激活策略。
- 实现策略切换器,即交易面板。
举例来说,我们在演示版 EA 中添加另一种策略。 再次,我们将准备一个非常简单的策略,以免令代码复杂化。 第一个策略开一笔空头持仓。 这有一个相似的逻辑:如果之前的烛条看涨,则以固定的止盈和止损点数开立多头持仓。 在前一笔持仓未平之前,不应开新仓。 第二个策略代码与第一个策略代码非常相似,可在随附的 zip 存档中找到。 新的策略初始化类也包含在存档中。
我们来研究制作 EA 文件时应做哪些修改,这些应包含输入参数和响应程序:
enum CSTRAT { strategy_1 = 1, strategy_2 = 2 }; input CSTRAT strt = strategy_1; CSTRAT str_curr = -1; int OnInit() { ... if (!SwitchStrategy(strt) ) return (INIT_FAILED); ... return (INIT_SUCCEEDED); } void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { if (id == CHARTEVENT_OBJECT_CLICK && StringCompare(...) == 0 ) { SwitchStrategy((CSTRAT)EDlg.GetStratID() ); } } bool SwitchStrategy(CSTRAT sr) { if (str_curr == sr) return true; CStrategy* s = NULL; switch(sr) { case strategy_1: { CInit1 ci; if (!ci.Initialize(GetPointer(par), GetPointer(snap), GetPointer(trade)) ) return false; s = new CRealStrat1(); s.Initialize(GetPointer(ci)); } break; case strategy_2: { CInit2 ci; if (!ci.Initialize(GetPointer(par), GetPointer(snap), GetPointer(trade)) ) return false; s = new CRealStrat2(); s.Initialize(GetPointer(ci)); } break; } if (CheckPointer(pS1) != POINTER_INVALID) delete pS1; pS1 = s; str_curr = sr; return true; }
策略切换器函数 SwitchStrategy(...) 和 OnChartEvent(...) 响应程序与交易面板连接。 本文未提供其代码,但随附在 zip 存档中。 此外,动态策略管理也不是一项复杂的任务。 根据策略创建一个新对象,删除前一个对象,然后将新指针赋值给该变量:
CStrategy* pS1;
此后,EA 将在 OnTick() 中访问新策略,从而切换到新的操盘逻辑。 对象的层次结构和主要依赖关系如下所示:
该图例未显示交易面板和连接,因为在初始化期间这些都是次要的。 在此阶段,我们可认定我们的任务已完成:智能交易系统已准备就绪,可以进行操作和进一步改进。 运用所用的模块方式,可以在相当短的时间内完成关键改进和修改。
结束语
我们运用标准设计范式观察者(Observer)和外观(Facade)等元素设计完成智能交易系统。 在“设计范式”一书中提供了这些(和其他)范式的完整阐述。 “可复用面向对象软件的基础(Elements of Reusable Object-Oriented Software)”,作者:Erich Gamma,Richard Helm,Ralph Johnson和John Vlissides。 我建议阅读这本书。
本文中用到的程序
# | 名称 |
类型 |
说明 |
---|---|---|---|
1 |
Ea&Modules.zip | 存档 | 含有智能交易系统文件的 zip 存档。 |