Linii directoare pentru scrierea expertilor de portofoliu in MQL5
[English version] [MQLmagazine.com in english] [Editia romaneasca]
Expertii de portofoliu, la fel ca si expertii multiasset, mai complecsi, au fost asteptati de comunitatea de retail trading de cand limitarile backtesterului MT4 au devenit evidente, adica de acum 4 – 5 ani. Diferenta intre expertii multiasset si cei de portofoliu e ca expertii multiasset au o strategie integrata in care fiecare activ e o componenta, in vreme ce expertii de portofoliu replica aceeasi strategie, cu unele diferente parametrice, pe mai multe instrumente. Bineinteles, in contextul MT4, expertii normali puteau fi rulati pe instrumente separate in vederea obtinerii efectelor de portofoliu,
dar backtestingul n-ar fi putut niciodata acoperi aceste efecte de portofoliu pentru ca nu se putea face backtest pe mai mult decat pe un instrument.
Avantajul principal al portofolizarii este impingerea rezultatului spre medie concomitent cu imprastierea riscului. Gradul insa pana la care aceasta se intampla, e dependent de corelatia de ansamblu a portofoliului. Cu cat acest grad este mai mic, cu atat riscul nesistematic ramas de acoperit este mai mic. Totusi, daca nu alegi portofoliul cum trebuie, in loc sa aduca echilibru sistemului de trading pe ansamblu, va adauga instabilitate.
Un sistem de trading pe un portofoliu trebuie sa:
- aplice o logica similara de trading la un numar mare de active;
- diferentieze logica de trading de la un instrument la altul;
- controleze expunerea.
E nevoie de o structura de date destul de mare pentru a face lucrurile controlabile:
De exemlu, aceasta:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | input double MarginUsagePerPosition=0.5; int SymbolsCount; struct IndicatorHandlersStruct { //... }; struct SystemParametersStruct { //... }; struct SymbolData { string Symbol; IndicatorHandlersStruct IndicatorHandlers; SystemParametersStruct SystemParameters; }; SymbolData SymbolsTable[300]; |
Prima variabila, MarginUsagePerPosition e un fel de parametru de alocare a activelor, controland volumul maxim pe fiecare pozitie. SymbolsCount va spune cate simboluri sunt folosite si pot fi folosite pentru enumerare, iar tabela mare SymbolsTable[] va contine datele necesare, de la simboluri pana la handlere. De exemplu, urmatoarea structura va calcula 4 medii mobile (doua pe M5 si doua pe H1) pentru fiecare simbol.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | struct IndicatorHandlersStruct { int MovingAveragesHandlers14[2]; int MovingAveragesHandlers9[2]; }; struct SymbolData { string Symbol; IndicatorHandlersStruct IndicatorHandlers; datetime LastBarTime; }; SymbolData SymbolsTable[300]; |
Inainte de OnInit() trebuie sa scriem o procedura care sa umple SymbolsTable[] cu handlerele indicatorilor.
MakeIndicatorHandlers() este apelata din OnInit() pentru a separa setupul simbolurilor de setupul indicatorilor.
Astfel poti modifica usor OnInit() pentru a schimba lista, prin adaugare, stergere sau introducerea unui selector automat de instrumente, dar MakeIndicatorHandlers() va ramane neschimbat, cu scopul de a umple structura cu handlerele necesare pentru indicatori.
1 2 3 4 5 6 7 8 9 10 11 12 | void MakeIndicatorHandlers() { datetime lastbar[1]; for (int i=0;i<SymbolsCount;i++) { SymbolsTable[i].IndicatorHandlers.MovingAverageHandlers14[0]=iTEMA(SymbolsTable[i].Symbol,PERIOD_M5,14,0,PRICE_OPEN); SymbolsTable[i].IndicatorHandlers.MovingAverageHandlers9[0]=iTEMA(SymbolsTable[i].Symbol,PERIOD_M5,9,0,PRICE_OPEN); SymbolsTable[i].IndicatorHandlers.MovingAverageHandlers14[1]=iTEMA(SymbolsTable[i].Symbol,PERIOD_H1,14,0,PRICE_OPEN); SymbolsTable[i].IndicatorHandlers.MovingAverageHandlers9[1]=iTEMA(SymbolsTable[i].Symbol,PERIOD_H1,9,0,PRICE_OPEN); } return; } |
In interiorul OnInit(), setam tabela simbolurilor. De exemplu, o facem sa mearga cu trei perechi forex: EURUSD, USDCHF, GBPJPY:
1 2 3 4 5 6 7 8 9 | int OnInit() { SymbolsTable[0].Symbol="EURUSD"; SymbolsTable[1].Symbol="USDCHF"; SymbolsTable[2].Symbol="GBPJPY"; SymbolsCount=3; MakeIndicatorHandlers(); return(0); } |
Si acum OnTick():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | void OnTick() { datetime datetime_array[1]; for (int i=0;i<SymbolsCount;i++) { CopyTime(SymbolsTable[i].Symbol,PERIOD_M5,0,1,datetime_array); if (datetime_array[0]!=SymbolsTable[i].LastBarTime) { TradeLogics(i); LastBarTime[i]=datetime_array[0]; }//if (datetime_array[0]!=SymbolsTable[i].LastBarTime) }//for (int i=0;i<SymbolsCount;i++) return; } |
Acum toate piesele sunt complete. OnTick() va verifica pentru fiecare instrument daca o noua bara a aparut si va apela TradeLogics() pentru fiecare instrument din tablou, care va implementa decizia si tradingul.
Nu ne vom opri asupra TradeLogics() in acest articol, pentru ca TradeLogics() e specific fiecarui EA. Dar cateva principii trebuie sa fie ghid la scrierea TradeLogics(). Prima diviziune a TradeLogics() este clasa de active cu care lucreaza. Nu poate lucra cu actiuni cum lucreaza cu forex. Actiunile nu se tranzactioneaza 24 de ore zi, 5 zile pe saptamana. Au sesiuni specifice si salturi pe chart de la o zi la alta, ca sa nu mai zic de la o saptamana la alta. A doua, e aceea ca e mai bine sa separam functiile de trading de TradeLogics(). De exemplu, TradeLogics() poate comanda cum sa se ajusteze pozitia pentru un anumit instrument, lasand functia de tranzactionare procedurii ManagePosition(). Bineinteles, TradeLogics() poate fi lansat in practica si de OnTrade() sau OnChartEvent(), poate chiar si de metoda EventsCallback() a unui motor CEP.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | double UnitsToLots(double units,string symbol) { double dlotsize=SymbolInfoDouble(symbol,SYMBOL_TRADE_CONTRACT_SIZE); double mag=SymbolInfoDouble(symbol,SYMBOL_VOLUME_STEP); double mini=SymbolInfoDouble(symbol,SYMBOL_VOLUME_MIN); double big=1/mag; double size=dlotsize/big; double dlots0,dlots1,back0,back1; dlots0=NormalizeDouble(units/size,0)*mag; dlots1=dlots0+mag; back0=dlots0*big*size; back1=dlots1*big*size; if (back1-units<units-back0) { if (dlots1<mini) return(mini); else return(dlots1); } else { if (dlots0==0) return(mini); else { if (dlots0<mini) return(mini); else return(dlots0); }//else if (dlots0==0) }//else if if (back1-units<units-back0) } |
UnitsToLots() este o portare a unei functii pe care am scris-o in vremurile MQL4, si care functiona la acea vreme cu MarketInfo(). UnitsToLots() va intoarce numarul de loturi ce corespunde unui volum dat in unitati. De exemplu poate raspunde 1.0 pentru 100000 unitati pe EURUSD.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | #define FLAT 0 #define LONG 1 #define SHORT 2 int GetPositionType(int asset_index) { string symbol=SymbolsTable[asset_index].Symbol; bool sel=PositionSelect(symbol); if (sel==false) return(FLAT); else { long p=PositionGetInteger(POSITION_TYPE); if (p==POSITION_TYPE_BUY) return(LONG); else return(SHORT); } } |
Aceasta functie este un proxy simplu pentru a obtine un raspuns clar privind tipul unei pozitii. Valoarea pentru POSITION_TYPE_BUY este 0 in MQL5, iar PositionInfoInteger() va raspunde 0 chiar daca pozitia nu a fost selectata inainte de apelare.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 | void ManagePosition(int asset_index,int operation,double forcevolume=0.00) { MqlTradeRequest request; MqlTradeResult result; int p=GetPositionType(asset_index); double now_volume; double current_volume,v0; if (PositionSelect(SymbolsTable[asset_index].Symbol)==true) current_volume=PositionGetDouble(POSITION_VOLUME); else current_volume=0; v0=current_volume; request.action=TRADE_ACTION_DEAL; request.symbol=SymbolsTable[asset_index].Symbol; request.deviation=Slippage; request.type_filling=ORDER_FILLING_AON; request.type_time=ORDER_TIME_GTC; if (p==FLAT) { if (DoubleToString(forcevolume,2)=="0.00") request.volume=UnitsToLots( (MarginUsagePerPosition/100)*AccountInfoDouble(ACCOUNT_EQUITY)*AccountInfoInteger(ACCOUNT_LEVERAGE),SymbolsTable[asset_index].Symbol ); else request.volume=forcevolume; if (operation==LONG) { request.type=ORDER_TYPE_BUY; request.price=NormalizeDouble(SymbolInfoDouble(SymbolsTable[asset_index].Symbol,SYMBOL_ASK),SymbolInfoInteger(SymbolsTable[asset_index].Symbol,SYMBOL_DIGITS)); } if (operation==SHORT) { request.type=ORDER_TYPE_SELL; request.price=NormalizeDouble(SymbolInfoDouble(SymbolsTable[asset_index].Symbol,SYMBOL_BID),SymbolInfoInteger(SymbolsTable[asset_index].Symbol,SYMBOL_DIGITS)); } if (operation==FLAT) return; } else//if (p==FLAT) { if (p==LONG&&operation==SHORT) { if (DoubleToString(forcevolume,2)=="0.00") request.volume=current_volume+UnitsToLots( (MarginUsagePerPosition/100)*AccountInfoDouble(ACCOUNT_EQUITY)*AccountInfoInteger(ACCOUNT_LEVERAGE),SymbolsTable[asset_index].Symbol ); else request.volume=current_volume+forcevolume; request.type=ORDER_TYPE_SELL; request.price=NormalizeDouble(SymbolInfoDouble(SymbolsTable[asset_index].Symbol,SYMBOL_BID),SymbolInfoInteger(SymbolsTable[asset_index].Symbol,SYMBOL_DIGITS)); } if (p==SHORT&&operation==LONG) { if (DoubleToString(forcevolume,2)=="0.00") request.volume=current_volume+UnitsToLots( (MarginUsagePerPosition/100)*AccountInfoDouble(ACCOUNT_EQUITY)*AccountInfoInteger(ACCOUNT_LEVERAGE),SymbolsTable[asset_index].Symbol ); else request.volume=current_volume+forcevolume; request.type=ORDER_TYPE_BUY; request.price=NormalizeDouble(SymbolInfoDouble(SymbolsTable[asset_index].Symbol,SYMBOL_ASK),SymbolInfoInteger(SymbolsTable[asset_index].Symbol,SYMBOL_DIGITS)); } if (p==LONG&&operation==FLAT) { if (DoubleToString(forcevolume,2)=="0.00") request.volume=current_volume; else request.volume=forcevolume; request.type=ORDER_TYPE_SELL; request.price=NormalizeDouble(SymbolInfoDouble(SymbolsTable[asset_index].Symbol,SYMBOL_BID),SymbolInfoInteger(SymbolsTable[asset_index].Symbol,SYMBOL_DIGITS)); } if (p==SHORT&&operation==FLAT) { if (DoubleToString(forcevolume,2)=="0.00") request.volume=current_volume; else request.volume=forcevolume; request.type=ORDER_TYPE_BUY; request.price=NormalizeDouble(SymbolInfoDouble(SymbolsTable[asset_index].Symbol,SYMBOL_ASK),SymbolInfoInteger(SymbolsTable[asset_index].Symbol,SYMBOL_DIGITS)); } if (p==LONG&&operation==LONG)//adjustment of the present LONG position { if (DoubleToString(forcevolume,2)=="0.00") request.volume=UnitsToLots( (MarginUsagePerPosition/100)*AccountInfoDouble(ACCOUNT_EQUITY)*AccountInfoInteger(ACCOUNT_LEVERAGE),SymbolsTable[asset_index].Symbol )-current_volume; else request.volume=forcevolume-current_volume; if (NormalizeDouble(request.volume,2)>0.00) { request.type=ORDER_TYPE_BUY; request.price=NormalizeDouble(SymbolInfoDouble(SymbolsTable[asset_index].Symbol,SYMBOL_ASK),SymbolInfoInteger(SymbolsTable[asset_index].Symbol,SYMBOL_DIGITS)); } else //we close the difference { request.volume=MathAbs(request.volume); request.type=ORDER_TYPE_SELL; request.price=NormalizeDouble(SymbolInfoDouble(SymbolsTable[asset_index].Symbol,SYMBOL_BID),SymbolInfoInteger(SymbolsTable[asset_index].Symbol,SYMBOL_DIGITS)); } } if (p==SHORT&&operation==SHORT)//adjustment of the present SHORT position { if (DoubleToString(forcevolume,2)=="0.00") request.volume=UnitsToLots( (MarginUsagePerPosition/100)*AccountInfoDouble(ACCOUNT_EQUITY)*AccountInfoInteger(ACCOUNT_LEVERAGE),SymbolsTable[asset_index].Symbol )-current_volume; else request.volume=forcevolume-current_volume; if (NormalizeDouble(request.volume,2)>0.00) { request.type=ORDER_TYPE_SELL; request.price=NormalizeDouble(SymbolInfoDouble(SymbolsTable[asset_index].Symbol,SYMBOL_BID),SymbolInfoInteger(SymbolsTable[asset_index].Symbol,SYMBOL_DIGITS)); } else { request.volume=MathAbs(request.volume); request.type=ORDER_TYPE_BUY; request.price=NormalizeDouble(SymbolInfoDouble(SymbolsTable[asset_index].Symbol,SYMBOL_ASK),SymbolInfoInteger(SymbolsTable[asset_index].Symbol,SYMBOL_DIGITS)); } } }//if (p==FLAT)] if (DoubleToString(request.volume,2)!="0.00") { OrderSend(request,result); PositionSetSLTP(StopLoss,TakeProfit); } } |
Ce face ManagePosition() ? Deschide o pozitie pentru activul dat prin index, la cerere, daca este inchisa dinainte (mai bine zis, “aplatizata”) ; sau inverseaza pozitia curenta, prin calcularea noii marime a lotului (o calculeaza cu formula, adauga la volumul curent si inverseaza operatiunea). Poate adauga sau taia din pozitia curenta. Parametrul forcevolume poate fi folosit pentru a forta un anumit volum a fi tranzactionat, sarind calculul. Ce parere ai de un stop loss ? Nimic mai usor:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | void PositionSetSLTP(int asset_index,int sl,int tp) { bool todosl, todotp; MqlTradeRequest request; MqlTradeResult result; double price=0.00; int p=GetPositionType(asset_index); if (p==LONG||p==SHORT) price=PositionGetDouble(POSITION_PRICE_OPEN); request.action=TRADE_ACTION_SLTP; request.symbol=SymbolsTable[asset_index].Symbol; todosl=false; todotp=false; if (DoubleToString(sl,4)!="0.0000") { todosl=true; if (p==LONG) request.sl=NormalizeDouble(price-sl*SymbolInfoDouble(SymbolsTable[asset_index].Symbol,SYMBOL_POINT),SymbolInfoInteger(SymbolsTable[asset_index].Symbol,SYMBOL_DIGITS)); if (p==SHORT) request.sl=NormalizeDouble(price+sl*SymbolInfoDouble(SymbolsTable[asset_index].Symbol,SYMBOL_POINT),SymbolInfoInteger(SymbolsTable[asset_index].Symbol,SYMBOL_DIGITS)); } if (DoubleToString(tp,4)!="0.0000") { todotp=true; if (p==LONG) request.tp=NormalizeDouble(price+tp*SymbolInfoDouble(SymbolsTable[asset_index].Symbol,SYMBOL_POINT),SymbolInfoInteger(SymbolsTable[asset_index].Symbol,SYMBOL_DIGITS)); if (p==SHORT) request.tp=NormalizeDouble(price-tp*SymbolInfoDouble(SymbolsTable[asset_index].Symbol,SYMBOL_POINT),SymbolInfoInteger(SymbolsTable[asset_index].Symbol,SYMBOL_DIGITS)); } request.deviation=Slippage; if (todosl==true||todotp==true) OrderSend(request,result); } |
De observat ca apelurile la OrderSend() nu sunt urmate de o analiza a rezultatului. Nu ne-am scufundat in analiza codurilor de intoarcere pentru ca unele dintre ele nu sunt complet clare.
Si in final OnDeinit(), care distruge handlerele indicatorilor.
1 2 3 4 5 6 7 8 9 10 11 | void OnDeinit(const int reason) { for (int i=0;i<SymbolsCount;i++) { IndicatorRelease(SymbolsTable[i].IndicatorHandlers.MovingAverageHandlers14[0]); IndicatorRelease(SymbolsTable[i].IndicatorHandlers.MovingAverageHandlers9[0]); IndicatorRelease(SymbolsTable[i].IndicatorHandlers.MovingAverageHandlers14[1]); IndicatorRelease(SymbolsTable[i].IndicatorHandlers.MovingAverageHandlers9[1]); } return; } |