使用MQL5实现Firebase数据的增删改查
引言
如果您开发过移动端或者网页应用,就会知道后端管理是开发过程中复杂度高、耗时较长的环节之一。而谷歌热门的后端即服务(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安全规则可以依据用户身份认证、文档字段、请求参数等条件进行配置。

数据库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/
如果您现在把该链接粘贴到浏览器地址栏,大概率会看到:

现在,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)); } }

使用上述代码,我们已经新增了一条名为“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"。

删除条目
这一组操作的最后一项就是删除数据记录。如果要删除类似“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
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
新手在交易中的10个基本错误
Parafrac震荡器:抛物线SAR与分形指标的组合