神经网络在交易中的实际应用。 是时候进行实践了·进阶篇
🧠

神经网络在交易中的实际应用。 是时候进行实践了·进阶篇

(2/3)· 承接基础篇,手把手在 MT5 把训练好的神经网络跑成自动交易系统

实战向进阶 第 2/3 篇
很多人以为写好神经网络就能直接下单,结果把训练、测试和优化全塞进交易模块,实盘一卡就崩。NNM 若背负过多功能,信号延迟和逻辑冲突会吃掉本就不高的胜率概率。把交易功能单独剥离,才是让模型在 EURUSD 上稳定跑起来的前提。

「EA加开仓限制后的回测表现与NNM中断排查」

在 EA 里补上开仓限制后重新跑测试,用最高可视化速度、即时报价模式进行,得到的结果与参考曲线接近。我倾向认为即便把测试速度降下来,旧版测试也可能靠拢参考值,你可以自己降速验证一遍。 NNM 模块终止的根因找到了:若神经网络没训练完,或交易 EA 启动前就点了模块,Input_mat.txt 不会生成,点“开始”后 NNM 无响应。早期我加了警告窗并从内部计时器强退,后来删了错误逻辑只留警告,弹窗点“确定”即可继续。在测试模式里暴露这问题比实盘才发现要好得多。 MT5 还有个坑:用策略数据写文件时,生成柱线数会大于图表显示数量,导致指标“超长”,但不影响测试质量,多余柱线只在测试中被取用。MT4 没这现象,其 1_MT4 指标无偏移。 下面这段初始化代码演示了如何从公共 CSV 读 EURUSD 测试数据填进输入数组,文件缺失会弹 Alert,是排查数据供给的第一道哨。

MQL5 / C++
class="macro">#include<Trade\Trade.mqh>
class=class="str">"cmt">//--- An object for performing trading operations
CTrade  trade;
class="kw">input class="type">int LossBuy;
class="kw">input class="type">int ProfitBuy;
class="kw">input class="type">int LossSell;
class="kw">input class="type">int ProfitSell;
class="type">int BarMax;
class="type">int BarMin;
class="type">int handleInput;
class="type">int handleInputPodkach;
class="type">int handleBar;
class="type">int Con;
class="type">int Bar;
class="type">class="kw">double DibMax;
class="type">class="kw">double DibMin;
class="type">class="kw">double in[class="num">32];
class="type">int Order01;
class="type">int Order1;
class="type">class="kw">ulong TicketBuy1;
class="type">class="kw">ulong TicketSell0;
class="type">bool send1;
class="type">bool send0;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//---
   handleInputPodkach=FileOpen("EURUSDTestPodkach.csv",FILE_READ|FILE_CSV|FILE_ANSI|FILE_COMMON,";");
   if(handleInputPodkach==INVALID_HANDLE)
      Alert("No file EURUSDTestPodkach.csv");
   in[class="num">0]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1;
   in[class="num">1]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1;
   in[class="num">2]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1;
   in[class="num">3]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1;
   in[class="num">4]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1;
   in[class="num">5]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1;
   in[class="num">6]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1;
   in[class="num">7]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1;
   in[class="num">8]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1;
   in[class="num">9]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1;
   in[class="num">10]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1;
   in[class="num">11]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1;
   in[class="num">12]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1;

◍ 从文件读参后怎么把交易内核钉死

初始化尾部这段把 13 到 29 号输入数组元素一口气塞满:每个值都来自同一个文件句柄 handleInputPodkach,用 FileReadString 读成字符串再 StringToDouble 转双精度,最后做 1/值-1 的换算。也就是说这 17 个参数本质是同一串外部文本的倒数偏移,改文件不改代码就能换一整组系数。 读完后立刻 FileClose(handleInputPodkach) 收掉句柄,避免 MT5 报文件句柄泄漏。紧接着把交易对象的身份与容差钉死:MagicNumber 写死 123456 用于区分本 EA 订单,deviation 设 10 点允许滑点,SetTypeFilling 用 ORDER_FILLING_RETURN 以适配大部分经纪商回退模式。 SetAsyncMode(true) 让下单走 OrderSendAsync,主线程不被确认阻塞, tick 内做高频判断时更不容易掉帧。最后 return(INIT_SUCCEEDED) 告诉终端初始化过关。 OnDeinit 里只干一件事:把 handleInput 与 handleBar 两个句柄关掉,防止退出时文件占用。外汇与贵金属杠杆高,异步下单若服务端拒绝填充会静默回退,实盘前务必在策略测试器用 10 点偏差跑一遍验证回报路径。

MQL5 / C++
  in[class="num">13]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1;
  in[class="num">14]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1;
  in[class="num">15]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1;
  in[class="num">16]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1;
  in[class="num">17]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1;
  in[class="num">18]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1;
  in[class="num">19]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1;
  in[class="num">20]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1;
  in[class="num">21]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1;
  in[class="num">22]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1;
  in[class="num">23]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1;
  in[class="num">24]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1;
  in[class="num">25]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1;
  in[class="num">26]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1;
  in[class="num">27]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1;
  in[class="num">28]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1;
  in[class="num">29]=class="num">1/StringToDouble(FileReadString(handleInputPodkach))-class="num">1;
  FileClose(handleInputPodkach);
class=class="str">"cmt">//--- Setting MagicNumber to identify EA&class="macro">#x27;s orders
  class="type">int MagicNumber=class="num">123456;
  trade.SetExpertMagicNumber(MagicNumber);
class=class="str">"cmt">//--- Setting allowable slippage in points for buying/selling
  class="type">int deviation=class="num">10;
  trade.SetDeviationInPoints(deviation);
class=class="str">"cmt">//--- order filling mode, use the mode that is allowed by the server
  trade.SetTypeFilling(ORDER_FILLING_RETURN);
class=class="str">"cmt">//--- The function to be used for trading: true - OrderSendAsync(), class="kw">false - OrderSend()
  trade.SetAsyncMode(true);
class=class="str">"cmt">//---
  class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert deinitialization function                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">const class="type">int reason)
  {
class=class="str">"cmt">//---
  FileClose(handleInput);
  FileClose(handleBar);
  }

用 OnTick 抓取日线开盘与小时极值差

这段逻辑跑在 MT5 的 OnTick 事件里,核心是在每天 01:00(服务器时区)重置参考基准,随后用小时 K 线去刷新日内的高点和低点边界。外汇与贵金属市场高杠杆、跳空频繁,这类边界值只作概率参考,不等于方向确认。 代码先用 TimeToStruct 拿当前时间结构,hour==1 时把 DibMax 设为上一根 H1 高点;hour>0 后只要上一根 H1 高点突破 DibMax 且当前 H1 开盘时间大于 1,就往 in[20]~in[24] 写五组日线开盘价与 H1/D1 低高差的数值。 下半段对称处理低点:hour==1 重置 DibMin,hour>0 后若上一根 H1 低点更低且时间条件满足,则填充 in[25]~in[27] 三组差价。开 MT5 新建 EA 把这段粘进 OnTick,把 in 数组打印到日志,能直接看到每天 01:00 后边界差如何随价格波动更新。

MQL5 / C++
class="type">void OnTick()
  {
   class="type">MqlDateTime stm;
   TimeToStruct(TimeCurrent(),stm);
   if(stm.hour==class="num">1)
      DibMax=iHigh(NULL,PERIOD_H1,class="num">1);
   if(stm.hour>class="num">0)
     {
      if(iHigh(NULL,PERIOD_H1,class="num">1)>DibMax && iTime(NULL,PERIOD_H1,class="num">0)>class="num">1)
        {
         in[class="num">20]=iOpen(NULL,PERIOD_D1,class="num">0)-iLow(NULL,PERIOD_H1,class="num">1);
         in[class="num">21]=iHigh(NULL,PERIOD_H1,class="num">1)-iOpen(NULL,PERIOD_D1,class="num">0);
         in[class="num">22]=iHigh(NULL,PERIOD_D1,class="num">1)-iLow(NULL,PERIOD_D1,class="num">1);
         in[class="num">23]=iHigh(NULL,PERIOD_D1,class="num">1)-iOpen(NULL,PERIOD_H1,class="num">0);
         in[class="num">24]=iOpen(NULL,PERIOD_H1,class="num">0)-iLow(NULL,PERIOD_D1,class="num">1);
        }
     }
   if(iHigh(NULL,PERIOD_H1,class="num">1)>DibMax)
      DibMax=iHigh(NULL,PERIOD_H1,class="num">1);
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
   if(stm.hour==class="num">1)
      DibMin=iLow(NULL,PERIOD_H1,class="num">1);
   if(stm.hour>class="num">0)
     {
      if(iLow(NULL,PERIOD_H1,class="num">1)<DibMin && iTime(NULL,PERIOD_H1,class="num">0)>class="num">1)
        {
         in[class="num">25]=iOpen(NULL,PERIOD_D1,class="num">0)-iLow(NULL,PERIOD_H1,class="num">1);
         in[class="num">26]=iHigh(NULL,PERIOD_H1,class="num">1)-iOpen(NULL,PERIOD_D1,class="num">0);
         in[class="num">27]=iHigh(NULL,PERIOD_D1,class="num">1)-iLow(NULL,PERIOD_D1,class="num">1);

「把跨周期价差塞进模型输入数组」

这段逻辑干的事很直接:用日线前一根的高低点与小时线当前开盘价做差,把跨周期相对位置量化成 in[28]、in[29] 这类特征。in[28] 取昨日最高减当前 H1 开盘,in[29] 取当前 H1 开盘减昨日最低,本质是在量小时线开盘相对昨日波幅区间的偏移。 当小时线前一根低点刷新 DibMin 时同步更新该变量,随后 in[30]、in[31] 再算一遍同样的日高减 H1 开、H1 开减日低,等于把最近两根 H1 棒的相对位置都留了备份。 每天 0 点(stm.hour==0 且 Bar 未超总棒数)触发一次数组前移:索引 19~10 挪到 9~0,29~20 挪到 19~10,老数据被覆盖,相当于滚动窗口只留最近约 20 根 H1 的跨周期差。 最后 FileOpen 打开公共目录下的 Input_mat.txt,以分号分隔写出 1/(in[i]+1) 的归一化值——加 1 是为避免零或负差导致发散,倒数压缩把大价差也拉回 0~1 区间,方便外部矩阵读取。外汇与贵金属波动跳空频繁,这类跨周期差在重大数据后会瞬时放大,回测时务必用真实点差验证。

MQL5 / C++
  in[class="num">28]=iHigh(NULL,PERIOD_D1,class="num">1)-iOpen(NULL,PERIOD_H1,class="num">0);
  in[class="num">29]=iOpen(NULL,PERIOD_H1,class="num">0)-iLow(NULL,PERIOD_D1,class="num">1);
  }
   }
  if(iLow(NULL,PERIOD_H1,class="num">1)<DibMin)
   DibMin=iLow(NULL,PERIOD_H1,class="num">1);
  in[class="num">30]=iHigh(NULL,PERIOD_D1,class="num">1)-iOpen(NULL,PERIOD_H1,class="num">0);
  in[class="num">31]=iOpen(NULL,PERIOD_H1,class="num">0)-iLow(NULL,PERIOD_D1,class="num">1);
  if(Bar<Bars(NULL,PERIOD_H1)&& stm.hour==class="num">0)
   {
    for(class="type">int i=class="num">19; i>=class="num">10; i--)
     {
     in[i-class="num">10]=in[i];
     }
    for(class="type">int i=class="num">29; i>=class="num">20; i--)
     {
     in[i-class="num">10]=in[i];
     }
   }
  handleInput=FileOpen("Input_mat.txt",FILE_TXT|FILE_WRITE|FILE_ANSI|FILE_SHARE_READ|FILE_COMMON,";");
  FileWrite(handleInput,
         class="num">1/(in[class="num">0]+class="num">1),class="num">1/(in[class="num">1]+class="num">1),class="num">1/(in[class="num">2]+class="num">1),class="num">1/(in[class="num">3]+class="num">1),class="num">1/(in[class="num">4]+class="num">1),class="num">1/(in[class="num">5]+class="num">1),class="num">1/(in[class="num">6]+class="num">1),class="num">1/(in[class="num">7]+class="num">1),class="num">1/(in[class="num">8]+class="num">1),class="num">1/(in[class="num">9]+class="num">1),class="num">1/(in[class="num">10]+class="num">1),class="num">1/(in[class="num">11]+class="num">1),class="num">1/(in[class="num">12]+class="num">1),class="num">1/(in[class="num">13]+class="num">1),class="num">1/(in[class="num">14]+class="num">1),class="num">1/(in[class="num">15]+class="num">1),

◍ 输入权重落盘与订单触发闸门

这段逻辑先把 16~31 号输入节点的倒数权重写回内存数组,随后用 FileClose 关掉输入文件,再开 Bar.txt 以分号分割把当前小时 stm.hour 落盘,相当于给外部调度脚本留一个时间锚点。 接着从 Open1.txt 读入一行字符串并转成整数 Order1,这个值决定后续是偏多还是偏空。注意 FILE_COMMON 标志让文件落在终端公共目录,多 EA 共享时才不会各写各的。 下面的 digits、point、Ask、Bid 全部用 SymbolInfo 系列实时取,SL/TP 按 LossBuy、ProfitBuy 等参数乘 point 后 NormalizeDouble 到品种精度;若对应止损止盈参数为 0,则直接把挂单价置 0 表示不设。 触发闸门看三条件:Order1 为 0 或 1、Bar 小于 H1 周期 Bars 总数、Con 为 0,分别把 send0 / send1 置真。外汇与贵金属杠杆高,这套自动发单逻辑在滑点扩大时可能漏单或错向,上 MT5 前先把 LossBuy 等参数打印一遍核对。

MQL5 / C++
class="num">1/(in[class="num">16]+class="num">1),class="num">1/(in[class="num">17]+class="num">1),class="num">1/(in[class="num">18]+class="num">1),class="num">1/(in[class="num">19]+class="num">1),class="num">1/(in[class="num">20]+class="num">1),class="num">1/(in[class="num">21]+class="num">1),class="num">1/(in[class="num">22]+class="num">1),class="num">1/(in[class="num">23]+class="num">1),class="num">1/(in[class="num">24]+class="num">1),class="num">1/(in[class="num">25]+class="num">1),class="num">1/(in[class="num">26]+class="num">1),class="num">1/(in[class="num">27]+class="num">1),class="num">1/(in[class="num">28]+class="num">1),class="num">1/(in[class="num">29]+class="num">1),class="num">1/(in[class="num">30]+class="num">1),class="num">1/(in[class="num">31]+class="num">1));
  FileClose(handleInput);
  handleBar=FileOpen("Bar.txt",FILE_TXT|FILE_WRITE|FILE_ANSI|FILE_SHARE_READ|FILE_COMMON,";");
  FileWrite(handleBar,stm.hour);
  FileClose(handleBar);
  Order01=FileOpen("Open1.txt",FILE_CSV|FILE_READ|FILE_ANSI|FILE_SHARE_READ|FILE_COMMON," ");
  Order1=StringToInteger(FileReadString(Order01));
  FileClose(Order01);
  class="type">int   digits=(class="type">int)SymbolInfoInteger(_Symbol,SYMBOL_DIGITS);
  class="type">class="kw">double point=SymbolInfoDouble(_Symbol,SYMBOL_POINT);
  class="type">class="kw">double PriceAsk=SymbolInfoDouble(_Symbol,SYMBOL_ASK);
  class="type">class="kw">double PriceBid=SymbolInfoDouble(_Symbol,SYMBOL_BID);
  class="type">class="kw">double SL1=NormalizeDouble(PriceBid-LossBuy*point,digits);
  class="type">class="kw">double TP1=NormalizeDouble(PriceAsk+ProfitBuy*point,digits);
  class="type">class="kw">double SL0=NormalizeDouble(PriceAsk+LossSell*point,digits);
  class="type">class="kw">double TP0=NormalizeDouble(PriceBid-ProfitSell*point,digits);
  if(Bar<Bars(NULL,PERIOD_H1))
    Con=class="num">0;
  Comment(Order1,"  ",Con);
  if(LossBuy==class="num">0)
    SL1=class="num">0;
  if(ProfitBuy==class="num">0)
    TP1=class="num">0;
  if(LossSell==class="num">0)
    SL0=class="num">0;
  if(ProfitSell==class="num">0)
    TP0=class="num">0;
  if(Order1==class="num">0 && Bar<Bars(NULL,PERIOD_H1) && Con==class="num">0)
    send0=true;
  if(Order1==class="num">1 && Bar<Bars(NULL,PERIOD_H1) && Con==class="num">0)
    send1=true;
class=class="str">"cmt">//---------Buy0

H1 时段内的双向触发与收尾兜底

这段逻辑把买单和卖单都绑在 H1 新柱生成那一刻:只有 Bar==Bars(NULL,PERIOD_H1) 才放行,避免一根柱子内重复发单。买侧限定在小时数 15–20 之间,且第 1 根 H1 低点低于第 2 根才允许开多;卖侧则卡在 11–14 点,第 1 根高点高于第 2 根才开空,属于典型的欧盘早段与美盘初段分时段处理。 平仓条件对称:买仓在 Order1 翻 0、且新低出现时平;卖仓在 Order1 翻 1、且新高出现时平。Con 计数每次循环自增,配合 Bar 刷新记录,用来过滤历史柱上的误触发。 代码末尾还有一段脱离 OnTick 的兜底:若 send1 为 false 且 Order1==1 直接 trade.Buy(1),等于在条件漏判时强行补一单,实盘里可能带来非预期敞口,建议先在策略测试器里把这段注释掉跑一遍。外汇与贵金属杠杆高,这类强制补单逻辑若未经回测直接上实盘,亏损概率会明显放大。 输入参数里 Period=5 与 H1~H4、Loss/Profit 系列都留空给外部调,说明止损止盈是在别处算好传进来的。想验证就开 MT5 把 SL1/TP1、SL0/TP0 接上 iATR 或固定点数,看 15–20 点买、11–14 点卖的胜率分布。

MQL5 / C++
if(send1==class="kw">false && Bar==Bars(NULL,PERIOD_H1) && Order1==class="num">1 && Con>=class="num">1 && iLow(NULL,PERIOD_H1,class="num">1)<iLow(NULL,PERIOD_H1,class="num">2) && stm.hour>class="num">15 && stm.hour<class="num">20)
  {
    send1=trade.PositionOpen(_Symbol,ORDER_TYPE_BUY,class="num">1,PriceAsk,SL1,TP1);
    TicketBuy1 = trade.ResultDeal();
  }
if(send1==true && Bar==Bars(NULL,PERIOD_H1) && Order1==class="num">0 && Con>=class="num">1 && iHigh(NULL,PERIOD_H1,class="num">1)>iHigh(NULL,PERIOD_H1,class="num">2))
  {
    trade.PositionClose(TicketBuy1);
    send1=class="kw">false;
  }
class=class="str">"cmt">//---------Sell0
if(send0==class="kw">false && Bar==Bars(NULL,PERIOD_H1) && Order1==class="num">0 && Con>=class="num">1 && iHigh(NULL,PERIOD_H1,class="num">1)>iHigh(NULL,PERIOD_H1,class="num">2) && stm.hour>class="num">11 && stm.hour<class="num">14)
  {
    send0=trade.PositionOpen(_Symbol,ORDER_TYPE_SELL,class="num">1,PriceBid,SL0,TP0);
    TicketSell0 = trade.ResultDeal();
  }
if(send0==true && Bar==Bars(NULL,PERIOD_H1) && Order1==class="num">1 && Con>=class="num">1 && iLow(NULL,PERIOD_H1,class="num">1)<iLow(NULL,PERIOD_H1,class="num">2))
  {
    trade.PositionClose(TicketSell0);
    send0=class="kw">false;
  }
class=class="str">"cmt">//-----------------------------------------------------------------------
Bar=Bars(NULL,PERIOD_H1);
Con++;
}
class=class="str">"cmt">//------------------------------------------------------------------------
if(send1==class="kw">false && Order1==class="num">1)
  {
    send1=trade.Buy(class="num">1);
    TicketBuy1 = trade.ResultDeal();
  }
if(send1==true && Order1==class="num">0)
  {
    trade.PositionClose(TicketBuy1);
    send1=class="kw">false;
  }
class="macro">#include<Trade\Trade.mqh>
CTrade  trade;
class="kw">input class="type">int Period=class="num">5;
class="kw">input class="type">int H1;
class="kw">input class="type">int H2;
class="kw">input class="type">int H3;
class="kw">input class="type">int H4;
class="kw">input class="type">int LossBuy;
class="kw">input class="type">int ProfitBuy;
class="kw">input class="type">int LossSell;
class="kw">input class="type">int ProfitSell;
class="type">class="kw">ulong TicketBuy1;
class="type">class="kw">ulong TicketSell0;
class="type">class="kw">datetime Count;
class="type">class="kw">double Per;
class="type">class="kw">double Buf_0[];
class="type">class="kw">double Buf_1[];
class="type">bool send1;
class="type">bool send0;
class="type">int h=class="num">4;
class="type">int k;
class="type">int K;
class="type">int bars;
让小布替你跑这套
这些多网络信号的盘口状态小布盯盘的 AIGC 已内置,打开对应品种页即可看到响应曲线与交叉条件,你只管判断是否跟单。

常见问题

Online 用于实盘或可视测试时启停神经网络并输出二元信号;Offline 只把神经网络对测试样本的响应写进数组,不介入交易执行。
训练、测试、优化放在交易综合体之外,能降低模块复杂度,减少实盘故障概率,交易部分只接收简单信号更稳健。
神经网络模块是通用的,可把不同品种训练出的多个网络合并到一个 NNM,但本文实操仅以 EURUSD 为例。
小布暂未开放自定义 NNM 导入,但其 AIGC 品种页已预置类似多周期响应诊断,可辅助你观察信号交叉。
它们是不同时间段训练的三个子模块,每个含两个神经网络,用于在 Online 模式覆盖多周期行情特征。