自定义品种(符号):实践基础·进阶篇
📊

自定义品种(符号):实践基础·进阶篇

(2/3)· 从等量图到Renko,手把手把经纪商真实报价转成可回测的自定义符号

案例拆解新手友好 第 2/3 篇
很多交易者直接在普通时间帧上硬套策略,却忽略了波动结构被固定周期切歪的问题。MT4/5 都不原生支持等量或 Renko 图,靠肉眼拼离线文件极易漏掉跳动。把真实品种报价转成自定义符号,才是能回测、能挂 EA 的正路。

「Renko 砖形在 MT5 里的时空错位陷阱」

用非交易 EA 把真实即时报价转成自定义品种报价,就能在 MT5 里造出 Renko 砖形图。源品种随便挑,EA 挂在哪个图表、用哪个时间帧都行,但生成的伪图表上不能跑指标或脚本——这是自定义品种备案方式决定的硬限制。 所有 Renko 柱线都落在 M1 时间帧内,因为 MT5 最小时间间隔就是 1 分钟,高波动时砖形可能几秒一根,必须让相邻柱线时间尽量贴近。切到别的周期没意义,每根 M1 的开盘时间对应砖形开盘时间,但收盘时间是假的,得看下一根 M1 的开盘。 麻烦出在 1 分钟内要出好几根砖形的时候。MT5 不允许,EA 就把它们排成相邻的 M1 序列,人为加计数。比如砖形大小 100 点,12:00:00 起 10 秒内走 300 点,本来该在 12:00:00、12:00:05、12:00:10 出三根,EA 却在 12:00、12:01、12:02 生成,正式时间比实际提前。 这种提前会在回放历史时撞车:假设 12:02 又走 100 点要生成开盘 12:02 的砖形,那个时间已被占用。SkipOverflows 参数管这事,默认 false,意思是强制把新柱时间再推后 1 分钟避免重叠;设 true 就直接覆盖,砖形会不准。剧烈行情下可能实时喷出多根“未来”柱,上面例子里那三根会在 12:00:10 就全冒出来,分析和下单都得把这事算进去。 EA 还会造虚拟即时报价,时间等于最后一根砖形开盘时间,唯一用途是触发 OnTick。若把源品种报价原样灌进自定义品种,会破坏 Renko 结构——例如真实时间 12:00:00 的报价对应索引 2 的柱而非当前柱,写进去就毁了历史或报错。慢速行情里报价憋在一根砖形内太久,新报价比第 0 根晚超 1 分钟,发过去会在“未来”长出幻像柱。 主要参数里 RenkoBoxSize 默认 100 点,ShowWicks 默认 true 显示烛芯,OutputSymbolName 留空时自动拼成“EURUSD_T_r100”这种格式。初次跑建议把 StartFrom 设近点,先看实时生成速度再放开全历史。Reset 平时关着,EA 能从最后可用砖形续算;只在柱线生成失败时才开。 EURUSD 从 2019 年起、砖形 100 点,在 H1 图表挂 EA 用默认除 StartFrom 外设置即可。重开 EA 时若 Renko 历史已在,时间帧才起作用,重算从最后一根含砖形的原周期柱时间开始。原 EURUSD H1 跑完会出纯砖形图,加两条均线看更清楚。外汇和贵金属波动剧烈,这种时间错位可能导致信号误读,实盘前务必在策略测试器里复现。

MQL5 / C++
class Renko
{
class="kw">protected:
class="type">bool incrementTime(class="kw">const class="type">class="kw">datetime time);
class="type">void doWriteStruct(class="kw">const class="type">class="kw">datetime dtTime, class="kw">const class="type">class="kw">double dOpen, class="kw">const class="type">class="kw">double dHigh, class="kw">const class="type">class="kw">double dLow, class="kw">const class="type">class="kw">double dClose, class="kw">const class="type">class="kw">double dVol, class="kw">const class="type">class="kw">double dRealVol, class="kw">const class="type">int spread);
class="kw">public:
class="type">class="kw">datetime checkEnding();
class="type">void continueFrom(class="kw">const class="type">class="kw">datetime time);
class="type">void doReset();
class="type">void onTick(class="kw">const class="type">MqlTick &t);
class="type">void updateChartWindow(class="kw">const class="type">class="kw">double bid = class="num">0, class="kw">const class="type">class="kw">double ask = class="num">0);
};
class TickProvider
{
class="kw">public:
class="kw">virtual class="type">bool hasNext() = class="num">0;
class="kw">virtual class="type">void getTick(class="type">MqlTick &t) = class="num">0;
class="type">bool read(Renko &r)
{
class="kw">while(hasNext() && !IsStopped())
{
class="type">MqlTick t;
getTick(t);
r.onTick(t);
}
class="kw">return IsStopped();
}
};

回放与实时两种 tick 供给的实现差异

把历史 tick 灌进 Renko 合成逻辑,靠的是 HistoryTickProvider 这个类。它从 start 到 stop 按 length 秒一段一段用 CopyTicksRange 拉数据,拉空就顺推时间窗口继续,直到覆盖完整个区间或撞到 stop。 fillArray 里有个防坑判断:若 array[0].time_msc 早于请求的 start*1000,说明接口吐了更老的数据,直接把 start 跳到 stop 并置 size=0 丢弃,避免脏 tick 污染砖形。daysCount 只在真正拿到 size>0 的段时累加,配合 Comment 里的 daysCount*100.0/(numberOfDays+1) 能看出回放进度百分比。 CurrentTickProvider 则是另一路:hasNext 用 ready 布尔翻转来模拟节拍,getTick 每次调 SymbolInfoTick(_Symbol,t) 抓最新盘口。它不缓存、不回溯,适合 StopAt==0 时的在线续砖。 外汇与贵金属 tick 回放受经纪商历史深度限制,实际能拉到的天数可能远小于 (stop-start)/86400,建议先在 MT5 用 CopyTicksRange 打点确认可用区间再跑全套。

MQL5 / C++
class HistoryTickProvider : class="kw">public TickProvider
{
class="kw">private:
class="type">class="kw">datetime start;
class="type">class="kw">datetime stop;
class="type">class="kw">ulong length;     class=class="str">"cmt">// in seconds
class="type">MqlTick array[];
class="type">int size;
class="type">int cursor;
class="type">int numberOfDays;
class="type">int daysCount;
class="kw">protected:
class="type">void fillArray()
{
cursor = class="num">0;
do
{
size = CopyTicksRange(_Symbol, array, COPY_TICKS_ALL, start * class="num">1000, MathMin(start + length, stop) * class="num">1000);
Comment("Processing: ", DoubleToString(daysCount * class="num">100.0 / (numberOfDays + class="num">1), class="num">0), "% ", TTSM(start));
if(size == -class="num">1)
{
Print("CopyTicksRange failed: ", GetLastError());
}
else
{
if(size > class="num">0 && array[class="num">0].time_msc < start * class="num">1000) class=class="str">"cmt">// prevent older than requested data returned
{
start = stop;
size = class="num">0;
}
else
{
start = (class="type">class="kw">datetime)MathMin(start + length, stop);
if(size > class="num">0) daysCount++;
}
}
}
class="kw">while(size == class="num">0 && start < stop);
}
class="kw">public:
HistoryTickProvider(class="kw">const class="type">class="kw">datetime from, class="kw">const class="type">long secs, class="kw">const class="type">class="kw">datetime to = class="num">0): start(from), stop(to), length(secs), cursor(class="num">0), size(class="num">0)
{
if(stop == class="num">0) stop = TimeCurrent();
numberOfDays = (class="type">int)((stop - start) / DAY_LONG);
daysCount = class="num">0;
fillArray();
}
class="type">bool hasNext() class="kw">override
{
class="kw">return cursor < size;
}
class="type">void getTick(class="type">MqlTick &t) class="kw">override
{
if(cursor < size)
{
t = array[cursor++];
if(cursor == size)
{
fillArray();
}
}
}
};
class CurrentTickProvider : class="kw">public TickProvider
{
class="kw">private:
class="type">bool ready;
class="kw">public:
class="type">bool hasNext() class="kw">override
{
ready = !ready;
class="kw">return ready;
}
class="type">void getTick(class="type">MqlTick &t) class="kw">override
{
SymbolInfoTick(_Symbol, t);
}
};
class="kw">const class="type">long DAY_LONG = class="num">60 * class="num">60 * class="num">24;
class="type">bool _FirstRun = true;
Renko renko;
CurrentTickProvider online;
class="type">void OnTick(class="type">void)
{
if(_FirstRun)
{
class=class="str">"cmt">// find existing renko tail to supersede StartFrom
class="kw">const class="type">class="kw">datetime trap = renko.checkEnding();
if(trap > TimeCurrent())
{
Print("Symbol/Timeframe data not ready...");
class="kw">return;
}
if((trap == class="num">0) || Reset) renko.doReset();
else renko.continueFrom(trap);
HistoryTickProvider htp((trap == class="num">0 || Reset) ? StartFrom : trap, DAY_LONG, StopAt);
class="kw">const class="type">bool interrupted = htp.read(renko);
_FirstRun = class="kw">false;
if(!interrupted)
{
Comment("RenkoChart(" + (class="type">class="kw">string)RenkoBoxSize + "pt): open ", _SymbolName, " / ", renko.getBoxCount(), " bars");
}
else
{
Print("Interrupted. Custom symbol data is inconsistent - please, reset or class="kw">delete");
}
}
else if(StopAt == class="num">0) class=class="str">"cmt">// process online if not stopped explicitly
{
online.read(renko);
}
}

◍ 双均线交叉EA的信号模块与Renko回测陷阱

两条均线交叉(快线穿慢线做多、穿下做空)属于逆势逻辑,MetaTrader 5 的 MQL 向导能基于标准库直接生成EA骨架,但标准库偏偏没内置双均线交叉信号,得自己写个 Signal2MACross.mqh 挂进向导。 信号类继承 CExpertSignal,内部放两个 CiMA 指标实例,暴露慢周期、快周期、均化方法、价格类型、偏移共5个参数;默认权重100,只认一种形态:快MA上穿慢MA给多、下穿给空。 代码头部那段特殊注释是给 MetaEditor 向导读的,Class 名必须和真实类名对上,否则向导拾取不到。快周期默认7、慢周期11、均化方式 LWMA、价格类型开盘价——这套默认值后面在 renko 上要改。 把文件丢进 MQL5/Include/Expert/Signal/MySignals 后重启 MetaEditor,向导里选「2条均线交叉的信号」、关掉尾随停止、用固定手数,就能吐出 MA2Cross.mq5。 用 EURUSD 生成砖形大小100点的自定义 renko 品种 EURUSD_T_r100,M1周期下跑2019上半年、周期7和11,成交价显示卖出精确贴合砖形、买入因点差偏移——看起来漂亮,但藏着坑。 Renko 第0根柱通常不完整,所以 Expert_EveryTick 设 false、从已收盘第1根取信号;且 Signal_2MACross_MAPrice 必须改 PRICE_CLOSE,别动。 测试器的 renko 回测无论用开盘价、M1 OHLC 还是即时报价模式,都会以等于砖形尺寸的步长「渗透未来」,等于白送圣杯。根因是 renko 柱标记时间和收盘价错位、逐柱测试只给当前柱开盘价当预测因子。 所以单独在 renko 品种上测系统必然失真,订单执行效力必须结合真实品种来验证;Renko 只负责分析「何时入场」的时序,不负责背回测真实的锅。 想让 EA 直接在 renko 图表上联线交易原品种,还得往生成代码里补几行,这也能顺手治一下过度优化的毛病。

MQL5 / C++
class=class="str">"cmt">//--- wizard description start
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Description of the class                                         |
class=class="str">"cmt">//| Title=Signals of class="num">2 MAs crosses                                   |
class=class="str">"cmt">//| Type=SignalAdvanced                                              |
class=class="str">"cmt">//| Name=2MA Cross                                                   |
class=class="str">"cmt">//| ShortName=2MACross                                               |
class=class="str">"cmt">//| Class=Signal2MACross                                             |
class=class="str">"cmt">//| Page=signal_2mac                                                 |
class=class="str">"cmt">//| Parameter=SlowPeriod,class="type">int,class="num">11,Slow MA period                       |
class=class="str">"cmt">//| Parameter=FastPeriod,class="type">int,class="num">7,Fast Ma period                        |
class=class="str">"cmt">//| Parameter=MAMethod,ENUM_MA_METHOD,MODE_LWMA,Method of averaging  |
class=class="str">"cmt">//| Parameter=MAPrice,ENUM_APPLIED_PRICE,PRICE_OPEN,Price type       |
class=class="str">"cmt">//| Parameter=Shift,class="type">int,class="num">0,Shift                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- wizard description end
class Signal2MACross : class="kw">public CExpertSignal
{
class="kw">protected:
CiMA              m_maSlow;         class=class="str">"cmt">// object-indicator
CiMA              m_maFast;         class=class="str">"cmt">// object-indicator
class=class="str">"cmt">// adjustable parameters
class="type">int               m_slow;
class="type">int               m_fast;
ENUM_MA_METHOD    m_method;
ENUM_APPLIED_PRICE m_type;
class="type">int               m_shift;
class=class="str">"cmt">// "weights" of market models(class="num">0-class="num">100)
class="type">int               m_pattern_0;      class=class="str">"cmt">// model class="num">0 "fast MA crosses slow MA"
class="kw">public:
Signal2MACross(class="type">void);
~Signal2MACross(class="type">void);
class=class="str">"cmt">// parameters setters
class="type">void              SlowPeriod(class="type">int value) { m_slow = value; }

「双均线交叉信号的类封装与EA挂接」

这段实现把双均线交叉逻辑封装成了一个可复用信号类 Signal2MACross。构造函数里默认慢线周期 11、快线周期 7、采用线性加权 LWMA、取开盘价 PRICE_OPEN,pattern_0 权重给到 100,意味着该形态在组合信号里占满额权重。 LongCondition 与 ShortCondition 只做一件事:取 StartIndex 位置,快线高于慢线返回 m_pattern_0(即 100)看多,快线低于慢线返回 100 看空,否则返回 0。也就是说交叉一旦发生,信号强度直接拉满,没有模糊区间。 类里还暴露了 FastPeriod、MAMethod、MAPrice、Shift、Pattern_0 等 setter,方便在 EA 层通过 input 动态改写参数,不用动核心代码。ValidationSettings 和 InitIndicators 是虚函数,留给派生层做配置校验和指标句柄初始化。 挂接部分很直白:EA 主文件 include 了 Signal2MACross.mqh,并配了 TrailingNone(不追踪止损)和 MoneyFixedLot(固定手数)。外部参数里 Signal_2MACross_SlowPeriod=11、FastPeriod=7 与类默认值一致,ThresholdOpen/Close 都设 10,代表信号强度超过 10 才触发开平。 [CODE]…[/CODE] 标记里的代码可直接拷进 MT5 的 MQL5/Include/Expert/Signal/MySignals 目录,配合官方 EA 框架编译。外汇与贵金属杠杆高,均线交叉在震荡市会连续假突破,实盘前先用策略测试器跑至少 3 个月tick数据验证。

MQL5 / C++
class="type">void              FastPeriod(class="type">int value) { m_fast = value; }
class="type">void              MAMethod(ENUM_MA_METHOD value) { m_method = value; }
class="type">void              MAPrice(ENUM_APPLIED_PRICE value) { m_type = value; }
class="type">void              Shift(class="type">int value) { m_shift = value; }
class=class="str">"cmt">// adjusting "weights" of market models
class="type">void              Pattern_0(class="type">int value) { m_pattern_0 = value; }
class=class="str">"cmt">// verification of settings
class="kw">virtual class="type">bool      ValidationSettings(class="type">void);
class=class="str">"cmt">// creating the indicator and timeseries
class="kw">virtual class="type">bool      InitIndicators(CIndicators *indicators);
class=class="str">"cmt">// checking if the market models are formed
class="kw">virtual class="type">int       LongCondition(class="type">void);
class="kw">virtual class="type">int       ShortCondition(class="type">void);
class="kw">protected:
class=class="str">"cmt">// initialization of the indicators
class="type">bool              InitMAs(CIndicators *indicators);
class=class="str">"cmt">// getting data
class="type">class="kw">double            FastMA(class="type">int ind) { class="kw">return(m_maFast.Main(ind)); }
class="type">class="kw">double            SlowMA(class="type">int ind) { class="kw">return(m_maSlow.Main(ind)); }
};
Signal2MACross::Signal2MACross(class="type">void) : m_slow(class="num">11), m_fast(class="num">7), m_method(MODE_LWMA), m_type(PRICE_OPEN), m_shift(class="num">0), m_pattern_0(class="num">100)
{
}
class="type">int Signal2MACross::LongCondition(class="type">void)
{
class="kw">const class="type">int idx = StartIndex();
if(FastMA(idx) > SlowMA(idx))
{
class="kw">return m_pattern_0;
}
class="kw">return class="num">0;
}
class="type">int Signal2MACross::ShortCondition(class="type">void)
{
class="kw">const class="type">int idx = StartIndex();
if(FastMA(idx) < SlowMA(idx))
{
class="kw">return m_pattern_0;
}
class="kw">return class="num">0;
}
class="macro">#include <Expert\Expert.mqh>
class="macro">#include <Expert\Signal\MySignals\Signal2MACross.mqh>
class="macro">#include <Expert\Trailing\TrailingNone.mqh>
class="macro">#include <Expert\Money\MoneyFixedLot.mqh>
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Inputs                                                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">// inputs for expert
class="kw">input class="type">class="kw">string             Expert_Title              = "MA2Cross";  class=class="str">"cmt">// Document name
class="type">class="kw">ulong                    Expert_MagicNumber        = class="num">7623;
class="type">bool                     Expert_EveryTick          = class="kw">false;
class=class="str">"cmt">// inputs for main signal
class="kw">input class="type">int                Signal_ThresholdOpen      = class="num">10;          class=class="str">"cmt">// Signal threshold value to open [class="num">0...class="num">100]
class="kw">input class="type">int                Signal_ThresholdClose     = class="num">10;          class=class="str">"cmt">// Signal threshold value to close [class="num">0...class="num">100]
class="kw">input class="type">class="kw">double             Signal_PriceLevel         = class="num">0.0;         class=class="str">"cmt">// Price level to execute a deal
class="kw">input class="type">class="kw">double             Signal_StopLevel          = class="num">0.0;         class=class="str">"cmt">// Stop Loss level(in points)
class="kw">input class="type">class="kw">double             Signal_TakeLevel          = class="num">0.0;         class=class="str">"cmt">// Take Profit level(in points)
class="kw">input class="type">int                Signal_Expiration         = class="num">0;           class=class="str">"cmt">// Expiration of pending orders(in bars)
class="kw">input class="type">int                Signal_2MACross_SlowPeriod = class="num">11;         class=class="str">"cmt">// 2MA Cross(class="num">11,class="num">7,MODE_LWMA,...) Slow MA period
class="kw">input class="type">int                Signal_2MACross_FastPeriod = class="num">7;          class=class="str">"cmt">// 2MA Cross(class="num">11,class="num">7,MODE_LWMA,...) Fast Ma period
class="kw">input ENUM_MA_METHOD     Signal_2MACross_MAMethod  = MODE_LWMA;   class=class="str">"cmt">// 2MA Cross(class="num">11,class="num">7,MODE_LWMA,...) Method of averaging
class="kw">input ENUM_APPLIED_PRICE Signal_2MACross_MAPrice   = PRICE_OPEN;  class=class="str">"cmt">// 2MA Cross(class="num">11,class="num">7,MODE_LWMA,...) Price type
class="kw">input class="type">int                Signal_2MACross_Shift     = class="num">0;           class=class="str">"cmt">// 2MA Cross(class="num">11,class="num">7,MODE_LWMA,...) Shift

把双均线过滤挂进专家对象

这段初始化逻辑展示了一个实用做法:在 MT5 的 CExpert 框架里,把 2MA 交叉(11,7,LWMA)当作信号过滤器挂进去,而不是单独跑一条 EA。Signal_2MACross_Weight 设成 1.0 表示这个过滤器的权重拉满,实际组合信号时它话语权最重;若调小到 0.3,系统会更偏向其他过滤器。 外汇与贵金属杠杆高,这类多过滤器 EA 在震荡市可能频繁触发伪信号,回测时建议把 Money_FixLot_Percent 从默认 10% 降到 2%–3% 观察最大回撤。 代码里先 new 一个 CExpertSignal,再 new Signal2MACross 作为 filter0 用 AddFilter 挂上,最后逐个写 SlowPeriod / FastPeriod / MAMethod 等参数。注意若任何一步返回 NULL 或 ValidationSettings 不通过,OnInit 直接 INIT_FAILED,EA 不会加载——开 MT5 编译时若卡在初始化,优先查这几行指针分配。

MQL5 / C++
class="kw">input class="type">class="kw">double             Signal_2MACross_Weight    = class="num">1.0;         class=class="str">"cmt">// 2MA Cross(class="num">11,class="num">7,MODE_LWMA,...) Weight [class="num">0...class="num">1.0]
class=class="str">"cmt">// inputs for money
class="kw">input class="type">class="kw">double             Money_FixLot_Percent      = class="num">10.0;        class=class="str">"cmt">// Percent
class="kw">input class="type">class="kw">double             Money_FixLot_Lots         = class="num">0.1;         class=class="str">"cmt">// Fixed volume
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Global expert object                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
CExpert ExtExpert;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Initialization function of the expert                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
{
class=class="str">"cmt">// Initializing expert
if(!ExtExpert.Init(Symbol(), Period(), Expert_EveryTick, Expert_MagicNumber))
{
printf(__FUNCTION__ + ": error initializing expert");
ExtExpert.Deinit();
class="kw">return(INIT_FAILED);
}
class=class="str">"cmt">// Creating signal
CExpertSignal *signal = new CExpertSignal;
if(signal == NULL)
{
printf(__FUNCTION__ + ": error creating signal");
ExtExpert.Deinit();
class="kw">return(INIT_FAILED);
}
ExtExpert.InitSignal(signal);
signal.ThresholdOpen(Signal_ThresholdOpen);
signal.ThresholdClose(Signal_ThresholdClose);
signal.PriceLevel(Signal_PriceLevel);
signal.StopLevel(Signal_StopLevel);
signal.TakeLevel(Signal_TakeLevel);
signal.Expiration(Signal_Expiration);
class=class="str">"cmt">// Creating filter Signal2MACross
Signal2MACross *filter0 = new Signal2MACross;
if(filter0 == NULL)
{
printf(__FUNCTION__ + ": error creating filter0");
ExtExpert.Deinit();
class="kw">return(INIT_FAILED);
}
signal.AddFilter(filter0);
class=class="str">"cmt">// Set filter parameters
filter0.SlowPeriod(Signal_2MACross_SlowPeriod);
filter0.FastPeriod(Signal_2MACross_FastPeriod);
filter0.MAMethod(Signal_2MACross_MAMethod);
filter0.MAPrice(Signal_2MACross_MAPrice);
filter0.Shift(Signal_2MACross_Shift);
filter0.Weight(Signal_2MACross_Weight);
...
class=class="str">"cmt">// Check all trading objects parameters
if(!ExtExpert.ValidationSettings())
{
ExtExpert.Deinit();
class="kw">return(INIT_FAILED);
}
class=class="str">"cmt">// Tuning of all necessary indicators
if(!ExtExpert.InitIndicators())
{
printf(__FUNCTION__ + ": error initializing indicators");
ExtExpert.Deinit();
class="kw">return(INIT_FAILED);
}
class="kw">return(INIT_SUCCEEDED);
}
让小布替你跑这套符号转换
这些自定义品种的报价映射与图表诊断,小布盯盘的 AIGC 已内置,打开对应品种页即可看到衍生符号的波动分布,你只需专注策略逻辑。

常见问题

可以,自定义符号支持引用标准品种即时报价,也可接入外部资源,通过 MQL API 写入派生图表。
等量柱按成交量或价格变动额度收盘,时间戳取决于波动节奏,虽不规整但四个价格值完整,可正常加载指标与 EA。
能,小布已内置衍生符号的识别与提醒,进入品种页可看到 Renko 砖形转换与均线交汇状态,省去手动刷图。
EA 运行在衍生符号图表,但下单指向真实品种,需在代码里用 Symbol 映射处理交易指令与图表源的差异。