English Русский Deutsch 日本語
preview
使用MQL5实现Firebase数据的增删改查

使用MQL5实现Firebase数据的增删改查

MetaTrader 5积分 |
30 1
Chioma Obunadike
Chioma Obunadike

引言

如果您开发过移动端或者网页应用,就会知道后端管理是开发过程中复杂度高、耗时较长的环节之一。而谷歌热门的后端即服务(BaaS)平台Firebase,其核心优势就体现在这里。它承接了绝大部分后端繁重工作,让开发者可以更多专注业务功能开发,从而减少在基础设施管理上的投入。

Firebase提供大量应用开发工具:身份认证、云函数、数据分析、静态托管、崩溃报告等等。其中功能最强且使用最广泛的就是数据库服务。Firebase提供两大NoSQL数据库:Firebase实时数据库云Firestore。二者用途相近,但各有独特优势;选型主要取决于项目的实际需求。



Firebase实时数据库

实时数据库是Firebase早期就推出的产品,非常适合对低延迟、多客户端实时数据同步要求高的应用场景。例如聊天软件、多人联机游戏、实时数据仪表盘,或是一切需要用户无需刷新页面即可看到数据更新的应用。

实时数据库采用简单扁平的JSON树结构存储数据。它上手简单,即便在网络较差的环境下,也能实现极快的数据更新。但受限于扁平的数据结构,当业务数据逐渐复杂后,维护难度会上升。与传统数据库相比,嵌套数据、深层关联关系和过滤查询都需要做更细致的前期规划。



云Firestore

云Firestore(常被简称为Firestore)是Firebase新一代数据库产品。其设计目标就是兼顾可扩展性、结构化数据以及高级查询能力。Firestore采用层级化的文档数据模型。数据以集合(collections)和文档(documents)的形式组织,方便开发者处理复杂的数据关联,并具备更好的可扩展性。

Firestore还内置多项强大能力:

  • 高级查询 —— 多字段过滤、结果排序、查询分页
  • 离线持久化 —— 设备恢复网络后自动同步本地数据
  • 服务端时间戳、原子操作
  • 粒度更精细的安全规则
  • 和谷歌云其他服务的深度集成

总体而言,如果应用规模不大,优先追求速度与简单易用,适合选择实时数据库;如果项目规模更大,需要复杂数据模型、大规模业务支撑,则优先选择Firestore。



逐步搭建Firebase项目与数据库

现在,您已经了解两款数据库及其差异,下面从零开始完成数据库搭建。涵盖内容包含创建Firebase项目、选择数据库、配置安全规则以及数据结构。

步骤1:创建Firebase项目
要使用Firebase,您需要有谷歌账号。打开Firebase控制台,点击“添加项目”。填写项目名称(自定义即可),可以选择是否为项目开启谷歌分析。暂时可以跳过,或者等后续需要追踪用户行为时再开启。

点击“创建项目”后,Firebase会在后台完成资源分配。等待数秒,项目即创建完成。

步骤2:将应用注册到Firebase
想要调用Firebase的能力,需要先注册您的应用,支持网页端、安卓或iOS平台。点击对应平台图标(例如网页符号</>),跟随指引完成应用注册。网页应用注册完成后,系统会给到您一段配置代码段(Firebase项目凭证),需要复制粘贴到业务代码中。

配置片段示例如下:

const firebaseConfig = {
  apiKey: "YOUR_API_KEY",
  authDomain: "your-app.firebaseapp.com",
  projectId: "your-app",
  storageBucket: "your-app.appspot.com",
  messagingSenderId: "SENDER_ID",
  appId: "APP_ID"
};

使用该配置在您的应用中初始化Firebase。

步骤3:启用您所需的数据库
当项目与应用配置完成后,接下来选择数据库。在Firebase控制台左侧导航栏找到“构建”栏目,根据您的需求选择实时数据库或者云Firestore

点击“创建数据库”,选择数据存储区域。绝大多数应用直接使用默认区域即可,但为获得更优性能,您也可以挑选距离用户群体更近的地域节点。

步骤4:配置安全规则
Firebase数据库灵活性极高,但高灵活性也意味着开发者需要做好权限管控。出于安全考虑,Firebase默认限制数据库的访问权限。创建过程中会提示您选择安全规则模式。开发或测试阶段,可以临时使用“测试模式”,该模式允许任何人读写数据库。切记:不要在生产环境中使用测试模式!

测试模式的示例如下:

{
  "rules": {
    ".read": "true",
    ".write": "true"
  }
}

对于生产环境,您需要编写更具体的规则,用以控制哪些用户可以读写数据的哪些部分。Firebase安全规则可以依据用户身份认证、文档字段、请求参数等条件进行配置。

图例1. 创建一个数据库

  • 数据库URL与API密钥:为完成请求鉴权,您需要获取Firebase项目的API密钥以及数据库访问地址。这些信息位于项目设置中,对应“网页API密钥”以及“实时数据库URL”(或Firestore的REST接口端点)。

  • 理解REST接口端点:Firebase数据库可以通过REST API进行访问。例如:

    • 实时数据库:https://PROJECT_ID.firebaseio.com/path/to/data.json

    • Firestore: https://firestore.googleapis.com/v1/projects/PROJECT_ID/databases/(default)/documents/path/to/document

为演示读写操作的运行方式,我们首先新建一个集合(用于组织文档的逻辑容器),手动创建若干示例用户文档。该配置将作为后续数据读取练习的基础,帮助您完整理解Firestore的工作流程。 



创建第一个集合

点击开始创建集合按钮,此时会跳出弹窗,要求输入集合名称。我们来模拟一个简易用户数据库,将集合命名为users。

集合命名完成后,需要新建集合内的第一条文档。如果不手动指定,Firestore会自动生成随机文档ID;但为了便于理解,这里手动设置文档ID,使用简单名称如users

为文档添加字段
设置完文档ID后,就可以为文档填充字段。可以把字段类比为传统数据库中的列,但它具备更高的灵活性。

示例字段如下:

  • name → "Jane Doe"

由于我们的目标是通过MQL5代码自动写入数据,上述手动操作部分并不重要。现在,我们来建立数据库连接。

连接数据库
示例项目"my-users-mql5"的REST接口端点如下:

 https://firestore.googleapis.com/v1/projects/my-users-mql5/databases/(default)/documents/users/

如果您现在把该链接粘贴到浏览器地址栏,大概率会看到:

图例2. 空数据库

现在,users集合已经创建完毕,您可以开始学习如何通过代码读取Firestore中的数据。后续我们还会介绍如何添加数据、拉取文档、监听实时更新,以及使用查询来过滤和排序数据。


创建数据

HTTP的POST方法用于向指定资源提交数据,也就是向服务端发送数据。当您发起POST请求时,通常会在请求体中携带数据;服务端处理该数据后,就会创建一条新资源(例如新建用户账号、博客文章,或是数据库内的一条记录)。

这与GET请求有所区别:GET请求仅用于获取数据,绝对不应修改服务端的任何内容。与之相对,POST的核心是发送变更,用来生成新的数据。

在上述场景中,客户端都会把结构化数据(通常为JSON格式)发送给后端,或是直接发送至无服务器函数,再由函数将数据存入数据库,例如Firestore。

void add_data()
  {
   string url = "https://firestore.googleapis.com/v1/projects/my-users-mql5/databases/(default)/documents/users";
   string jsonData = "{\"fields\":{\"Name\":{\"stringValue\":\"Jane Doe\"}}}";
   uchar postData[], result[];
   StringToCharArray(jsonData, postData, 0, StringLen(jsonData), CP_UTF8);
   string header;
   int res = WebRequest("POST", url, "Content-Type: application/json\r\n", 5000, postData, result, header);
   if(res == 200)
     {
      Print("Success: ", CharArrayToString(result));
     }
   else
     {
      Print("Error ", res, ": ", CharArrayToString(result));
     }
  }

图例3. 创建新条目

使用上述代码,我们已经新增了一条名为“Jane Doe”的数据记录。该记录使用Firebase自动生成的唯一ID,即便并发写入多条数据,也可以避免键名冲突。我们可以按需添加任意数量的数据条目,还可以设置email、age或timestamp这类嵌套字段,只需按键值对方式组织这些字段并写入相应文档即可。下一步是读取数据。



读取数据

现在,数据库正常运行,接下来我们开展实际的数据交互操作。我们已经创建集合并写入若干示例文档,也掌握了使用POST向Firestore提交数据的方法。这是一个良好的开端,但读取数据与写入数据同等重要。事实上,对大量应用而言,获取并展示数据就是用户体验的核心。

无论是加载用户列表、展示资讯流,还是渲染商品目录,都需要从数据库拉取信息。这正是GET请求方法的作用。

GET是HTTP的核心请求方法之一。POST用于提交数据、创建新资源,与之不同的是,GET用于从服务端获取信息。其本质属于只读操作:发送GET请求,只是请求服务端返回对应数据,不会修改服务端的任何内容。

打个通俗的比方:POST请求如同提交表单注册新账号,而GET请求就像登录之后查看个人资料。您没有改动任何内容,只是获取已经存在的数据。

void authorize()
  {

// 1. Construct Firestore URL
   string url = "https://firestore.googleapis.com/v1/projects/my-users-mql5/databases/(default)/documents/users/";
// 2. Prepare headers
   string headers;
   headers += "Content-Type: application/json\r\n";
// 3. Send request
   char data[], result[];
   int timeout = 5000;
   int status = WebRequest("GET", url, headers, timeout, data, result, headers);
// 4. Handle response
   if(status == 200)
     {
      // Print("Firestore Data Received:");
      int filehandle = FileOpen("firebase-temp.txt", FILE_WRITE | FILE_BIN);
      if(filehandle != INVALID_HANDLE)
        {
         //--- Saving the contents of the result[] array to a file
         FileWriteArray(filehandle, result, 0, ArraySize(result));
         //--- Closing the file
         FileFlush(filehandle);
         FileClose(filehandle);
        }
      else
         Print("Error in FileOpen. Error code =", GetLastError());
     }
   else
     {
      Print("Error Code: ", status);
      Print("Response: ", CharArrayToString(result));
     }
  }

只需寥寥几行代码,我们就能得到文件"firebase-temp.txt",该文件保存了数据库返回的全部信息。

权限提醒:务必确认Firestore安全规则在对应位置开放读权限。公开测试阶段,可以使用开放规则(allow read: if true;),但生产环境务必收紧权限!



更新数据

到这里您应该已经注意到,无论通过控制台手动新增文档,还是调用Firebase SDK代码写入,Firestore数据库中的每一条文档都会被分配一个唯一文档ID。该ID是一串由字母、数字组成的随机字符串,例如kJ73sd98ASQv,乍看之下似乎没有太大意义。

Firestore的结构由集合与文档构成。一个集合包含若干文档,每份文档内部以键‑值对形式存储数据。向集合新增文档时,既可以交由Firestore自动生成ID,也可以手动自定义ID。

例如,向users集合新增一位用户后,会得到如下访问路径:

users/kJ73sd98ASQv

这意味着该用户文档存放于users集合内,将kJ73sd98ASQv作为唯一ID。后续如果您要读取、更新或者删除这份特定文档,就必须使用完整路径,其中要带上文档ID。

但千万不要忽视文档ID的重要性。实际上,它是唯一标识符,Firestore(以及您的应用程序)依靠它来快速定位、修改或移除指定的数据。可以把文档ID类比为指纹 —— 不存在两份ID完全相同的文档,根据ID才可以精准地操作单份文档。

因此,在Firestore中执行更新或删除操作时,获取并使用文档ID是必不可少的关键步骤。

以“Jane Doe”这条示例数据为例,该文档接口端点如下: 

https://firestore.googleapis.com/v1/projects/my-users-mql5/databases/(default)/documents/users/NoQ8m2vYLnGkykNhUjFE
对应的文档ID为:       
NoQ8m2vYLnGkykNhUjFE

想要更新这份数据,可以使用"PATCH"方法:

void update_entry(string doc_id)
  {
   string url = "https://firestore.googleapis.com/v1/projects/my-users-mql5/databases/(default)/documents/users/" + doc_id; //end point url
   string jsonData = "{\"fields\":{\"exampleField\":{\"stringValue\":\"Princess Doe\"}}}"; //specifiy data to be added
   uchar postData[], result[];
   StringToCharArray(jsonData, postData, 0, StringLen(jsonData), CP_UTF8); //convert plain string to char array to be sent to the db
   string header;
   int res = WebRequest("PATCH", url, "Content-Type: application/json\r\n", 5000, postData, result, header);
   if(res == 200)
     {
      Print("Success: ", CharArrayToString(result)); //if successful, print
     }
   else
     {
      Print("Error ", res, ": ", CharArrayToString(result)); //return error
     }
  }

运行此段代码,我们就可以把姓名字段的值由"Jane Doe"更新为"Princess Doe"。

图例4. 更新条目



删除条目

这一组操作的最后一项就是删除数据记录。如果要删除类似“Jane Doe”的记录,可以先定位到该记录的唯一ID,再针对对应的路径发送DELETE请求。例如,对存储Jane数据、由系统自动生成键的子节点调用DELETE请求,该记录就会从数据库中被永久清除。该操作可以结合Promise链式调用或者回调函数,处理执行成功与异常失败的状态,让应用能够给出合理的反馈。另外,需要配置Firebase安全规则来管控删除权限,避免未授权用户误删或者恶意销毁数据。

void delete_entry(string documentId)
  {
   string url = "https://firestore.googleapis.com/v1/projects/my-users-mql5" +
                "/databases/(default)/documents/users/" + documentId; //end point url with specified document ID
   uchar result[];
   uchar postData[]; // Empty payload for DELETE
   string headers = "Content-Type: application/json\r\n";
   string responseHeaders; // To store response headers (unused here)
// Correct WebRequest overload:
   int res = WebRequest(
                "DELETE",       // HTTP method
                url,            // Full URL with document ID
                headers,        // Request headers
                5000,           // Timeout (5 seconds)
                postData,       // Empty payload (uchar array, not NULL)
                result,         // Response data
                responseHeaders // Response headers (ignored)
             );
   if(res == 200)
     {
      Print("Document deleted"); //sucess
     }
   else
     {
      Print("Error ", res, ": ", CharArrayToString(result));
     }
  }

指定文档ID,即可完成该条记录的删除。 



在算法交易中使用Firebase

到目前为止,我们已经掌握了如何在MQL5中实现基础的增删改查功能,接下来通过实际示例,讲解如何把上述知识应用到算法交易场景中。对于本示例,我们将交易终端的交易数据上传至Firebase,之后在数据看板上读取这些交易数据。

向Firebase推送交易数据
我们可以编写基础MQL5代码获取已平仓订单的相关信息,并把这些数据发送到数据库做后续处理。 

datetime last_closed_time;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   last_closed_time = TimeCurrent();
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void add_data(ulong ticket, string symbol, double lot, double profit)
  {
   string url = "https://firestore.googleapis.com/v1/projects/my-users-mql5/databases/(default)/documents/users";
// Build JSON dynamically using function parameters
   string trade_id = "Trade_" + IntegerToString((int)ticket);
   string jsonData =
      "{"
      "\"fields\":{"
      "\"" + trade_id + "\":{"
      "\"mapValue\":{"
      "\"fields\":{"
      "\"symbol\":{\"stringValue\":\"" + symbol + "\"},"
      "\"lot\":{\"doubleValue\":" + DoubleToString(lot, 2) + "},"
      "\"profit\":{\"doubleValue\":" + DoubleToString(profit, 2) + "}"
      "}"
      "}"
      "}"
      "}"
      "}";
   uchar postData[], result[];
   StringToCharArray(jsonData, postData, 0, StringLen(jsonData), CP_UTF8);
   string header;
   int res = WebRequest("POST", url, "Content-Type: application/json\r\n", 5000, postData, result, header);
   if(res == 200)
     {
      Print("Success: ", CharArrayToString(result));
     }
   else
     {
      Print("Error ", res, ": ", CharArrayToString(result));
     }
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void OnTrade()
  {
   HistorySelect(0, TimeCurrent());
   for(int i = 0; i < HistoryDealsTotal(); i++)
     {
      if(deal.SelectByIndex(i))
        {
         if(deal.Time() >= last_closed_time && last_closed_time != 0)
           {
            if(deal.Entry() == DEAL_ENTRY_OUT)
              {
               add_data(deal.Ticket(), deal.Symbol(), deal.Volume(), deal.Profit());
              }
           }
        }
     }
   last_closed_time = TimeCurrent();
  }

将MQL5交易数据同步至Firestore
可以对所有上传的数据进行存储、下载,还可以逐行读取,之后展示在数据看板上。相比翻找海量日志文件或是依靠截图复盘,这种方式能够清晰且规整地查看交易历史。通过该数据组织形式,可以更直观地呈现交易绩效,让交易者发现那些原本容易被忽略的规律与模式。

int OnInit()
  {
//---
   draw_dashboard();
   get_data();
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void draw_dashboard()
  {
   ObjectCreate(0, "dash-board", OBJ_RECTANGLE_LABEL, 0, 0, 0);
   ObjectSetInteger(0, "dash-board", OBJPROP_XDISTANCE, 20);
   ObjectSetInteger(0, "dash-board", OBJPROP_YDISTANCE, 20);
   ObjectSetInteger(0, "dash-board", OBJPROP_XSIZE, 250);
   ObjectSetInteger(0, "dash-board", OBJPROP_YSIZE, 150);
   ObjectSetInteger(0, "dash-board", OBJPROP_BGCOLOR, clrDarkSlateGray);
   ObjectCreate(0, "dash-tv", OBJ_LABEL, 0, 0, 0);
   ObjectSetInteger(0, "dash-tv", OBJPROP_XDISTANCE, 50);
   ObjectSetInteger(0, "dash-tv", OBJPROP_YDISTANCE, 40);
   ObjectSetString(0, "dash-tv", OBJPROP_TEXT, "Total Volume: " +  DoubleToString(trading_volume_from_file(), 2));
   ObjectSetInteger(0, "dash-tv", OBJPROP_COLOR, clrWhite);
   ObjectCreate(0, "dash-profit", OBJ_LABEL, 0, 0, 0);
   ObjectSetInteger(0, "dash-profit", OBJPROP_XDISTANCE, 50);
   ObjectSetInteger(0, "dash-profit", OBJPROP_YDISTANCE, 80);
   ObjectSetString(0, "dash-profit", OBJPROP_TEXT, "Total Profit: " + DoubleToString(pnl_from_file(), 2));
   ObjectSetInteger(0, "dash-profit", OBJPROP_COLOR, clrWhite);
  }
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Get total trading volume                                         |
//+------------------------------------------------------------------+
double trading_volume_from_file(string filename = "firebase-temp-trade.txt")
  {
   double tv = 0;
   int filehandle = FileOpen(filename, FILE_READ | FILE_TXT | FILE_ANSI);
   if(filehandle == INVALID_HANDLE)
     {
      Print("Error opening file: ", GetLastError());
      return 0;
     }
   string line;
   while(!FileIsEnding(filehandle))
     {
      line = FileReadString(filehandle);
      // Look for lot values
      if(StringFind(line, "lot") != -1)
        {
         string raw0 = FileReadString(filehandle);
         int stat_int = StringFind(raw0, "d", 0);
         string raw = StringSubstr(raw0, stat_int + 14, -1);
         double profit = StringToDouble(raw);
         //---
         tv += NormalizeDouble(profit, 2);
        }
     }
   FileClose(filehandle);
   return tv;
  }

//+------------------------------------------------------------------+
//| Get total PnL                                                    |
//+------------------------------------------------------------------+
double pnl_from_file(string filename = "firebase-temp-trade.txt")
  {
   double p_l = 0;
   int filehandle = FileOpen(filename, FILE_READ | FILE_TXT | FILE_ANSI);
   if(filehandle == INVALID_HANDLE)
     {
      Print("Error opening file: ", GetLastError());
      return 0;
     }
   int line_no = 0;
   while(!FileIsEnding(filehandle))
     {
      string line = FileReadString(filehandle);
      line_no++;
      // Look for profit values
      if(StringFind(line, "profit") != -1)
        {
         string raw0 = FileReadString(filehandle);
         int stat_int = StringFind(raw0, "d", 0);
         string raw = StringSubstr(raw0, stat_int + 14, -1);
         double profit = StringToDouble(raw);
         //---
         p_l += profit;
        }
     }
   FileClose(filehandle);
   return p_l;
  }

//+------------------------------------------------------------------+
void get_data()
  {
// 1. Construct Firestore URL
   string url = "https://firestore.googleapis.com/v1/projects/my-users-mql5/databases/(default)/documents/users";
// 2. Prepare headers
   string headers;
   headers += "Content-Type: application/json\r\n";
// 3. Send request
   char data[], result[];
   int timeout = 5000;
   int status = WebRequest("GET", url, headers, timeout, data, result, headers);
// 4. Handle response
   if(status == 200)
     {
      // Print("Firestore Data Received:");
      int filehandle = FileOpen("firebase-temp-trade.txt", FILE_WRITE | FILE_BIN);
      if(filehandle != INVALID_HANDLE)
        {
         //--- Saving the contents of the result[] array to a file
         FileWriteArray(filehandle, result, 0, ArraySize(result));
         //--- Closing the file
         FileFlush(filehandle);
         FileClose(filehandle);
        }
      else
         Print("Error in FileOpen. Error code =", GetLastError());
     }
   else
     {
      Print("Error Code: ", status);
      Print("Response: ", CharArrayToString(result));
     }
  }
//+------------------------------------------------------------------+

输出示例如下:



结论

数据库是现代应用的基石,掌握数据库开发是开发者不可或缺的技能。学会对接Firebase后,无论是利用实时数据库的即时同步能力,还是利用Firestore强大的查询功能,您都已经掌握了一套核心工具集,可以用来开发动态、数据驱动型应用程序。 

文件名描述
 firebase-users.mq5 实现Firestore增删改查功能的代码文件
Download_Infor.mq5 从Firestore中拉取数据并在图表上展示的代码文件
 Sending_Trading_Info.mq5 将交易信息发送至Firestore的代码文件

本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/17854

附加的文件 |
Download_Info.mq5 (5.93 KB)
最近评论 | 前往讨论 (1)
joaopedrodev
joaopedrodev | 26 8月 2025 在 16:43
我注意到你正在使用 MQL5 中的原生 HTTP 请求函数与 Firebase 进行集成。

建议你看看Connexus——这是我开发的一个用于 HTTP 请求的库。它专门设计用于简化此类集成,使代码更简洁、更具可复用性,也更容易维护。
交易策略 交易策略
各种交易策略的分类都是任意的,下面这种分类强调从交易的基本概念上分类。
机器学习中的高斯过程(第一部分):MQL5 中的分类模型 机器学习中的高斯过程(第一部分):MQL5 中的分类模型
本文探讨了高斯过程的分类模型。我们将首先研究其理论原理,然后转向 MQL5 中 GP(高斯过程)库的实际开发。
新手在交易中的10个基本错误 新手在交易中的10个基本错误
新手在交易中会犯的10个基本错误: 在市场刚开始时交易, 获利时不适当地仓促, 在损失的时候追加投资, 从最好的仓位开始平仓, 翻本心理, 最优越的仓位, 用永远买进的规则进行交易, 在第一天就平掉获利的仓位,当发出建一个相反的仓位警示时平仓, 犹豫。
Parafrac震荡器:抛物线SAR与分形指标的组合 Parafrac震荡器:抛物线SAR与分形指标的组合
我们将探究如何把抛物线SAR与分形指标相结合,构建一款全新的震荡器。通过融合这两种工具各自独有的优势,交易者有望开发出一套更完善且更高效的交易策略。