
Guida alla scrittura di una DLL per MQL5 in Delphi
Introduzione
Il meccanismo di scrittura di una DLL verrà considerato utilizzando un esempio dell'ambiente di sviluppo di Delphi 2009. Questa versione è stata scelta poiché in MQL5 tutte le righe sono memorizzate in formato Unicode. Nelle versioni precedenti di Delphi, al modulo SysUtils manca la funzione per lavorare con le righe in formato Unicode.
Se, per qualsiasi motivo, stai utilizzando una versione precedente (Delphi 2007 e precedenti), devi lavorare con righe in formato ANSI e, per scambiare dati con MetaTrader 5, devi produrre conversioni dirette e inverse in Unicode . Per evitare queste complicazioni, consiglio di sviluppare il modulo DLL per MQL5 in un ambiente non più vecchio di Delphi 2009. Dal sito ufficiale http://embarcadero.com puoi scaricare una versione di prova gratuita di Delphi per 30 giorni.
1. Creazione del Progetto
Per creare il progetto dobbiamo eseguire una DLL Wizard scegliendo la voce di menu: 'File -> Nuovo -> Altro... -> DLL Wizard' Come mostrato nella Figura 1.
Figura 1. Creazione di un progetto utilizzando una DLL Wizard
Di conseguenza, creeremo un progetto DLL vuoto, come mostrato nella Figura 2.
Figura 2. Un progetto DLL vuoto
Il significato di un commento lungo nel titolo del progetto è quello di ricordare una connessione corretta e l'uso di un gestore di memoria quando si lavora con la memoria allocata dinamicamente. Questo argomento verrà trattato in maniera più dettagliata nella sezione relativa alle stringhe.
Prima di iniziare a riempire la nuova DLL con le funzioni, è importante configurare il progetto.
Aprire la finestra delle proprietà del progetto dal menu: 'Progetto -> Opzioni...' o tramite la tastiera 'Shift + Ctrl + F11' .
Per semplificare il processo di debug, è necessario che il file DLL venga creato direttamente nella cartella '.. \\MQL5\\Libraries' Trade Terminal MetaTrtader5. Per fare ciò, nella scheda DelphiCompiler impostare il valore della proprietà corrispondente Output directory , come mostrato nella Figura 3. Ciò elimina la necessità di copiare costantemente il file generato dalla DLL dalla cartella del progetto nella cartella del terminale.
Figure 3. Specificare la cartella in cui memorizzare il file DLL risultante
Per evitare l'aggancio di moduli BPL durante l'assemblaggio, senza la cui presenza nella cartella di sistema di Windows la DLL creata non funzionerà in futuro, è importante verificare che nella scheda Packages, il flag Build con i pacchetti runtime sia deselezionato, come mostrato nella Figura 4.
Figure 4. Esclusione dei moduli BPL dall'assemblaggio
Dopo aver completato la configurazione del progetto, salvalo nella tua cartella di lavoro, il nome specificato del progetto sarà il nome futuro del file DLL compilato.
2. Aggiunta di procedure e funzioni
Consideriamo la situazione generale quando si scrivono le procedure e le funzioni esportate nel modulo DLL, su un esempio di una procedura senza parametri. La comunicazione e la trasmissione dei parametri saranno discusse nella sezione successiva.
Una piccola digressione. Durante la scrittura delle procedure e delle funzioni nel linguaggio Object Pascal, il programmatore ha la possibilità di utilizzare le funzioni integrate della libreria Delphi, per non parlare degli svariati componenti sviluppati per questo ambiente. Ad esempio, per eseguire la stessa azione, come ad esempio aprire la visualizzazione di una finestra modale con un messaggio di testo, è possibile utilizzare come funzione API - MessageBox nonché una procedura della libreria VCL - ShowMessage.
La seconda opzione porta all'inclusione del modulo Dialogs e offre il vantaggio di lavorare facilmente con le finestre di dialogo standard di Windows. Tuttavia, la dimensione del file DLL risultante aumenterà di circa 500 KB. Pertanto, se preferisci creare piccoli file DLL, che non occupano molto spazio su disco, ti sconsiglio di utilizzare i componenti VCL.
Di seguito, viene riportato un esempio di progetto di prova con relative spiegazioni:
library dll_mql5; uses Windows, // necessary for the work of the MessageBox function Dialogs; // necessary for the work of the ShowMessage procedure from the Dialogs module var Buffer: PWideChar; //------------------------------------------------------+ procedure MsgBox(); stdcall; // //to avoid errors, use the stdcall (or cdecl) for the exported functions //------------------------------------------------------+ begin {1} MessageBox(0,'Hello World!','terminal', MB_OK); {2} ShowMessage('Hello World!');// alternative to the MessageBox function end; //----------------------------------------------------------+ exports //----------------------------------------------------------+ {A} MsgBox, {B} MsgBox name 'MessageBox';// renaming of the exported function //----------------------------------------------------------+ procedure DLLEntryPoint(dwReason: DWord); // event handler //----------------------------------------------------------+ begin case dwReason of DLL_PROCESS_ATTACH: // DLL attached to the process; // allocate memory Buffer:=AllocMem(BUFFER_SIZE); DLL_PROCESS_DETACH: // DLL detached from the process; // release memory FreeMem(Buffer); end; end; //----------------------------------------------------------+ begin DllProc := @DLLEntryPoint; //Assign event handler DLLEntryPoint(DLL_PROCESS_ATTACH); end. //----------------------------------------------------------+
Tutte le funzioni esportate devono essere annunciate con il modificatore stdcall o cdecl. Se nessuno di questi modificatori viene specificato, Delphi utilizza l'accordo fastcall predefinito che utilizza principalmente i registri della CPU per trasmettere i parametri piuttosto che lo stack. Porterà senza dubbio a un errore nel lavorare con i parametri trasmessi, nella fase di calling up delle funzioni esterne DLL.
La sezione "begin end" contiene un codice di inizializzazione standard di un gestore di eventi DLL. La procedura di callback DLLEntryPoint verrà chiamata durante la connessione e la disconnessione dal processo che l'ha chiamata. Questi eventi possono essere utilizzati per la corretta gestione della memoria dinamica, allocata per le nostre esigenze, come mostrato nell'esempio.
Chiamata per MQL5:
#import "dll_mql5.dll" void MsgBox(void); void MessageBox(void); #import // Call of procedure MsgBox(); // If the names of the function coincide with the names of MQL5 standard library function // use the DLL name when calling the function dll_mql5::MessageBox();
3. Trasmissione dei Parametri alla Funzione e Valori Restituiti
Prima di considerare la trasmissione dei parametri, analizziamo la tabella di corrispondenza dei dati per MQL5 ed Object Pascal.
Tipo di dati per MQL5 | Tipo di dati per Object Pascal (Delphi) | Nota |
---|---|---|
char | ShortInt | |
uchar | Byte | |
short | SmallInt | |
ushort | Word | |
int | Integer | |
uint | Cardinal | |
long | Int64 | |
ulong | UInt64 | |
float | Single | |
double | Double | |
ushort (символ) | WideChar | |
string | PWideChar | |
bool | Boolean | |
datetime | TDateTime | è richiesta la conversione (vedi sotto in questa sezione) |
color | TColor |
Tabella 1. La tabella di corrispondenza dei dati per MQL5 e Object Pascal
Come puoi vedere dalla tabella, per tutti i tipi di dati diversi da datetime, Delphi ha un equivalente completo.
Consideriamo ora due modi per passare i parametri: per valore e per riferimento. Il formato della dichiarazione dei parametri per entrambe le versioni viene riportato nella Tabella 2.
Metodo di trasmissione dei parametri | Annuncio per MQL5 | Annuncio per Delphi | Nota |
---|---|---|---|
per valore | int funzione (int a); | func (a:Integer): Integer; | corretto |
int funzione (int a); | funzione (var a: Integer): Integer; | Errore: violazione di accesso scrivere su <memory address> | |
per collegamento | int funzione (int &a); | funzione (var a: Integer): Integer; | corretto, tuttavia le linee vengono trasmesse senza un modificatore var! |
int funzione (int &a); | func (a: Integer): Integer; | errore: al posto del valore della variabile, contiene l'indirizzo della cella di memoria |
Tabella 2. Metodi di trasferimento dei parametri
Consideriamo ora gli esempi di utilizzo dei parametri trasferiti e dei valori restituiti.
3.1 Conversione di data e ora
Innanzitutto, ci occupiamo del tipo di data e ora che si desidera convertire, poiché il tipo datetime corrisponde a TDateTime solo nelle sue dimensioni ma non nel formato. Per facilitare la trasformazione, utilizzare Int64 come tipo di dati ricevuti, invece di TDateTime. Di seguito, vengono riportate le funzioni per la trasformazione diretta ed inversa:
uses SysUtils, // used for the constant UnixDateDelta DateUtils; // used for the function IncSecon, DateTimeToUnix //----------------------------------------------------------+ Function MQL5_Time_To_TDateTime(dt: Int64): TDateTime; //----------------------------------------------------------+ begin Result:= IncSecond(UnixDateDelta, dt); end; //----------------------------------------------------------+ Function TDateTime_To_MQL5_Time(dt: TDateTime):Int64; //----------------------------------------------------------+ begin Result:= DateTimeToUnix(dt); end;
3.2 Lavorare con tipi di dati semplici
Analizziamo come trasmettere tipi di dati semplici, sull'esempio di quelli più comunemente usati: int, double, bool e datetime.
Chiamata per Object Pascal:
//----------------------------------------------------------+ function SetParam(var i: Integer; d: Double; const b: Boolean; var dt: Int64): PWideChar; stdcall; //----------------------------------------------------------+ begin if (b) then d:=0; // the value of the variable d is not changed in the calling program i:= 10; // assign a new value for i dt:= TDateTime_To_MQL5_Time(Now()); // assign the current time for dt Result:= 'value of variables i and dt are changed'; end;
Chiamata per MQL5:
#import "dll_mql5.dll" string SetParam(int &i, double d, bool b, datetime &dt); #import // initialization of variables int i = 5; double d = 2.8; bool b = true; datetime dt= D'05.05.2010 08:31:27'; // calling the function s=SetParam(i,d,b,dt); // output of results printf("%s i=%s d=%s b=%s dt=%s",s,IntegerToString(i),DoubleToString(d),b?"true":"false",TimeToString(dt));Risultato:
The values of variables i and dt are changed i = 10 d = 2.80000000 b = true dt = 2009.05 . 05 08 : 42
Il valore di d non è cambiato da quando è stato trasferito per valore. Per evitare che si verifichino cambiamenti del valore di una variabile, all'interno di una funzione DLL, un modificatore const. è stato utilizzato sulla variabile b.
3.3 Lavorare con strutture e array
In diverse occasioni, è utile raggruppare i parametri di tipi diversi in strutture e i parametri di un tipo in array. Supponiamo di lavorare con tutti i parametri trasferiti della funzione SetParam, dall'esempio precedente, integrandoli in una struttura.
Chiamata per Object Pascal:
type StructData = packed record i: Integer; d: Double; b: Boolean; dt: Int64; end; //----------------------------------------------------------+ function SetStruct(var data: StructData): PWideChar; stdcall; //----------------------------------------------------------+ begin if (data.b) then data.d:=0; data.i:= 10; // assign a new value for i data.dt:= TDateTime_To_MQL5_Time(Now()); // assign the current time for dt Result:= 'The values of variables i, d and dt are changed'; end;
Chiamata per MQL5:
struct STRUCT_DATA { int i; double d; bool b; datetime dt; }; #import "dll_mql5.dll" string SetStruct(STRUCT_DATA &data); #import STRUCT_DATA data; data.i = 5; data.d = 2.8; data.b = true; data.dt = D'05.05.2010 08:31:27'; s = SetStruct(data); printf("%s i=%s d=%s b=%s dt=%s", s, IntegerToString(data.i),DoubleToString(data.d), data.b?"true":"false",TimeToString(data.dt));Risultato:
The values of variables i, d and dt are changed i = 10 d = 0.00000000 b = true dt = 2009.05 . 05 12 : 19
È necessario notare una differenza significativa rispetto al risultato dell'esempio precedente. Poiché la struttura viene trasferita attraverso un riferimento, non è possibile proteggere i campi selezionati dalla modifica nella funzione chiamata. Il compito di monitorare l'integrità dei dati, in questo caso, spetta interamente al programmatore.
Supponiamo di lavorare con gli array, su un esempio di riempimento dell'array con la sequenza dei numeri di Fibonacci:
Chiamata per Object Pascal:
//----------------------------------------------------------+ function SetArray(var arr: IntegerArray; const len: Cardinal): PWideChar; stdcall; //----------------------------------------------------------+ var i:Integer; begin Result:='Fibonacci numbers:'; if (len < 3) then exit; arr[0]:= 0; arr[1]:= 1; for i := 2 to len-1 do arr[i]:= arr[i-1] + arr[i-2]; end;
Chiamata per MQL5:
#import "dll_mql5.dll" string SetArray(int &arr[],int len); #import int arr[12]; int len = ArraySize(arr); // passing the array by reference to be filled by data in DLL s = SetArray(arr,len); //output of result for(int i=0; i<len; i++) s = s + " " + IntegerToString(arr[i]); printf(s);Risultato:
Fibonacci numbers 0 1 1 2 3 5 8 13 21 34 55 89
3.4 Lavorare con lo storage
Torniamo alla gestione della memoria. Nella DLL è possibile utilizzare il proprio gestore di memoria. Tuttavia, poiché laDLL e il programma che la chiama, sono spesso scritti in linguaggi di programmazione diversi e nel lavoro vengono utilizzati i propri gestori di memoria, piuttosto che la memoria di sistema generale, l'intero onere della responsabilità per il corretto funzionamento della memoria nella DLL di giunzione e dell'applicazione, ricade sul programmatore.
Per lavorare con la memoria, è importante rispettare la regola d'oro, secondo la quale: "Coloro che hanno allocato la memoria, devono essere quelli che la liberano." Cioè non dovresti provare a rilasciare la memoria nel codice mql 5-program, allocato nella DLL, e viceversa.
Consideriamo un esempio di gestione della memoria in uno stile di chiamate di funzioni API di Windows. Nel nostro caso il programma mql5 alloca memoria per il buffer, un puntatore al buffer trasferito alla DLL come PWideChar e la DLL riempie solo questo buffer con il valore desiderato, come mostrato nel seguente esempio:
Chiamata per Object Pascal:
//----------------------------------------------------------+ procedure SetString(const str:PWideChar) stdcall; //----------------------------------------------------------+ begin StrCat(str,'Current time:'); strCat(str, PWideChar(TimeToStr(Now))); end;
Chiamata per MQL5:
#import "dll_mql5.dll" void SetString(string &a); #import // the string must be initialized before the use // the size of the buffer must be initially larger or equal to the string length StringInit(s,255,0); //passing the buffer reference to DLL SetString(s); // output of result printf(s);
Risultato:
Current Time: 11: 48:51
La memoria per il buffer di linea può essere selezionata nella DLL in diversi modi, come si può vedere dall'esempio seguente:
Chiamata per Object Pascal:
//----------------------------------------------------------+ function GetStringBuffer():PWideChar; stdcall; //----------------------------------------------------------+ var StrLocal: WideString; begin // working through the dynamically allocated memory buffer StrPCopy(Buffer, WideFormat('Current date and time: %s', [DateTimeToStr(Now)])); // working through the global varialble of WideString type StrGlobal:=WideFormat('Current time: %s', [TimeToStr(Time)]); // working through the local varialble of WideString type StrLocal:= WideFormat('Current data: %s', [DateToStr(Date)]); {A} Result := Buffer; {B} Result := PWideChar(StrGlobal); // it's equal to the following Result := @StrGlobal[1]; {С} Result := 'Return of the line stored in the code section'; // pointer to the memory, that can be released when exit from the function {D} Result := @StrLocal[1]; end;Chiamata per MQL5:
#import "dll_mql5.dll" string GetStringBuffer(void); #import printf(GetStringBuffer());
Risultato:
Current Date: 19.05.2010
L’aspetto significativo è che tutte e quattro le opzioni funzionano. Nelle prime due opzioni, il lavoro con la riga viene eseguito tramite una memoria allocata globalmente.
Nell'opzione A, la memoria è allocata in modo indipendente e nell'opzione B, il lavoro con la gestione della memoria è assunto dal gestore della memoria.
Nell'opzione C, la costante di riga non viene memorizzata nella memoria ma nel segmento di codice; quindi il gestore della memoria non alloca memoria dinamica per la sua memorizzazione. L'opzione D è un grave errore di programmazione, perché la memoria, allocata per la variabile locale, può essere rilasciata immediatamente dopo l'uscita dalla funzione.
E sebbene il gestore della memoria non rilasci questa memoria in maniera istantanea e non vi sia tempo per riempirsi di spazzatura, consiglio di escludere quest'ultima opzione dall'uso.
3.5 Utilizzo dei parametri di default
Parliamo dell'uso dei parametri opzionali. Sono interessanti perché i loro valori non devono essere specificati quando si richiamano procedure e funzioni. Nel frattempo, devono essere descritti, rigorosamente dopo tutti i parametri obbligatori nella dichiarazione di procedure e funzioni, come mostrato nel seguente esempio:
Chiamata per Object Pascal:
//----------------------------------------------------------+ function SetOptional(var a:Integer; b:Integer=0):PWideChar; stdcall; //----------------------------------------------------------+ begin if (b=0) then Result:='Call with default parameters' else Result:='Call without default parameters'; end;Chiamata per MQL5:
#import "dll_mql5.dll" string SetOptional(int &a, int b=0); #import i = 1; s = SetOptional(i); // second parameter is optional printf(s);
Risultato:
Call with default parameters
Per facilitare il debug, il codice degli esempi precedenti è organizzato come script, che si trova nel file Testing_DLL.mq5.
4. Possibili errori in fase di progettazione
Errore: Il caricamento della DLL non è consentito.
Soluzione: Vai alle impostazioni di MetaTrader 5 attraverso il menu 'Strumenti-Opzioni' e consenti l'importazione della funzione DLL, come mostrato nella Figura 5.
Figura 5. Permesso di importare funzioni DLL
Errore: Impossibile trovare "nome funzione" in "nome DLL".
Soluzione: Verificare se la funzione di callback è specificata nella sezione Esportazioni del progetto DLL. Se lo è, dovresti controllare la corrispondenza completa del nome della funzione nella DLL e nel programma mql5, considerando che è sensibile ai caratteri!
Errore: Violazione di accesso scrivere a [memory address]
Soluzione: È necessario verificare la correttezza della descrizione dei parametri trasmessi (vedi tabella 2). Poiché di solito questo errore è associato all'elaborazione delle righe, è importante seguire le raccomandazioni per lavorare con le righe illustrate al paragrafo 3.4 di questo articolo.
5. Esempio di codice DLL
Come esempio visivo dell'uso della DLL, si consideri il calcolo dei parametri del canale di regressione, costituito da tre righe. Per verificare la correttezza della costruzione del canale, utilizzeremo l'oggetto integrato "Canale di regressione". Il calcolo della linea di approssimazione per LS (metodo dei minimi quadrati) è tratto dal sito http://alglib.sources.ru/, dove è presente una raccolta di algoritmi per l'elaborazione dei dati. Il codice degli algoritmi è presentato in diversi linguaggi di programmazione, incluso Delphi.
Per calcolare i coefficienti di a e b mediante la retta approssimante y = a + b * x, utilizzare la procedura descritta nel file LRLine linreg.pas.
procedure LRLine ( const XY: TReal2DArray; / / Two-dimensional array of real numbers for X and Y coordinates N : AlglibInteger; // number of points var Info : AlglibInteger; // conversion status var A: Double; / / Coefficients of the approximating line var B: Double);
Per calcolare i parametri del canale, utilizzare la funzione CalcLRChannel.
Chiamata per Object Pascal:
//----------------------------------------------------------+ function CalcLRChannel(var rates: DoubleArray; const len: Integer; var A, B, max: Double):Integer; stdcall; //----------------------------------------------------------+ var arr: TReal2DArray; info: Integer; value: Double; begin SetLength(arr,len,2); // copy the data to a two-dimensional array for info:= 0 to len - 1 do begin arr[info,0]:= rates[info,0]; arr[info,1]:= rates[info,1]; end; // calculation of linear regression coefficients LRLine(arr, len, info, A, B); // find the maximal deviation from the approximation line found // and determine the width of the channel max:= rates[0,1] - A; for info := 1 to len - 1 do begin value:= Abs(rates[info,1]- (A + B*info)); if (value > max) then max := value; end; Result:=0; end;
Chiamata per MQL5:
#import "dll_mql5.dll" int CalcLRChannel(double &rates[][2],int len,double &A,double &B,double &max); #import double arr[][2], //data array for processing in the ALGLIB format a, b, // Coefficients of the approximating line max; // maximum deviation from the approximating line is equal to half the width of the channel int len = period; //number of points for calculation ArrayResize(arr,len); // copying the history to a two-dimensional array int j=0; for(int i=rates_total-1; i>=rates_total-len; i--) { arr[j][0] = j; arr[j][1] = close[i]; j++; } // calculation of channel parameters CalcLRChannel(arr,len,a,b,max);
Il codice dell'indicatore, che utilizza la funzione CalcLRChannel per i calcoli, si trova nel file LR_Channel.mq5 e di seguito:
//+------------------------------------------------------------------+ //| LR_Channel.mq5 | //| Copyright 2009, MetaQuotes Software Corp. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "2009, MetaQuotes Software Corp." #property link "https://www.mql5.com" #property version "1.00" #property indicator_chart_window #include <Charts\Chart.mqh> #include <ChartObjects\ChartObjectsChannels.mqh> #import "dll_mql5.dll" int CalcLRChannel(double &rates[][2],int len,double &A,double &B,double &max); #import input int period=75; CChart *chart; CChartObjectChannel *line_up,*line_dn,*line_md; double arr[][2]; //+------------------------------------------------------------------+ int OnInit() //+------------------------------------------------------------------+ { if((chart=new CChart)==NULL) {printf("Chart not created"); return(false);} chart.Attach(); if(chart.ChartId()==0) {printf("Chart not opened");return(false);} if((line_up=new CChartObjectChannel)==NULL) {printf("Channel not created"); return(false);} if((line_dn=new CChartObjectChannel)==NULL) {printf("Channel not created"); return(false);} if((line_md=new CChartObjectChannel)==NULL) {printf("Channel not created"); return(false);} return(0); } //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) //+------------------------------------------------------------------+ { double a,b,max; static double save_max; int len=period; ArrayResize(arr,len); // copying of history to a two-dimensional array int j=0; for(int i=rates_total-1; i>=rates_total-len; i--) { arr[j][0] = j; arr[j][1] = close[i]; j++; } // procedure of calculating the channel parameters CalcLRChannel(arr,len,a,b,max); // if the width of the channel has changed if(max!=save_max) { save_max=max; // Delete the channel line_md.Delete(); line_up.Delete(); line_dn.Delete(); // Creating a channel with new coordinates line_md.Create(chart.ChartId(),"LR_Md_Line",0, time[rates_total-1], a, time[rates_total-len], a+b*(len-1) ); line_up.Create(chart.ChartId(),"LR_Up_Line",0, time[rates_total-1], a+max, time[rates_total-len], a+b*(len-1)+max); line_dn.Create(chart.ChartId(),"LR_Dn_Line",0, time[rates_total-1], a-max, time[rates_total-len], a+b*(len-1)-max); // assigning the color of channel lines line_up.Color(RoyalBlue); line_dn.Color(RoyalBlue); line_md.Color(RoyalBlue); // assigning the line width line_up.Width(2); line_dn.Width(2); line_md.Width(2); } return(len); } //+------------------------------------------------------------------+ void OnDeinit(const int reason) //+------------------------------------------------------------------+ { // Deleting the created objects chart.Detach(); delete line_dn; delete line_up; delete line_md; delete chart; }
Il risultato del lavoro dell'indicatore è la creazione di un canale di regressione blu, come mostrato nella Figura 6. Per verificare la correttezza della costruzione del canale, il grafico mostra un "Canale di regressione", dall'arsenale di strumenti di analisi tecnica MetaTrader 5, contrassegnato in rosso.
Come si può vedere nella figura, le linee centrali del canale si fondono insieme. Nel frattempo, vi è una leggera differenza della larghezza del canale (pochi punti), dovuta ai diversi approcci nel suo calcolo.
Figura 6. Confronto dei canali di regressione
Conclusione
Questo articolo descrive le funzionalità di scrittura di una DLL, utilizzando una piattaforma di sviluppo di applicazioni Delphi.
Tradotto dal russo da MetaQuotes Ltd.
Articolo originale: https://www.mql5.com/ru/articles/96





- App di trading gratuite
- Oltre 8.000 segnali per il copy trading
- Notizie economiche per esplorare i mercati finanziari
Accetti la politica del sito e le condizioni d’uso