English Русский 中文 Español Deutsch Português
CSV ファイルを介した MetaTrader 4 と Matlab 間の連携

CSV ファイルを介した MetaTrader 4 と Matlab 間の連携

MetaTrader 4 | 15 3月 2016, 17:29
2 382 0
Dmitriy
Dmitriy

はじめに

Matlab 環境の計算能力は、MQL4 を含めたあらゆるプログラム言語の計算能力より各段に優れていることで知られています。Matlab が提供する数学関数の幅広さにより、行われる処理の理論的根拠を完全に度外視して複雑な計算を行うことができます。

トレーディングターミナルと Matlab の間のリアルタイムの連携は簡単ではありません。。本稿では、CSV ファイルを介した MetaTrader 4 と Matlab 間のデータ交換を編成する方法を提供します。



1. 相互連携

新規バーの入信時、MetaTrader 4 が直近のバー100本についてのデータを Matlab に対して送信し、処理結果を受信すると仮定します。

この課題を解くためには、データをテキストファイルに書き込み、処理結果を Matlab によって作成される別のテキストファイルから読み取る MetaTrader 4 にインディケータを作成する必要があります。

MetaTrader 4 は新規バー受信ごとにそのデータファイルを作成しなければなりません。また、ティックごとに結果を読み取るようにする必要もあります。Matlab が更新する前に結果を読まないようにするため、出力ファイル作成前に結果を持つファイルを削除してしまうのです。この場合、読み取りの試みは Matlab が計算を終了し、新規ファイルを作成したあとに初めて成功するのです。

Matlab は、MetaTrader 4 によって作成されるファイル属性を毎秒分析し、作成時刻が変わるとき処理を開始する必要があります。処理が終わると、データのレコード前に MetaTrader 4 によって削除されたファイルが再作成されます。MetaTrader 4 はそれを正常に削除し、新規データをロードしてへ返事を待ちます。



2. 出力データファイル作成

データをファイルとして保存することを専門に扱った記事は数多くあるので、その点についてここで手間をかけることはしません。われわれはデータを7列に書き込む点だけはっきりさせておきます。それは、“DATE”, “TIME”, “HI”, “LOW”, “CLOSE”, “OPEN”, “VOLUME”です。分離する文字は“;”です。バーの優先順位は早いものから後のものへ、すなわちゼロバー特性を持つ行は最後にレコードされるのです。ファイルには列名を持つ行が当てられます。ファイル名はシンボル名とタイムフレームで構成されます。

#property indicator_chart_window
extern int length = 100;   // The amount of bars sent to be processed
double ExtMap[];           // Chart buffer
string nameData;
int init()
{
   nameData = Symbol()+".txt";         // name of the data file to be sent
   return(0);
}
int start()
{
   static int old_bars = 0;   // remember the amount of bars already known   
   if (old_bars != Bars)      // if a new bar is received 
   {
      write_data();                             // write the data file                              
   }      
   old_bars = Bars;              // remember how many bars are known
   return(0);
}
//+------------------------------------------------------------------+
void write_data()
{
  int handle;
  handle = FileOpen(nameData, FILE_CSV|FILE_WRITE,';');
  if(handle < 1)
  {
    Comment("Creation of "+nameData+" failed. Error #", GetLastError());
    return(0);
  }
  FileWrite(handle, ServerAddress(), Symbol(), Period());                  // heading
  FileWrite(handle, "DATE","TIME","HIGH","LOW","CLOSE","OPEN","VOLUME");   // heading
  int i;
  for (i=length-1; i>=0; i--)
  {
    FileWrite(handle, TimeToStr(Time[i], TIME_DATE), TimeToStr(Time[i], TIME_SECONDS),
                      High[i], Low[i], Close[i], Open[i], Volume[i]);
  }
  FileClose(handle);
  Comment("File "+nameData+" has been created. "+TimeToStr(TimeCurrent(), TIME_SECONDS) );
  return(0);
}

これらデータをすべて必要とはしませんが、未知の数字の入ったただの列セットよりも、常に意味のあるファイルを取得する方がよいものです。



3. グラフィカルユーザーインターフェース(GUI)の作成

というわけで、ファイルは準備できました。Matlab を起動します。

ファイルからテキストデータを読み出すアプリケーションを作成し、データを処理し、結果を別のファイルに記録します。GUI を作成し、ファイル名を指定、チャートを閲覧し、処理を開始します。始めましょう。

GUI を作成するには、コンソールに“guide”と打ち込むか、Matlabのメインパネルで押して『GUIDE クイックスタート』を起動します。表示されるダイアログボックスで『新規 GUI を作成する』→『空の GUI(デフォルト)』を選択します。 ここで空のフォームで GUI 作成用インターフェースが表示されます。このフォームに以下のオブジェクトを設定します。それは『テキストを編集する』、『ボタンを押す』、『静的テキスト』、『軸』、『ボタンを押す』です。結果、このようなものが表示されます。

次にそれぞれのオブジェクトに対してビジュアルプロパティビルダーを呼び出し、以下のようにプロパティを設定します。

静的テキスト:水平位置 -左、タグ- textInfo、文字列- Info
テキストを編集する:水平位置 -左、タグ- editPath、文字列- パス選択
ボタンを押す:タグ- pushBrowse、文字列-Browse
軸:ボックス- on、フォント名-MS Sans Serif、フォントサイズ- 8、タグ- axesChart.
ボタンを押す:タグ- pushSrart、文字列- Start

「タグ」のプロパティを変更することでオブジェクトごとにユニークな名前を選択します。その他項目を変更し、表示を変えます。

すべて準備が整ったら、『実行する」を押してインターフェースを起動し、インターフェース ファイルの保存とM ファイルの保存を確認し、名前をつけ(たとえば“FromTo”)、『保存する』を押します。その後、GUI が起動し動作中の表示どおりに表示されます。Matlab はのちのプログラムの基となるように M ファイルを作成し、埋め込まれたエディタにそれを開きます。

なんらかの理由で、表示が適切でなければ、動作中の GUI を閉じ、エディタでオブジェクトの装飾を修正します。たとえば、わたしのディストリビューションは MS Sans Serif を正確に表示しませんでした。そのため私はフォント名を“Sans Serif”に変更しなければなりませんでした。



4. ユーザーインターフェースの構築

インターフェースの動作は Matlab 言語で 「M ファイル エディタ」でプログラムします。 Matlab によって作成されるスケルトン プログラムは、インターフェースオブジェクトで作業するときユーザーによって呼び出される関数リストを表示します。関数は空なので、GUI はまだ何も行いません。必要な内容で関数に書き込むのはわれわれの仕事です。



4.1ブラウズ ボタンのプログラミング

まず、MetaTrader 4 が作成したファイルにアクセスする必要があります。そうして『ブラウズ』ボタンを押すことで呼び出される関数を起動するのです。

ボタンを押すことで呼び出される関数名は、ボタン名(『タグ』プロパティで設定されます)と接尾辞 "_Callback" で構成されます。ファイルテキストで関数“pushBrowse_Callback”を見つけるか、ツールバーの『関数を表示する』を押してリストで“pushBrowse_Callback”を選択します。

Matlab プログラム言語の構文は C 言語や C 類似言語でコーディングする従来のルールとは異なります。特に、関数本文を中括弧でマークしたり、関数に渡されるデータタイプや1から始まる配列(ベクトル)インデックスを指定する必要はなく、コメント文字は“%”です。よって、上のグリーンのテキストは全体にプログラムではなく、われわれがこのケースを把握できるよう Matlab の開発者によってつけられたコメントなのです。

われわれはファイルのフルネームを入力するためのダイアログを作成する必要があります。これには、関数“uigetfile”を使用します。

% --- Executes on button press in pushBrowse.
function pushBrowse_Callback(hObject, eventdata, handles)
[fileName, filePath] = uigetfile('*.txt'); % receive and the path from the user
if fileName==0          % if it is canceled
    fileName='';        % create an empty name
    filePath='';        % create an empty path
end
fullname = [filePath fileName] % form a new name
set(handles.editPath,'String', fullname); % write in the editPath

ここでの“handles”は、オブジェクトすべてのディスクリプタを GUI に格納するストラクチャです。 それにはディスクリプタを設定する形式も含まれます。ストラクチャは関数から別の関数に渡され、オブジェクトにアクセスできるようにします。
“hObject”は関数を呼んだオブジェクトのディスクリプタです。
“set”は特定の値にオブジェクト値を設定するのに役立ち、以下の構文になっています:set(object_descriptor, object_property_name, property_value)。

オブジェクト プロパティの値を検索するには次の関数を使用します:property_value = get(object_descriptor, object_descriptor_name)。
ただし、名前は文字列タイプの値であることを忘れないようにします。よってそれは単一クオートの必要があります。

オブジェクトについて知っておくべき最後の事柄と、そのプロパティです。GUI エレメントを設定した形式自体が“root” オブジェクトに設定されたオブジェクトなのです(its はその派生です)。それはまた変更可能なプロパティ セットを持ちます。プロパティは『オブジェクト エディタ』という名前のツールをインターフェース エディタのメイン ツールバーから呼び出すことによって閲覧可能です。名前からわかるようにオブジェクト“root”はグラフィカルオブジェクト階層の根本で、上位階層は持ちません。

それではここで、結果取得したものを確認します。M ファイルエディタのメインツールバーで「実行する」を押して GUI を起動します。「ブラウズ」をクリックしてみて、ファイルを選択します。オンになっていますか?それから動作中の GUI を閉じて、先に進みます。



4.2「開始」ボタン、チャート描画のプログラミング

ファイルからデータを読み出しそれをチャートに表示する関数を呼び出して「開始する」ボタンを割り当てます。

まず、関数そのものを作成します。インプットとして 'handles' オブジェクト ディスクリプタのストラクチャが必要です。オブジェクトにアクセスしたら、それを読みプロパティを設定することができます。

% data reading, chart drawing, processing, storage
function process(handles)
fullname = get(handles.editPath, 'String'); % read the name from editPath
data = dlmread(fullname, ';', 2, 2);    % read the matrix from file
info = ['Last update: ' datestr(now)];  % form an informative message
set(handles.textInfo, 'String',info);   % write info into the status bar
 
high = data(:,1);   % it is now high where the first column of the data matrix is
low = data(:,2);    % d low -- the second
close = data(:,3);  % --/--
open = data(:,4);   %
len = length(open); % the amount of elements in open
 
axes(handles.axesChart); % make the axes active
hold off; % clean axes off before adding a new chart
candle(high, low, close, open); % draw candlesticks (in the current axes)
set(handles.axesChart,'XLim',[1 len]); % set limits for charting

以下、説明です。

“dlmread” は分離子を伴ってテキストファイルからデータを読み出します。それは次の構文になっています。:dlmread(full_file_name, separator, skip_strings, skip_columns)
“length(qqq)” -行列 qqq の大きいサイズ
”now” -現在日時
“datestr(now)” -テキストへ変換した日時

Matlab は理論と例でひじょうに大きなヘルプ情報を提供することも知っておく必要があります。

プログラムの末尾にわれわれの関数を入れ(そこにある方が探しやすいでしょう)、“pushStart_Callback”にその呼び出しを追加します。

% --- Executes on button press in pushStart.
function pushStart_Callback(hObject, eventdata, handles)
process(handles);

『実行する』ボタンで起動し、ファイルを選択したら、『開始する』を押して、結果を楽しんでください。



4.3ファイルへのパス保存

これですべて上首尾ですが、『ブラウズ』を押した後、マウスをクリックしてファイルの選択を永久に行うのはやや面倒です。一度選択したら、パスを保存します。
読み出しから始めます。パスを格納するファイル名は GUI 名と接尾辞“_saveparam”で構成され、拡張子“.mat”を持ちます。
関数“FromTo_OpeningFcn”は GUI フォームが作成されたら直ちに実行されます。そこにファイルからのパス読み出しをするよう追加します。その試みが失敗すれば、デフォルト値が使用されます。

% --- Executes just before FromTo is made visible.
function FromTo_OpeningFcn(hObject, eventdata, handles, varargin)
guiName = get(handles.figure1, 'Name'); % get the name of our GUI
name = [guiName '_saveparam.mat']       % define the file name
h=fopen(name);                          % try to open the file
if h==-1                                % if the file does not open
    path='D:\';                         % the default value
else
    load(name);                         % read the file    
    fclose(h);                          % close the file
end
set(handles.editPath,'String', path);   % write the name into object "editPath"

関数“FromTo_OpeningFcn”のその他文字列は変更されません。


関数“pushBrowse_Callback”を以下のように変更します。

% --- Executes on button press in pushBrowse.
function pushBrowse_Callback(hObject, eventdata, handles)
path = get(handles.editPath,'String'); % read the path from object editPath 
[partPath, partName, partExt] = fileparts(path);    % divide the path into parts
template = [partPath '\*.txt'];                     % create a template of parts
[userName, userPath] = uigetfile(template);         % get the user name and the path from the user
if userName~=0                                      % if "Cancel" is not pressed        
    path = [userPath userName];                     % reassemble the path
end
set(handles.editPath,'String', path);               % write the path into object "editPath"
guiName = get(handles.figure1, 'Name');             % get to know the name of our GUI
save([guiName '_saveparam.mat'], 'path');           % save the path


4.4データ処理

通常の手順として、四次多項式関数によって列“OPEN”を補間します。
関数“process”の末尾に次のコードを追加します。

fitPoly2 = fit((1:len)',open,'poly4'); % get the polynomial formula
fresult = fitPoly2(1:len); % calculate Y values for X=(from 1 to len)
hold on; % a new chart has been added to the old one
stairs(fresult,'r'); % plot stepwise by color - 'r'- red

起動し、『開始する』を押します。

上記とだいたい同様の結果が得られれば、ファイルとしてデータを保存するのに最適なタイミングです。



4.5データのファイルとしての保存

データ保存は読み出しほど複雑ではありません。ただ一つ『慎重に扱うべきこと』は、ベクトル“fresult”を逆に数えることです。すなわち、最後から始めに向かって数えるのです。これは MetaTrader 4 において、ゼロバーから始めファイルの終わりまで、ファイルの読み出しを簡単にするためです。

次に、以下のコードで関数“process”を補います。

[pathstr,name,ext,versn] = fileparts(fullname); % divide the full name
                                                % of the file into parts
newName = [pathstr '\' name '_result' ext];     % re-compose the new file name
fresult = flipud(fresult);  % turn vector fresult
dlmwrite(newName, fresult);    % write in the file

ここで、結果を持つファイルが作成され、このファイルは最初のファイルがあるのと同じ場所に入っていて、接尾辞“_result”が追加された同名であることに留意ください。



4.6タイマー制御

これはこの作業でもっともむつかしい部分です。MetaTrader 4 形式のファイル作成時間を毎秒確認するタイマーを作成する必要があります。時間が変われば、関数“process”が起動しなければなりません。タイマーの開始と停止は“Start”によって行われます。GUI が開くとき、以前に作成されたタイマーはすべて削除します。

関数“FromTo_OpeningFcn”内に以下のコードを入れてタイマーを作成します。

timers = timerfind; % find timers
if ~isempty(timers) % if timers are available
    delete(timers); % delete all timers
end
handles.t = timer('TimerFcn',{@checktime, handles},'ExecutionMode','fixedRate','Period',1.0,'UserData', 'NONE');

上記のコードはこの関数への以前の挿入後すぐに挿入する必要があります。すなわち文字列“handles.output = hObject;” と “guidata(hObject, handles);”の前です。

このコードを実行することで、GUI の作成直後に Matlab がタイマーが使用可能であるかチェックし、既存のタイマーを削除して、新規にタイマーを作成します。タイマーは毎秒、関数“checktime”を呼び出し、そこにディスクリプタ“handles”のリストを渡します。“handles”とは別に、タイマーは関数に、呼出し時刻と理由を持つストラクチャと共に、自分のディスクリプタを渡します。われわれはこれに関与することはできませんが、タイマーに呼び出される関数をコーディングする際、このことに配慮が必要です。

関数自体は好きな箇所に入れることができます。その関数に 呼び出された時刻を自分でMatlab ステータスバーに書き込ませます。

% function called by the timer
function checktime(obj, event, handles)
set(handles.textInfo,'String',datestr(now));

それが作成されるとき、タイマーは停止します。これからそれを取り入れます。関数“pushStart_Callback”を検索します。そこにある'process(handles)' 呼び出しをコメントし、タイマー管理をそこに書き込みます。

% --- Executes on button press in pushStart.
function pushStart_Callback(hObject, eventdata, handles)
% process(handles);
statusT = get(handles.t, 'Running'); % Get to know the timer status
if strcmp(statusT,'on')     % If it is enabled - 
    stop(handles.t);        % disable it
    set(hObject,'ForegroundColor','b'); % change color of pushStart marking
    set(hObject,'String',['Start' datestr(now)]); % change the button top marking
end     
if strcmp(statusT,'off')    % If it is disabled - 
    start(handles.t);       % enable it
    set(hObject,'ForegroundColor','r');% change color of pushStart marking
    set(hObject,'String',['Stop' datestr(now)]); % change the button top marking
end

それぞれがどのように動作するか確認します。『開始する』でタイマーを有効に、また無効にしてみます。タイマーが有効になったら、パス入力フィールドの上にある時計が機能する必要があります。

GUI を閉じる際、"X" ボタンでタイマーを削除する方がよいでしょう。それを行いたい場合は、

stop(handles.t) ; % stop the timer
delete(handles.t); % delete the timer

を関数“figure1_CloseRequestFcn”の冒頭に追加します。この関数は GUI を閉じるときに呼び出されます。それには GUI エディタからアクセス可能です。

ただし、開いている GUI を閉じずにエディタの『実行する』を押すと、新規にタイマーが作成されるときに古いタイマーが削除されないことに配慮してください。そして、次のとき、もう一つ作成される、などとなります。『未処理』タイマーは Matlab コンソールのコマンド“delete(timerfind)”で処理することができます。


すべてうまく動作すれば、MetaTrader 4 からの直近のファイル変更時刻を確認する関数を作成します。

% function to be called by the timer
function checktime(obj, event, handles)
filename = get(handles.editPath, 'String'); % get to know the file name 
fileInfo = dir(filename);        % get info about the file
oldTime = get(obj, 'UserData');  % recall the time
if ~strcmp(fileInfo.date, oldTime) % if the time has changed
    process(handles);
end
set(obj,'UserData',fileInfo.date); % save the time
set(handles.pushStart,'String',['Stop ' datestr(now)]); % override the time

関数 "dir(full_file_name)" はファイル情報(名前、日付、バイト数、isdir)を持つストラクチャを返します。前回のファイル作成に関する情報はタイマーオブジェクトの『ユーザーデータ』プロパティに格納されます。そのディスクリプタは obj という名前で関数 "checktime" に渡されます。

ここで、MetaTrader 4 によって作成されたファイルを変更すると、われわれのプログラムは結果を上書きします。これはファイルをマニュアルで変更し(たとえば、最後の文字列を削除する)導かれたチャートやファイルで変更を追跡することで確認ができます。もちろんそこで『開始する』ボタンを押す必要があります。

プログラム操作中にチャートのコピーがある別のウィンドウが作成されれば、関数"process"の始めに以下の文字列を追加します。

set(handles.figure1,'HandleVisibility','on');


5. MetaTrader 4 での結果描画

では MetaTrader 4 に戻ります。ファイルから結果を読み、それをチャートに描画する関数でわれわれのインディケータを補う必要があります。プログラムの動作は以下に説明しています。

1. 新規バーが受け取られたら、古い結果ファイルを削除し、チャートを消去し、データファイルを保存します。
2. 結果ファイルが読み取り可能であれば、ファイルを読み、チャートを描画し、結果ファイルを削除します。

以下のコードがどのように動作するかはここで述べません。それは、ファイルからデータを読むことと、インディケータを描くことは別の記事で参照できるからです。ここでの結果ファイルはチャートに入れられた直後に削除される、ということだけお知らせしておきます。よって複数の読み取りエラーメッセージを見ても心配しないでください。

読み取りエラーは次の2つのケースで発生します。
1. 新規バーが受信された直後。これは結果ファイルがまだ作成されていないためです。
2. 結果が読まれ、チャートが描かれた直後。同じデータが再度読まれないようファイルが削除されたためです。

このため、プログラムは実質上常に『読み込みエラー』状態を維持するのです。:)

#property indicator_chart_window
#property indicator_buffers 1
#property indicator_width1 2
#property indicator_color1 Tomato
extern int length = 100;   // The amount of bars to be sent for processing
double ExtMap[];           // Chart buffer
string nameData;
string nameResult;

int init()
{
   nameData = Symbol()+".txt";         // the name of the data file to be sent
   nameResult = Symbol()+"_result.txt";// the name of the received file containing results
   SetIndexStyle(0, DRAW_LINE);
   SetIndexBuffer(0, ExtMap);
   return(0);
}
int deinit()
  {
   Comment("");
   return(0);
  }
int start()
{
   static int attempt = 0;    // the amount of attempts to read the result
   static int old_bars = 0;   // remember the amount of the known bars
   
   if (old_bars != Bars)      // if a new bar has income
   {
      FileDelete(nameResult);                   // delete the result file
      ArrayInitialize( ExtMap, EMPTY_VALUE);    // empty the chart
      write_data();                             // save the data file
      
      old_bars = Bars; return(0);               // nothing should be done this time
   }
   //
   int handle_read = FileOpen(nameResult,
                              FILE_CSV|FILE_READ,
                              ';'); // try to open the result file
   attempt++;                       // count the attempt to open
   
   if(handle_read >= 0)             // if the file has opened for reading
   {
      Comment(nameResult+". Opened with attempt #"+ attempt); // opening report
      read_n_draw(handle_read);  // read the result and draw a chart
      FileClose(handle_read);    // close the file
      FileDelete(nameResult);    // delete the result file
      attempt=0;                 // zeroize the amount of attempts to read
   }
   else                          // if we cannot open the result file
   {
      Comment( "Failed reading "+nameResult+
               ". Amount of attempts: "+attempt+
               ". Error #"+GetLastError()); //Report about failed reading
   }
   old_bars = Bars;              // remember how many bars are known
   return(0);
}
//+------------------------------------------------------------------+
void read_n_draw(int handle_read)
{
   int i=0;
   while ( !FileIsEnding(handle_read))
   {
      ExtMap[i] = FileReadNumber(handle_read);
      i++;
   }
   ExtMap[i-1] = EMPTY_VALUE;
}

 
void write_data()
{
  int handle;
  handle = FileOpen(nameData, FILE_CSV|FILE_WRITE,';');
  if(handle < 1)
  {
    Comment("Failed creating "+nameData+". Error #", GetLastError());
    return(0);
  }
  FileWrite(handle, ServerAddress(), Symbol(), Period());                  // header
  FileWrite(handle, "DATE","TIME","HIGH","LOW","CLOSE","OPEN","VOLUME");   // header
  int i;
  for (i=length-1; i>=0; i--)
  {
    FileWrite(handle, TimeToStr(Time[i], TIME_DATE), TimeToStr(Time[i], TIME_SECONDS),
                      High[i], Low[i], Close[i], Open[i], Volume[i]);
  }
  FileClose(handle);
  Comment("File "+nameData+" has been created. "+TimeToStr(TimeCurrent(), TIME_SECONDS) );
  return(0);
}

以下が私の最終結果です。誤りなくみなさんがそれを再現できることを願っています。




おわりに

本稿では、CSV ファイルを介した MetaTrader 4 と Matlab 間の連携を構築する方法を説明しました。この方法は特異なものでも最適なものでもありません。この方法の価値は、MetaTrader 4 とMatlab 以外のプログラムツールを使わず操作の特別な技能がなくてもデータ配列を交換できるのに役立つ、というものです。


MetaQuotes Ltdによってロシア語から翻訳されました。
元の記事: https://www.mql5.com/ru/articles/1489

添付されたファイル |
work.zip (9.98 KB)

この著者による他の記事

楽になりエラーが少なくてすむように EA コードを短くする方法 楽になりエラーが少なくてすむように EA コードを短くする方法
本稿で述べられるシンプルなコンセプトにより、MQL4 で自動売買システムを作成する人が既存のトレーディングシステムをシンプルにすることができ、またコードが短くなることで新規にシステムを作成するのに必要な時間を削減することもできます。
MT4TerminalSync - MetaTrader 4 ターミナルの同期のためのシステム MT4TerminalSync - MetaTrader 4 ターミナルの同期のためのシステム
本稿は『オペレーションシステム関数やその他プログラム作成手法を使用してMQL4 プログラムの機能を広げる』がテーマです。1つのソーステンプレートを基に複数のターミナルコピーを同期するタスクを実装するプログラムシステム例について説明します。
ユニバーサルな Expert Advisor のテンプレート ユニバーサルな Expert Advisor のテンプレート
本稿は取引初心者が柔軟に調整可能なExpert Advisor を作成するお手伝いをします。
非標準自動取引 非標準自動取引
詳しく市場分析をせず MT4 のプラットフォームで行う首尾よく快適な取引。そんなこと、できるのでしょうか?そのような取引を実際に実装することはできますか?私はできると思います。特に自動取引では!