
連続ウォークスルー最適化(パート2):ロボットの最適化レポート作成のメカニズム
イントロダクション
これは、トレーディング戦略のウォークスルー最適化を実行できる自動オプティマイザの作成に特化した一連の記事の2番目の記事です。 前回の記事では、オートオプティマイザやEAで使用するDLLの作成について説明しました。 今回は MQL5 言語に完全に専念します。 最適化レポート生成メソッドと、アルゴリズム内でこの関数の適用を検討します。
ストラテジーテスターは、提供された結果の詳細を欠いている間、EAからそのデータへのアクセスを許可しません。 この機能の別々の部分が変更されたのに対し、他のパートは以前の記事で完全には取り上げられていません。しかし、この機能がプログラムの重要な部分を構成するので、もう一度考えてみましょう。 新しい特徴から始めましょう。つまり、カスタムコミッションの追加です。 この記事で説明するすべてのクラスと関数は、Include/Historyマネージャーディレクトリにあります。
カスタムコミッションとスリッページの実装
MetaTrader5 プラットフォームのテスターは、無限の可能性があります、ディレクティブ。 しかし、一部のブローカーは、ヒストリーにトレード手数料を追加していません。 さらに、追加の戦略テストにコミッションを追加したい場合もあります。 この目的のために、各別個のシンボルの手数料を考慮するクラスを追加しました。 適切なメソッドの呼び出し時に、クラスは手数料と指定されたスリッページを返します。 クラス自体は次のように権限があります。
class CCCM { private: struct Keeper { string symbol; double comission; double shift; }; Keeper comission_data[]; public: void add(string symbol,double comission,double shift); double get(string symbol,double price,double volume); void remove(string symbol); };
Keeper 構造体は、指定された資産のコミッションとスリッページを格納するこのクラスに対して作成されています。 渡されたすべてのコミッションとスリッページ値を格納する配列が作成されました。 宣言された 3 つのメソッドは、データの追加、受信、および削除を行います。 資産の追加方法は、次のように実装されます。
void CCCM::add(string symbol,double comission,double shift) { int s=ArraySize(comission_data); for(int i=0;i<s;i++) { if(comission_data[i].symbol==symbol) return; } ArrayResize(comission_data,s+1,s+1); Keeper keeper; keeper.symbol=symbol; keeper.comission=MathAbs(comission); keeper.shift=MathAbs(shift); comission_data[s]=keeper; }
このメソッドは、同じ資産が既に追加済みかどうかを事前にチェックした後、コレクションに新しい資産を追加を実装します。 スリッページとコミッションは追加のモジュロを消費することに注意してください。 したがって、すべての原価が合計される場合、符号は計算に影響しません。 注意を払うもう1つのポイントは、計算単位です。
- コミッション:資産タイプに応じて、手数料は利益通貨で、またはトレード量の割合として追加することができます。
- スリッページ: 常にポイントで指定します。
また、値は、完全なポジション(つまり、オープン + クローズ)ごとに追加されるのではなく、各トレードごとに追加されることに注意してください。 したがって、ポジションは次の値を持ちます: n*コミッション + n*スリッページ。ただし、n はポジション内のすべてのトレードの数です。
removeメソッドは、選択した資産を削除します。 シンボル名はキーに使用します。
void CCCM::remove(string symbol) { int total=ArraySize(comission_data); int ind=-1; for(int i=0;i<total;i++) { if(comission_data[i].symbol==symbol) { ind=i; break; } } if(ind!=-1) ArrayRemove(comission_data,ind,1); }
適切なシンボルが見つからない場合、メソッドはアセットを削除せずに終了します。
選択したシフトとコミッションを取得するには、getメソッドを使用します。 メソッドの実装は、資産の種類によって異なります。
double CCCM::get(string symbol,double price,double volume) { int total=ArraySize(comission_data); for(int i=0;i<total;i++) { if(comission_data[i].symbol==symbol) { ENUM_SYMBOL_CALC_MODE mode=(ENUM_SYMBOL_CALC_MODE)SymbolInfoInteger(symbol,SYMBOL_TRADE_CALC_MODE); double shift=comission_data[i].shift*SymbolInfoDouble(symbol,SYMBOL_TRADE_TICK_VALUE); double ans; switch(mode) { case SYMBOL_CALC_MODE_FOREX : ans=(comission_data[i].comission+shift)*volume; break; case SYMBOL_CALC_MODE_FOREX_NO_LEVERAGE : ans=(comission_data[i].comission+shift)*volume; break; case SYMBOL_CALC_MODE_FUTURES : ans=(comission_data[i].comission+shift)*volume; break; case SYMBOL_CALC_MODE_CFD : ans=(comission_data[i].comission+shift)*volume; break; case SYMBOL_CALC_MODE_CFDINDEX : ans=(comission_data[i].comission+shift)*volume; break; case SYMBOL_CALC_MODE_CFDLEVERAGE : ans=(comission_data[i].comission+shift)*volume; break; case SYMBOL_CALC_MODE_EXCH_STOCKS : { double trading_volume=price*volume*SymbolInfoDouble(symbol,SYMBOL_TRADE_CONTRACT_SIZE); ans=trading_volume*comission_data[i].comission/100+shift*volume; } break; case SYMBOL_CALC_MODE_EXCH_FUTURES : ans=(comission_data[i].comission+shift)*volume; break; case SYMBOL_CALC_MODE_EXCH_FUTURES_FORTS : ans=(comission_data[i].comission+shift)*volume; break; case SYMBOL_CALC_MODE_EXCH_BONDS : { double trading_volume=price*volume*SymbolInfoDouble(symbol,SYMBOL_TRADE_CONTRACT_SIZE); ans=trading_volume*comission_data[i].comission/100+shift*volume; } break; case SYMBOL_CALC_MODE_EXCH_STOCKS_MOEX : { double trading_volume=price*volume*SymbolInfoDouble(symbol,SYMBOL_TRADE_CONTRACT_SIZE); ans=trading_volume*comission_data[i].comission/100+shift*volume; } break; case SYMBOL_CALC_MODE_EXCH_BONDS_MOEX : { double trading_volume=price*volume*SymbolInfoDouble(symbol,SYMBOL_TRADE_CONTRACT_SIZE); ans=trading_volume*comission_data[i].comission/100+shift*volume; } break; case SYMBOL_CALC_MODE_SERV_COLLATERAL : ans=(comission_data[i].comission+shift)*volume; break; default: ans=0; break; } if(ans!=0) return -ans; } } return 0; }
配列内の指定されたシンボルを詳しく見ることができます。 異なるシンボルタイプに対して異なるコミッション計算タイプが使用するため、コミッション設定タイプも異なります。 たとえば、株式および債券手数料は売上高の割合として設定され、売上高はロットあたりの取引数とトレード価格によってロット数の積として計算されます。
その結果、実行された操作に相当する金額が得られます。 メソッド実行結果は、常に手数料と金額でのスリッページの合計です。 スリッページはティック値に基づいて計算されます。 さらに、記述されたクラスは、次のクラスダウンロードレポートで使用します。 各資産のコミッションパラメータは、ハードコード化したり、データベースから自動的にリクエストしたりできます。あるいは、EAにインプットとして渡すことができます。 今回のアルゴリズムでは、後者のメソッドが好ましいです。
CDealHistoryGetterクラスのイノベーション
この前のパートで検討されたクラスは、以前の記事で言及されました。 よって、以前に考察されたクラスについて深く入り込むつもりはありません。 しかし、トレーディングレポートのダウンロードアルゴリズム内のキーアルゴリズムはダウンロードしたレポートの作成であるため、新しいクラスの包括的な説明をします。
最初の記事以降、変更で使用されているCDealHistoryGetterクラスから始めましょう。 最初の記事は主にこのクラスを説明することに専念しました。 最新バージョンは下記に添付されています。 新関数とマイナーな修正があります。 読みやすい形式でレポートをダウンロードするメカニズムについては、最初の記事で詳しく説明します。 この記事では、コミッションとスリッページのレポートへの追加について、より詳細に検討します。 OOP の原則に従い、1 つのオブジェクトが特定の目的を 1 つ実行する必要があることを意味し、このオブジェクトは、すべてのタイプのトレードレポート結果を受け取るために作成されます。 次のパブリック メソッドがあります。
- getHistory — このメソッドは、ポジション別にグループ化されたトレーディングヒストリーをダウンロードすることができます。 標準的なメソッドを使用して、トレードヒストリーをダウンロードする場合,任意のフィルタなしで, 取引データ構造によってトレードを受け取ります:
struct DealData { long ticket; // Deal ticket long order; // The number of the order that opened the position datetime DT; // Position open date long DT_msc; // Position open date in milliseconds ENUM_DEAL_TYPE type; // Open position type ENUM_DEAL_ENTRY entry; // Position entry type long magic; // Unique position number ENUM_DEAL_REASON reason; // Order placing reason long ID; // Position ID double volume; // Position volume (lots) double price; // Position entry price double comission; // Commission paid double swap; // Swap double profit; // Profit / loss string symbol; // Symbol string comment; // Comment specified when at opening string ID_external; // External ID };
受信したデータはポジションオープン時間でソートされ、他の方法ではグループ化されません。 この記事では、複数のアルゴリズムをトレードする際にトレード間の混乱が発生する可能性があるため、この形式でレポートを読む難しさを示す例を紹介します。 特に、基礎となるアルゴリズムに従って資産を追加で売買するピラミッティングを使用する場合、大量のエントリーと決済が発生します。
このメソッドは、ポジションによってトレードをグループ化します。 オーダーとの混同はありますが、分析されたポジションを参照しない不必要なトレードを排除します。 この結果は、上記のトレード構造から配列を格納する構造体として保存されます。
struct DealKeeper { DealData deals[]; /* List of all deals for this position (or several positions in case of position reversal)*/ string symbol; // Symbol long ID; // ID of the position (s) datetime DT_min; // Open date (or the date of the very first position) datetime DT_max; // Close date };
2つ以上のアルゴリズムが1つのポジションでトレードする場合、しばしばクロスするため、このクラスはグループ化におけるマジックナンバーを考慮しないことに注意してください。 少なくとも完全な分離は技術的に不可能です。モスクワ証券取引所では、主にこのアルゴリズムを書いています。 また、このツールは、トレード結果やテスト/最適化結果をダウンロードするように設計されています。 最初のケースでは、選択したシンボルの統計で十分ですが、2 番目のケースでは、ストラテジーテスターは一度に 1 つのアルゴリズムを実行するため、マジックナンバーは問題ではありません。
メソッドコアの実装は、最初の記事以降変更されていません。 カスタムコミッションを追加します。 このタスクでは、上記で説明した CCCM クラスはクラスコンストラクタへの参照によって渡され、対応するフィールドに保存されます。 次に、DealData構造のフィリング時、すなわち手数料フィリング時に、渡されたCCCMクラスに格納されたカスタムコミッションが追加されます。
#ifndef ONLY_CUSTOM_COMISSION if(data.comission==0 && comission_manager != NULL) { data.comission=comission_manager.get(data.symbol,data.price,data.volume); } #else data.comission=comission_manager.get(data.symbol,data.price,data.volume); #endif
手数料は、ディレクティブと条件付きで追加されます。 ロボットでこのクラスでファイルを接続する前に、ONLY_CUSTOM_COMISSIONパラメータを定義する場合、コミッションフィールドには、ブローカーが提供する値ではなく、常に渡されたコミッションが含まれます。 このパラメータが定義されていない場合、ブローカーがクオートを提供しない場合に限り、渡されたコミッションが条件付きで追加されます。 それ以外の場合は、ユーザーコミッション値は無視されます。
- getIDArr — リクエストされた時間枠内にすべてのシンボルに対してオープンされたポジションの ID の配列を返します。 ポジションIDは、すべてのトレードをメソッドのポジションに組み合わせられるようにします。 実際には、DealData.IDフィールドの一意のリストです。
- getDealsDetales — メソッドは getHistory に似ていますが、詳細は少なくなります。 このメソッドの考え方は、各行が 1 つの特定のトレードに対応する、読みやすい形式でポジションのテーブルを提供することです。 各ポジションは、次の構造体で記述されます。
struct DealDetales { string symbol; // Symbol datetime DT_open; // Open date ENUM_DAY_OF_WEEK day_open; // Open day datetime DT_close; // Cloe date ENUM_DAY_OF_WEEK day_close; // Close day double volume; // Volume (lots) bool isLong; // Long/Short double price_in; // Position entry price double price_out; // Position exit price double pl_oneLot; // Profit / loss is trading one lot double pl_forDeal; // Real profit/loss taking into account commission string open_comment; // Comment at the time of opening string close_comment; // Comment at the time of closing };
ポジションの決済日でソートされたポジションのテーブルを表します。 値の配列は、次のクラスの係数を計算するために使用します。 また、提示されたデータに基づく最終テストレポートを受け取ります。 さらに、このようなデータに基づいて、テスターはトレード後にPLチャート線を作成します。
テスターについては、さらに計算を行う場合、ターミナルによって計算されるリカバリーファクターは、受信データに基づいて計算されるリカバリーファクターとは異なることを確認してください。 データのダウンロードが正しく、計算式が同じであるが、ソースデータが異なるという事実によるものです。 テスターは、緑の線、すなわち詳細なレポートを使用してリカバリーファクターを計算し、青い線、すなわちポジションの開始と決済の間に発生する価格変動を無視したデータを使用して計算します。
- getBalance — このメソッドは、指定された日付のトレード操作を考慮しない残高データを取得するように設計されています。
double CDealHistoryGetter::getBalance(datetime toDate) { if(HistorySelect(0,(toDate>0 ? toDate : TimeCurrent()))) { int total=HistoryDealsTotal(); // Get the total number of positions double balance=0; for(int i=0; i<total; i++) { long ticket=(long)HistoryDealGetTicket(i); ENUM_DEAL_TYPE dealType=(ENUM_DEAL_TYPE)HistoryDealGetInteger(ticket,DEAL_TYPE); if(dealType==DEAL_TYPE_BALANCE || dealType == DEAL_TYPE_CORRECTION || dealType == DEAL_TYPE_COMMISSION) { balance+=HistoryDealGetDouble(ticket,DEAL_PROFIT); if(toDate<=0) break; } } return balance; } else return 0; }
タスクを達成するために、最初の時間間隔から指定された時間間隔までの全トレードのヒストリーが最初にリクエストされます。 その後、残高はサイクルで保存され、すべての資産と引き出しは、ブローカーによって提供された手数料と修正を考慮に入れて、元の残高に追加されます。 ゼロ日付がインプットとして渡された場合、最初の日付の時点の残高のみがリクエストされました。
- getBalanceWithPL — このメソッドは前のと似ていますが、バランスの変更に加えて、前述の原則に従った手数料を含む実行された操作の損益を考慮します。
最適化レポートを作成するクラス — 計算に使用する構造
以前の記事ですでに言及されていたもう一つのクラスは、CReportCreatorです。 この記事は、セクション "計算パート"の下の記事100ベスト最適化パスで説明されました。 このクラスはすべての係数を計算し、アルゴリズムパラメータのこの組み合わせがリクエストされた条件に対応するかどうかを自動オプティマイザが決定する際に、より詳細な説明を提供する必要があります。
まずクラス実装で使用するアプローチの基本的な考え方を説明します。 機能性が低い同様のクラスが最初の記事で実装されました。 しかし、リクエストされたパラメータの次のグループまたは次のチャートを計算するために、すべてのトレードヒストリーを新たにダウンロードしてループする必要があったので、遅かったです。 これが各パラメータリクエストで行われました。
データが多すぎる場合、このアプローチに数秒かかる場合があります。 計算を加速します。 別のクラスの実装を使用し、さらに多くのデータ(標準最適化結果で利用できないものも含む)を提供しました。 たとえば、最大利益/損失や累積損益など、多くの係数の計算に類似したデータが必要になることがあります。
したがって、1つのループで係数を計算し、クラスフィールドに保存することで、データが必要とされる他のすべてのパラメータを計算するために、このデータをさらに適用することができます。 したがって、ダウンロードしたヒストリーを一度ループし、必要なすべてのパラメータを計算し、次の計算まで格納するクラスを取得します。 その後、必要なパラメータを取得する必要がある場合、クラスは、保存されたデータを再計算する代わりにコピーします。
ここで、パラメータの計算方法を見てみましょう。 では、さらに計算に使用するデータを格納するオブジェクトから始めましょう。 オブジェクトは、プライベート スコープで宣言されたネストになったクラス オブジェクトとして作成されます。 これは、2つの理由で行われます。 まず、この関数を使用する他のクラスでの使用を防ぐことです。 宣言された構造体やクラスの数が多いのは混乱します:外部計算に必要なものもあれば、技術的なもの、つまり内部計算に使用するものもあります。 そして、第2の理由は、純粋に技術的な目的を強調することです。
The PL_Keeper structure:
struct PL_keeper
{
PLChart_item PL_total[];
PLChart_item PL_oneLot[];
PLChart_item PL_Indicative[];
};
この構造は、すべての可能な損益チャートを格納するために作成されます。 最初の記事で詳細に説明されました(上記のリンクを参照)。 構造体宣言の下に、そのインスタンスが作成されます。
PL_keeper PL,PL_hist,BH,BH_hist;
各インスタンスには、さまざまなソース データに対して 4 種類のチャートが格納されます。 PL接頭辞付きのデータは、ターミナルで入手可能なPLチャートの前述の青い線に基づいて計算されます。 BH 接頭辞付きのデータは、買いとホールド戦略によって得られた損益チャートに基づいて計算されます。 'hist' のポストフィックスを持つデータは、損益ヒストグラムに基づいて計算されます。
DailyPL_keeper structure:
// The structure of Daily PL graphs struct DailyPL_keeper { DailyPL avarage_open,avarage_close,absolute_open,absolute_close; };
この構造には、4 つの使用可能な日の損益チャート タイプが格納されます。 'average' プレフィックスを持つ DailyPL 構造インスタンスは、平均損益データを使用して計算されます。 「absolute」接頭辞を持つものは、損益の合計値を使用します。 したがって、差は明らかです。 最初のケースでは、トレード期間全体の平均日次利益を反映し、2番目のケースでは合計利益が表示されます。 'open' プレフィックスのデータは、開始日に従って日で並べ替えられ、"close" プレフィックスを持つデータは、閉じる日付に従って並べ替えられます。 構造体インスタンスの宣言は、次のコードに示されています。
RationTable_keeperキーパー:
// Table structure of extreme points struct RatioTable_keeper { ProfitDrawdown Total_max,Total_absolute,Total_percent; ProfitDrawdown OneLot_max,OneLot_absolute,OneLot_percent; };
この構造体は、ProfitDrawdown 構造体のインスタンスで構成されます。
struct ProfitDrawdown { double Profit; // In some cases Profit, in other Profit / Loss double Drawdown; // Drawdown };
特定の基準に応じて損益率を格納します。 'Total' 接頭辞を持つデータは、ロットの変更を考慮して、損益チャートのビルドを使用して計算されます。 'OneLot' プレフィックスを持つデータは、1 ロットが常にトレードされたかのように計算されます。 非標準の1ロット計算の考え方は、前述の第1稿に記載されています。 つまり、このメソッドは、トレードシステムの結果を評価するために作成されました。 タイムリーなロット管理またはシステム自体のロジックから結果がどこから来るかを評価することができます:。 'max' のポストフィックスは、インスタンスがトレードヒストリーの中で発生した最高の利益とドローダウンに関するデータを特徴としていることを示します。 「absolute」ポストフィックスは、インスタンスがトレーディングヒストリー全体の総利益とドローダウンデータを含むということを意味します。 'percent' のサフィックスは、利益とドローダウンの値がテストされた時間枠内の PL 曲線の最大値に対する割合の比率として計算されることを意味します。 構造体の宣言はシンプルで、この記事 に添付されたコードに示されています。
次の構造体グループはクラス フィールドとして宣言されていませんが、メインの Create メソッドでローカル宣言として使用します。 記述されたすべての構造は一緒に組み合わされているので、すべての宣言を見てみましょう。
// Structures for calculating consecutive profits and losses struct S_dealsCounter { int Profit,DD; }; struct S_dealsInARow : public S_dealsCounter { S_dealsCounter Counter; }; // Structures for calculating auxiliary data struct CalculationData_item { S_dealsInARow dealsCounter; int R_arr[]; double DD_percent; double Accomulated_DD,Accomulated_Profit; double PL; double Max_DD_forDeal,Max_Profit_forDeal; double Max_DD_byPL,Max_Profit_byPL; datetime DT_Max_DD_byPL,DT_Max_Profit_byPL; datetime DT_Max_DD_forDeal,DT_Max_Profit_forDeal; int Total_DD_numDeals,Total_Profit_numDeals; }; struct CalculationData { CalculationData_item total,oneLot; int num_deals; bool isNot_firstDeal; };
S_dealsCounterおよびS_dealsInARow構造は、基本的に単一のエンティティです。 このような奇妙な連想と継承の組み合わせは、パラメータの特定の計算と結びついています。 S_dealsInARow構造は、行のトレード数(ポジションの計算、ポジションのオープンからクローズまで)を正または負の値で保存および計算するために作成されます。 S_dealsCounter構造体のネストになったインスタンスは、中間計算結果を格納するために宣言されます。 継承されたフィールドには合計が格納されます。 後で収益性の高い/低いトレードを数える操作に戻ります。
CalculationData_item構造には、係数の計算に必要な項目があります。
- R_arr — 連続した勝ちトレード/負けトレードの配列を、それぞれ1 / 0として示します。 配列は Z スコアの計算に使用します。
- DD_percent — ドローダウン率。
- Accomulated_DD、Accomulated_Profit - 総損失と利益の値を格納します。
- PL — 利益/損失。
- Max_DD_forDeal、Max_Profit_forDeal — 名前付けが示すように、すべてのトレードの中で最大のドローダウンと利益を保存します。
- Max_DD_byPL, Mаx_Profit_byPL — PLチャートで計算された最大ドローダウンと利益を保存します。
- DT_Max_DD_byPL、DT_Max_Profit_byPL — PLチャートによる最高ドローダウンと利益の日付を保存します。
- DT_Max_DD_forDeal、DT_Max_Profit_forDeal — 最高ドローダウンの日付とトレードによる利益。
- Total_DD_numDeals、TotalProfit_numDeals — 利益と損失のトレードの合計数。
更なる計算は上記のデータに基づいています。
計算データは、記述されたすべての構造を結合する累積構造です。 必要なデータをすべて格納します。 また、num_dealsフィールドも含まれていますが、実際にはCalculationData_item::Total_DD_numDeals と CalculationData_item:TotalProfit_numDeals の合計です。 sNot_firstDealフィールドは、最初のトレードではなく計算が実行されることを示す技術的フラグです。
The CoefChart_keeper structure:
struct CoefChart_keeper
{
CoefChart_item OneLot_ShartRatio_chart[],Total_ShartRatio_chart[];
CoefChart_item OneLot_WinCoef_chart[],Total_WinCoef_chart[];
CoefChart_item OneLot_RecoveryFactor_chart[],Total_RecoveryFactor_chart[];
CoefChart_item OneLot_ProfitFactor_chart[],Total_ProfitFactor_chart[];
CoefChart_item OneLot_AltmanZScore_chart[],Total_AltmanZScore_chart[];
};
これは、係数チャートを保存することを目的とします。 このクラスは利益チャートとロットチャートだけでなく、係数チャートも作成するため、記述されたデータ型に対して別の構造が作成されました。 プレフィックス 'OneLot' は、1 ロットをトレードする場合、インスタンスが損益分析から受け取ったデータを保存することを示します。 「Total」とは、ロット管理を考慮した計算を意味します。 この戦略でロット管理が使用されない場合、2 つのチャートは同一になります。
The СHistoryComparer class:
同様に、データの並べ替えで使用するクラスも定義されます。 記事 "100 最適最適化パス" には、任意のデータ型を降順および昇順で並べ替えることができる CGenericSorter クラスがあります。 さらに、渡された型を比較できるクラスが必要です。 そのようなクラスは СHisoryComparerです。
class CHistoryComparer : public ICustomComparer<DealDetales> { public: int Compare(DealDetales &x,DealDetales &y); };
メソッドの実装はシンプルです: 並べ替えが決済日によって実行されると、決済日を比較します。
int CReportCreator::CHistoryComparer::Compare(DealDetales &x,DealDetales &y) { return(x.DT_close == y.DT_close ? 0 : (x.DT_close > y.DT_close? 1 : -1)); }
また、同様のクラスで、係数チャートをソートします。 2 つのクラスと並べ替えクラスは、CReportCreator クラスのグローバル フィールドとしてインスタンス化されます。 説明されているオブジェクトに加えて、2 つのフィールドがあります。 型は、ネストになっていない個別のオブジェクトとして記述されます。
PL_detales PL_detales_data; DistributionChart OneLot_PDF_chart,Total_PDF_chart;
PL_detales構造には、収益性の高いポジションと損失ポジションのショートトレード情報があります。
//+------------------------------------------------------------------+ struct PL_detales_PLDD { int orders; // Number of deals double orders_in_Percent; // Number of orders as % of total number of orders int dealsInARow; // Deals in a row double totalResult; // Total result in money double averageResult; // Average result in money }; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ struct PL_detales_item { PL_detales_PLDD profit; // Information on profitable deals PL_detales_PLDD drawdown; // Information on losing deals }; //+-------------------------------------------------------------------+ //| A brief PL graph summary divided into 2 main blocks | //+-------------------------------------------------------------------+ struct PL_detales { PL_detales_item total,oneLot; };
2番目の構造分布チャートには、係数が計算された分布図と同様に、VaR値があります。 分布は正規分布として計算されます。
//+------------------------------------------------------------------+ //| Structure used for saving distribution charts | //+------------------------------------------------------------------+ struct Chart_item { double y; // y axis double x; // x axis }; //+------------------------------------------------------------------+ //| Structure contains the VaR value | //+------------------------------------------------------------------+ struct VAR { double VAR_90,VAR_95,VAR_99; double Mx,Std; }; //+------------------------------------------------------------------+ //| Structure - it is used to store distribution charts and | //| the VaR values | //+------------------------------------------------------------------+ struct Distribution_item { Chart_item distribution[]; // Distribution chart VAR VaR; // VaR }; //+------------------------------------------------------------------+ //| Structure - Stores distribution data. Divided into 2 blocks | //+------------------------------------------------------------------+ struct DistributionChart { Distribution_item absolute,growth; };
VaR係数は、計算式に従って計算されます: ヒストリー的VaRは、十分に正確ではないかもしれませんが、現在の実装に適しています。
トレード結果を記述する係数を計算するメソッド
データ格納構造を検討したので、このクラスで計算される膨大な量の統計量を想像できます。 CReportCreator クラスで名前が付けられたように、記述された値を 1 つずつ計算するための特定のメソッドを表示してみましょう。
Pl チャートを計算するために CalcPL が作成されます。 次のように実装されます。
void CReportCreator::CalcPL(const DealDetales &deal,CalculationData &data,PLChart_item &pl_out[],CalcType type) { PLChart_item item; ZeroMemory(item); item.DT=deal.DT_close; // Saving the date if(type!=_Indicative) { item.Profit=(type==_Total ? data.total.PL : data.oneLot.PL); // Saving the profit item.Drawdown=(type==_Total ? data.total.DD_percent : data.oneLot.DD_percent); // Saving the drawdown } else // Calculating the indicative chart { if(data.isNot_firstDeal) { if(data.total.PL!=0) { if(data.total.PL > 0 && data.total.Max_DD_forDeal < 0) item.Profit=data.total.PL/MathAbs(data.total.Max_DD_forDeal); else if(data.total.PL<0 && data.total.Max_Profit_forDeal>0) item.Profit=data.total.PL/data.total.Max_Profit_forDeal; } } } // Adding data to array int s=ArraySize(pl_out); ArrayResize(pl_out,s+1,s+1); pl_out[s]=item; }
実装からわかるように、すべての計算は、インプットとして渡される以前に記述された構造のデータに基づいています。
ノンインディケーティブPLグラフを計算する必要がある場合は、既知のデータをコピーするだけです。 それ以外の場合、計算は2つの条件に従います:最初の繰り返しはサイクルで見つからず、PLチャートはゼロではありません。 この計算は、次のロジックに従って実行されます。
- PL が 0 より大きく、ドローダウンが 小さい場合は、現在の PL 値をドローダウン値で除算します。 したがって、現在のPLをゼロに減らすのに必要な最大ドローダウンの数を示す係数を取得します。
- PL がゼロより小さく、すべてのトレードの最大利益がゼロより大きい場合、PL値 (現在はドローダウン) を達成した最大利益で割ります。 したがって、現在のドローダウンをゼロにするために必要な最大利益を行う係数を取得します。
次の方法CalcPLHistは同様のメカニズムに基づいていますが、Accomulated_Profit Accomulated_Profit Accomulated_DD Accomulated_DD 計算には他の構造体フィールドを使用します。 すでにそのアルゴリズムを先に検討したので、次の2つのメソッドに移りましょう。
CalcDataとCalcData_item:
これらのメソッドは、すべての補助係数と主係数を計算します。 CalcData_itemから始めましょう。 その目的は、主係数が計算されるに基づいて、上記の追加係数を計算することです。
//+------------------------------------------------------------------+ //| Calculating auxiliary data | //+------------------------------------------------------------------+ void CReportCreator::CalcData_item(const DealDetales &deal,CalculationData_item &out, bool isOneLot) { double pl=(isOneLot ? deal.pl_oneLot : deal.pl_forDeal); //PL int n=0; // Number of profits and losses if(pl>=0) { out.Total_Profit_numDeals++; n=1; out.dealsCounter.Counter.DD=0; out.dealsCounter.Counter.Profit++; } else { out.Total_DD_numDeals++; out.dealsCounter.Counter.DD++; out.dealsCounter.Counter.Profit=0; } out.dealsCounter.DD=MathMax(out.dealsCounter.DD,out.dealsCounter.Counter.DD); out.dealsCounter.Profit=MathMax(out.dealsCounter.Profit,out.dealsCounter.Counter.Profit); // Series of profits and losses int s=ArraySize(out.R_arr); if(!(s>0 && out.R_arr[s-1]==n)) { ArrayResize(out.R_arr,s+1,s+1); out.R_arr[s]=n; } out.PL+=pl; //Total PL // Max Profit / DD if(out.Max_DD_forDeal>pl) { out.Max_DD_forDeal=pl; out.DT_Max_DD_forDeal=deal.DT_close; } if(out.Max_Profit_forDeal<pl) { out.Max_Profit_forDeal=pl; out.DT_Max_Profit_forDeal=deal.DT_close; } // Accumulated Profit / DD out.Accomulated_DD+=(pl>0 ? 0 : pl); out.Accomulated_Profit+=(pl>0 ? pl : 0); // Extreme profit values double maxPL=MathMax(out.Max_Profit_byPL,out.PL); if(compareDouble(maxPL,out.Max_Profit_byPL)==1/* || !isNot_firstDeal*/)// another check is needed to save the date { out.DT_Max_Profit_byPL=deal.DT_close; out.Max_Profit_byPL=maxPL; } double maxDD=out.Max_DD_byPL; double DD=0; if(out.PL>0) DD=out.PL-maxPL; else DD=-(MathAbs(out.PL)+maxPL); maxDD=MathMin(maxDD,DD); if(compareDouble(maxDD,out.Max_DD_byPL)==-1/* || !isNot_firstDeal*/)// another check is needed to save the date { out.Max_DD_byPL=maxDD; out.DT_Max_DD_byPL=deal.DT_close; } out.DD_percent=(balance>0 ?(MathAbs(DD)/(maxPL>0 ? maxPL : balance)) :(maxPL>0 ?(MathAbs(DD)/maxPL) : 0)); }
まず、PLはi番目の繰り返しで計算されます。 その後、この繰り返しで利益があった場合は、収益性の高いトレードカウンタを増やし、連続損失のカウンタをゼロにします。 また、n 変数に値 1 を設定すると、トレードが利益を上げたことを意味します。 PLがゼロを下回った場合は、損失カウンタを増やし、収益性の高いトレードカウンタをゼロにします。 その後、連続して収益性と損失の最大数を割り当てます。
次のステップは、収益性の高いトレードと損失のトレードのシリーズを計算することです。 シリーズとは、連続的な勝敗を意味します。 この配列では、0 の後に常に 1 が続き、1 の後には常にゼロが続きます。 トレードの勝敗の交互を示しますが、0または1は複数のトレードを意味する可能性があります。 この配列は、トレードのランダム性の度合いを示すZスコアの計算に使用します。 次のステップは、最大利益/ドローダウン値を割り当て、計算された累積利益/損失に割り当てることです。 このメソッドの最後に、極値が計算され、すなわち最大の損益値を持つ構造が満たされます。
CalcData データは、取得した中間データを使用して、必要な係数を計算し、各繰り返しで計算を更新します。 次のように実装されます。
void CReportCreator::CalcData(const DealDetales &deal,CalculationData &out,bool isBH) { out.num_deals++; // Counting the number of deals CalcData_item(deal,out.oneLot,true); CalcData_item(deal,out.total,false); if(!isBH) { // Fill PL graphs CalcPL(deal,out,PL.PL_total,_Total); CalcPL(deal,out,PL.PL_oneLot,_OneLot); CalcPL(deal,out,PL.PL_Indicative,_Indicative); // Fill PL Histogram graphs CalcPLHist(deal,out,PL_hist.PL_total,_Total); CalcPLHist(deal,out,PL_hist.PL_oneLot,_OneLot); CalcPLHist(deal,out,PL_hist.PL_Indicative,_Indicative); // Fill PL graphs by days CalcDailyPL(DailyPL_data.absolute_close,CALC_FOR_CLOSE,deal); CalcDailyPL(DailyPL_data.absolute_open,CALC_FOR_OPEN,deal); CalcDailyPL(DailyPL_data.avarage_close,CALC_FOR_CLOSE,deal); CalcDailyPL(DailyPL_data.avarage_open,CALC_FOR_OPEN,deal); // Fill Profit Factor graphs ProfitFactor_chart_calc(CoefChart_data.OneLot_ProfitFactor_chart,out,deal,true); ProfitFactor_chart_calc(CoefChart_data.Total_ProfitFactor_chart,out,deal,false); // Fill Recovery Factor graphs RecoveryFactor_chart_calc(CoefChart_data.OneLot_RecoveryFactor_chart,out,deal,true); RecoveryFactor_chart_calc(CoefChart_data.Total_RecoveryFactor_chart,out,deal,false); // Fill winning coefficient graphs WinCoef_chart_calc(CoefChart_data.OneLot_WinCoef_chart,out,deal,true); WinCoef_chart_calc(CoefChart_data.Total_WinCoef_chart,out,deal,false); // Fill Sharpe Ration graphs ShartRatio_chart_calc(CoefChart_data.OneLot_ShartRatio_chart,PL.PL_oneLot,deal/*,out.isNot_firstDeal*/); ShartRatio_chart_calc(CoefChart_data.Total_ShartRatio_chart,PL.PL_total,deal/*,out.isNot_firstDeal*/); // Fill Z Score graphs AltmanZScore_chart_calc(CoefChart_data.OneLot_AltmanZScore_chart,(double)out.num_deals, (double)ArraySize(out.oneLot.R_arr),(double)out.oneLot.Total_Profit_numDeals, (double)out.oneLot.Total_DD_numDeals/*,out.isNot_firstDeal*/,deal); AltmanZScore_chart_calc(CoefChart_data.Total_AltmanZScore_chart,(double)out.num_deals, (double)ArraySize(out.total.R_arr),(double)out.total.Total_Profit_numDeals, (double)out.total.Total_DD_numDeals/*,out.isNot_firstDeal*/,deal); } else // Fill PL Buy and Hold graphs { CalcPL(deal,out,BH.PL_total,_Total); CalcPL(deal,out,BH.PL_oneLot,_OneLot); CalcPL(deal,out,BH.PL_Indicative,_Indicative); CalcPLHist(deal,out,BH_hist.PL_total,_Total); CalcPLHist(deal,out,BH_hist.PL_oneLot,_OneLot); CalcPLHist(deal,out,BH_hist.PL_Indicative,_Indicative); } if(!out.isNot_firstDeal) out.isNot_firstDeal=true; // Flag "It is NOT the first deal" }
まず、中間係数は、両方のデータ型に対して記述されたメソッドを呼び出すことによって、1つのロットおよび管理ロットトレードシステムに対して計算されます。 次に、計算はBHと反対のタイプのデータの係数に分割されます。 解釈可能係数は各ブロック内で計算されます。 買いとホールド戦略のチャートのみが計算され、したがって、係数計算メソッドは呼び出されません。
次の一グループのメソッドでは、日別の利益/損失分割を計算します。
//+------------------------------------------------------------------+ //| Create a structure of trading during a day | //+------------------------------------------------------------------+ void CReportCreator::CalcDailyPL(DailyPL &out,DailyPL_calcBy calcBy,const DealDetales &deal) { cmpDay(deal,MONDAY,out.Mn,calcBy); cmpDay(deal,TUESDAY,out.Tu,calcBy); cmpDay(deal,WEDNESDAY,out.We,calcBy); cmpDay(deal,THURSDAY,out.Th,calcBy); cmpDay(deal,FRIDAY,out.Fr,calcBy); } //+------------------------------------------------------------------+ //| Save resulting PL/DD for the day | //+------------------------------------------------------------------+ void CReportCreator::cmpDay(const DealDetales &deal,ENUM_DAY_OF_WEEK etalone,PLDrawdown &ans,DailyPL_calcBy calcBy) { ENUM_DAY_OF_WEEK day=(calcBy==CALC_FOR_CLOSE ? deal.day_close : deal.day_open); if(day==etalone) { if(deal.pl_forDeal>0) { ans.Profit+=deal.pl_forDeal; ans.numTrades_profit++; } else if(deal.pl_forDeal<0) { ans.Drawdown+=MathAbs(deal.pl_forDeal); ans.numTrades_drawdown++; } } } //+------------------------------------------------------------------+ //| Average resulting PL/DD for the day | //+------------------------------------------------------------------+ void CReportCreator::avarageDay(PLDrawdown &day) { if(day.numTrades_profit>0) day.Profit/=day.numTrades_profit; if(day.numTrades_drawdown > 0) day.Drawdown/=day.numTrades_drawdown; }
主要なタスクで利益/DD を日別に分割する処理は、最初に、その日がリクエストされた日に対応しているかどうかをチェックし、損益の値を追加する cmpDay メソッドで実行されます。 損失はモジュロを合計します。 CalcDailyPL は、現在の合格 PL を 5 営業日のうちの 1 つに追加する試みが行われる集約メソッドです。 avarageDayメソッドは、メインの作成メソッドでの平均利益/損失に呼び出されます。 このメソッドは特定のアクションを実行しませんが、以前に計算された絶対損益の値に基づいて平均を計算するだけです。
プロフィットファクター計算方法
//+------------------------------------------------------------------+ //| Calculate Profit Factor | //+------------------------------------------------------------------+ void CReportCreator::ProfitFactor_chart_calc(CoefChart_item &out[],CalculationData &data,const DealDetales &deal,bool isOneLot) { CoefChart_item item; item.DT=deal.DT_close; double profit=(isOneLot ? data.oneLot.Accomulated_Profit : data.total.Accomulated_Profit); double dd=MathAbs(isOneLot ? data.oneLot.Accomulated_DD : data.total.Accomulated_DD); if(dd==0) item.coef=0; else item.coef=profit/dd; int s=ArraySize(out); ArrayResize(out,s+1,s+1); out[s]=item; }
このメソッドは、トレード全体の利益率の変化を反映したチャートを計算します。 直近の値は、テスト レポートに示されている値です。 数式はシンプルです。=累積利益/累積ドローダウンです。 ドローダウンがゼロの場合、古典的な算術ではリミットを使用せずにゼロで除算することは不可能であり、同じルールが言語に適用されるため、係数はゼロになります。 したがって、すべての算術演算に対する除数チェックを追加します。
リカバリーファクターの計算原則は似ています。
//+------------------------------------------------------------------+ //| Calculate Recovery Factor | //+------------------------------------------------------------------+ void CReportCreator::RecoveryFactor_chart_calc(CoefChart_item &out[],CalculationData &data,const DealDetales &deal,bool isOneLot) { CoefChart_item item; item.DT=deal.DT_close; double pl=(isOneLot ? data.oneLot.PL : data.total.PL); double dd=MathAbs(isOneLot ? data.oneLot.Max_DD_byPL : data.total.Max_DD_byPL); if(dd==0) item.coef=0;//ideally it should be plus infinity else item.coef=pl/dd; int s=ArraySize(out); ArrayResize(out,s+1,s+1); out[s]=item; }
係数計算式: i 番目の繰り返し/ドローダウンにおける利益 i 番目の繰り返しでの利益. また、係数の計算中に利益がゼロまたは負になることがあるため、係数自体はゼロまたは負になることがあります。
勝率
//+------------------------------------------------------------------+ //| Calculate Win Rate | //+------------------------------------------------------------------+ void CReportCreator::WinCoef_chart_calc(CoefChart_item &out[],CalculationData &data,const DealDetales &deal,bool isOneLot) { CoefChart_item item; item.DT=deal.DT_close; double profit=(isOneLot ? data.oneLot.Accomulated_Profit : data.total.Accomulated_Profit); double dd=MathAbs(isOneLot ? data.oneLot.Accomulated_DD : data.total.Accomulated_DD); int n_profit=(isOneLot ? data.oneLot.Total_Profit_numDeals : data.total.Total_Profit_numDeals); int n_dd=(isOneLot ? data.oneLot.Total_DD_numDeals : data.total.Total_DD_numDeals); if(n_dd == 0 || n_profit == 0) item.coef = 0; else item.coef=(profit/n_profit)/(dd/n_dd); int s=ArraySize(out); ArrayResize(out,s+1,s+1); out[s]=item; }
勝率計算式 = (利益/収益性の高いトレードの数) / (ドローダウン / 損失トレードの数). 計算時に利益がない場合、この係数は負になることもあります。
シャープ比の計算は、もう少し複雑です。
//+------------------------------------------------------------------+ //| Calculate Sharpe Ratio | //+------------------------------------------------------------------+ double CReportCreator::ShartRatio_calc(PLChart_item &data[]) { int total=ArraySize(data); double ans=0; if(total>=2) { double pl_r=0; int n=0; for(int i=1; i<total; i++) { if(data[i-1].Profit!=0) { pl_r+=(data[i].Profit-data[i-1].Profit)/data[i-1].Profit; n++; } } if(n>=2) pl_r/=(double)n; double std=0; n=0; for(int i=1; i<total; i++) { if(data[i-1].Profit!=0) { std+=MathPow((data[i].Profit-data[i-1].Profit)/data[i-1].Profit-pl_r,2); n++; } } if(n>=2) std=MathSqrt(std/(double)(n-1)); ans=(std!=0 ?(pl_r-r)/std : 0); } return ans; }
第1サイクルにおいて、平均収益性はPLチャートによって計算され、各i-th収益性は、前回のPL値に対するPLに対する増加の割合として計算されます。 この計算は、時系列の評価に使用する価格シリーズ正規化の例に基づいています。
次のサイクルでは、ボラティリティは同じ正規化された収益性シリーズを使用して計算されます。
その後、係数自体は式(平均利益 - リスクフリーレート)/ボラティリティ(リターンの標準偏差)を使用して計算されます。
おそらく、シリーズの正規化とおそらく式で非伝統的なアプローチを適用しましたが、この計算はかなり合理的なようです。 エラーが見つかる場合は、記事にコメントを追加してください。
VaRと正規分布チャートを計算します。 この部分は3つの方法で構成されています。 そのうちの2つは計算中で、3つ目はすべての計算を集計します。 このメソッドについて考えてみましょう。
//+------------------------------------------------------------------+ //| Distribution calculation | //+------------------------------------------------------------------+ void CReportCreator::NormalPDF_chart_calc(DistributionChart &out,PLChart_item &data[]) { double Mx_absolute=0,Mx_growth=0,Std_absolute=0,Std_growth=0; int total=ArraySize(data); ZeroMemory(out.absolute); ZeroMemory(out.growth); ZeroMemory(out.absolute.VaR); ZeroMemory(out.growth.VaR); ArrayFree(out.absolute.distribution); ArrayFree(out.growth.distribution); // Calculation of distribution parameters if(total>=2) { int n=0; for(int i=0; i<total; i++) { Mx_absolute+=data[i].Profit; if(i>0 && data[i-1].Profit!=0) { Mx_growth+=(data[i].Profit-data[i-1].Profit)/data[i-1].Profit; n++; } } Mx_absolute/=(double)total; if(n>=2) Mx_growth/=(double)n; n=0; for(int i=0; i<total; i++) { Std_absolute+=MathPow(data[i].Profit-Mx_absolute,2); if(i>0 && data[i-1].Profit!=0) { Std_growth+=MathPow((data[i].Profit-data[i-1].Profit)/data[i-1].Profit-Mx_growth,2); n++; } } Std_absolute=MathSqrt(Std_absolute/(double)(total-1)); if(n>=2) Std_growth=MathSqrt(Std_growth/(double)(n-1)); VaR の計算 out.absolute.VaR.Mx=Mx_absolute; out.absolute.VaR.Std=Std_absolute; out.absolute.VaR.VAR_90=VaR(Q_90,Mx_absolute,Std_absolute); out.absolute.VaR.VAR_95=VaR(Q_95,Mx_absolute,Std_absolute); out.absolute.VaR.VAR_99=VaR(Q_99,Mx_absolute,Std_absolute); out.growth.VaR.Mx=Mx_growth; out.growth.VaR.Std=Std_growth; out.growth.VaR.VAR_90=VaR(Q_90,Mx_growth,Std_growth); out.growth.VaR.VAR_95=VaR(Q_95,Mx_growth,Std_growth); out.growth.VaR.VAR_99=VaR(Q_99,Mx_growth,Std_growth); // Calculate distribution for(int i=0; i<total; i++) { Chart_item item_a,item_g; ZeroMemory(item_a); ZeroMemory(item_g); item_a.x=data[i].Profit; item_a.y=PDF_calc(Mx_absolute,Std_absolute,data[i].Profit); if(i>0) { item_g.x=(data[i-1].Profit != 0 ?(data[i].Profit-data[i-1].Profit)/data[i-1].Profit : 0); item_g.y=PDF_calc(Mx_growth,Std_growth,item_g.x); } int s=ArraySize(out.absolute.distribution); ArrayResize(out.absolute.distribution,s+1,s+1); out.absolute.distribution[s]=item_a; s=ArraySize(out.growth.distribution); ArrayResize(out.growth.distribution,s+1,s+1); out.growth.distribution[s]=item_g; } // Ascending sorter.Sort<Chart_item>(out.absolute.distribution,&chartComparer); sorter.Sort<Chart_item>(out.growth.distribution,&chartComparer); } } //+------------------------------------------------------------------+ //| Calculate VaR | //+------------------------------------------------------------------+ double CReportCreator::VaR(double quantile,double Mx,double Std) { return Mx-quantile*Std; } //+------------------------------------------------------------------+ //| Distribution calculation | //+------------------------------------------------------------------+ double CReportCreator::PDF_calc(double Mx,double Std,double x) { if(Std!=0) return MathExp(-0.5*MathPow((x-Mx)/Std,2))/(MathSqrt(2*M_PI)*Std); else return 0; }
VaR計算方法は最も簡単な方法です。 計算にヒストリー的なVaRモデルを使用します。
正規化された分布計算方法は、Matlab 統計解析パッケージで使用できるメソッドです。
正規化された分布計算およびチャート構築方法は、上記の方法が適用される集計方法です。 最初のサイクルでは、平均利益値が計算されます。 第2 サイクルでは、標準偏差が計算されます。 成長によって計算されたチャートと VaR のリターンも正規化時系列として計算されます。 また、VaR値をフィリングした後、正規分布チャートは上記のメソッドを用いて計算されます。 x 軸として、成長ベースのチャートに収益性を使用し、利益ベースのチャートに絶対利益値を使用します。
Zスコアを計算するために、このサイトの記事の1つを形成する式を使用しました。 その完全な実装は、添付ファイルで利用可能です。
すべての計算は次の呼び出しシグネチャを持つ Calculate メソッドで始まります。
void CReportCreator::Create(DealDetales &history[],DealDetales &BH_history[],const double _balance,const string &Symb[],double _r);
その実装は、前述の記事 "100 ベスト最適化パス"で説明されました。 すべてのパブリック メソッドは論理演算を実行しませんが、リクエストされたデータをインプットパラメータに従って、必要な情報の型を示す getter として機能します。
結論
前回の記事では、C# 言語でのライブラリ開発プロセスについて検討しました。 この記事では、次のステップに移り、作成されたメソッドを使用して取得できるトレーディングレポートの作成に移りました。 レポート生成メカニズムは、以前の記事で既に検討されていました。 しかし、改善され、改訂されています。 この記事では、開発の最新バージョンを紹介しました。 提供されたソリューションは、さまざまな最適化とテスト プロセスでテストされました。
添付されたアーカイブには 2 つのフォルダがあります。 両方とも MQL/Includeディレクトリに解凍します。
添付ファイルには、次のファイルが含まれます。
- CustomGeneric
- GenericSorter.mqh
- ICustomComparer.mqh
- History manager
- CustomComissionManager.mqh
- DealHistoryGetter.mqh
- ReportCreator.mqh
MetaQuotes Ltdによってロシア語から翻訳されました。
元の記事: https://www.mql5.com/ru/articles/7452





- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索