English Deutsch
preview
JSONをマスターする:MQL5で独自のJSONリーダーをゼロから作成する

JSONをマスターする:MQL5で独自のJSONリーダーをゼロから作成する

MetaTrader 5統合 | 17 6月 2025, 07:08
61 0
Sahil Bagdi
Sahil Bagdi

はじめに

こんにちは、ようこそ。MQL5でJSONデータを解析または操作しようとしたことがある方は、もっと簡単で柔軟な方法がないのかと疑問に思ったことがあるかもしれません。JSON (JavaScript Object Notation)は、人間に読みやすく、かつマシンにとっても扱いやすい軽量なデータ交換フォーマットとして広く普及しています。MQL5は主にMetaTrader 5プラットフォーム向けのエキスパートアドバイザー(EA)、インジケーター、スクリプトを作成するための言語として知られていますが、残念ながらネイティブのJSONライブラリは搭載されていません。つまり、Web APIや外部サーバー、ローカルファイルからのJSONデータを扱いたい場合、自作の仕組みを構築するか、既存のライブラリを統合する必要があります。

本記事では、そのギャップを埋めるべく、MQL5で自作のJSONリーダーを作成する方法を紹介します。その過程で、JSONを解析するための基本概念を解説し、さまざまなJSON要素(オブジェクト、配列、文字列、数値、ブール値、null値など)を柔軟に扱えるクラス構造の作り方を一歩一歩説明します。最終的には、MetaTrader 5環境でJSON文字列を自在に解析・操作できるようになることを目指します。

本記事の構成は、他のMQL5関連記事と同様の流れに沿いながらも、JSONの解析と活用に特化した内容となっています。本記事は全体で5つの主要セクションに分かれています。「はじめに」(今ご覧いただいているこのセクション)に始まり、JSONの基礎とそれがMQL5においてどのように活用できるかを深掘りした解説へと続きます。その後、基本的なJSONパーサーをゼロから構築するためのステップバイステップのガイドを紹介し、さらにJSONを扱う上での高度な機能について探っていきます。最後に、全体のコードリストとまとめを掲載し、記事を締めくくります。

JSONはあらゆる場面で使われています。サードパーティのサービスからマーケットデータを取得する時、自分の取引記録をアップロードする時、柔軟な設定が求められる戦略を試す時など、JSONはほぼ標準のフォーマットとなっています。アルゴリズム取引の世界におけるJSONの代表的な活用例は以下の通りです。

  1. マーケットデータの取得: 現在、多くのブローカーのAPIや金融データサービスは、リアルタイムや過去のデータをJSON形式で提供しています。JSONリーダーがあれば、そのデータをすぐに解析し、取引戦略に統合できます。

  2. 戦略の設定: たとえば、最大スプレッド、許容リスクレベル、取引可能時間帯など、複数のパラメータをサポートするEAがあるとします。こうした設定をJSONファイルに保存しておけば、コードを再コンパイルせずにJSONリーダーを使って動的に読み込み・更新できます。

  3. ログやデータの送信: 特定のシステムでは、取引ログやデバッグメッセージを外部サーバーへ送信したい場合があります。JSON形式で送信することで、ログを一貫性のある構造で管理でき、外部の解析ツールとも容易に統合できます。

Python、JavaScript、C++のような言語では、JSONを解析する方法が豊富に紹介されていますが、MQL5には、メモリ管理や配列の扱い、データ型の厳格さなど、言語特有の制約があります。

そこで、JSONの解析と操作に特化したカスタムクラス(またはクラス群)を作成します。目指すのは、次のようなことができるように設計することです。

CMyJsonParser parser;
parser.LoadString("{\"symbol\":\"EURUSD\",\"lots\":0.1,\"settings\":{\"slippage\":2,\"retries\":3}}");

// Access top-level fields:
Print("Symbol = ", parser.GetObject("symbol").ToStr());
Print("Lots = ", parser.GetObject("lots").ToDbl());

// Access nested fields:
CMyJsonElement settings = parser.GetObject("settings");
Print("Slippage = ", settings.GetObject("slippage").ToInt());
Print("Retries = ", settings.GetObject("retries").ToInt());

もちろん、最終的な実装では名前付けや構造が多少異なるかもしれませんが、目指すべき使いやすさは同じです。堅牢なパーサーを構築することで、たとえばMQL5のデータ構造をJSONに変換して出力する機能や、繰り返しおこなわれるJSONクエリに対するキャッシュロジックの追加など、今後の拡張に対応できる基盤が整います。

すでに世の中にはさまざまなJSONライブラリが存在し、文字配列を使ってJSONを解析するような簡単なスクリプトも見かけるかもしれません。私たちは、そうした既存のアプローチから学びつつも、コードをそのままコピーすることはしません。その代わり、同様の考え方に基づきながらも、より理解しやすく保守しやすい独自の実装を一から構築していきます。コードはスニペットごとに分解して解説していくので、記事の最後には、自分のトレーディングプログラムに組み込める、まとまりのある完成形を手にすることができるでしょう。

このように、ライブラリを一から作り上げていくアプローチを取り、各セグメントを平易な言葉で説明していくことで、単に完成品を渡されるよりも、より深い理解が得られることを期待しています。パーサーの仕組みをしっかりと理解することで、将来的なデバッグやカスタマイズも容易になるはずです。

JSONはテキストベースのフォーマットですが、MQL5の文字列には、改行や復帰、Unicode文字など、さまざまな特殊文字が含まれる可能性があります。今回の実装では、こうした要素にもある程度配慮し、できるだけ柔軟に対応できるようにします。ただし、入力データが有効なJSON形式であることを常に確認してください。不正なJSONや、正しい形式に見えるが実際は壊れているテキストを受け取った場合には、より強固なエラーハンドリングが必要になるかもしれません。

この記事の構成を簡単に紹介します。

  1. セクション1(今のセクション)- はじめに
    ここでは、JSONとは何か、それがなぜ重要なのか、そしてMQL5でカスタムパーサーをどのように作成していくかについて概観しました。これが、以降のすべての土台となります。

  2. セクション2 - 基本編:JSONとMQL5の基礎
    JSONの主要な構造要素を確認し、それらをMQL5のデータ型にどう対応させるかを解説します。特に注意すべき点についても触れていきます。

  3. セクション3 - パーサーの拡張:高度な機能の実装
    このセクションでは、パーサーの機能を拡張する方法について説明します。具体的には、配列の処理、エラーチェックの追加、MQL5のデータをJSONに再変換する方法などを扱います。

  4. セクション4 - 完全なコード
    これまでに構築してきたライブラリ全体を1つの場所にまとめ、参照用の完成ファイルとして提供します。

  5. セクション5 - 結論
    学んだ主要なポイントを振り返り、今後のプロジェクトで検討すべき次のステップについて簡単に紹介します。

この記事を読み終える頃には、MQL5でJSONの解析と操作が可能な、実用的なライブラリが手に入るでしょう。さらにその内部構造を理解することで、JSONを自動売買システムに統合する際の技術的な自信と柔軟性も得られるはずです。


基本編 - JSONとMQL5の基礎

おかえりなさい。これまでにMQL5向けカスタムJSONリーダーの全体的な計画を整理してきましたが、ここからはJSONの詳細に踏み込み、それをMQL5にどう対応させるかを見ていきましょう。JSONの構造を掘り下げて理解し、どのデータ型が解析しやすいかを確認しながら、MetaTrader 5でJSONデータを扱う際に起こり得る注意点も明らかにしていきます。このセクションを読み終える頃には、MQL5環境でJSONを扱うための具体的な方針が明確になるはずです。そして、次のハンズオンコーディングに向けた準備が整うことでしょう。

JSON (JavaScript Object Notation)は、データの送受信や保存によく使われるテキストベースのフォーマットです。XMLと比べて軽量で、構造もシンプルです。オブジェクトは波括弧{}で、配列は角括弧[]で囲まれ、それぞれのフィールドはキーと値のペアで記述されます。以下はその一例です。

{
  "symbol": "EURUSD",
  "lots": 0.05,
  "enableTrade": true
}

これは人間にとって読みやすく、機械にとっても解析しやすい形式です。symbolやenableTradeのような情報の各項目は「キー」と呼ばれ、それぞれ何らかの「値」を持ちます。値は、文字列、数値、真偽値、あるいは入れ子になったオブジェクトや配列である場合もあります。要するに、JSONはデータをツリー状に階層構造で整理する形式であり、基本的なパラメータから複雑な構造の情報まで表現できます。

JSONとMQL5のデータ型の対応関係:

  1. 文字列:JSONにおける文字列は、二重引用符で囲まれています(例:"Hello World")。MQL5にもstring型がありますが、こちらでは特殊文字、エスケープシーケンス、Unicodeコードなどを含むことができます。つまり、最初の注意点は、パーサーで引用符(")やエスケープされた記号(例:\")、さらにはUnicode(例:\u00A9)を正しく処理できるようにすることです。
  2. 数値:JSONでは数値は整数(例:42)や小数(例:3.14159)として表されます。MQL5では、数値は主にint(整数)またはdouble(浮動小数点数)として扱われます。ただし、すべてのJSON数値がint型にすんなり収まるわけではありません。たとえば、1234567890のような大きな値は、MQL5ではlong型が必要になる場合もあります。特に32ビット整数の範囲を超える場合には注意が必要です。また、非常に大きな整数をdoubleに変換して処理する選択肢もありますが、その場合は丸め誤差に注意する必要があります。
  3. 真偽値:JSONでは、小文字のtrueやfalseが使われます。一方、MQL5ではbool型を用います。ここは比較的単純な対応関係ですが、パーサーでtrueおよびfalseというトークンを正確に認識する必要があります。注意点として、たとえばTrueやFALSEのように大文字を含む表記は、正確なJSONでは無効です。他言語のパーサーでは許容される場合もありますが、MQL5で扱う場合には、データの整合性を保つか、例外的なフォーマットに柔軟に対応するロジックを組み込む必要があります。
  4. NULL:JSONでのnull値は、フィールドが空または欠落していることを示します。MQL5には専用の「null型」は存在しません。そのため、nullを何らかの内部表現(たとえば、JSON要素型を定義したenumにjtNULLのようなものを追加)として管理するか、空文字列や初期値で代用する必要があります。このあたりの処理方法については、後ほどパーサーの中で具体的に見ていきます。
  5. オブジェクト:波括弧{...}は、JSONオブジェクトを示しています。これはキーと値のペアの集まりです。MQL5には組み込みの辞書型はないため、動的配列でキーと値のペアを保持するか、それらを管理するためのカスタムクラスを作成する必要があります。たとえばCMyJsonObjectのようなクラスを定義して、内部に子要素(キーと値)を保持する仕組みを構築します。各子要素の値は、文字列でも数値でも、配列やオブジェクトであっても構いません。
  6. 配列:JSONの配列は、角括弧[...]で囲まれた順序付きリストです。中には文字列、数値、オブジェクト、さらには別の配列など、あらゆるJSON要素を含むことができます。MQL5では、ArrayResize関数や直接インデックス指定を使って配列を扱います。JSON配列は、おそらく要素の動的配列として保持されることになるでしょう。また、コード内では、あるノードが配列であること、そしてその中に含まれる子要素を追跡できるようにしておく必要があります。

潜在的な課題をいくつか見ていきましょう。

  1. エスケープシーケンスの処理:JSONでは、バックスラッシュ(\)が引用符や改行などの文字の前に置かれることがあります。たとえば、"description": "Line one\\nLine two"のような表現では、\\nを実際の改行文字として解釈する必要があります。以下のような特殊なシーケンスが存在します。
    • \":二重引用符
    • \\:バックスラッシュ
    • \/:スラッシュ(使用されることもある)
    • \n:改行
    • \t:タブ
    • \u:Unicodeコードポイント

    こうしたエスケープシーケンスを、JSONの生文字列からMQL5の実際の文字列に正確に変換する必要があります。これを怠ると、パーサーが誤った内容を保持したり、標準的なエスケープパターンを含む入力データの解析に失敗したりします。

  2. 空白と制御文字のトリミング:有効なJSON文字列には、スペース、タブ、改行などが含まれることがあります(特に要素間)。これらはほとんどの場合、意味を持たず無視されますが、解析処理を複雑にする原因になります。堅牢なパーサーは、引用符で囲まれていない範囲の空白文字を無視するのが一般的です。つまり、トークンを1つずつ読み進める際には、それらをスキップするような設計が望まれます。
  3. 大規模データへの対応:もしJSON文字列が非常に大きい場合、MQL5のメモリ制限を心配することになるかもしれません。MQL5は配列の処理に比較的強いものの、数千万要素に近づくと上限があります。多くのトレーダーにとって、そこまで大きなJSONを扱うことは稀ですが、そうした場合には「ストリーミング処理」や反復的な読み取りが必要になる可能性があります。とはいえ、設定ファイルや中規模なデータセットを扱う程度であれば、私たちの単純な実装で十分対応できます。
  4. すべてのJSONが正しいとは限りません。たとえば、引用符の閉じ忘れや末尾のカンマなど、構文エラーがある構造を読み取ろうとしたときに、パーサーがそれを適切に処理できる必要があります。エラーコードを定義したり、内部的にエラーメッセージを保持したりして、呼び出し元のコードが解析エラーを検知し、対処できるようにすることが望ましいです。取引システムにおいては、たとえば以下のような対応が考えられます。

    • メッセージボックスでエラーを表示したり、操作ログに出力する
    • JSONが不正な場合、安全なデフォルト設定にフォールバックする
    • 重要なデータの解析に失敗した場合、EAの実行を停止する

基本的な構文チェックを取り入れ、たとえば括弧の不一致や認識できないトークンといったミスを検出できるようにします。より高度なエラー報告機能も実装可能ですが、それはどれだけ厳密さを求めるかによります。

JSONは入れ子構造(ネスト)を取れるため、私たちのパーサーは、1つのクラスまたはクラス階層を用いて設計されることが多く、各ノードが以下のいずれかの型を持つようになるでしょう。

  • Object(オブジェクト):キーと値のペアを保持
  • Array(配列):インデックス付きの要素を保持
  • String(文字列):テキストデータを保持
  • Number(数値):数値データをdoubleあるいはlongとして保持
  • Boolean(真偽値):trueまたはfalse
  • Null:値なし

これらのノード型を表現するために、以下のように列挙型を実装することになるかもしれません。

enum JSONNodeType
  {
   JSON_UNDEFINED = 0,
   JSON_OBJECT,
   JSON_ARRAY,
   JSON_STRING,
   JSON_NUMBER,
   JSON_BOOL,
   JSON_NULL
  };
enum JSONNodeType
  {
   JSON_UNDEFINED = 0,
   JSON_OBJECT,
   JSON_ARRAY,
   JSON_STRING,
   JSON_NUMBER,
   JSON_BOOL,
   JSON_NULL
  };

次に、パーサークラスに「現在のノードがどの型か」を保持するための変数を用意します。また、ノードの中身も保存します。ノードがオブジェクトなら、文字列キーで子ノードを管理する配列を使います。配列であれば、0から始まるインデックス付きの子ノードリストを保持します。文字列ならそのまま文字列を保存し、数値ならdoubleに加えて、整数かどうかの情報を内部に保持する、というように使い分けます。

別のアプローチとして、オブジェクト用、配列用、文字列用といった具合に、それぞれ異なるクラスを用意する方法もあります。ただし、MQL5では型キャストが頻繁に必要になってしまい、かえって複雑になります。そのため、本記事では1つの統合クラス(あるいはメインクラスに補助構造体を加えた設計)を使って、任意のJSON型を動的に表現する方法を採用します。この一体化された設計は、入れ子構造の要素を扱う際に特に有利です。なぜなら、すべての子要素が同じ型(ただし内部表現は異なる)として扱えるため、コードをより簡潔かつ汎用的に保てるからです。

たとえば現時点ではJSONを読み取り専用で扱う予定でも、た将来的にはMQL5のデータをJSONとして出力したくなるかもしれません。たとえば、取引シグナルを生成してJSON形式でサーバーへ送信したり、取引履歴を構造化されたJSONファイルに記録したりしたいケースです。そのためには、「エンコーダー」または「シリアライザー」が必要になります。今回作るパーサーは、あとでそのような機能に拡張できるよう設計されます。文字列や配列を扱う基本コードは、JSONの生成にも活用できます。クラスメソッドを設計する際には「内部データからJSON文字列を出力するために、今の処理をどう反転させればよいか」という視点を持っておくとよいでしょう。

ここまでで、JSON構造とMQL5との対応関係がしっかり見えてきました。私たちは、次のような機能を備えた柔軟なクラスを設計する必要があります。

  1. ノードの型を保持する:数値、文字列、オブジェクト、配列、真偽値、nullのいずれか
  2. 解析処理:テキストを1文字ずつ読み取り、波括弧、角括弧、引用符、特殊トークンを解釈
  3. アクセス機能:キー(オブジェクトの場合)またはインデックス(配列の場合)で子ノードを取得・設定する便利なメソッド
  4. 型変換:数値や真偽値のノードをdoubleやint、boolなどのMQL5の基本型に変換
  5. エスケープ処理:JSONエンコードされた文字列をMQL5の通常の文字列に変換(逆方向の処理も将来追加可能にする)
  6. エラーチェック:不正な入力や不明なトークンを検出し、適切に処理する

これらの機能を次のセクションで一つずつ実装していきます。パフォーマンスやメモリ消費が心配な方もいるかもしれませんが、通常の用途であれば、シンプルな実装で十分高速かつ効率的です。もし本当にボトルネックが生じた場合は、プロファイリングしたり、部分解析に切り替えたりする余地もあります。

セクション3では、パーサーの本格的な実装に入ります。CJsonNodeのようなクラスを定義し、まずはノードの型と値を保持する仕組み、そして括弧や引用符などのJSONトークンを識別する「トークナイザー」メソッドの実装から始めます。基礎ができたら、オブジェクトや配列、入れ子構造の処理、データの取り出しへと進みます。

小さな設定ファイルを読むだけでも、外部のWebサービスから大規模なデータを取得する場合でも、基本原理は同じです。たとえMQL5で外部データを扱うのが初めてでも、ステップごとにロジックを見ていけば、十分に理解できます。

では、ひと息ついて、次のセクションで本格的なコーディングに入りましょう。次のセクションでは、カスタムJSONパーサーを一歩ずつ実装しながら、データを確実に処理するための実践的なヒントも紹介していきます。MetaTrader 5がJSONを「話せる」ようにしていきましょう。

パーサーの中核クラス: 私たちのパーサークラスの目的は、JSONデータの任意の要素(ツリー構造で言う「ノード」)を表現することです。次のような構成を想定しています。

  1. ノード型の列挙:  JSONの型(オブジェクト、配列、文字列など)を明確に区別するため、以下のように定義しておくと便利です。
    enum JsonNodeType
      {
       JSON_UNDEF  = 0,
       JSON_OBJ,
       JSON_ARRAY,
       JSON_STRING,
       JSON_NUMBER,
       JSON_BOOL,
       JSON_NULL
      };
    
  2. メンバー変数 
    各CJsonNodeは以下の情報を保持します。
    • ノード型を識別するJsonNodeType m_type
    • オブジェクトの場合:キーと値のペアを保持するための構造(配列のようなもの)
    • 配列の場合:インデックス付きの子ノードを保持する構造
    • 文字列の場合:文字列型のm_value
    • 数値の場合:double m_numVal、必要に応じてlong m_intValを追加で保持
    • ブール値の場合:bool m_boolVal
  3. 解析メソッドとユーティリティメソッド
    • 生のJSONテキストを解析するためのメソッド
    • 子ノードをインデックスまたはキーで取得するためのメソッド
    • 入力を「トークン化」して括弧・中括弧・文字列・ブール値などを識別するためのメソッドを用意する可能性もある

これらの設計方針を念頭に置きながら、これから実装に取りかかります。以下は、CJsonNode.mqhのようなファイルにこのクラスを定義する際の一例です。ここから段階的に構築していきます。

//+------------------------------------------------------------------+
//|      CJsonNode.mqh                                               |
//+------------------------------------------------------------------+
#pragma once

enum JsonNodeType
  {
   JSON_UNDEF  = 0,
   JSON_OBJ,
   JSON_ARRAY,
   JSON_STRING,
   JSON_NUMBER,
   JSON_BOOL,
   JSON_NULL
  };

// Class representing a single JSON node
class CJsonNode
  {
private:
   JsonNodeType     m_type;         // The type of this node
   string           m_value;        // Used if this node is a string
   double           m_numVal;       // Used if this node is a number
   bool             m_boolVal;      // Used if this node is a boolean

   // For arrays and objects, we'll keep child nodes in a dynamic array:
   CJsonNode        m_children[];   // The array for child nodes
   string           m_keys[];       // Only used if node is an object
                                     // For arrays, we’ll just rely on index

public:
   // Constructor & destructor
   CJsonNode();
   ~CJsonNode();

   // Parsing interface
   bool ParseString(const string jsonText);

   // Utility methods (we will define them soon)
   void  SetType(JsonNodeType nodeType);
   JsonNodeType GetType() const;
   int   ChildCount() const;

   // Accessing children
   CJsonNode* AddChild();
   CJsonNode* GetChild(int index);
   CJsonNode* GetChild(const string key);
   void        SetKey(int childIndex,const string key);

   // Setting and getting values
   void SetString(const string val);
   void SetNumber(const double val);
   void SetBool(bool val);
   void SetNull();

   string AsString() const;
   double AsNumber() const;
   bool   AsBool()   const;

   // We’ll add the actual parse logic in a dedicated private method
private:
   bool ParseRoot(string jsonText);
   bool ParseObject(string text, int &pos);
   bool ParseArray(string text, int &pos);
   bool ParseValue(string text, int &pos);
   bool SkipWhitespace(const string text, int &pos);
   // ... other helpers
  };

上記のコードの解説

  • m_children[]:複数の子CJsonNodeオブジェクトを保持するための動的配列です。配列の場合は各要素がインデックスで管理され、オブジェクトの場合は各要素に対応するキーがm_keys[]に保存されます。
  • ParseString(const string jsonText):このpublicメソッドが、JSONパーサーの「メインエントリーポイント」です。JSON形式の文字列を入力として受け取り、ノードの内部データを解析・構築していきます。
  • ParseRoot、ParseObject、ParseArray、ParseValue:それぞれのJSON構造(ルート、オブジェクト、配列、値)を処理するためのprivateメソッドです。

今はまだクラスの骨組みを見せている段階ですが、これから詳細を段階的に実装していきます。JSONを解析する際には、テキストを左から右に読み進めていきます。空白文字は無視し、構造を示す文字が現れるまで読み飛ばします。以下はその代表例です。

  • 「{」はオブジェクトの開始を意味します
  • 「[」は配列の開始を意味します
  • 「\"」は文字列の開始を意味します
  • 数字またはマイナス記号は数値を意味する場合があります。
  • true、false、nullといったシーケンスもJSONでよく使われます

ParseStringメソッド内で、どのようにJSON全体のテキストを解析するか、その簡略版を見ていきましょう。

bool CJsonNode::ParseString(const string jsonText)
  {
   // Reset existing data first
   m_type   = JSON_UNDEF;
   m_value  = "";
   ArrayResize(m_children,0);
   ArrayResize(m_keys,0);

   int pos=0;
   return ParseRoot(jsonText) && SkipWhitespace(jsonText,pos) && pos>=StringLen(jsonText)-1;
  }
  • リセット:以前のデータをすべて消去します。
  • pos=0:これは文字列内の文字の位置です。
  • ParseRoot(jsonText)の呼び出し:この関数は後ほど定義しますが、m_typeを設定し、必要に応じてm_childrenやm_valueを初期化・格納する役割を担います。
  • SkipWhitespace(jsonText,pos):空白、タブ、改行などの不要な文字を読み飛ばします。
  • 解析後の位置チェック:すべて正しく解析できていれば、posは文字列の末尾近くにあるはずです。そうでなければ、末尾に余分なテキストがあるか、構文エラーの可能性があります。

では、次にParseRoot関数の中身をもう少し詳しく見ていきましょう。簡潔にするため、以下のような形を想定してみます。

bool CJsonNode::ParseRoot(string jsonText)
  {
   int pos=0;
   SkipWhitespace(jsonText,pos);

   // If it begins with '{', parse as object
   if(StringSubstr(jsonText,pos,1)=="{")
     {
      return ParseObject(jsonText,pos);
     }
   // If it begins with '[', parse as array
   if(StringSubstr(jsonText,pos,1)=="[")
     {
      return ParseArray(jsonText,pos);
     }

   // Otherwise, parse as a single value
   return ParseValue(jsonText,pos);
  }

デモンストレーションのために、最初に現れる空白以外の文字をチェックし、それがオブジェクト({)、配列([)、またはその他(文字列、数値、ブール値、またはnullの可能性あり)かを判断しています。本番用の実装では、予期しない文字が現れた場合に備えて、より堅牢なエラーハンドリングを加えることができます。

それでは、各ケースの解析処理について見ていきましょう。

  1. オブジェクトの解析 :開始波かっこ({)が見つかった場合、オブジェクトノードを作成します。続いて、閉じ波かっこ(})に到達するまで、キーと値のペアを繰り返し読み取っていきます。以下はParseObjectがどのように動作するかを示す概念的なスニペットです。
    bool CJsonNode::ParseObject(string text, int &pos)
          {
           // We already know text[pos] == '{'
           m_type = JSON_OBJ;
           pos++; // move past '{'
           SkipWhitespace(text,pos);
        
           // If the next char is '}', it's an empty object
           if(StringSubstr(text,pos,1)=="}")
             {
              pos++;
              return true;
             }
        
           // Otherwise, parse key-value pairs in a loop
           while(true)
             {
              SkipWhitespace(text,pos);
              // The key must be a string in double quotes
              if(StringSubstr(text,pos,1)!="\"")
                return false; // or set an error
              // parse the string key (we’ll show a helper soon)
              string objKey = "";
              if(!ParseStringLiteral(text,pos,objKey))
                 return false;
        
              SkipWhitespace(text,pos);
              // Expect a colon
              if(StringSubstr(text,pos,1)!=":")
                 return false;
              pos++;
              
              // Now parse the value
              CJsonNode child;
              if(!child.ParseValue(text,pos))
                 return false;
        
              // Add the child to our arrays
              int newIndex = ArraySize(m_children);
              ArrayResize(m_children,newIndex+1);
              ArrayResize(m_keys,newIndex+1);
              m_children[newIndex] = child;
              m_keys[newIndex]     = objKey;
        
              SkipWhitespace(text,pos);
              // If next char is '}', object ends
              if(StringSubstr(text,pos,1)=="}")
                {
                 pos++;
                 return true;
                }
              // Otherwise, we expect a comma before the next pair
              if(StringSubstr(text,pos,1)!=",")
                 return false;
              pos++;
             }
           // unreachable
           return false;
          } 

    説明:

    • 最初に文字が「{」であることを確認し、ノード型をJSON_OBJに設定して、posをインクリメントします。
    • 次に、すぐ「}」が続く場合、そのオブジェクトは空です。
    • そうでなければ、「}」またはエラーに遭遇するまでループします。各反復処理では:
      • 引用符で囲まれた文字列キーを解析します
      • 空白をスキップし、次にコロン(:)があることを確認します
      • 次の値を解析します(文字列、数値、配列、オブジェクトなどの可能性があります)
      • それをm_childrenおよびm_keysの配列に保存します
      • 「}」が現れたら終了です。カンマがあれば次の要素に進みます。

    このループがJSONオブジェクトの読み取りの中核です。配列に関しては構造がよく似ていますが、キーが存在せず、インデックス付きの要素のみを持つ点が異なります。

  2. 配列の解析: 配列は「[」で始まります。その中には、カンマで区切られた0個以上の要素が含まれます。たとえば次のようになります。

    [ "Hello", 123, false, {"nestedObj": 1}, [10, 20] ]
    

    コード:

    bool CJsonNode::ParseArray(string text, int &pos)
      {
       m_type = JSON_ARRAY;
       pos++; // skip '['
       SkipWhitespace(text,pos);
    
       // If it's immediately ']', it's an empty array
       if(StringSubstr(text,pos,1)=="]")
         {
          pos++;
          return true;
         }
    
       // Otherwise, parse elements in a loop
       while(true)
         {
          SkipWhitespace(text,pos);
          CJsonNode child;
          if(!child.ParseValue(text,pos))
             return false;
    
          // store the child
          int newIndex = ArraySize(m_children);
          ArrayResize(m_children,newIndex+1);
          m_children[newIndex] = child;
    
          SkipWhitespace(text,pos);
          // if next char is ']', array ends
          if(StringSubstr(text,pos,1)=="]")
            {
             pos++;
             return true;
            }
          // must find a comma otherwise
          if(StringSubstr(text,pos,1)!=",")
             return false;
          pos++;
         }
    
       return false;
      }
    

    「[」と空白をスキップします。次にすぐ「]」が現れた場合、その配列は空です。そうでなければ、「]」に到達するまでループで要素を解析していきます。オブジェクトとの主な違いは、キーと値のペアを解析するのではなく、値だけを順番に処理する点です。

  3. 値の解析:JSONの値は、文字列、数値、オブジェクト、配列、ブール値、またはnullになります。ParseValueは次のようになります。

    bool CJsonNode::ParseValue(string text, int &pos)
      {
       SkipWhitespace(text,pos);
       string c = StringSubstr(text,pos,1);
    
       // Object
       if(c=="{")
         {
          return ParseObject(text,pos);
         }
       // Array
       if(c=="[")
         {
          return ParseArray(text,pos);
         }
       // String
       if(c=="\"")
         {
          m_type = JSON_STRING;
          return ParseStringLiteral(text,pos,m_value);
         }
       // Boolean or null
       // We’ll look for 'true', 'false', or 'null'
       if(StringSubstr(text,pos,4)=="true")
         {
          m_type    = JSON_BOOL;
          m_boolVal = true;
          pos+=4;
          return true;
         }
       if(StringSubstr(text,pos,5)=="false")
         {
          m_type    = JSON_BOOL;
          m_boolVal = false;
          pos+=5;
          return true;
         }
       if(StringSubstr(text,pos,4)=="null")
         {
          m_type = JSON_NULL;
          pos+=4;
          return true;
         }
    
       // Otherwise, treat it as a number or fail
       return ParseNumber(text,pos);
      }
    

    ここでは、次の操作をおこないます。

    1. 空白をスキップします。
    2. 現在の文字(または文字列の一部)を見て、それが{、[、"などかどうか確認します。
    3. 関連する解析関数を呼び出します。
    4. true、false、またはnullが見つかったら、それらを直接処理します。
    5. どれにも該当しなければ、数値だと仮定します。

    必要に応じて、より良いエラーハンドリングを追加することもできます。たとえば、文字列が認識されないパターンだった場合は、エラーを設定することが可能です。

  4. 数値の解析:123、3.14、-0.001のような、数値のように見えるものを解析する必要があります。非数値文字に達するまでスキャンする簡単な方法で実装できます。

    bool CJsonNode::ParseNumber(string text, int &pos)
      {
       m_type = JSON_NUMBER;
    
       // capture starting point
       int startPos = pos;
       while(pos < StringLen(text))
         {
          string c = StringSubstr(text,pos,1);
          if(c=="-" || c=="+" || c=="." || c=="e" || c=="E" || (c>="0" && c<="9"))
            {
             pos++;
            }
          else break;
         }
    
       // substring from startPos to pos
       string numStr = StringSubstr(text,startPos,pos-startPos);
       if(StringLen(numStr)==0)
         return false;
    
       // convert to double
       m_numVal = StringToDouble(numStr);
       return true;
      }
    

    数字、オプションの符号(-または+)、小数点、そして指数表記(eまたはE)を許可します。スペース、カンマ、または括弧などの別の文字に遭遇したらそこで処理を止めます。その後、その部分文字列をdouble型に変換して解析します。コード上で整数と小数を区別する必要があれば、追加の判定処理を加えることも可能です。


高度な機能を備えたパーサーの拡張

これまでに、オブジェクト、配列、文字列、数値、ブール値、null値を扱えるMQL5用の基本的なJSONパーサーを作成してきました。このセクションでは、さらに高度な機能や改良点を探っていきます。具体的には、子要素をより便利に取得する方法、エラーを適切に処理する方法、さらにはデータをJSON形式のテキストに再変換する方法などを紹介します。ここで紹介する機能を既存のパーサーに重ねることで、より堅牢で柔軟性のあるツールに進化させることができます。こうした改善により、さまざまな実用的なニーズに応えることができるようになります。
  1. キーやインデックスによる子要素の取得

    実用的なパーサーを目指すなら、オブジェクト内の特定のキーの値や、配列内の特定のインデックスにある要素を簡単に取得できるようにしたいところです。たとえば、以下のようなJSONを考えてみましょう。

    {
      "symbol": "EURUSD",
      "lots": 0.02,
      "settings": {
        "slippage": 2,
        "retries": 3
      }
    }

    たとえば、これを解析してrootNodeという名前のCJsonNodeオブジェクトに変換したとしましょう。そこから次のような操作をおこないたくなります。

    string sym = rootNode.GetChild("symbol").AsString();
    double lot = rootNode.GetChild("lots").AsNumber();
    int slip   = rootNode.GetChild("settings").GetChild("slippage").AsNumber();

    現在のコード構造では、パーサーにGetChild(const string key)を定義すれば、こうした操作が可能になるかもしれません。以下は、CJsonNodeクラスにおけるそのようなメソッドの例です。

    CJsonNode* CJsonNode::GetChild(const string key)
      {
       if(m_type != JSON_OBJ) 
          return NULL;
    
       // We look through m_keys to find a match
       for(int i=0; i<ArraySize(m_keys); i++)
         {
          if(m_keys[i] == key)
             return &m_children[i];
         }
       return NULL;
      }
    

    このようにすれば、現在のノードがオブジェクトでない場合は単にNULLを返します。そうであれば、m_keysをすべて走査して一致するキーを探し、見つかれば対応する子ノードへのポインタを返します。

    同様に、配列用のメソッドも定義できます。

    CJsonNode* CJsonNode::GetChild(int index)
      {
       if(m_type != JSON_ARRAY) 
          return NULL;
    
       if(index < 0 || index >= ArraySize(m_children))
          return NULL;
    
       return &m_children[index];
      }

    ノードが配列であれば、単にインデックスが範囲内かを確認し、該当する要素を返します。ノードが配列でない場合や、インデックスが範囲外であれば、NULLを返します。実際のコードでは、この戻り値がNULLかどうかを確認してから間接参照することが非常に重要です。」

  2. 適切なエラー処理

    現実の多くのケースでは、JSONが壊れている(たとえば、引用符の欠落、カンマの付けすぎ、予期しない記号など)状態で届くことがあります。堅牢なパーサーは、こうしたエラーを検出し、報告すべきです。そのためには、次のような方法があります。

    1. ブール値を返す:ほとんどの解析用メソッドはすでにboolを返すようになっています。何かに失敗した場合はfalseを返します。しかし、内部的にm_errorMsgのようなエラーメッセージを格納することで、呼び出し元のコードが何が起きたかを確認できるようにもできます。

    2. 解析を続行するか中止するか:致命的な解析エラー(例:予期しない文字や閉じられていない中括弧)を検出した場合、解析全体を中止してノードを無効な状態のままにする判断もできます。あるいは、スキップまたは回復を試みることができますが、それにはより高度な処理が必要です。

    概念的な工夫として、ParseArrayやParseObjectの中で予期しないもの(例:引用符のないキーやコロンの欠落)を見つけたときには、次のように書くことができます。

    Print("Parse Error: Missing colon after key at position ", pos);
    return false;
    

    次に、呼び出しコードで次の操作を実行します。

    CJsonNode root;
        if(!root.ParseString(jsonText))
          {
           Print("Failed to parse JSON data. Check structure and try again.");
           // Perhaps handle defaults or stop execution
          }
    

    これらのエラーメッセージをどれだけ詳細にするかは、あなた次第です。取引戦略の中では、単に「解析失敗」とだけ出せば十分な場合もあります。一方で、JSONの入力をデバッグしたいときには、もっと細かな情報が欲しくなることもあるでしょう。

  3. MQL5データをJSONに変換する

    JSONを読み込むだけがすべてではありません。サーバーにデータを送信したり、自分の取引ログをJSON形式で書き出したい場合もあるでしょう。こうしたときには、CJsonNodeクラスに「シリアライズ用」のメソッドを追加して、ノード内部のデータ構造をたどりながら、元のJSON文字列を再構成できるようにします。たとえば、ToJsonString()という名前のメソッドを追加してみましょう。

    string CJsonNode::ToJsonString() const
      {
       // We can define a helper that does the real recursion
       return SerializeNode(0);
      }
    
    string CJsonNode::SerializeNode(int depth) const
      {
       // If you prefer pretty-print with indentation, use 'depth'
       // For now, let's keep it simple:
       switch(m_type)
         {
          case JSON_OBJ:
             return SerializeObject(depth);
          case JSON_ARRAY:
             return SerializeArray(depth);
          case JSON_STRING:
             return "\""+EscapeString(m_value)+"\"";
          case JSON_NUMBER:
          {
             // Convert double to string carefully
             return DoubleToString(m_numVal, 10); 
          }
          case JSON_BOOL:
             return m_boolVal ? "true":"false";
          case JSON_NULL:
             return "null";
          default:
             return "\"\""; // or some placeholder
         }
      }
    

    次に、たとえばSerializeObjectを定義できます。

    string CJsonNode::SerializeObject(int depth) const
      {
       string result = "{";
       for(int i=0; i<ArraySize(m_children); i++)
         {
          if(i>0) result += ",";
          string key   = EscapeString(m_keys[i]);
          string value = m_children[i].SerializeNode(depth+1);
          result += "\""+key+"\":";
          result += value;
         }
       result += "}";
       return result;
      }
    

    配列の場合も同様です。

    string CJsonNode::SerializeArray(int depth) const
      {
       string result = "[";
       for(int i=0; i<ArraySize(m_children); i++)
         {
          if(i>0) result += ",";
          result += m_children[i].SerializeNode(depth+1);
         }
       result += "]";
       return result;
      }
    

    ご覧のとおり、ここではEscapeString関数を使っています。これは、JSONの文字列エスケープ(例:特殊文字を\"、\\、\nなどに変換する処理)を扱うコードを再利用したものです。これにより、引用符や改行を含む場合でも、出力が正しいJSON形式になるよう保証できます。

    「整形済み(pretty-printed)」のJSONを出力したい場合は、改行("\n")やインデントを挿入することで見やすくすることができます。一つの方法としては、深さに応じた空白文字列を作成し、それをインデントとして使うことで、JSONの構造を視覚的に分かりやすくすることができます。

    string indentation = "";
    for(int d=0; d<depth; d++)
       indentation += "  ";
    

    そのインデントを各行や要素の前に挿入すればOKです。これは必須ではありませんが、JSON出力を手動で読み取ったりデバッグしたりする必要がある場合には非常に便利です。

    JJSONデータが非常に大きく、数万行にも及ぶような場合は、パフォーマンスへの配慮が必要になります。

    1. 効率的な文字列操作
      StringSubstrなどの部分文字列処理は、繰り返し使用するとコストがかさむ可能性があります。MQL5は比較的高速ですが、データが本当に巨大な場合には、チャンク単位で処理する方法や、逐次的な処理アプローチを検討した方が良いかもしれません。

    2. ストリーミングvs.DOM解析
      現在の戦略は「DOM型」アプローチであり、入力全体を解析してツリー構造として保持します。もしデータが非常に大きくてメモリに収まりきらない場合には、一度に少しずつ処理する「ストリーミングパーサー」が必要になるかもしれません。これはより複雑になりますが、極端に大きなデータを扱う場合には避けられない手法です。

    3. キャッシュ処理
      同じオブジェクトに対して頻繁に同じキーでアクセスする場合は、小さなマップにそれらを格納したり、繰り返し参照するためのポインタを保持したりすることで、検索処理を高速化できるかもしれません。通常の取引関連のタスクでは、こうした最適化はほとんど必要ありませんが、パフォーマンスが重要な状況では選択肢の一つになります。

  4. ベストプラクティス

    以下は、コードを安全かつ保守しやすく保つためのいくつかのベストプラクティスです。

    • 常にNULLをチェックする
      GetChild(...)を呼び出したら、必ず結果がNULLでないことを確認します。MQL5でnullポインタにアクセスしようとすると、クラッシュや不安定な動作を引き起こす可能性があります。

    • 型を検証する
      たとえば、数値を期待しているのに実際には子ノードが文字列だった場合、問題が発生するかもしれません。GetType()を使って型を検証したり、防御的なコードを書いたりすることを検討します。例:

    CJsonNode* node = parent.GetChild("lots");
    if(node != NULL && node.GetType() == JSON_NUMBER)
      double myLots = node.AsNumber();
    

    これにより、データが期待どおりのものであることが保証されます。

    デフォルト値

    JSONにキーが欠けている場合に、安全なフォールバックを用意したいことがよくあります。そのために、以下のような補助関数を書くことができます。

    double getDoubleOrDefault(CJsonNode &obj, const string key, double defaultVal)
      {
       CJsonNode* c = obj.GetChild(key);
       if(c == NULL || c.GetType() != JSON_NUMBER) 
         return defaultVal;
       return c.AsNumber();
      }
    

    そうすることで、コードは欠落したり無効なフィールドを優雅に処理できるようになります。

    • MQL5の文字列や配列の制約に注意する
      MQL5は大きな文字列も扱えますが、メモリ使用量には注意が必要です。JSONが非常に大きい場合は、十分にテストしてください。
      同様に、配列サイズは変更可能ですが、数十万要素など極端に大きな配列は扱いにくくなることがあります。

    • テスト
      履歴データを使用してEAのロジックをテストするのと同じように、さまざまなサンプル入力を使用してJSONパーサーをテストします。

      • 単純なオブジェクト
      • ネストされたオブジェクト
      • 異なるデータ型が混在する配列
      • 大きな数、負の数
      • ブール値やnull
      • 特殊文字やエスケープシーケンスを含む文字列

    多様なパターンを試せば試すほど、パーサーの堅牢性に自信が持てます。

ここまでで、基本的なパーサーを強力なJSONユーティリティへと昇華させました。JSON文字列を階層構造に変換し、キーやインデックスでデータを取得でき、解析失敗にも対応し、ノードを再びJSONテキストとしてシリアライズも可能です。これは多くのMQL5の用途に十分で、たとえば設定ファイルの読み込み、(HTTPリクエストブリッジがあれば)Webからのデータ取得、自作のJSONログ生成などに活用できます。

最終章では、ここまで説明した内容をすべてまとめた完全なコードを提示します。このコードはそのままMQL5エディタに貼り付けて、.mqhファイルや.mq5スクリプトとして利用可能です。命名規則に合わせて調整し、すぐにJSONデータの取り扱いを開始できます。また、最後にライブラリを拡張したい場合のヒントや考察も紹介します。


完全なコード

ここまで読まれたことおめでとうございます。MQL5でのJSONの基本を学び、ステップバイステップでパーサーを構築し、高度な機能を追加し、実際の利用に役立つベストプラクティスも探りました。いよいよ、これまでのコード断片を一つにまとめた統合版をお見せする時です。この最終コードは.mqhファイル(または直接.mq5ファイル)に保存して、MetaTrader 5のプロジェクト内でJSON処理が必要な箇所に組み込めます。

以下は「CJsonNode.mqh」という名前のサンプル実装です。オブジェクトや配列の解析、エラーチェック、JSONへの再シリアライズ、キーやインデックスによるデータ取得機能を統合しています。

重要:のコードはオリジナルであり、前述の参考コードのコピーではありません。似たような解析ロジックを踏襲していますが、要件に合わせて新たに設計された独自の実装です。もちろん、メソッド名を変更したり、より堅牢なエラーハンドリングを追加したり、特定用途向けの機能を実装したりすることも自由です。

#ifndef __CJSONNODE_MQH__
#define __CJSONNODE_MQH__

//+------------------------------------------------------------------+
//| CJsonNode.mqh - A Minimalistic JSON Parser & Serializer in MQL5  |
//| Feel free to adapt as needed.                                    |
//+------------------------------------------------------------------+
#property strict

//--- Enumeration of possible JSON node types
enum JsonNodeType
  {
   JSON_UNDEF  = 0,
   JSON_OBJ,
   JSON_ARRAY,
   JSON_STRING,
   JSON_NUMBER,
   JSON_BOOL,
   JSON_NULL
  };

//+-----------------------------------------------------------------+
//| Class representing a single JSON node                           |
//+-----------------------------------------------------------------+
class CJsonNode
  {
public:
   //--- Constructor & Destructor
   CJsonNode();
   ~CJsonNode();

   //--- Parse entire JSON text
   bool        ParseString(string jsonText);

   //--- Check if node is valid
   bool        IsValid();

   //--- Get potential error message if not valid
   string      GetErrorMsg();

   //--- Access node type
   JsonNodeType GetType();

   //--- For arrays
   int         ChildCount();

   //--- For objects: get child by key
   CJsonNode*  GetChild(string key);

   //--- For arrays: get child by index
   CJsonNode*  GetChild(int index);

   //--- Convert to string / number / bool
   string      AsString();
   double      AsNumber();
   bool        AsBool();

   //--- Serialize back to JSON
   string      ToJsonString();

private:
   //--- Data members
   JsonNodeType m_type;       // Type of this node (object, array, etc.)
   string       m_value;      // For storing string content if node is string
   double       m_numVal;     // For numeric values
   bool         m_boolVal;    // For boolean values
   CJsonNode    m_children[]; // Child nodes (for objects and arrays)
   string       m_keys[];     // Keys for child nodes (valid if JSON_OBJ)
   bool         m_valid;      // True if node is validly parsed
   string       m_errMsg;     // Optional error message for debugging

   //--- Internal methods
   void         Reset();
   bool         ParseValue(string text,int &pos);
   bool         ParseObject(string text,int &pos);
   bool         ParseArray(string text,int &pos);
   bool         ParseNumber(string text,int &pos);
   bool         ParseStringLiteral(string text,int &pos);
   bool         ParseKeyLiteral(string text,int &pos,string &keyOut);
   string       UnescapeString(string input_);
   bool         SkipWhitespace(string text,int &pos);
   bool         AllWhitespace(string text,int pos);
   string       SerializeNode();
   string       SerializeObject();
   string       SerializeArray();
   string       EscapeString(string s);
};

//+-----------------------------------------------------------------+
//| Constructor                                                     |
//+-----------------------------------------------------------------+
CJsonNode::CJsonNode()
  {
   m_type    = JSON_UNDEF;
   m_value   = "";
   m_numVal  = 0.0;
   m_boolVal = false;
   m_valid   = true;
   ArrayResize(m_children,0);
   ArrayResize(m_keys,0);
   m_errMsg  = "";
  }

//+-----------------------------------------------------------------+
//| Destructor                                                      |
//+-----------------------------------------------------------------+
CJsonNode::~CJsonNode()
  {
   // No dynamic pointers to free; arrays are handled by MQL itself
  }

//+-----------------------------------------------------------------+
//| Parse entire JSON text                                          |
//+-----------------------------------------------------------------+
bool CJsonNode::ParseString(string jsonText)
  {
   Reset();
   int pos = 0;
   bool res = (ParseValue(jsonText,pos) && SkipWhitespace(jsonText,pos));

   // If there's leftover text that's not whitespace, it's an error
   if(pos < StringLen(jsonText))
     {
      if(!AllWhitespace(jsonText,pos))
        {
         m_valid  = false;
         m_errMsg = "Extra data after JSON parsing.";
         res      = false;
        }
     }
   return (res && m_valid);
  }

//+-----------------------------------------------------------------+
//| Check if node is valid                                          |
//+-----------------------------------------------------------------+
bool CJsonNode::IsValid()
  {
   return m_valid;
  }

//+-----------------------------------------------------------------+
//| Get potential error message if not valid                        |
//+-----------------------------------------------------------------+
string CJsonNode::GetErrorMsg()
  {
   return m_errMsg;
  }

//+-----------------------------------------------------------------+
//| Access node type                                                |
//+-----------------------------------------------------------------+
JsonNodeType CJsonNode::GetType()
  {
   return m_type;
  }

//+------------------------------------------------------------------+
//| For arrays: get number of children                               |
//+------------------------------------------------------------------+
int CJsonNode::ChildCount()
  {
   return ArraySize(m_children);
  }

//+------------------------------------------------------------------+
//| For objects: get child by key                                    |
//+------------------------------------------------------------------+
CJsonNode* CJsonNode::GetChild(string key)
  {
   if(m_type != JSON_OBJ)
      return NULL;
   for(int i=0; i<ArraySize(m_keys); i++)
     {
      if(m_keys[i] == key)
         return &m_children[i];
     }
   return NULL;
  }

//+------------------------------------------------------------------+
//| For arrays: get child by index                                   |
//+------------------------------------------------------------------+
CJsonNode* CJsonNode::GetChild(int index)
  {
   if(m_type != JSON_ARRAY)
      return NULL;
   if(index<0 || index>=ArraySize(m_children))
      return NULL;
   return &m_children[index];
  }

//+------------------------------------------------------------------+
//| Convert to string / number / bool                                |
//+------------------------------------------------------------------+
string CJsonNode::AsString()
  {
   if(m_type == JSON_STRING) return m_value;
   if(m_type == JSON_NUMBER) return DoubleToString(m_numVal,8);
   if(m_type == JSON_BOOL)   return m_boolVal ? "true" : "false";
   if(m_type == JSON_NULL)   return "null";
   // For object/array/undefined, return empty or handle as needed
   return "";
  }

//+------------------------------------------------------------------+
//| Convert node to numeric                                          |
//+------------------------------------------------------------------+
double CJsonNode::AsNumber()
  {
   if(m_type == JSON_NUMBER) return m_numVal;
   // If bool, return 1 or 0
   if(m_type == JSON_BOOL)   return (m_boolVal ? 1.0 : 0.0);
   return 0.0;
  }

//+------------------------------------------------------------------+
//| Convert node to boolean                                          |
//+------------------------------------------------------------------+
bool CJsonNode::AsBool()
  {
   if(m_type == JSON_BOOL)   return m_boolVal;
   if(m_type == JSON_NUMBER) return (m_numVal != 0.0);
   if(m_type == JSON_STRING) return (StringLen(m_value) > 0);
   return false;
  }

//+------------------------------------------------------------------+
//| Serialize node back to JSON                                      |
//+------------------------------------------------------------------+
string CJsonNode::ToJsonString()
  {
   return SerializeNode();
  }

//+------------------------------------------------------------------+
//| Reset node to initial state                                      |
//+------------------------------------------------------------------+
void CJsonNode::Reset()
  {
   m_type    = JSON_UNDEF;
   m_value   = "";
   m_numVal  = 0.0;
   m_boolVal = false;
   m_valid   = true;
   ArrayResize(m_children,0);
   ArrayResize(m_keys,0);
   m_errMsg  = "";
  }

//+------------------------------------------------------------------+
//| Dispatch parse based on first character                          |
//+------------------------------------------------------------------+
bool CJsonNode::ParseValue(string text,int &pos)
  {
   if(!SkipWhitespace(text,pos)) return false;
   if(pos >= StringLen(text))    return false;

   string c = StringSubstr(text,pos,1);

   //--- Object
   if(c == "{")
      return ParseObject(text,pos);

   //--- Array
   if(c == "[")
      return ParseArray(text,pos);

   //--- String
   if(c == "\"")
      return ParseStringLiteral(text,pos);

   //--- Boolean / null
   if(StringSubstr(text,pos,4) == "true")
     {
      m_type    = JSON_BOOL;
      m_boolVal = true;
      pos      += 4;
      return true;
     }
   if(StringSubstr(text,pos,5) == "false")
     {
      m_type    = JSON_BOOL;
      m_boolVal = false;
      pos      += 5;
      return true;
     }
   if(StringSubstr(text,pos,4) == "null")
     {
      m_type = JSON_NULL;
      pos   += 4;
      return true;
     }

   //--- Otherwise, parse number
   return ParseNumber(text,pos);
  }

//+------------------------------------------------------------------+
//| Parse object: { ... }                                            |
//+------------------------------------------------------------------+
bool CJsonNode::ParseObject(string text,int &pos)
  {
   m_type = JSON_OBJ;
   pos++; // skip '{'
   if(!SkipWhitespace(text,pos)) return false;

   //--- Check for empty object
   if(pos < StringLen(text) && StringSubstr(text,pos,1) == "}")
     {
      pos++;
      return true;
     }

   //--- Parse key-value pairs
   while(pos < StringLen(text))
     {
      if(!SkipWhitespace(text,pos)) return false;

      // Expect key in quotes
      if(pos >= StringLen(text) || StringSubstr(text,pos,1) != "\"")
        {
         m_valid  = false;
         m_errMsg = "Object key must start with double quote.";
         return false;
        }

      string key = "";
      if(!ParseKeyLiteral(text,pos,key))
         return false;

      if(!SkipWhitespace(text,pos)) return false;
      // Expect a colon
      if(pos >= StringLen(text) || StringSubstr(text,pos,1) != ":")
        {
         m_valid  = false;
         m_errMsg = "Missing colon after object key.";
         return false;
        }
      pos++; // skip ':'
      if(!SkipWhitespace(text,pos)) return false;

      // Parse the child value
      CJsonNode child;
      if(!child.ParseValue(text,pos))
        {
         m_valid  = false;
         m_errMsg = "Failed to parse object value.";
         return false;
        }

      // Store
      int idx = ArraySize(m_children);
      ArrayResize(m_children,idx+1);
      ArrayResize(m_keys,idx+1);
      m_children[idx] = child;
      m_keys[idx]     = key;

      if(!SkipWhitespace(text,pos)) return false;
      if(pos >= StringLen(text))    return false;

      string nextC = StringSubstr(text,pos,1);
      if(nextC == "}")
        {
         pos++;
         return true;
        }
      if(nextC != ",")
        {
         m_valid  = false;
         m_errMsg = "Missing comma in object.";
         return false;
        }
      pos++; // skip comma
     }

   return false; // didn't see closing '}'
  }

//+------------------------------------------------------------------+
//| Parse array: [ ... ]                                             |
//+------------------------------------------------------------------+
bool CJsonNode::ParseArray(string text,int &pos)
  {
   m_type = JSON_ARRAY;
   pos++; // skip '['
   if(!SkipWhitespace(text,pos)) return false;

   //--- Check for empty array
   if(pos < StringLen(text) && StringSubstr(text,pos,1) == "]")
     {
      pos++;
      return true;
     }

   //--- Parse elements
   while(pos < StringLen(text))
     {
      CJsonNode child;
      if(!child.ParseValue(text,pos))
        {
         m_valid  = false;
         m_errMsg = "Failed to parse array element.";
         return false;
        }
      int idx = ArraySize(m_children);
      ArrayResize(m_children,idx+1);
      m_children[idx] = child;

      if(!SkipWhitespace(text,pos)) return false;
      if(pos >= StringLen(text))    return false;

      string nextC = StringSubstr(text,pos,1);
      if(nextC == "]")
        {
         pos++;
         return true;
        }
      if(nextC != ",")
        {
         m_valid  = false;
         m_errMsg = "Missing comma in array.";
         return false;
        }
      pos++; // skip comma
      if(!SkipWhitespace(text,pos)) return false;
     }

   return false; // didn't see closing ']'
  }

//+------------------------------------------------------------------+
//| Parse a numeric value                                            |
//+------------------------------------------------------------------+
bool CJsonNode::ParseNumber(string text,int &pos)
  {
   m_type = JSON_NUMBER;
   int startPos = pos;

   // Scan allowed chars in a JSON number
   while(pos < StringLen(text))
     {
      string c = StringSubstr(text,pos,1);
      if(c=="-" || c=="+" || c=="." || c=="e" || c=="E" || (c>="0" && c<="9"))
        pos++;
      else
        break;
     }

   string numStr = StringSubstr(text,startPos,pos - startPos);
   if(StringLen(numStr) == 0)
     {
      m_valid  = false;
      m_errMsg = "Expected number, found empty.";
      return false;
     }

   m_numVal = StringToDouble(numStr);
   return true;
  }

//+------------------------------------------------------------------+
//| Parse a string literal (leading quote already checked)           |
//+------------------------------------------------------------------+
bool CJsonNode::ParseStringLiteral(string text,int &pos)
  {
   pos++;  // skip leading quote
   string result = "";

   while(pos < StringLen(text))
     {
      string c = StringSubstr(text,pos,1);
      if(c == "\"")
        {
         // closing quote
         pos++;
         m_type  = JSON_STRING;
         m_value = UnescapeString(result);
         return true;
        }
      if(c == "\\")
        {
         // handle escape
         pos++;
         if(pos >= StringLen(text))
            break;
         string ec = StringSubstr(text,pos,1);
         result += ("\\" + ec); // accumulate, we'll decode later
         pos++;
        }
      else
        {
         result += c;
         pos++;
        }
     }

   // If we get here, string was not closed
   m_valid  = false;
   m_errMsg = "Unclosed string literal.";
   return false;
  }

//+------------------------------------------------------------------+
//| Parse a string key (similar to a literal)                        |
//+------------------------------------------------------------------+
bool CJsonNode::ParseKeyLiteral(string text,int &pos,string &keyOut)
  {
   pos++;  // skip leading quote
   string buffer = "";

   while(pos < StringLen(text))
     {
      string c = StringSubstr(text,pos,1);
      if(c == "\"")
        {
         pos++;
         keyOut = UnescapeString(buffer);
         return true;
        }
      if(c == "\\")
        {
         pos++;
         if(pos >= StringLen(text))
            break;
         string ec = StringSubstr(text,pos,1);
         buffer += ("\\" + ec);
         pos++;
        }
      else
        {
         buffer += c;
         pos++;
        }
     }

   m_valid  = false;
   m_errMsg = "Unclosed key string.";
   return false;
  }

//+------------------------------------------------------------------+
//| Unescape sequences like \" \\ \n etc.                            |
//+------------------------------------------------------------------+
string CJsonNode::UnescapeString(string input_)
  {
   string out = "";
   int i      = 0;

   while(i < StringLen(input_))
     {
      string c = StringSubstr(input_,i,1);
      if(c == "\\")
        {
         i++;
         if(i >= StringLen(input_))
           {
            // Single backslash at end
            out += "\\";
            break;
           }
         string ec = StringSubstr(input_,i,1);

         if(ec == "\"")      out += "\"";
         else if(ec == "\\") out += "\\";
         else if(ec == "n")  out += "\n";
         else if(ec == "r")  out += "\r";
         else if(ec == "t")  out += "\t";
         else if(ec == "b")  out += CharToString(8);   // ASCII backspace
         else if(ec == "f")  out += CharToString(12);  // ASCII formfeed
         else                out += ("\\" + ec);

         i++;
        }
      else
        {
         out += c;
         i++;
        }
     }
   return out;
  }

//+------------------------------------------------------------------+
//| Skip whitespace                                                  |
//+------------------------------------------------------------------+
bool CJsonNode::SkipWhitespace(string text,int &pos)
  {
   while(pos < StringLen(text))
     {
      ushort c = StringGetCharacter(text,pos);
      if(c == ' ' || c == '\t' || c == '\n' || c == '\r')
        pos++;
      else
        break;
     }
   // Return true if we haven't gone beyond string length
   return (pos <= StringLen(text));
  }

//+------------------------------------------------------------------+
//| Check if remainder is all whitespace                             |
//+------------------------------------------------------------------+
bool CJsonNode::AllWhitespace(string text,int pos)
  {
   while(pos < StringLen(text))
     {
      ushort c = StringGetCharacter(text,pos);
      if(c != ' ' && c != '\t' && c != '\n' && c != '\r')
         return false;
      pos++;
     }
   return true;
  }

//+------------------------------------------------------------------+
//| Serialization dispatcher                                         |
//+------------------------------------------------------------------+
string CJsonNode::SerializeNode()
  {
   switch(m_type)
     {
      case JSON_OBJ:    return SerializeObject();
      case JSON_ARRAY:  return SerializeArray();
      case JSON_STRING: return "\""+EscapeString(m_value)+"\"";
      case JSON_NUMBER: return DoubleToString(m_numVal,8);
      case JSON_BOOL:   return (m_boolVal ? "true" : "false");
      case JSON_NULL:   return "null";
      default:          return "\"\""; // undefined => empty string
     }
  }

//+------------------------------------------------------------------+
//| Serialize object                                                 |
//+------------------------------------------------------------------+
string CJsonNode::SerializeObject()
  {
   string out = "{";
   for(int i=0; i<ArraySize(m_children); i++)
     {
      if(i > 0) out += ",";
      out += "\""+EscapeString(m_keys[i])+"\":";
      out += m_children[i].SerializeNode();
     }
   out += "}";
   return out;
  }

//+------------------------------------------------------------------+
//| Serialize array                                                  |
//+------------------------------------------------------------------+
string CJsonNode::SerializeArray()
  {
   string out = "[";
   for(int i=0; i<ArraySize(m_children); i++)
     {
      if(i > 0) out += ",";
      out += m_children[i].SerializeNode();
     }
   out += "]";
   return out;
  }

//+------------------------------------------------------------------+
//| Escape a string for JSON output (backslashes, quotes, etc.)      |
//+------------------------------------------------------------------+
string CJsonNode::EscapeString(string s)
  {
   string out = "";
   for(int i=0; i<StringLen(s); i++)
     {
      ushort c = StringGetCharacter(s,i);
      switch(c)
        {
         case 34:  // '"'
            out += "\\\"";
            break;
         case 92:  // '\\'
            out += "\\\\";
            break;
         case 10:  // '\n'
            out += "\\n";
            break;
         case 13:  // '\r'
            out += "\\r";
            break;
         case 9:   // '\t'
            out += "\\t";
            break;
         case 8:   // backspace
            out += "\\b";
            break;
         case 12:  // formfeed
            out += "\\f";
            break;
         default:
            // Directly append character
            out += CharToString(c);
            break;
        }
     }
   return out;
  }

#endif // __CJSONNODE_MQH__

スクリプトでの使用例を見てみましょう。

//+------------------------------------------------------------------+
//|                                                      ProjectName |
//|                                      Copyright 2020, CompanyName |
//|                                       http://www.companyname.net |
//+------------------------------------------------------------------+
#property strict
#include <CJsonNode.mqh>

void OnStart()
  {
   // Some JSON text
   string jsonText = "{\"name\":\"Alice\",\"age\":30,\"admin\":true,\"items\":[1,2,3],\"misc\":null}";

   CJsonNode parser;
   if(parser.ParseString(jsonText))
     {
      Print("JSON parsed successfully!");
      Print("Name: ", parser.GetChild("name").AsString());
      Print("Age: ",  parser.GetChild("age").AsNumber());
      Print("Admin?",  parser.GetChild("admin").AsBool());
      // Serialize back
      Print("Re-serialized JSON: ", parser.ToJsonString());
     }
   else
     {
      Print("JSON parsing error: ", parser.GetErrorMsg());
     }
  }

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

予想される出力は一目瞭然です。お気軽にテストしてください。


結論

この最終コードがあれば、MetaTrader 5上でJSONを直接解析し、操作し、さらには生成するために必要な機能はすべて揃っています。

  • JSONの解析:ParseString()が生のテキストを構造化されたノード階層に変換します。
  • データのクエリ:GetChild(key)とGetChild(index)でオブジェクトや配列を簡単にたどれます。
  • 検証:IsValid()とGetErrorMsg()で、解析の成功有無や括弧の不整合などの問題を確認できます。
  • シリアル化:ToJsonString()はノード(および子ノード)を有効なJSONテキストに再構築します。

用途に応じて、このライブラリを自由に拡張してください。たとえば、より詳細なエラー報告機能や特殊な数値変換、大規模データ向けのストリーミング処理の追加などが考えられます。しかし、この基盤だけでも、設定ファイルの読み込みやウェブAPIとのやり取りなど、多くの典型的なユースケースに十分対応できるでしょう。

これでMQL5でのJSON処理の深掘りは終了です。複雑でデータ駆動型の取引エンジンを作るにしても、単にローカルファイルから設定パラメータを読み込むにしても、信頼できるJSONパーサーとシリアライザーは大いに役立ちます。この記事とコードが、あなたの自動売買ワークフローにJSONをスムーズに組み込む助けになれば幸いです。

コーディングと取引をお楽しみください。

MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/16791

添付されたファイル |
CJsonNode.mqh (41.39 KB)
知っておくべきMQL5ウィザードのテクニック(第54回):SACとテンソルのハイブリッドによる強化学習 知っておくべきMQL5ウィザードのテクニック(第54回):SACとテンソルのハイブリッドによる強化学習
Soft Actor Critic (SAC)は、以前の記事で紹介した強化学習アルゴリズムです。その際には、効率的にネットワークを学習させる手法としてPythonやONNXの活用についても触れました。今回は、このアルゴリズムを改めて取り上げ、Pythonでよく使われるテンソルや計算グラフを活用することを目的としています。
MQL5での取引戦略の自動化(第6回):スマートマネートレーディングのためのオーダーブロック検出の習得 MQL5での取引戦略の自動化(第6回):スマートマネートレーディングのためのオーダーブロック検出の習得
この記事では、純粋なプライスアクション分析を用いてMQL5でオーダーブロック検出を自動化します。オーダーブロックの定義、検出の実装、自動売買への統合をおこない、最後に戦略のバックテストを通じてパフォーマンスを評価します。
MQL5でカスタムキャンバスグラフィックを使用したケルトナーチャネルインジケーターの構築 MQL5でカスタムキャンバスグラフィックを使用したケルトナーチャネルインジケーターの構築
本記事では、MQL5を用いてカスタムキャンバスグラフィック付きのケルトナーチャネルインジケーターを構築します。移動平均の統合、ATRの計算、そして視覚的に強化されたチャート表示について詳しく解説します。また、インジケーターの実用性を評価するためのバックテスト手法についても取り上げ、実際の取引に役立つ洞察を提供します。
プライスアクション分析ツールキットの開発(第12回):External Flow (III)トレンドマップ プライスアクション分析ツールキットの開発(第12回):External Flow (III)トレンドマップ
市場の流れは、ブル(買い手)とベア(売り手)の力関係によって決まります。市場が反応する特定の水準には、そうした力が作用しています。中でも、フィボナッチとVWAPの水準は、市場の動きに強い影響を与える傾向があります。この記事では、VWAPとフィボナッチ水準に基づいたシグナル生成の戦略を一緒に探っていきましょう。