//+------------------------------------------------------------------+
//|                                                        EaKit.mqh |
//|                                     冏掉的Alan羅  orzalanluo.com |
//+------------------------------------------------------------------+
// MT5 教學 10：多隻 EA 共用的框架。放 MQL5\Include\OrzBlog\，
// EA 用 #include <OrzBlog\EaKit.mqh> 引入。
// 下面 //=== 的編號對應文章小節；1a/1b/1c/1d 是文章沒貼的段
//=== 2. 共用的參數跟著 .mqh 走 ======================================
#include <Trade\Trade.mqh>

input group "共用：風險與出場"
input double i_RiskPct    = 1.0;      // 每筆風險（餘額的 %）
input double i_MaxLots    = 1.0;      // 手數上限
input double i_SlAtr      = 2.0;      // 停損 = ATR x 倍數
input double i_TpAtr      = 3.0;      // 停利倍數，0 不設
input double i_BeAtr      = 1.0;      // 保本門檻倍數，0 不用
input int    i_BeLockPts  = 10;       // 保本時多鎖幾點
input double i_TrailAtr   = 2.0;      // 移動停損倍數，0 不用
input int    i_Retry      = 3;        // 下單最多送幾次
input group "共用：通知"
input bool   i_NotifyPush  = true;    // 推播到手機
input bool   i_NotifyMail  = false;   // 寄電子郵件
input bool   i_NotifyAlert = false;   // 本機跳視窗
input string i_WebhookUrl  = "";      // 空白＝不送 WebRequest
input int    i_StaleMin    = 30;      // 幾分鐘沒報價就警告，0 不用

//=== 3. 用類別把狀態收起來 ==========================================
class CEaKit
{
private:
   CTrade   m_Trade;
   long     m_Magic;
   datetime m_LastBarTime;
   bool     m_Ready;                  // 自檢過了沒
   string   m_LastBad;                // 上次自檢的結果，變了才印
   string   m_LogName;                // 事件檔檔名（Common\Files）
   datetime m_DayStart;               // 日結從幾點起算
   bool     m_StaleSaid;              // 「報價停了」印過了沒

   bool     Preflight();
   void     AdoptPosition();
   double   CalcLots(double slDist, double &risk);
   bool     EnoughMargin(ENUM_ORDER_TYPE type, double lots,
                         double price);
   bool     SendMarket(bool isBuy, double lots, double sl, double tp);
   bool     OpenPosition(int dir, double atr);
   void     ManagePosition(ulong ticket, int type, double atr);
   double   PositionNet(long pos);
   void     DailySummary(datetime from, datetime to);
   void     CheckStale(datetime now);
   bool     InSession(datetime now, datetime &open);

public:
            CEaKit() : m_Magic(0), m_LastBarTime(0), m_Ready(false),
                       m_LastBad("?"), m_DayStart(0),
                       m_StaleSaid(false) {}
   bool     Init(long magic);         // OnInit 叫
   void     Deinit();                 // OnDeinit 叫
   bool     Ready();                  // 自檢＋接手，過了才 true
   bool     IsNewBar();
   void     ForgetBar() { m_LastBarTime = 0; } // 這根下個 tick 重來
   int      FindMyPosition(ulong &ticket);
   void     ShowStatus();
   void     OnSignal(int dir, double atr);     // 管倉＋照訊號進出
   void     OnTrade(const MqlTradeTransaction &trans);
   void     OnTimer();
   void     Log(string msg);
   void     Notify(string msg);
};

//=== 1a. Init／Deinit ================================================
bool CEaKit::Init(long magic)
{
   m_Magic = magic;
   m_Trade.SetExpertMagicNumber(magic);
   m_Trade.SetDeviationInPoints(20);
   m_Trade.SetTypeFillingBySymbol(_Symbol);
   m_Ready   = false;                    // 每次重來都要重新自檢
   m_LastBad = "?";
   Print("EA 啟動：保本 ", DoubleToString(i_BeAtr, 1), " ATR +",
         i_BeLockPts, " 點，移動停損 ",
         DoubleToString(i_TrailAtr, 1), " ATR，停利 ",
         DoubleToString(i_TpAtr, 1), " ATR");

   // 事件檔：一隻 EA 一個商品一個檔；測試器另起檔名，每趟從頭寫
   m_LogName = MQLInfoString(MQL_PROGRAM_NAME) + "_" + _Symbol +
               (MQLInfoInteger(MQL_TESTER) ? "_tester" : "") + ".log";
   if(MQLInfoInteger(MQL_TESTER))
      FileDelete(m_LogName, FILE_COMMON);
   Print("Common=", TerminalInfoString(TERMINAL_COMMONDATA_PATH));
   Log("事件檔 Common\\Files\\" + m_LogName);
   return EventSetTimer(60);             // 每分鐘進一次 OnTimer
}

void CEaKit::Deinit()
{
   EventKillTimer();
   Comment("");
}

//=== 1b. 開倉前後：自檢、接手、新 K 線、持倉、手數、保證金 ==========
bool CEaKit::Ready()
{
   if(m_Ready)
      return true;
   if(!Preflight())                  // 沒過就什麼都不做，下個 tick
      return false;                  // 再檢查一次
   m_Ready = true;
   AdoptPosition();                  // 過了才看手上有沒有倉
   return true;
}

bool CEaKit::Preflight()
{
   if(TerminalInfoInteger(TERMINAL_CONNECTED) == 0)
      return false;                    // 還沒連上，下一個 tick 再看

   string bad = "";
   if(TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) == 0)
      bad += "終端的演算法交易沒開；";
   if(MQLInfoInteger(MQL_TRADE_ALLOWED) == 0)
      bad += "這張圖的 EA 沒允許演算法交易；";
   if(AccountInfoInteger(ACCOUNT_TRADE_ALLOWED) == 0)
      bad += "帳戶不能交易；";
   if(AccountInfoInteger(ACCOUNT_TRADE_EXPERT) == 0)
      bad += "帳戶不收 EA 的單；";
   if(SymbolInfoInteger(_Symbol, SYMBOL_TRADE_MODE)
      != SYMBOL_TRADE_MODE_FULL)
      bad += _Symbol + " 現在不能完整交易；";
   if(SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE) <= 0 ||
      SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE_LOSS) <= 0)
      bad += _Symbol + " 規格讀不到；";

   if(bad != m_LastBad)                 // 狀況變了才印，免得洗版
   {
      if(bad == "")
         Print("自檢通過：", _Symbol, "  ",
               AccountInfoInteger(ACCOUNT_MARGIN_MODE) ==
               ACCOUNT_MARGIN_MODE_RETAIL_HEDGING ? "鎖倉" : "單邊",
               "帳戶  餘額 ", DoubleToString(
               AccountInfoDouble(ACCOUNT_BALANCE), 2));
      else
         Print("不能交易：", bad);
      m_LastBad = bad;
   }
   return bad == "";
}

void CEaKit::AdoptPosition()
{
   ulong ticket = 0;
   int   type   = FindMyPosition(ticket);
   if(type < 0)
   {
      Print("沒有要接手的持倉");
      return;
   }
   Print("接手 #", ticket, " ",
         type == POSITION_TYPE_BUY ? "多單" : "空單", " ",
         DoubleToString(PositionGetDouble(POSITION_VOLUME), 2),
         " 手 @", DoubleToString(
         PositionGetDouble(POSITION_PRICE_OPEN), _Digits),
         "  停損 ", DoubleToString(
         PositionGetDouble(POSITION_SL), _Digits),
         "  停利 ", DoubleToString(
         PositionGetDouble(POSITION_TP), _Digits));
}

bool CEaKit::IsNewBar()
{
   datetime t = iTime(_Symbol, _Period, 0);
   if(t == m_LastBarTime)
      return false;
   m_LastBarTime = t;
   return true;
}

int CEaKit::FindMyPosition(ulong &ticket)
{
   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong t = PositionGetTicket(i);
      if(t == 0)
         continue;
      if(PositionGetString(POSITION_SYMBOL) != _Symbol)
         continue;
      if(PositionGetInteger(POSITION_MAGIC) != m_Magic)
         continue;
      ticket = t;
      return (int)PositionGetInteger(POSITION_TYPE);
   }
   return -1;
}

void CEaKit::ShowStatus()
{
   ulong ticket = 0;
   int   have   = FindMyPosition(ticket);
   if(have < 0)
   {
      Comment("沒有持倉");
      return;
   }
   double open   = PositionGetDouble(POSITION_PRICE_OPEN);
   double sl     = PositionGetDouble(POSITION_SL);
   double tp     = PositionGetDouble(POSITION_TP);
   double profit = PositionGetDouble(POSITION_PROFIT);
   Comment(have == POSITION_TYPE_BUY ? "多單" : "空單", " #", ticket,
           "  開倉價 ", DoubleToString(open, _Digits),
           "\nSL ", DoubleToString(sl, _Digits),
           "  TP ", DoubleToString(tp, _Digits),
           "\n浮動盈虧 ", DoubleToString(profit, 2));
}

double CEaKit::CalcLots(double slDist, double &risk)
{
   double balance   = AccountInfoDouble(ACCOUNT_BALANCE);
   double riskMoney = balance * i_RiskPct / 100.0;   // 這筆最多賠多少
   double tickSize  = SymbolInfoDouble(_Symbol,
                                       SYMBOL_TRADE_TICK_SIZE);
   double tickValue = SymbolInfoDouble(_Symbol,
                                       SYMBOL_TRADE_TICK_VALUE_LOSS);
   double minLot    = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
   double maxLot    = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
   double stepLot   = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
   risk = 0;
   if(tickSize <= 0 || tickValue <= 0 || stepLot <= 0 || slDist <= 0)
   {
      Print("規格讀不到或停損距離為 0，這筆不做");
      return 0;
   }

   double lossPerLot = slDist / tickSize * tickValue; // 1 手賠多少
   double lots = riskMoney / lossPerLot;
   long steps = (long)MathFloor(lots / stepLot + 1e-9); // 捨去到步進
   lots = NormalizeDouble(steps * stepLot, 8);          // 修浮點尾數
   if(lots < minLot)
   {
      Print("風險 ", DoubleToString(riskMoney, 2), " 連最小手數 ",
            DoubleToString(minLot, 2), " 手都不夠，這筆不做");
      return 0;
   }
   lots = MathMin(lots, MathMin(maxLot, i_MaxLots));
   risk = lots * lossPerLot;
   return lots;
}

bool CEaKit::EnoughMargin(ENUM_ORDER_TYPE type, double lots,
                          double price)
{
   double need = 0;
   if(!OrderCalcMargin(type, _Symbol, lots, price, need))
   {
      Print("OrderCalcMargin 失敗，錯誤碼 ", GetLastError());
      return false;
   }
   double freeMargin = AccountInfoDouble(ACCOUNT_MARGIN_FREE);
   if(need > freeMargin)
   {
      Print("保證金不夠：", DoubleToString(lots, 2), " 手要 ",
            DoubleToString(need, 2), "，可用 ",
            DoubleToString(freeMargin, 2), "，這筆不做");
      return false;
   }
   return true;
}

//=== 1c. 下單、開倉、保本移動、照訊號進出 ============================
enum ENUM_KIT_FAIL
{
   KIT_FAIL_RETRY,                      // 換個價再送一次就好
   KIT_FAIL_UNKNOWN,                    // 送成功了沒？先查持倉
   KIT_FAIL_STOP                        // 自己送錯的，重送一樣錯
};

ENUM_KIT_FAIL KitClassifyFail(uint code)
{
   switch(code)
   {
      case TRADE_RETCODE_REQUOTE:           // 10004 重新報價
      case TRADE_RETCODE_PRICE_CHANGED:     // 10020 價格變了
      case TRADE_RETCODE_PRICE_OFF:         // 10021 沒有報價
      case TRADE_RETCODE_TOO_MANY_REQUESTS: // 10024 請求太頻繁
         return KIT_FAIL_RETRY;
      case TRADE_RETCODE_TIMEOUT:           // 10012 請求逾時
      case TRADE_RETCODE_CONNECTION:        // 10031 沒有連線
         return KIT_FAIL_UNKNOWN;
   }
   return KIT_FAIL_STOP;                    // 手數、停損、錢不夠…
}

bool CEaKit::SendMarket(bool isBuy, double lots, double sl, double tp)
{
   for(int n = 1; n <= i_Retry; n++)
   {
      bool ok = isBuy ? m_Trade.Buy(lots, _Symbol, 0, sl, tp)
                      : m_Trade.Sell(lots, _Symbol, 0, sl, tp);
      uint code = m_Trade.ResultRetcode();
      if(ok && (code == TRADE_RETCODE_DONE ||        // 10009 全部成交
                code == TRADE_RETCODE_DONE_PARTIAL)) // 10010 部分成交
         return true;                    // 部分成交也是有倉，別當失敗
      Print("下單失敗 ", code, " ",
            m_Trade.ResultRetcodeDescription(), "（第 ", n, " 次）");
      ENUM_KIT_FAIL what = KitClassifyFail(code);
      if(what == KIT_FAIL_STOP)
         return false;
      if(what == KIT_FAIL_UNKNOWN)
      {
         Sleep(1000);                   // 給伺服器一點時間回報
         ulong t = 0;
         if(FindMyPosition(t) >= 0)
         {
            Print("  查到持倉 #", t, "，這張其實送成功了");
            return true;
         }
         Print("  結果不明又查不到持倉，這根 K 線不再送");
         return false;
      }
      Sleep(200 * n);                   // 可重送：退一步再來
   }
   return false;
}

bool CEaKit::OpenPosition(int dir, double atr)
{
   double ask     = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
   double bid     = SymbolInfoDouble(_Symbol, SYMBOL_BID);
   double slDist  = atr * i_SlAtr;
   double tpDist  = atr * i_TpAtr;
   bool   useTp   = (i_TpAtr > 0);
   long stops = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
   double minDist = stops * _Point + (ask - bid);
   if(slDist < minDist) slDist = minDist;
   if(useTp && tpDist < minDist) tpDist = minDist;

   // 先把停損價修到商品的小數位，再用修過的價差算手數
   bool   isBuy = (dir == POSITION_TYPE_BUY);
   double entry = isBuy ? ask : bid;
   double sl = NormalizeDouble(isBuy ? ask - slDist : bid + slDist,
                               _Digits);
   double tp = 0;                                 // 0 = 不設停利
   if(useTp)
      tp = NormalizeDouble(isBuy ? ask + tpDist : bid - tpDist,
                           _Digits);
   slDist = MathAbs(entry - sl);

   double risk = 0;
   double lots = CalcLots(slDist, risk);
   if(lots <= 0)
      return false;
   if(!EnoughMargin(isBuy ? ORDER_TYPE_BUY : ORDER_TYPE_SELL,
                    lots, entry))
      return false;

   if(!SendMarket(isBuy, lots, sl, tp))
      return false;
   // 成交了幾手、成交在哪，從持倉讀回來，別用自己送出去的數字
   ulong ticket = 0;
   if(FindMyPosition(ticket) < 0)
      return false;
   double filled = PositionGetDouble(POSITION_VOLUME);
   Print(isBuy ? "開多" : "開空", " #", ticket,
         "  ", DoubleToString(filled, 2), " 手 @",
         DoubleToString(PositionGetDouble(POSITION_PRICE_OPEN),
                        _Digits),
         "  停損 ", DoubleToString(slDist / _Point, 0), " 點",
         "  風險 ", DoubleToString(risk * filled / lots, 2));
   return true;
}

void CEaKit::ManagePosition(ulong ticket, int type, double atr)
{
   if(!PositionSelectByTicket(ticket))
      return;
   double open  = PositionGetDouble(POSITION_PRICE_OPEN);
   double sl    = PositionGetDouble(POSITION_SL);
   double tp    = PositionGetDouble(POSITION_TP);
   bool   isBuy = (type == POSITION_TYPE_BUY);
   double dir   = isBuy ? 1 : -1;                 // 多 +1、空 -1
   double price = SymbolInfoDouble(_Symbol, isBuy ? SYMBOL_BID
                                                  : SYMBOL_ASK);
   double gain  = dir * (price - open);           // 現在賺的價差

   // 兩個候選：保本價、移動停損價，留對自己最有利的
   double want = sl;                              // 目前的停損
   if(sl == 0)                                   // 沒停損：從最差起算
      want = isBuy ? 0 : DBL_MAX;
   string why = "";
   if(i_BeAtr > 0 && gain >= atr * i_BeAtr)
   {
      double be = open + dir * i_BeLockPts * _Point;
      if(dir * (be - want) > 0) { want = be; why = "保本"; }
   }
   if(i_TrailAtr > 0)
   {
      double tr = price - dir * atr * i_TrailAtr;
      if(dir * (tr - want) > 0) { want = tr; why = "移動"; }
   }
   want = NormalizeDouble(want, _Digits);
   if(why == "")
      return;                                     // 沒有比現在好
   if(sl != 0 && dir * (want - sl) < _Point * 0.5)
      return;                                     // 好不到 1 點，不改

   // 新停損離現價至少要 StopsLevel，太近伺服器會拒
   long stops = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
   if(dir * (price - want) < stops * _Point)
      return;

   if(!m_Trade.PositionModify(ticket, want, tp) ||
      m_Trade.ResultRetcode() != TRADE_RETCODE_DONE)
   {
      Print("改停損失敗 ", m_Trade.ResultRetcode(), " ",
            m_Trade.ResultRetcodeDescription());
      return;
   }
   Print(why, " #", ticket, "  停損 ", DoubleToString(sl, _Digits),
         " -> ", DoubleToString(want, _Digits),
         "  現價 ", DoubleToString(price, _Digits));
}

void CEaKit::OnSignal(int dir, double atr)
{
   ulong ticket = 0;
   int   have   = FindMyPosition(ticket);
   if(have >= 0)
      ManagePosition(ticket, have, atr);

   if(dir < 0 || have == dir)             // 沒訊號，或已經是這個方向
      return;
   if(have >= 0)
   {
      double profit = PositionGetDouble(POSITION_PROFIT);
      bool ok   = m_Trade.PositionClose(ticket);
      uint code = m_Trade.ResultRetcode();
      if(!ok || (code != TRADE_RETCODE_DONE &&
                 code != TRADE_RETCODE_DONE_PARTIAL))
      {
         Print("平倉失敗 ", code, " ",
               m_Trade.ResultRetcodeDescription());
         return;
      }
      Print("平倉 #", ticket, "  浮動盈虧 ",
            DoubleToString(profit, 2));
      if(FindMyPosition(ticket) >= 0)      // 只平掉一部分，還有剩
      {
         Print("  還有剩倉沒平掉，這根 K 線不開新倉");
         return;
      }
   }
   OpenPosition(dir, atr);
}

//=== 1d. 事件檔、通知、交易事件、定時器 ==============================
void CEaKit::Log(string msg)
{
   Print(msg);                            // 專家頁 + MQL5\Logs
   int h = FileOpen(m_LogName, FILE_READ|FILE_WRITE|FILE_CSV|
                    FILE_ANSI|FILE_COMMON, '\t', CP_UTF8);
   if(h == INVALID_HANDLE)
   {
      Print("事件檔打不開，錯誤碼 ", GetLastError());
      return;
   }
   FileSeek(h, 0, SEEK_END);              // 接在檔尾
   string when = TimeToString(TimeTradeServer(),
                              TIME_DATE|TIME_SECONDS);
   FileWrite(h, when, _Symbol, msg);      // 三欄，Tab 隔開
   FileClose(h);
}

void CEaKit::Notify(string msg)
{
   Log("[通知] " + msg);                  // 先留紀錄，再往外送
   if(MQLInfoInteger(MQL_TESTER))
      return;                             // 測試器裡四個函數都不會動
   string text = _Symbol + " " + msg;
   if(i_NotifyAlert)
      Alert(text);
   ResetLastError();                      // 每條路各看自己的錯誤碼
   if(i_NotifyPush && !SendNotification(text))
      Print("推播失敗，錯誤碼 ", GetLastError());
   ResetLastError();
   if(i_NotifyMail && !SendMail("EA " + _Symbol, text))
      Print("寄信失敗，錯誤碼 ", GetLastError());
   if(i_WebhookUrl == "")
      return;
   char   body[], reply[];
   string replyHdr;
   int n = StringToCharArray(text, body, 0, WHOLE_ARRAY, CP_UTF8);
   ArrayResize(body, n - 1);              // 去掉結尾的 0
   ResetLastError();
   int code = WebRequest("POST", i_WebhookUrl,
                         "Content-Type: text/plain; charset=utf-8",
                         5000, body, reply, replyHdr);
   if(code != 200)
      Print("Webhook 回 ", code, "，錯誤碼 ", GetLastError());
}

void CEaKit::OnTrade(const MqlTradeTransaction &trans)
{
   if(trans.type != TRADE_TRANSACTION_DEAL_ADD)
      return;                             // 只看「多了一筆成交」
   ulong deal = trans.deal;
   if(!HistoryDealSelect(deal))           // 先把這筆成交載進來
   {
      Print("成交 #", deal, " 載不進來，錯誤碼 ", GetLastError());
      return;
   }
   if(HistoryDealGetInteger(deal, DEAL_MAGIC) != m_Magic ||
      HistoryDealGetString(deal, DEAL_SYMBOL) != _Symbol)
      return;                             // 不是這隻 EA 的
   if(HistoryDealGetInteger(deal, DEAL_ENTRY) == DEAL_ENTRY_IN)
      return;                             // 進場那筆開倉時已經印過
   long   reason = HistoryDealGetInteger(deal, DEAL_REASON);
   string why    = "其他";
   if(reason <= DEAL_REASON_WEB)    why = "手動";   // 桌面/手機/網頁
   if(reason == DEAL_REASON_SL)     why = "停損";
   if(reason == DEAL_REASON_TP)     why = "停利";
   if(reason == DEAL_REASON_EXPERT) why = "EA 平倉";
   if(reason == DEAL_REASON_SO)     why = "強制平倉";
   long pos = HistoryDealGetInteger(deal, DEAL_POSITION_ID);
   Notify("出場 #" + IntegerToString(pos) + " " + why + " " +
          DoubleToString(HistoryDealGetDouble(deal, DEAL_VOLUME), 2) +
          " 手  損益 " + DoubleToString(PositionNet(pos), 2));
}

double CEaKit::PositionNet(long pos)      // 這筆部位所有成交的淨損益
{
   double net = 0;
   if(!HistorySelectByPosition(pos))
      return 0;
   for(int i = HistoryDealsTotal() - 1; i >= 0; i--)
   {
      ulong d = HistoryDealGetTicket(i);
      net += HistoryDealGetDouble(d, DEAL_PROFIT) +
             HistoryDealGetDouble(d, DEAL_SWAP) +
             HistoryDealGetDouble(d, DEAL_COMMISSION);
   }
   return net;
}

void CEaKit::OnTimer()
{
   datetime now   = TimeTradeServer();    // 沒報價時 TimeCurrent 停住
   datetime today = now - now % 86400;    // 今天 00:00（伺服器時間）
   if(m_DayStart == 0)
      m_DayStart = today;
   if(today > m_DayStart)                 // 過了午夜：結上一天的帳
   {
      DailySummary(m_DayStart, today);
      m_DayStart = today;
   }
   CheckStale(now);
}

void CEaKit::DailySummary(datetime from, datetime to)
{
   int    n   = 0;
   double net = 0;
   if(HistorySelect(from, to - 1))        // to 那一秒算下一天
      for(int i = HistoryDealsTotal() - 1; i >= 0; i--)
      {
         ulong d = HistoryDealGetTicket(i);
         if(HistoryDealGetInteger(d, DEAL_MAGIC) != m_Magic ||
            HistoryDealGetString(d, DEAL_SYMBOL) != _Symbol)
            continue;
         net += HistoryDealGetDouble(d, DEAL_PROFIT) +
                HistoryDealGetDouble(d, DEAL_SWAP) +
                HistoryDealGetDouble(d, DEAL_COMMISSION);
         if(HistoryDealGetInteger(d, DEAL_ENTRY) != DEAL_ENTRY_IN)
            n++;                          // 只數出場
      }
   Notify("日結 " + TimeToString(from, TIME_DATE) + " " +
          IntegerToString(n) + " 筆 損益 " + DoubleToString(net, 2) +
          " 餘額 " + DoubleToString(
          AccountInfoDouble(ACCOUNT_BALANCE), 2) + " 權益 " +
          DoubleToString(AccountInfoDouble(ACCOUNT_EQUITY), 2));
}

void CEaKit::CheckStale(datetime now)
{
   datetime open;
   if(i_StaleMin <= 0 || !InSession(now, open))
      return;                             // 休市時間不算
   datetime last = (datetime)SymbolInfoInteger(_Symbol, SYMBOL_TIME);
   if(last < open)
      last = open;                        // 開盤之後才起算
   int idle = (int)(now - last) / 60;
   if(idle >= i_StaleMin && !m_StaleSaid)
   {
      m_StaleSaid = true;
      Notify("報價停了 " + IntegerToString(idle) + " 分鐘");
   }
   if(idle < i_StaleMin && m_StaleSaid)
   {
      m_StaleSaid = false;
      Log("報價恢復");
   }
}

bool CEaKit::InSession(datetime now, datetime &open)
{
   MqlDateTime dt;
   TimeToStruct(now, dt);
   datetime day = now - now % 86400;
   datetime from, to;
   for(int i = 0; SymbolInfoSessionTrade(_Symbol,
       (ENUM_DAY_OF_WEEK)dt.day_of_week, i, from, to); i++)
   {
      from += day;                        // 時段是「幾點」，加上今天
      to   += day;
      if(now >= from && now < to)
      {
         open = from;
         return true;
      }
   }
   return false;
}

//+------------------------------------------------------------------+
