//+------------------------------------------------------------------+
//|                                         LangChain_MemoryEA.mq5   |
//|                                          Evgeniy Koshtenko, 2025 |
//|                                   https://www.mql5.com/en/users/ |
//+------------------------------------------------------------------+
//
//  ARTICLE: "From Signal to Trade via a Chain of Agents:
//           LangChain Architecture on Top of MQL5"
//
//  ARCHITECTURE:
//    EA (data collection + execution)
//      ↕ WebSocket
//    langchain_memory_server.py (port 8977)
//      Signal Agent → News Agent → Risk Agent
//      ↕ SQLite (langchain_memory.db)
//      Memory of decisions between sessions
//
//  SERVER COMMANDS:
//    ANALYZE:SYM:TF:csv         → signal + decision_id
//    RESULT:SYM:decision_id:pnl → update the trade outcome
//    MEM_STATUS:SYM:TF          → memory statistics
//    STOP                       → disconnect
//
//  REQUIREMENTS:
//    python langchain_memory_server.py  (port 8977)
//
//+------------------------------------------------------------------+
#property copyright   "Evgeniy Koshtenko"
#property link        "https://www.mql5.com/en/users/"
#property version     "1.00"
#property description "LangChain Memory Agent EA — agent chain + SQL memory"

#include <llmhttp\winhttp.mqh>

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
input group "=== SERVER ==="
input string   InpHost        = "127.0.0.1";   // Server host
input ushort InpPort = 8977;   // Port (0-65535)
input int      InpReceiveMs   = 90000;          // Receive timeout (ms) — chain ~30-60 s

input group "=== TRADING ==="
input double   InpLotSize     = 0.01;           // Lot (if the server does not specify)
input int      InpSL_Points   = 300;            // SL in points (0 = from server response)
input int      InpTP_Points   = 600;            // TP in points (0 = from server response)
input double   InpMinConf     = 0.55;           // Minimum confidence
input ushort InpBaseMagic = 891;   // Magic Number
input int      InpSlippage    = 10;             // Slippage

input group "=== AI ANALYSIS ==="
input int      InpPriceBars   = 60;             // History bars per server
input int      InpAnalysisBars = 1;             // Every N new bars
input string   InpTimeframe   = "H1";           // Timeframe for the memory base
input bool     InpSendResults = true;           // Send RESULT after closing
input bool     InpSendAccount = true;           // Send account data (for Risk Agent)

input group "=== DISPLAY ==="
input bool     InpShowComment = true;           // Show status on the chart
input int      InpStatusBars  = 10;             // Request MEM_STATUS every N bars

//+------------------------------------------------------------------+
//| WebSocket variables                                              |
//+------------------------------------------------------------------+
HINTERNET g_hSession   = NULL;
HINTERNET g_hConnect   = NULL;
HINTERNET g_hRequest   = NULL;
HINTERNET g_hWebSocket = NULL;
int       g_socket     = INVALID_HANDLE;
bool      g_useWS      = false;

//+------------------------------------------------------------------+
//| State                                                            |
//+------------------------------------------------------------------+
int      g_barCount    = 0;
datetime g_lastBar     = 0;
int      g_totalTrades = 0;
int      g_totalBuys   = 0;
int      g_totalSells  = 0;

string   g_signal      = "hold";
string   g_comment     = "";
double   g_conf        = 0.0;
int      g_decisionId  = -1;

// Track position for RESULT
ulong    g_posTicket     = 0;
int      g_posDecisionId = -1;
string   g_posSym        = "";
bool     g_resultSent    = false;

// Statistics from memory
int      g_memTotal   = 0;
int      g_memWins    = 0;
double   g_memWinRate = 0.0;
double   g_memAvgPnl  = 0.0;
string   g_memBest    = "";

double   g_pipValue   = 0.0001;
ulong    g_closedTicket     = 0;
int      g_closedDecisionId = -1;
string   g_closedSym        = "";

// Last chain_log for comment
string   g_chainLog   = "";

//+------------------------------------------------------------------+
//| OnInit                                                           |
//+------------------------------------------------------------------+
int OnInit()
  {
   int digits    = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
   g_pipValue    = (digits == 3 || digits == 5) ? 0.00001 : 0.0001;

   Print("╔══════════════════════════════════════════════╗");
   Print("║  LANGCHAIN MEMORY AGENT  v1.0                ║");
   Print("║  Signal → News → Risk + SQL memory           ║");
   Print("╚══════════════════════════════════════════════╝");
   PrintFormat("Connecting to ws://%s:%d", InpHost, InpPort);

   if(InitWebSocket())
     {
      g_useWS = true;
      PrintFormat("✓ WebSocket connected to %s:%d", InpHost, InpPort);
     }
   else
     {
      g_socket = SocketCreate();
      if(g_socket != INVALID_HANDLE &&
         SocketConnect(g_socket, InpHost, InpPort, 3000))
         Print("✓ Socket connected");
      else
        {
         if(g_socket != INVALID_HANDLE)
           { SocketClose(g_socket); g_socket = INVALID_HANDLE; }
         Print("✗ No connection to the server! Run langchain_memory_server.py");
         return INIT_FAILED;
        }
     }

   RequestMemStatus();
   return INIT_SUCCEEDED;
  }

//+------------------------------------------------------------------+
//| OnDeinit                                                         |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   SendRaw("STOP");
   Sleep(300);
   if(g_useWS)
      CloseWebSocket();
   if(g_socket != INVALID_HANDLE)
     { SocketClose(g_socket); g_socket = INVALID_HANDLE; }
   if(InpShowComment)
      Comment("");

   Print("═══════════════════════════════════════");
   PrintFormat("Result: BUY=%d  SELL=%d  Trades=%d",
               g_totalBuys, g_totalSells, g_totalTrades);
   PrintFormat("Memory: %d entries  Win-rate=%.1f%%  AvgPnL=%+.2f pts",
               g_memTotal, g_memWinRate, g_memAvgPnl);
   Print("LangChain Memory EA disabled.");
  }

//+------------------------------------------------------------------+
//| OnTick                                                           |
//+------------------------------------------------------------------+
void OnTick()
  {
// Check closed positions to send RESULT
   if(InpSendResults)
      CheckClosedPosition();

// New bar?
   datetime barTime = iTime(_Symbol, PERIOD_CURRENT, 0);
   if(barTime == g_lastBar)
      return;
   g_lastBar = barTime;
   g_barCount++;

   if(g_barCount % InpAnalysisBars != 0)
      return;

// Collect close prices
   double prices[];
   ArraySetAsSeries(prices, true);
   int copied = CopyClose(_Symbol, PERIOD_CURRENT, 0, InpPriceBars, prices);
   if(copied < 20)
     {
      PrintFormat("Insufficient bars: %d", copied);
      return;
     }

// CSV of prices (oldest to newest)
   string csv = "";
   for(int i = copied - 1; i >= 0; i--)
     {
      csv += DoubleToString(prices[i], _Digits);
      if(i > 0)
         csv += ",";
     }

// ANALYZE command with optional account data
   string cmd = "ANALYZE:" + _Symbol + ":" + InpTimeframe + ":" + csv;

// Add account data via "|" for Risk Agent
   if(InpSendAccount)
     {
      string acc = StringFormat(
                      "|{\"balance\":%.2f,\"equity\":%.2f,"
                      "\"free_margin\":%.2f,\"open_positions\":%d}",
                      AccountInfoDouble(ACCOUNT_BALANCE),
                      AccountInfoDouble(ACCOUNT_EQUITY),
                      AccountInfoDouble(ACCOUNT_MARGIN_FREE),
                      Count_OpenPositions()
                   );
      cmd += acc;
     }

   PrintFormat("▶ ANALYZE (%d bars, TF=%s, LangChain)...", copied, InpTimeframe);
   string response = SendCommand(cmd);

   if(StringLen(response) == 0)
     {
      Print("⚠ No response from the server (the agent chain may take up to 60 s)");
      return;
     }

   ParseAnalyzeResponse(response);

   if(g_barCount % InpStatusBars == 0)
      RequestMemStatus();

   if(InpShowComment)
      UpdateComment(barTime);

   ExecuteSignal();
  }

//+------------------------------------------------------------------+
//| Parse ANALYZE response                                           |
//+------------------------------------------------------------------+
void ParseAnalyzeResponse(const string response)
  {
   g_signal    = ParseJson(response, "signal");
   g_comment   = ParseJson(response, "comment");
   g_chainLog  = ParseJson(response, "chain_log");

   string conf_s = ParseJson(response, "confidence");
   g_conf = (conf_s != "") ? StringToDouble(conf_s) : 0.0;

   string id_s = ParseJson(response, "decision_id");
   g_decisionId = (id_s != "") ? (int)StringToInteger(id_s) : -1;

   string news = ParseJson(response, "news_risk");

   PrintFormat("◀ signal=%s conf=%.2f decision_id=%d news=%s",
               g_signal, g_conf, g_decisionId, news);
   if(StringLen(g_chainLog) > 0)
      PrintFormat("   chain: %s", g_chainLog);
   if(StringLen(g_comment) > 0)
      PrintFormat("   comment: %s", g_comment);
  }

//+------------------------------------------------------------------+
//| Trading signal execution                                         |
//+------------------------------------------------------------------+
void ExecuteSignal()
  {
   if(g_conf < InpMinConf)
     {
      PrintFormat("HOLD — conf=%.2f below minimum %.2f", g_conf, InpMinConf);
      return;
     }

   if(g_signal == "buy")
     {
      if(PositionExistsByType(POSITION_TYPE_BUY))
         return;
      CloseByType(POSITION_TYPE_SELL);

      double ask   = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
      double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);

      // Priority: SL/TP from Risk Agent, otherwise inputs
      double vol = InpLotSize;
      int sl_pts = InpSL_Points;
      int tp_pts = InpTP_Points;

      // If the server returned volume and pips, use them
      // (they are already normalized by Risk Agent)
      double sl = (sl_pts > 0) ? ask - sl_pts * point : 0;
      double tp = (tp_pts > 0) ? ask + tp_pts * point : 0;

      if(OpenOrder(ORDER_TYPE_BUY, vol, ask, sl, tp))
        {
         g_totalBuys++;
         g_totalTrades++;
         g_posTicket     = LastOpenTicket();
         g_posDecisionId = g_decisionId;
         g_posSym        = _Symbol;
         g_resultSent    = false;
         PrintFormat("✓ BUY %.2f | SL=%.5f TP=%.5f | decision_id=%d",
                     vol, sl, tp, g_decisionId);
        }
     }
   else
      if(g_signal == "sell")
        {
         if(PositionExistsByType(POSITION_TYPE_SELL))
            return;
         CloseByType(POSITION_TYPE_BUY);

         double bid   = SymbolInfoDouble(_Symbol, SYMBOL_BID);
         double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);

         double vol = InpLotSize;
         int sl_pts = InpSL_Points;
         int tp_pts = InpTP_Points;

         double sl = (sl_pts > 0) ? bid + sl_pts * point : 0;
         double tp = (tp_pts > 0) ? bid - tp_pts * point : 0;

         if(OpenOrder(ORDER_TYPE_SELL, vol, bid, sl, tp))
           {
            g_totalSells++;
            g_totalTrades++;
            g_posTicket     = LastOpenTicket();
            g_posDecisionId = g_decisionId;
            g_posSym        = _Symbol;
            g_resultSent    = false;
            PrintFormat("✓ SELL %.2f | SL=%.5f TP=%.5f | decision_id=%d",
                        vol, sl, tp, g_decisionId);
           }
        }
      else
        {
         Print("HOLD — signal from the chain of agents");
        }
  }

//+------------------------------------------------------------------+
//| Open order via OrderSend                                         |
//+------------------------------------------------------------------+
bool OpenOrder(ENUM_ORDER_TYPE type, double volume,
               double price, double sl, double tp)
  {
   MqlTradeRequest req;
   MqlTradeResult  res;
   ZeroMemory(req);
   ZeroMemory(res);

   req.action       = TRADE_ACTION_DEAL;
   req.symbol       = _Symbol;
   req.volume       = NormalizeVolume(volume);
   req.type         = type;
   req.price        = price;
   req.sl           = sl;
   req.tp           = tp;
   req.magic = InpBaseMagic;   // now InpBaseMagic is already ushort
   req.deviation    = InpSlippage;
   req.type_filling = ORDER_FILLING_IOC;
   req.comment      = StringFormat("LC_Memory_d%d", g_decisionId);

   bool ok = OrderSend(req, res);
   if(!ok || res.retcode > 10008)
      PrintFormat("✗ OrderSend error: %d", res.retcode);
   return ok;
  }

//+------------------------------------------------------------------+
//| Last open EA ticket                                              |
//+------------------------------------------------------------------+
ulong LastOpenTicket()
  {
   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      ulong t = PositionGetTicket(i);
      if(!PositionSelectByTicket(t))
         continue;
      if(PositionGetString(POSITION_SYMBOL) == _Symbol &&
         (int)PositionGetInteger(POSITION_MAGIC) == InpBaseMagic)
         return t;
     }
   return 0;
  }

//+------------------------------------------------------------------+
//| Lot normalization                                                |
//+------------------------------------------------------------------+
double NormalizeVolume(double volume)
  {
   double min_lot  = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
   double max_lot  = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
   double lot_step = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
   if(volume <= 0)
      volume = min_lot;
   if(volume > max_lot)
      volume = max_lot;
   volume = MathRound(volume / lot_step) * lot_step;
   return NormalizeDouble(MathMax(volume, min_lot), 2);
  }

//+------------------------------------------------------------------+
//| Check the position closure and send RESULT                       |
//+------------------------------------------------------------------+
void CheckClosedPosition()
  {
   if(g_posTicket == 0 || g_resultSent)
      return;

// Is the position still open?
   if(PositionSelectByTicket(g_posTicket))
      return;

// Position closed — looking from 'profit' in history
   g_closedTicket     = g_posTicket;
   g_closedDecisionId = g_posDecisionId;
   g_closedSym        = g_posSym;

   g_posTicket     = 0;
   g_posDecisionId = -1;
   g_resultSent    = true;

   SendResultForClosed(g_closedTicket, g_closedDecisionId, g_closedSym);
  }

//+------------------------------------------------------------------+
//| Send RESULT to the server                                        |
//+------------------------------------------------------------------+
void SendResultForClosed(ulong ticket, int decId, string sym)
  {
   if(ticket == 0 || decId < 0)
      return;

   double profit = GetDealProfitByPosition(ticket);
   if(profit == EMPTY_VALUE)
     {
      Sleep(200);
      profit = GetDealProfitByPosition(ticket);
     }
   if(profit == EMPTY_VALUE)
      return;

   double contract = SymbolInfoDouble(sym, SYMBOL_TRADE_CONTRACT_SIZE);
   double pnlPts   = (contract > 0 && InpLotSize > 0 && g_pipValue > 0)
                     ? profit / (InpLotSize * contract * g_pipValue)
                     : profit;

   string cmd  = StringFormat("RESULT:%s:%d:%.5f", sym, decId, pnlPts);
   string resp = SendCommand(cmd);

   if(StringLen(resp) > 0)
     {
      string outcome = (profit > 0) ? "WIN  🏆" : "LOSS 💸";
      PrintFormat("📊 RESULT → decision_id=%d  %s  pnl=%.2f (%.1f pts)",
                  decId, outcome, profit, pnlPts);
     }
  }

//+------------------------------------------------------------------+
//| Profit of a closed trade from history                            |
//+------------------------------------------------------------------+
double GetDealProfitByPosition(ulong posTicket)
  {
   if(!HistorySelectByPosition(posTicket))
      return EMPTY_VALUE;
   double profit = 0;
   bool   found  = false;
   for(int i = HistoryDealsTotal() - 1; i >= 0; i--)
     {
      ulong deal = HistoryDealGetTicket(i);
      if(HistoryDealGetInteger(deal, DEAL_POSITION_ID) == (long)posTicket)
        {
         ENUM_DEAL_ENTRY entry =
            (ENUM_DEAL_ENTRY)HistoryDealGetInteger(deal, DEAL_ENTRY);
         if(entry == DEAL_ENTRY_OUT || entry == DEAL_ENTRY_INOUT)
           {
            profit += HistoryDealGetDouble(deal, DEAL_PROFIT);
            profit += HistoryDealGetDouble(deal, DEAL_SWAP);
            profit += HistoryDealGetDouble(deal, DEAL_COMMISSION);
            found   = true;
           }
        }
     }
   return found ? profit : EMPTY_VALUE;
  }

//+------------------------------------------------------------------+
//| Request memory status                                            |
//+------------------------------------------------------------------+
void RequestMemStatus()
  {
   string cmd  = "MEM_STATUS:" + _Symbol + ":" + InpTimeframe;
   string resp = SendCommand(cmd);
   if(StringLen(resp) == 0)
      return;

   string total_s = ParseJson(resp, "total");
   string wins_s  = ParseJson(resp, "wins");
   string wr_s    = ParseJson(resp, "win_rate");
   string pnl_s   = ParseJson(resp, "avg_pnl");
   string best_s  = ParseJson(resp, "best_signal");

   g_memTotal   = (total_s != "") ? (int)StringToInteger(total_s) : 0;
   g_memWins    = (wins_s  != "") ? (int)StringToInteger(wins_s)  : 0;
   g_memWinRate = (wr_s    != "") ? StringToDouble(wr_s)          : 0.0;
   g_memAvgPnl  = (pnl_s   != "") ? StringToDouble(pnl_s)         : 0.0;
   g_memBest    = best_s;

   PrintFormat("📚 Memory: %d trades | WR=%.1f%% | AvgPnL=%+.2f | Best=%s",
               g_memTotal, g_memWinRate, g_memAvgPnl, g_memBest);
  }

//+------------------------------------------------------------------+
//| Update the chart comment                                         |
//+------------------------------------------------------------------+
void UpdateComment(datetime barTime)
  {
   string posIcon = (PositionsTotal() > 0) ? "📈" : "—";
   string memStr  = StringFormat("%d entries | WR=%.0f%% | %+.2f pts",
                                 g_memTotal, g_memWinRate, g_memAvgPnl);
   string chainStr = (StringLen(g_chainLog) > 50)
                     ? StringSubstr(g_chainLog, 0, 50) + "..."
                     : g_chainLog;

   Comment(StringFormat(
              "╔ LANGCHAIN MEMORY AGENT v1.0 ╗\n"
              "Time: %s  Bars: %d\n"
              "Positions: %d  Trades: %d\n"
              "Equity: %.2f\n"
              "─────────────────────────\n"
              "Signal: %s  conf=%.2f  %s\n"
              "decision_id: %d\n"
              "Chain: %s\n"
              "Comment: %s\n"
              "─────────────────────────\n"
              "📚 Memory: %s\n"
              "Best: %s  BUY: %d  SELL: %d\n",
              TimeToString(barTime, TIME_DATE|TIME_MINUTES),
              g_barCount,
              PositionsTotal(), g_totalTrades,
              AccountInfoDouble(ACCOUNT_EQUITY),
              g_signal, g_conf, posIcon,
              g_decisionId,
              chainStr,
              StringLen(g_comment) > 55 ? StringSubstr(g_comment,0,55)+"..." : g_comment,
              memStr,
              g_memBest, g_totalBuys, g_totalSells
           ));
  }

//+------------------------------------------------------------------+
//| Number of EA open positions                                      |
//+------------------------------------------------------------------+
int Count_OpenPositions()
  {
   int count = 0;
   for(int i = 0; i < PositionsTotal(); i++)
     {
      ulong t = PositionGetTicket(i);
      if(!PositionSelectByTicket(t))
         continue;
      if(PositionGetString(POSITION_SYMBOL) == _Symbol &&
         (int)PositionGetInteger(POSITION_MAGIC) == InpBaseMagic)
         count++;
     }
   return count;
  }

//+------------------------------------------------------------------+
//| Check the availability of a position by type                     |
//+------------------------------------------------------------------+
bool PositionExistsByType(ENUM_POSITION_TYPE type)
  {
   for(int i = 0; i < PositionsTotal(); i++)
     {
      ulong t = PositionGetTicket(i);
      if(!PositionSelectByTicket(t))
         continue;
      if(PositionGetString(POSITION_SYMBOL)  != _Symbol)
         continue;
      if((int)PositionGetInteger(POSITION_MAGIC) != InpBaseMagic)
         continue;
      if((ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE) == type)
         return true;
     }
   return false;
  }

//+------------------------------------------------------------------+
//| Close positions by type                                          |
//+------------------------------------------------------------------+
void CloseByType(ENUM_POSITION_TYPE type)
  {
   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      ulong t = PositionGetTicket(i);
      if(!PositionSelectByTicket(t))         continue;
      if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
      if((int)PositionGetInteger(POSITION_MAGIC) != InpBaseMagic) continue;
      if((ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE) != type) continue;

      MqlTradeRequest req = {};
      MqlTradeResult  res = {};

      req.action   = TRADE_ACTION_DEAL;
      req.symbol   = _Symbol;
      req.position = t;
      req.volume   = PositionGetDouble(POSITION_VOLUME);
      req.magic    = InpBaseMagic;          // if InpBaseMagic ushort
      // req.magic = (ushort)InpBaseMagic;  // if retained int
      req.deviation= InpSlippage;
      req.type     = (type == POSITION_TYPE_BUY) ? ORDER_TYPE_SELL : ORDER_TYPE_BUY;
      req.price    = (type == POSITION_TYPE_BUY)
                     ? SymbolInfoDouble(_Symbol, SYMBOL_BID)
                     : SymbolInfoDouble(_Symbol, SYMBOL_ASK);
      req.type_filling = ORDER_FILLING_IOC;

      if(!OrderSend(req, res))
         Print("CloseByType: OrderSend failed, error = ", GetLastError());
      else if(res.retcode != TRADE_RETCODE_DONE)
         PrintFormat("CloseByType: retcode %d", res.retcode);
     }
  }
//+------------------------------------------------------------------+
//| JSON parser                                                      |
//+------------------------------------------------------------------+
string ParseJson(string json, string key)
  {
   string search = "\"" + key + "\"";
   int pos = StringFind(json, search);
   if(pos < 0)
      return "";
   pos = StringFind(json, ":", pos);
   if(pos < 0)
      return "";
   pos++;
   while(pos < StringLen(json) && StringSubstr(json, pos, 1) == " ")
      pos++;
   if(StringSubstr(json, pos, 1) == "\"")
     {
      pos++;
      string val = "";
      while(pos < StringLen(json))
        {
         string ch = StringSubstr(json, pos, 1);
         if(ch == "\"")
            break;
         if(ch == "\\" && pos + 1 < StringLen(json))
           { pos++; ch = StringSubstr(json, pos, 1); }
         val += ch;
         pos++;
        }
      return val;
     }
   else
     {
      string val = "";
      while(pos < StringLen(json))
        {
         string ch = StringSubstr(json, pos, 1);
         if(ch == "," || ch == "}" || ch == "]")
            break;
         val += ch;
         pos++;
        }
      StringTrimLeft(val);
      StringTrimRight(val);
      return val;
     }
  }

//+------------------------------------------------------------------+
//| WebSocket — initialization                                       |
//+------------------------------------------------------------------+
bool InitWebSocket()
  {
   uchar nullbuf[] = {};
   g_hSession = WinHttpOpen("MT5/LCMemAgent", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
                            NULL, NULL, 0);
   if(!g_hSession)
      return false;
   g_hConnect = WinHttpConnect(g_hSession, InpHost, InpPort, 0);
   if(!g_hConnect)
     {
      CloseWebSocket();
      return false;
     }
   g_hRequest = WinHttpOpenRequest(g_hConnect, "GET", NULL, NULL, NULL, NULL, 0);
   if(!g_hRequest)
     {
      CloseWebSocket();
      return false;
     }
   if(!WinHttpSetOption(g_hRequest, WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET,
                        nullbuf, 0))
     {
      CloseWebSocket();
      return false;
     }
   if(!WinHttpSendRequest(g_hRequest, NULL, 0, nullbuf, 0, 0, 0))
     { CloseWebSocket(); return false; }
   if(!WinHttpReceiveResponse(g_hRequest, nullbuf))
     { CloseWebSocket(); return false; }
   ulong nv = 0;
   g_hWebSocket = WinHttpWebSocketCompleteUpgrade(g_hRequest, nv);
   if(!g_hWebSocket)
     {
      CloseWebSocket();
      return false;
     }
   WinHttpCloseHandle(g_hRequest);
   g_hRequest = NULL;
   return true;
  }

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CloseWebSocket()
  {
   if(g_hWebSocket)
     {
      uchar r[];
      WinHttpWebSocketClose(g_hWebSocket,
                            WINHTTP_WEB_SOCKET_SUCCESS_CLOSE_STATUS, r, 0);
      g_hWebSocket = NULL;
     }
   if(g_hRequest)
     {
      WinHttpCloseHandle(g_hRequest);
      g_hRequest = NULL;
     }
   if(g_hConnect)
     {
      WinHttpCloseHandle(g_hConnect);
      g_hConnect = NULL;
     }
   if(g_hSession)
     {
      WinHttpCloseHandle(g_hSession);
      g_hSession = NULL;
     }
  }

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool WsSend(string msg)
  {
   if(!g_hWebSocket)
      return false;
   uchar buf[];
   int n = StringToCharArray(msg, buf, 0, StringLen(msg), CP_UTF8);
   return (WinHttpWebSocketSend(g_hWebSocket,
                                WINHTTP_WEB_SOCKET_UTF8_MESSAGE_BUFFER_TYPE, buf, n) == 0);
  }

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
string WsReceive(int ms = 90000)
  {
   if(!g_hWebSocket)
      return "";
   uchar  buf[131072];
   ZeroMemory(buf);
   ulong  bytesRead = 0;
   WINHTTP_WEB_SOCKET_BUFFER_TYPE bufType =
      (WINHTTP_WEB_SOCKET_BUFFER_TYPE)(-1);
   string result = "";
   uint   t0 = GetTickCount();

   while(GetTickCount() - t0 < (uint)ms)
     {
      ulong ret = WinHttpWebSocketReceive(g_hWebSocket, buf,
                                          ArraySize(buf), bytesRead, bufType);
      if(ret == 0 && bytesRead > 0)
        {
         result += CharArrayToString(buf, 0, (int)bytesRead, CP_UTF8);
         if(bufType == WINHTTP_WEB_SOCKET_UTF8_FRAGMENT_BUFFER_TYPE ||
            bufType == WINHTTP_WEB_SOCKET_BINARY_FRAGMENT_BUFFER_TYPE)
           { t0 = GetTickCount(); continue; }
         return result;
        }
      Sleep(50);
     }
   return result;
  }

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool SocketSendStr(string msg)
  {
   if(g_socket == INVALID_HANDLE)
      return false;
   uchar buf[];
   int n = StringToCharArray(msg, buf, 0, StringLen(msg), CP_UTF8);
   return (SocketSend(g_socket, buf, n) == n);
  }

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
string SocketReceiveStr(int ms = 90000)
  {
   if(g_socket == INVALID_HANDLE)
      return "";
   string res = "";
   uint   t0  = GetTickCount();
   while(GetTickCount() - t0 < (uint)ms)
     {
      uint avail = SocketIsReadable(g_socket);
      if(avail > 0)
        {
         uchar buf[];
         int n = SocketRead(g_socket, buf, avail, 2000);
         if(n > 0)
            res += CharArrayToString(buf, 0, n, CP_UTF8);
         if(StringLen(res) > 0)
            break;
        }
      Sleep(50);
     }
   return res;
  }

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool SendRaw(string msg)
  { return g_useWS ? WsSend(msg) : SocketSendStr(msg); }

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
string ReceiveRaw()
  { return g_useWS ? WsReceive(InpReceiveMs) : SocketReceiveStr(InpReceiveMs); }

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
string SendCommand(string cmd)
  {
   if(!SendRaw(cmd))
      return "";
   return ReceiveRaw();
  }
//+------------------------------------------------------------------+
