
SQLite: MQL5'te SQL veritabanlarıyla yerel olarak çalışma
İçindekiler
- MetaTrader 5'te modern algoritmik ticaret
- Veritabanlarıyla çalışmak için fonksiyonlar
- Basit sorgu örneği
- MetaEditor'da SQL sorgularında hata ayıklama
- DatabaseReadBind() kullanılarak sorgu sonuçlarının yapıya otomatik olarak okunması
- İşlemleri DatabaseTransactionBegin()/DatabaseTransactionCommit() içerisine sararak hızlandırma
- İşlem geçmişindeki işlemlerle çalışma
- Stratejilere göre portföy analizi
- İşlemlerin sembollere göre analizi
- İşlemlerin giriş saatlerine göre analizi
- DatabasePrint() ile Uzman Danışman günlüğüne zahmetsiz veri çıkışı
- Verileri içe/dışa aktarma
- Optimizasyon sonuçlarını veritabanına kaydetme
- İndeksleri kullanarak sorgu yürütmeyi optimize etme
- Veritabanlarıyla çalışmanın MetaEditor'a entegrasyonu
MetaTrader 5'te modern algoritmik ticaret
MQL5, hem sözdizimi hem de hesaplama hızı açısından C++'a olabildiğince yakın olduğundan dolayı algoritmik ticaret için mükemmel bir çözümdür. MetaTrader 5 platformu, kullanıcılarının basit ticaret görevlerinin ötesine geçmelerini ve her türlü karmaşıklıkta analiz sistemleri oluşturmalarını mümkün kılan, böylece ticaret robotları ve özel göstergeler geliştirmelerine olanak sağlayan modern ve özelleşmiş bir dil sunmaktadır.
Asenkron ticaret fonksiyonlarına ve matematik kütüphanelerine ek olarak, yatırımcılar ayrıca ağ fonksiyonlarına, verileri Python'a aktarmaya, OpenCL'de paralel hesaplamaya, "akıllı" fonksiyon aktarmayla .NET kütüphaneleri için yerel desteğe, MS Visual Studio ile entegrasyona ve DirectX kullanarak veri görselleştirmeye erişebilir. Modern algoritmik ticaretin cephaneliğindeki bu vazgeçilmez araçlar, günümüzde kullanıcıların MetaTrader 5 işlem platformundan ayrılmadan çok çeşitli görevleri çözmelerine olanak sağlamaktadır.Veritabanlarıyla çalışmak için fonksiyonlar
Ticaret stratejilerinin geliştirilmesi, büyük miktarda verinin işlenmesiyle ilişkilidir. Bugün artık sadece hızlıca bir MQL5 programı şeklinde bir ticaret algoritması yazmak yeterli değildir. Güvenilir sonuçlar elde etmek için, yatırımcıların ayrıca çeşitli ticaret enstrümanları üzerinde çok sayıda test ve optimizasyon gerçekleştirmesi, sonuçları kaydetmesi, işlemesi ve analiz yapması ve devamında bir sonraki adıma karar vermesi gerekmektedir.
Artık basit ve popüler SQLite motorunu doğrudan MQL5'te kullanarak veritabanlarıyla çalışabilirsiniz. Geliştiricilerin web sitesindeki testlerin sonuçları, SQL sorgularının yüksek hızda yürütüldüğünü göstermektedir. Çoğu görevde PostgreSQL ve MySQL'den daha iyi performans göstermektedir. Biz de bu testlerin MQL5 ve LLVM 9.0.0 üzerinde uygulanma hızlarını karşılaştırdık ve sonuçları tablo halinde düzenledik. Sonuçlar milisaniye cinsinden verilmiştir - ne kadar düşükse o kadar iyidir.
Ad | Açıklama | LLVM | MQL5 |
---|---|---|---|
Test 1 | 1000 INSERTs | 11572 | 8488 |
Test 2 | 25000 INSERTs in a transaction | 59 | 60 |
Test 3 | 25000 INSERTs into an indexed table | 102 | 105 |
Test 4 | 100 SELECTs without an index | 142 | 150 |
Test 5 | 100 SELECTs on a string comparison | 391 | 390 |
Test 6 | Creating an index | 43 | 33 |
Test 7 | 5000 SELECTs with an index | 385 | 307 |
Test 8 | 1000 UPDATEs without an index | 58 | 54 |
Test 9 | 25000 UPDATEs with an index | 161 | 165 |
Test 10 | 25000 text UPDATEs with an index | 124 | 120 |
Test 11 | INSERTs from a SELECT | 84 | 84 |
Test 12 | DELETE without an index | 25 | 74 |
Test 13 | DELETE with an index | 70 | 72 |
Test 14 | A big INSERT after a big DELETE | 62 | 66 |
Test 15 | A big DELETE followed by many small INSERTs | 33 | 33 |
Test 16 | DROP TABLE: finished | 42 | 40 |
Test ayrıntılarını ekteki SqLiteTest.zip dosyasında bulabilirsiniz. Ölçümlerin yapıldığı bilgisayarın teknik özellikleri: Windows 10 x64, Intel Xeon E5-2690 v3 @ 2.60GHz.
Yukarıdaki sonuçlar, MQL5'te veritabanlarıyla çalışırken maksimum performanstan emin olabileceğimizi göstermektedir. SQL ile daha önce hiç karşılaşmamış olanlar, yapılandırılmış sorgu dilinin (Structured Query Language, SQL), karmaşık döngülere ve örneklemelere ihtiyaç duymadan birçok görevi hızlı ve zarif bir şekilde çözmelerine olanak tanıdığını görecektir.
Basit sorgu örneği
Veritabanları bilgileri tablolar şeklinde depolar, yeni veriler alma/değiştirme ve ekleme işlemleri ise SQL dilinde sorgular kullanılarak yapılır. Şimdi basit bir veritabanının nasıl oluşturulacağını ve ondan nasıl veri alınacağını görmek adına bir örneğe göz atalım.
//+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { string filename="company.sqlite"; //--- create or open the database in the common terminal folder int db=DatabaseOpen(filename, DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE |DATABASE_OPEN_COMMON); if(db==INVALID_HANDLE) { Print("DB: ", filename, " open failed with code ", GetLastError()); return; } ... working with the database //--- close the database DatabaseClose(db); }
Veritabanı oluşturma ve kapatma, dosyalarla çalışmaya benzerdir. Önce veritabanı için bir tanıtıcı oluşturuyoruz, sonra kontrol ediyoruz ve son olarak kapatıyoruz.
Ardından, veritabanında tablonun var olup olmadığını kontrol ediyoruz. Tablo halihazırda mevcutsa, örneğimizde olduğu gibi aynı verileri ekleme girişimi bir hatayla sonuçlanacaktır.
//--- if the COMPANY table exists, delete it if(DatabaseTableExists(db, "COMPANY")) { //--- delete the table if(!DatabaseExecute(db, "DROP TABLE COMPANY")) { Print("Failed to drop table COMPANY with code ", GetLastError()); DatabaseClose(db); return; } } //--- create the COMPANY table if(!DatabaseExecute(db, "CREATE TABLE COMPANY(" "ID INT PRIMARY KEY NOT NULL," "NAME TEXT NOT NULL," "AGE INT NOT NULL," "ADDRESS CHAR(50)," "SALARY REAL );")) { Print("DB: ", filename, " create table failed with code ", GetLastError()); DatabaseClose(db); return; }
Tablo, sorgular kullanılarak oluşturulur ve silinir. Yürütme sonucu her zaman kontrol edilmelidir. COMPANY tablosunda yalnızca beş alan bulunmaktadır: ID, NAME, AGE, ADDRESS ve SALARY. ID alanı bir anahtardır, yani benzersiz bir indekstir. İndeksler, her girdinin güvenilir bir şekilde tanımlanmasına olanak sağlar ve onları birbirine bağlamak için farklı tablolarda kullanılabilir. Bu, bir pozisyon kimliğinin o pozisyona ait tüm işlemleri ve emirleri nasıl birbirine bağladığına benzerdir.
Şimdi tabloyu verilerle doldurmamız gerekiyor. Bu, INSERT sorgusu kullanılarak yapılır:
//--- enter data to the table if(!DatabaseExecute(db, "INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) VALUES (1,'Paul',32,'California',25000.00); " "INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) VALUES (2,'Allen',25,'Texas',15000.00); " "INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) VALUES (3,'Teddy',23,'Norway',20000.00);" "INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) VALUES (4,'Mark',25,'Rich-Mond',65000.00);")) { Print("DB: ", filename, " insert failed with code ", GetLastError()); DatabaseClose(db); return; }
Gördüğümüz gibi, COMPANY tablosuna dört girdi eklendi. Her girdi için alanların sırası ve bu alanlara eklenecek değerler belirtilir. Her girdi, tek bir sorgu halinde ayrı "INSERT..." sorguları tarafından eklenir. Başka bir deyişle, her girdiyi ayrı bir DatabaseExecute() çağrısı ile tabloya ekleyebiliriz.
Komut dosyasının sonunda veritabanı company.sqlite dosyasına kaydedileceğinden, bir dahaki sefere çalıştırıldığında, aynı verileri aynı ID’lerle COMPANY tablosuna yazmaya çalışacağız. Bu, hataya neden olacaktır. Dolayısıyla, komut dosyası her çalıştırıldığında sıfırdan başlamak adına önce tabloyu siliyoruz.
Şimdi COMPANY tablosundan SALARY alanı > 15000 olan tüm girdileri alalım. Bu, sorgu metnini derleyen ve daha sonra DatabaseRead() veya DatabaseReadBind()’da kullanılmak üzere ona bir tanıtıcı geri döndüren DatabasePrepare() fonksiyonu kullanılarak yapılır.
//--- create a query and get a handle for it int request=DatabasePrepare(db, "SELECT * FROM COMPANY WHERE SALARY>15000"); if(request==INVALID_HANDLE) { Print("DB: ", filename, " request failed with code ", GetLastError()); DatabaseClose(db); return; }
Sorgu başarıyla oluşturulduktan sonra yürütme sonuçlarını almamız gerekiyor. Bunu, ilk çağrı sırasında sorguyu yürüten ve sonuçlardaki ilk girdiye hareket eden DatabaseRead()’i kullanarak yapacağız. Sonraki her çağrıda, sona ulaşana kadar bir sonraki girdiyi okur. Bizim durumumuzda false geri döndürecektir, bu, başka girdi yok anlamına gelir.
//--- print all entries with the salary greater than 15000 int id, age; string name, address; double salary; Print("Persons with salary > 15000:"); for(int i=0; DatabaseRead(request); i++) { //--- read the values of each field from the obtained entry if(DatabaseColumnInteger(request, 0, id) && DatabaseColumnText(request, 1, name) && DatabaseColumnInteger(request, 2, age) && DatabaseColumnText(request, 3, address) && DatabaseColumnDouble(request, 4, salary)) Print(i, ": ", id, " ", name, " ", age, " ", address, " ", salary); else { Print(i, ": DatabaseRead() failed with code ", GetLastError()); DatabaseFinalize(request); DatabaseClose(db); return; } } //--- remove the query after use DatabaseFinalize(request);
Yürütme sonucu aşağıdaki gibi olacaktır:
Persons with salary > 15000: 0: 1 Paul 32 California 25000.0 1: 3 Teddy 23 Norway 20000.0 2: 4 Mark 25 Rich-Mond 65000.0Örneğin tam kodu DatabaseRead.mq5 dosyasındadır.
MetaEditor'da SQL sorgularında hata ayıklama
Veritabanıyla çalışmaya yönelik tüm fonksiyonlar, başarısız bir çağrı durumunda bir hata kodu geri döndürür. Şu dört basit kuralı izlerseniz, onlarla çalışırken herhangi bir sorun oluşmasını önlersiniz:
- DatabaseFinalize() tarafından kullanıldıktan sonra tüm sorgu tanıtıcıları yok edilmelidir;
- Veritabanı, tamamlanmadan önce DatabaseClose() ile kapatılmalıdır;
- Sorgu yürütme sonuçları kontrol edilmelidir;
- Bir hata durumunda, önce sorgu yok edilmeli, ardından veritabanı kapatılmalıdır.
En zor şey, sorgu oluşturulmamışsa hatanın ne olduğunu anlamaktır. MetaEditor *.sqlite dosyalarının açılmasına ve SQL sorguları kullanılarak onlarla çalışılmasına olanak tanır. Oluşturulan company.sqlite örnek dosyasını kullanarak bunun nasıl yapıldığını görelim:
1. Ortak terminal klasöründen company.sqlite dosyasını açıyoruz.
2. Veritabanını açtıktan sonra Kılavuzda COMPANY tablosunu görebiliriz. Üzerine çift tıklayın.
3. SELECT * FROM COMPANY sorgusu, durum çubuğunda otomatik olarak oluşturulur.
4. Sorgu otomatik olarak yürütülür. Ayrıca F9'a basılarak veya Yürüt düğmesine tıklanarak da yürütülebilir.
5. Sorgunun yürütme sonucuna bakıyoruz.
6. Bir şeyler ters giderse, hatalar düzenleyicinin günlüğünde görüntülenir.
SQL sorgularını kullanarak örneğin toplam ve ortalama gibi tablo alanları hakkında istatistikler elde edebiliriz. Sorguları yapalım ve çalışıp çalışmadıklarını kontrol edelim.
Şimdi bu sorguları MQL5 kodunda uygulayabiliriz:
Print("Some statistics:"); //--- prepare a new query about the sum of salaries request=DatabasePrepare(db, "SELECT SUM(SALARY) FROM COMPANY"); if(request==INVALID_HANDLE) { Print("DB: ", filename, " request failed with code ", GetLastError()); DatabaseClose(db); return; } while(DatabaseRead(request)) { double total_salary; DatabaseColumnDouble(request, 0, total_salary); Print("Total salary=", total_salary); } //--- remove the query after use DatabaseFinalize(request); //--- prepare a new query about the average salary request=DatabasePrepare(db, "SELECT AVG(SALARY) FROM COMPANY"); if(request==INVALID_HANDLE) { Print("DB: ", filename, " request failed with code ", GetLastError()); ResetLastError(); DatabaseClose(db); return; } while(DatabaseRead(request)) { double aver_salary; DatabaseColumnDouble(request, 0, aver_salary); Print("Average salary=", aver_salary); } //--- remove the query after use DatabaseFinalize(request);
Yürütme sonuçlarını karşılaştırabiliriz:
Some statistics: Total salary=125000.0 Average salary=31250.0
DatabaseReadBind() kullanılarak sorgu sonuçlarının yapıya otomatik olarak okunması
DatabaseRead() fonksiyonu, sorgu sonucundaki tüm girdilerin üzerinden geçilmesine ve böylece sonuç tablosundaki her bir sütun hakkında tam veri elde edilmesine olanak tanır:
- DatabaseColumnName — ad,
- DatabaseColumnType — veri türü,
- DatabaseColumnSize — bayt cinsinden veri boyutu,
- DatabaseColumnText - metin olarak al,
- DatabaseColumnInteger — int tipinde al,
- DatabaseColumnLong —long tipinde al,
- DatabaseColumnDouble — double tipinde al,
- DatabaseColumnBlob —dizi olarak al.
Bu fonksiyonlar, herhangi bir sorgunun sonuçlarıyla toplu bir şekilde çalışılmasına olanak sağlar. Ancak bunun bedeli ise aşırı koddur. Sorgu sonuçlarının yapısı önceden biliniyorsa, tüm girdinin yapıya hemen okunmasını sağlayan DatabaseReadBind() fonksiyonunu kullanmak daha iyidir. Önceki örneği şu şekilde yeniden yazabiliriz - ilk olarak Person yapısını bildiriyoruz:
struct Person { int id; string name; int age; string address; double salary; };
Ardından, DatabaseReadBind(request, person) ile sorgu sonuçlarından her girdiyi okuyacağız:
//--- display obtained query results Person person; Print("Persons with salary > 15000:"); for(int i=0; DatabaseReadBind(request, person); i++) Print(i, ": ", person.id, " ", person.name, " ", person.age, " ", person.address, " ", person.salary); //--- remove the query after use DatabaseFinalize(request);
Böylece mevcut girdiden tüm alanların değerlerini, ayrı ayrı okumaya gerek kalmadan, anında alıyoruz.
İşlemleri DatabaseTransactionBegin()/DatabaseTransactionCommit() içerisine sararak hızlandırma
Bir tabloyla çalışırken, INSERT, UPDATE veya DELETE komutlarını toplu olarak kullanmak gerekebilir. Bunu yapmanın en iyi yolu işlemleri kullanmaktır. İşlemler yapılırken önce veritabanı kilitlenir (DatabaseTransactionBegin). Devamında toplu değişiklik komutları gerçekleştirilir ve kaydedilir (DatabaseTransactionCommit), bir hata varlığında ise iptal edilir (DatabaseTransactionRollback).
DatabasePrepare fonksiyonunun açıklaması, işlemlerin kullanımına ilişkin bir örnek içermektedir:
//--- auxiliary variables ulong deal_ticket; // deal ticket long order_ticket; // a ticket of an order a deal was executed by long position_ticket; // ID of a position a deal belongs to datetime time; // deal execution time long type ; // deal type long entry ; // deal direction string symbol; // a symbol a deal was executed for double volume; // operation volume double price; // price double profit; // financial result double swap; // swap double commission; // commission long magic; // Magic number (Expert Advisor ID) long reason; // deal execution reason or source //--- go through all deals and add them to the database bool failed=false; int deals=HistoryDealsTotal(); // --- lock the database before executing transactions DatabaseTransactionBegin(database); for(int i=0; i<deals; i++) { deal_ticket= HistoryDealGetTicket(i); order_ticket= HistoryDealGetInteger(deal_ticket, DEAL_ORDER); position_ticket=HistoryDealGetInteger(deal_ticket, DEAL_POSITION_ID); time= (datetime)HistoryDealGetInteger(deal_ticket, DEAL_TIME); type= HistoryDealGetInteger(deal_ticket, DEAL_TYPE); entry= HistoryDealGetInteger(deal_ticket, DEAL_ENTRY); symbol= HistoryDealGetString(deal_ticket, DEAL_SYMBOL); volume= HistoryDealGetDouble(deal_ticket, DEAL_VOLUME); price= HistoryDealGetDouble(deal_ticket, DEAL_PRICE); profit= HistoryDealGetDouble(deal_ticket, DEAL_PROFIT); swap= HistoryDealGetDouble(deal_ticket, DEAL_SWAP); commission= HistoryDealGetDouble(deal_ticket, DEAL_COMMISSION); magic= HistoryDealGetInteger(deal_ticket, DEAL_MAGIC); reason= HistoryDealGetInteger(deal_ticket, DEAL_REASON); //--- add each deal to the table using the following query string request_text=StringFormat("INSERT INTO DEALS (ID,ORDER_ID,POSITION_ID,TIME,TYPE,ENTRY,SYMBOL,VOLUME,PRICE,PROFIT,SWAP,COMMISSION,MAGIC,REASON)" "VALUES (%d, %d, %d, %d, %d, %d, '%s', %G, %G, %G, %G, %G, %d, %d)", deal_ticket, order_ticket, position_ticket, time, type, entry, symbol, volume, price, profit, swap, commission, magic, reason); if(!DatabaseExecute(database, request_text)) { PrintFormat("%s: failed to insert deal #%d with code %d", __FUNCTION__, deal_ticket, GetLastError()); PrintFormat("i=%d: deal #%d %s", i, deal_ticket, symbol); failed=true; break; } } //--- check for transaction execution errors if(failed) { //--- roll back all transactions and unlock the database DatabaseTransactionRollback(database); PrintFormat("%s: DatabaseExecute() failed with code %d", __FUNCTION__, GetLastError()); return(false); } //--- all transactions have been performed successfully - record changes and unlock the database DatabaseTransactionCommit(database);
İşlemler, DatabaseTransactionBegin örneğinde gösterildiği gibi toplu tablo işlemlerinin yüzlerce kat hızlandırılmasına olanak sağlar:
Result: Deals in the trading history: 2737 Transations WITH DatabaseTransactionBegin/DatabaseTransactionCommit: time=48.5 milliseconds Transations WITHOUT DatabaseTransactionBegin/DatabaseTransactionCommit: time=25818.9 milliseconds Use of DatabaseTransactionBegin/DatabaseTransactionCommit provided acceleration by 532.8 times
İşlem geçmişindeki işlemlerle çalışma
SQL sorgularının gücü, kod yazmak zorunda kalmadan kaynak verileri kolayca sıralayabilmemiz, seçebilmemiz ve değiştirebilmeniz gerçeğinde yatar. DatabasePrepare fonksiyonunun açıklamasından örneği analiz etmeye devam edelim - tek bir sorgu aracılığıyla ticaret işlemlerinin nasıl alınacağını göreceğiz. Bir ticaret işlemi, giriş ve çıkış tarihleri, giriş ve çıkış fiyatları, sembol, yön ve hacim hakkında bilgiler içerir. Ticaret işleminin yapısına bakarsak, giriş/çıkış işlemlerinin ortak bir pozisyon kimliği ile bağlı olduğunu görebiliriz. Dolayısıyla, bir hedging hesapta basit bir ticaret sistemimiz varsa, alış ve satış işlemlerini tek bir ticaret işlemi halinde kolayca birleştirebiliriz. Bu, aşağıdaki sorgu kullanılarak yapılır:
//--- fill in the TRADES table using an SQL query based on DEALS table data ulong start=GetMicrosecondCount(); if(DatabaseTableExists(db, "DEALS")) { //--- fill in the TRADES table if(!DatabaseExecute(db, "INSERT INTO TRADES(TIME_IN,TICKET,TYPE,VOLUME,SYMBOL,PRICE_IN,TIME_OUT,PRICE_OUT,COMMISSION,SWAP,PROFIT) " "SELECT " " d1.time as time_in," " d1.position_id as ticket," " d1.type as type," " d1.volume as volume," " d1.symbol as symbol," " d1.price as price_in," " d2.time as time_out," " d2.price as price_out," " d1.commission+d2.commission as commission," " d2.swap as swap," " d2.profit as profit " "FROM DEALS d1 " "INNER JOIN DEALS d2 ON d1.position_id=d2.position_id " "WHERE d1.entry=0 AND d2.entry=1")) { Print("DB: fillng the TRADES table failed with code ", GetLastError()); return; } } ulong transaction_time=GetMicrosecondCount()-start;
Burada mevcut DEALS tablosu kullanılır. Girdiler, INNER JOIN aracılığıyla aynı DEAL_POSITION_ID'ye sahip işlemlerden oluşturulur. İlgili DatabasePrepare komut dosyasını bir işlem hesabında çalıştıralım:
Result: Deals in the trading history: 2741 The first 10 deals: [ticket] [order_ticket] [position_ticket] [time] [type] [entry] [symbol] [volume] [price] [profit] [swap] [commission] [magic] [reason] [0] 34429573 0 0 2019.09.05 22:39:59 2 0 "" 0.00000 0.00000 2000.00000 0.0000 0.00000 0 0 [1] 34432127 51447238 51447238 2019.09.06 06:00:03 0 0 "USDCAD" 0.10000 1.32320 0.00000 0.0000 -0.16000 500 3 [2] 34432128 51447239 51447239 2019.09.06 06:00:03 1 0 "USDCHF" 0.10000 0.98697 0.00000 0.0000 -0.16000 500 3 [3] 34432450 51447565 51447565 2019.09.06 07:00:00 0 0 "EURUSD" 0.10000 1.10348 0.00000 0.0000 -0.18000 400 3 [4] 34432456 51447571 51447571 2019.09.06 07:00:00 1 0 "AUDUSD" 0.10000 0.68203 0.00000 0.0000 -0.11000 400 3 [5] 34432879 51448053 51448053 2019.09.06 08:00:00 1 0 "USDCHF" 0.10000 0.98701 0.00000 0.0000 -0.16000 600 3 [6] 34432888 51448064 51448064 2019.09.06 08:00:00 0 0 "USDJPY" 0.10000 106.96200 0.00000 0.0000 -0.16000 600 3 [7] 34435147 51450470 51450470 2019.09.06 10:30:00 1 0 "EURUSD" 0.10000 1.10399 0.00000 0.0000 -0.18000 100 3 [8] 34435152 51450476 51450476 2019.09.06 10:30:00 0 0 "GBPUSD" 0.10000 1.23038 0.00000 0.0000 -0.20000 100 3 [9] 34435154 51450479 51450479 2019.09.06 10:30:00 1 0 "EURJPY" 0.10000 118.12000 0.00000 0.0000 -0.18000 200 3 The first 10 trades: [time_in] [ticket] [type] [volume] [symbol] [price_in] [time_out] [price_out] [commission] [swap] [profit] [0] 2019.09.06 06:00:03 51447238 0 0.10000 "USDCAD" 1.32320 2019.09.06 18:00:00 1.31761 -0.32000 0.00000 -42.43000 [1] 2019.09.06 06:00:03 51447239 1 0.10000 "USDCHF" 0.98697 2019.09.06 18:00:00 0.98641 -0.32000 0.00000 5.68000 [2] 2019.09.06 07:00:00 51447565 0 0.10000 "EURUSD" 1.10348 2019.09.09 03:30:00 1.10217 -0.36000 -1.31000 -13.10000 [3] 2019.09.06 07:00:00 51447571 1 0.10000 "AUDUSD" 0.68203 2019.09.09 03:30:00 0.68419 -0.22000 0.03000 -21.60000 [4] 2019.09.06 08:00:00 51448053 1 0.10000 "USDCHF" 0.98701 2019.09.06 18:00:01 0.98640 -0.32000 0.00000 6.18000 [5] 2019.09.06 08:00:00 51448064 0 0.10000 "USDJPY" 106.96200 2019.09.06 18:00:01 106.77000 -0.32000 0.00000 -17.98000 [6] 2019.09.06 10:30:00 51450470 1 0.10000 "EURUSD" 1.10399 2019.09.06 14:30:00 1.10242 -0.36000 0.00000 15.70000 [7] 2019.09.06 10:30:00 51450476 0 0.10000 "GBPUSD" 1.23038 2019.09.06 14:30:00 1.23040 -0.40000 0.00000 0.20000 [8] 2019.09.06 10:30:00 51450479 1 0.10000 "EURJPY" 118.12000 2019.09.06 14:30:00 117.94100 -0.36000 0.00000 16.73000 [9] 2019.09.06 10:30:00 51450480 0 0.10000 "GBPJPY" 131.65300 2019.09.06 14:30:01 131.62500 -0.40000 0.00000 -2.62000 Filling the TRADES table took 12.51 milliseconds
Bu komut dosyasını siz de hedging hesabınızda çalıştırın ve sonuçları işlem geçmişindeki pozisyonlarla karşılaştırın. Böyle bir sonucu elde etmek için döngüler kodlama konusunda yeterli bilgiye veya zamana sahip olmayabilirsiniz. Dolayısıyla artık bunu tek bir SQL sorgusu ile yapabilirsiniz. Komut dosyasının sonucunu MetaEditor'da görüntüleyebilirsiniz - bunun için ekteki trades.sqlite dosyasını açın.
Stratejilere göre portföy analizi
Yukarıda gösterilen DatabasePrepare komut dosyasının sonuçları, ticaretin birden fazla döviz çiftinde gerçekleştirildiğini göstermektedir. Ek olarak, [magic] sütununda da 100 ile 600 arasında değerler görmekteyiz. Bu, işlem hesabının, ticaret işlemlerini tanımlamak için her birinin kendi sihirli sayısına sahip olduğu birkaç strateji tarafından yönetildiği anlamına gelir.
Bir SQL sorgusu yardımıyla ticareti sihirli sayılar açısından analiz edebiliriz:
//--- get trading statistics for Expert Advisors by Magic Number request=DatabasePrepare(db, "SELECT r.*," " (case when r.trades != 0 then (r.gross_profit+r.gross_loss)/r.trades else 0 end) as expected_payoff," " (case when r.trades != 0 then r.win_trades*100.0/r.trades else 0 end) as win_percent," " (case when r.trades != 0 then r.loss_trades*100.0/r.trades else 0 end) as loss_percent," " r.gross_profit/r.win_trades as average_profit," " r.gross_loss/r.loss_trades as average_loss," " (case when r.gross_loss!=0.0 then r.gross_profit/(-r.gross_loss) else 0 end) as profit_factor " "FROM " " (" " SELECT MAGIC," " sum(case when entry =1 then 1 else 0 end) as trades," " sum(case when profit > 0 then profit else 0 end) as gross_profit," " sum(case when profit < 0 then profit else 0 end) as gross_loss," " sum(swap) as total_swap," " sum(commission) as total_commission," " sum(profit) as total_profit," " sum(profit+swap+commission) as net_profit," " sum(case when profit > 0 then 1 else 0 end) as win_trades," " sum(case when profit < 0 then 1 else 0 end) as loss_trades " " FROM DEALS " " WHERE SYMBOL <> '' and SYMBOL is not NULL " " GROUP BY MAGIC" " ) as r");
Sonuç:
Trade statistics by Magic Number [magic] [trades] [gross_profit] [gross_loss] [total_commission] [total_swap] [total_profit] [net_profit] [win_trades] [loss_trades] [expected_payoff] [win_percent] [loss_percent] [average_profit] [average_loss] [profit_factor] [0] 100 242 2584.80000 -2110.00000 -33.36000 -93.53000 474.80000 347.91000 143 99 1.96198 59.09091 40.90909 18.07552 -21.31313 1.22502 [1] 200 254 3021.92000 -2834.50000 -29.45000 -98.22000 187.42000 59.75000 140 114 0.73787 55.11811 44.88189 21.58514 -24.86404 1.06612 [2] 300 250 2489.08000 -2381.57000 -34.37000 -96.58000 107.51000 -23.44000 134 116 0.43004 53.60000 46.40000 18.57522 -20.53078 1.04514 [3] 400 224 1272.50000 -1283.00000 -24.43000 -64.80000 -10.50000 -99.73000 131 93 -0.04687 58.48214 41.51786 9.71374 -13.79570 0.99182 [4] 500 198 1141.23000 -1051.91000 -27.66000 -63.36000 89.32000 -1.70000 116 82 0.45111 58.58586 41.41414 9.83819 -12.82817 1.08491 [5] 600 214 1317.10000 -1396.03000 -34.12000 -68.48000 -78.93000 -181.53000 116 98 -0.36883 54.20561 45.79439 11.35431 -14.24520 0.94346
6 stratejiden 4'ünün karlı olduğunu görüyoruz. Her strateji için birçok istatistiksel değer de elde ettik:
- trades - stratejinin gerçekleştirdiği işlem sayısı,
- gross_profit — stratejinin toplam kârı (tüm pozitif kâr değerlerinin toplamı),
- gross_loss — stratejinin toplam zararı (tüm negatif kâr değerlerinin toplamı),
- total_commission — stratejinin işlemlerine ilişkin tüm komisyonlarının toplamı,
- total_swap - stratejinin işlemlerine ilişkin tüm swapların toplamı,
- total_profit — gross_profit+gross_loss,
- net_profit — gross_profit+gross_loss+total_commission+total_swap,
- win_trades — kâr>0 olduğu işlem sayısı,
- loss_trades — kâr<0 olduğu işlem sayısı,
- expected_payoff — swaplar ve komisyonlar hariç işlem başına beklenen getiri = net_profit/trades,
- win_percent — kârla kapanan işlemlerin yüzdesi,
- loss_percent — zararla kapanan işlemlerin yüzdesi,
- average_profit — ortalama kâr = gross_profit/win_trades,
- average_loss — ortalama zarar = gross_loss/loss_trades,
- profit_factor — kâr faktörü = gross_profit/gross_loss.
Kâr ve zararın hesaplanmasına yönelik bu istatistikler, pozisyondan alınan swapları ve komisyonları dikkate almaz. Bu, net maliyetlerin görülmesine olanak sağlar. Bir stratejinin az miktarda kâr oluşturduğu, swaplar ve komisyonlar da dahil olunca genellikle kârsız olduğu ortaya çıkabilir.
İşlemlerin sembollere göre analizi
Ticareti sembollerle analiz edebiliriz. Bunu yapmak için aşağıdaki sorguyu gerçekleştireceğiz:
//--- get trading statistics per symbols int request=DatabasePrepare(db, "SELECT r.*," " (case when r.trades != 0 then (r.gross_profit+r.gross_loss)/r.trades else 0 end) as expected_payoff," " (case when r.trades != 0 then r.win_trades*100.0/r.trades else 0 end) as win_percent," " (case when r.trades != 0 then r.loss_trades*100.0/r.trades else 0 end) as loss_percent," " r.gross_profit/r.win_trades as average_profit," " r.gross_loss/r.loss_trades as average_loss," " (case when r.gross_loss!=0.0 then r.gross_profit/(-r.gross_loss) else 0 end) as profit_factor " "FROM " " (" " SELECT SYMBOL," " sum(case when entry =1 then 1 else 0 end) as trades," " sum(case when profit > 0 then profit else 0 end) as gross_profit," " sum(case when profit < 0 then profit else 0 end) as gross_loss," " sum(swap) as total_swap," " sum(commission) as total_commission," " sum(profit) as total_profit," " sum(profit+swap+commission) as net_profit," " sum(case when profit > 0 then 1 else 0 end) as win_trades," " sum(case when profit < 0 then 1 else 0 end) as loss_trades " " FROM DEALS " " WHERE SYMBOL <> '' and SYMBOL is not NULL " " GROUP BY SYMBOL" " ) as r");
Sonuç:
Trade statistics by Symbol [name] [trades] [gross_profit] [gross_loss] [total_commission] [total_swap] [total_profit] [net_profit] [win_trades] [loss_trades] [expected_payoff] [win_percent] [loss_percent] [average_profit] [average_loss] [profit_factor] [0] "AUDUSD" 112 503.20000 -568.00000 -8.83000 -24.64000 -64.80000 -98.27000 70 42 -0.57857 62.50000 37.50000 7.18857 -13.52381 0.88592 [1] "EURCHF" 125 607.71000 -956.85000 -11.77000 -45.02000 -349.14000 -405.93000 54 71 -2.79312 43.20000 56.80000 11.25389 -13.47676 0.63512 [2] "EURJPY" 127 1078.49000 -1057.83000 -10.61000 -45.76000 20.66000 -35.71000 64 63 0.16268 50.39370 49.60630 16.85141 -16.79095 1.01953 [3] "EURUSD" 233 1685.60000 -1386.80000 -41.00000 -83.76000 298.80000 174.04000 127 106 1.28240 54.50644 45.49356 13.27244 -13.08302 1.21546 [4] "GBPCHF" 125 1881.37000 -1424.72000 -22.60000 -51.56000 456.65000 382.49000 80 45 3.65320 64.00000 36.00000 23.51712 -31.66044 1.32052 [5] "GBPJPY" 127 1943.43000 -1776.67000 -18.84000 -52.46000 166.76000 95.46000 76 51 1.31307 59.84252 40.15748 25.57145 -34.83667 1.09386 [6] "GBPUSD" 121 1668.50000 -1438.20000 -7.96000 -49.93000 230.30000 172.41000 77 44 1.90331 63.63636 36.36364 21.66883 -32.68636 1.16013 [7] "USDCAD" 99 405.28000 -475.47000 -8.68000 -31.68000 -70.19000 -110.55000 51 48 -0.70899 51.51515 48.48485 7.94667 -9.90563 0.85238 [8] "USDCHF" 206 1588.32000 -1241.83000 -17.98000 -65.92000 346.49000 262.59000 131 75 1.68199 63.59223 36.40777 12.12458 -16.55773 1.27902 [9] "USDJPY" 107 464.73000 -730.64000 -35.12000 -34.24000 -265.91000 -335.27000 50 57 -2.48514 46.72897 53.27103 9.29460 -12.81825 0.63606
İstatistikler, 10 sembolden 5'inde net kârın elde edildiğini (net_profit>0), kâr faktörünün ise 10 sembolden 6'sında pozitif olduğunu (profit_factor>1) göstermektedir. Swaplar ve komisyonlar sonucunda EURJPY’nin kârsız hale geldiği görülmektedir.
İşlemlerin giriş saatlerine göre analizi
Ticaret tek bir sembol üzerinde tek bir strateji ile yapılsa bile, işlemleri piyasaya giriş saatlerine göre analiz etmek faydalı olabilir. Bu, aşağıdaki SQL sorgusu tarafından yapılır:
//--- get trading statistics by market entry hours request=DatabasePrepare(db, "SELECT r.*," " (case when r.trades != 0 then (r.gross_profit+r.gross_loss)/r.trades else 0 end) as expected_payoff," " (case when r.trades != 0 then r.win_trades*100.0/r.trades else 0 end) as win_percent," " (case when r.trades != 0 then r.loss_trades*100.0/r.trades else 0 end) as loss_percent," " r.gross_profit/r.win_trades as average_profit," " r.gross_loss/r.loss_trades as average_loss," " (case when r.gross_loss!=0.0 then r.gross_profit/(-r.gross_loss) else 0 end) as profit_factor " "FROM " " (" " SELECT HOUR_IN," " count() as trades," " sum(volume) as volume," " sum(case when profit > 0 then profit else 0 end) as gross_profit," " sum(case when profit < 0 then profit else 0 end) as gross_loss," " sum(profit) as net_profit," " sum(case when profit > 0 then 1 else 0 end) as win_trades," " sum(case when profit < 0 then 1 else 0 end) as loss_trades " " FROM TRADES " " WHERE SYMBOL <> '' and SYMBOL is not NULL " " GROUP BY HOUR_IN" " ) as r");
Sonuç:
Trade statistics by entry hour [hour_in] [trades] [volume] [gross_profit] [gross_loss] [net_profit] [win_trades] [loss_trades] [expected_payoff] [win_percent] [loss_percent] [average_profit] [average_loss] [profit_factor] [ 0] 0 50 5.00000 336.51000 -747.47000 -410.96000 21 29 -8.21920 42.00000 58.00000 16.02429 -25.77483 0.45020 [ 1] 1 20 2.00000 102.56000 -57.20000 45.36000 12 8 2.26800 60.00000 40.00000 8.54667 -7.15000 1.79301 [ 2] 2 6 0.60000 38.55000 -14.60000 23.95000 5 1 3.99167 83.33333 16.66667 7.71000 -14.60000 2.64041 [ 3] 3 38 3.80000 173.84000 -200.15000 -26.31000 22 16 -0.69237 57.89474 42.10526 7.90182 -12.50938 0.86855 [ 4] 4 60 6.00000 361.44000 -389.40000 -27.96000 27 33 -0.46600 45.00000 55.00000 13.38667 -11.80000 0.92820 [ 5] 5 32 3.20000 157.43000 -179.89000 -22.46000 20 12 -0.70187 62.50000 37.50000 7.87150 -14.99083 0.87515 [ 6] 6 18 1.80000 95.59000 -162.33000 -66.74000 11 7 -3.70778 61.11111 38.88889 8.69000 -23.19000 0.58886 [ 7] 7 14 1.40000 38.48000 -134.30000 -95.82000 9 5 -6.84429 64.28571 35.71429 4.27556 -26.86000 0.28652 [ 8] 8 42 4.20000 368.48000 -322.30000 46.18000 24 18 1.09952 57.14286 42.85714 15.35333 -17.90556 1.14328 [ 9] 9 118 11.80000 1121.62000 -875.21000 246.41000 72 46 2.08822 61.01695 38.98305 15.57806 -19.02630 1.28154 [10] 10 206 20.60000 2280.59000 -2021.80000 258.79000 115 91 1.25626 55.82524 44.17476 19.83122 -22.21758 1.12800 [11] 11 138 13.80000 1377.02000 -994.18000 382.84000 84 54 2.77420 60.86957 39.13043 16.39310 -18.41074 1.38508 [12] 12 152 15.20000 1247.56000 -1463.80000 -216.24000 84 68 -1.42263 55.26316 44.73684 14.85190 -21.52647 0.85227 [13] 13 64 6.40000 778.27000 -516.22000 262.05000 36 28 4.09453 56.25000 43.75000 21.61861 -18.43643 1.50763 [14] 14 62 6.20000 536.93000 -427.47000 109.46000 38 24 1.76548 61.29032 38.70968 14.12974 -17.81125 1.25606 [15] 15 50 5.00000 699.92000 -413.00000 286.92000 28 22 5.73840 56.00000 44.00000 24.99714 -18.77273 1.69472 [16] 16 88 8.80000 778.55000 -514.00000 264.55000 51 37 3.00625 57.95455 42.04545 15.26569 -13.89189 1.51469 [17] 17 76 7.60000 533.92000 -1019.46000 -485.54000 44 32 -6.38868 57.89474 42.10526 12.13455 -31.85813 0.52373 [18] 18 52 5.20000 237.17000 -246.78000 -9.61000 24 28 -0.18481 46.15385 53.84615 9.88208 -8.81357 0.96106 [19] 19 52 5.20000 407.67000 -150.36000 257.31000 30 22 4.94827 57.69231 42.30769 13.58900 -6.83455 2.71129 [20] 20 18 1.80000 65.92000 -89.09000 -23.17000 9 9 -1.28722 50.00000 50.00000 7.32444 -9.89889 0.73993 [21] 21 10 1.00000 41.86000 -32.38000 9.48000 7 3 0.94800 70.00000 30.00000 5.98000 -10.79333 1.29277 [22] 22 14 1.40000 45.55000 -83.72000 -38.17000 6 8 -2.72643 42.85714 57.14286 7.59167 -10.46500 0.54408 [23] 23 2 0.20000 1.20000 -1.90000 -0.70000 1 1 -0.35000 50.00000 50.00000 1.20000 -1.90000 0.63158
En fazla sayıda işlemin 9 ile 16 saatleri (dahil) arasında gerçekleştirildiği açıkça görülmektedir. Diğer saatlerdeki ticaret daha az işlem sağlamaktadır ve çoğunlukla kârsızdır. DatabaseExecute() fonksiyonuna ilişkin örnekten bu üç sorgu türünü içeren kaynak kodunun tamamını bulabilirsiniz.
DatabasePrint() ile Uzman Danışman günlüğüne zahmetsiz veri çıkışı
Önceki örneklerde, sorgu sonuçlarını görüntülemek için her girdiyi yapıya okumamız ve onları teker teker yazdırmamız gerekiyordu. Yalnızca tabloyu veya sorgu sonuçlarını görüntülemek için bir yapı oluşturmak çoğu zaman uğraştırıcı olabilir. Bu gibi durumlar için DatabasePrint() fonksiyonu eklenmiştir:
long DatabasePrint( int database, // database handle received in DatabaseOpen string table_or_sql, // a table or an SQL query uint flags // combination of flags );
Sadece mevcut tablonun değil, aynı zamanda tablo olarak görüntülenebilen sorgu sonuçlarının da kolayca yazdırılmasına olanak sağlar. Örneğin, aşağıdaki sorguyu kullanarak DEALS tablosunu görüntüleyelim:
DatabasePrint(db,"SELECT * from DEALS",0);
Sonuç (tablonun ilk 10 satırı görüntüleniyor):
#| ID ORDER_ID POSITION_ID TIME TYPE ENTRY SYMBOL VOLUME PRICE PROFIT SWAP COMMISSION MAGIC REASON ---+---------------------------------------------------------------------------------------------------------------- 1| 34429573 0 0 1567723199 2 0 0.0 0.0 2000.0 0.0 0.0 0 0 2| 34432127 51447238 51447238 1567749603 0 0 USDCAD 0.1 1.3232 0.0 0.0 -0.16 500 3 3| 34432128 51447239 51447239 1567749603 1 0 USDCHF 0.1 0.98697 0.0 0.0 -0.16 500 3 4| 34432450 51447565 51447565 1567753200 0 0 EURUSD 0.1 1.10348 0.0 0.0 -0.18 400 3 5| 34432456 51447571 51447571 1567753200 1 0 AUDUSD 0.1 0.68203 0.0 0.0 -0.11 400 3 6| 34432879 51448053 51448053 1567756800 1 0 USDCHF 0.1 0.98701 0.0 0.0 -0.16 600 3 7| 34432888 51448064 51448064 1567756800 0 0 USDJPY 0.1 106.962 0.0 0.0 -0.16 600 3 8| 34435147 51450470 51450470 1567765800 1 0 EURUSD 0.1 1.10399 0.0 0.0 -0.18 100 3 9| 34435152 51450476 51450476 1567765800 0 0 GBPUSD 0.1 1.23038 0.0 0.0 -0.2 100 3 10| 34435154 51450479 51450479 1567765800 1 0 EURJPY 0.1 118.12 0.0 0.0 -0.18 200 3
Verileri içe/dışa aktarma
Ayrıca, verileri içe/dışa aktarmayı kolaylaştırmak için de DatabaseImport() ve DatabaseExport() fonksiyonları eklenmiştir. Bu fonksiyonlar, CSV dosyaları ve ZIP arşivlerindeki verilerle çalışılmasına olanak sağlar.
DatabaseImport(), verileri belirtilen tabloya aktarır. Belirtilen ada sahip bir tablo yoksa, otomatik olarak oluşturulur. Oluşturulan tablodaki alanların adları ve türleri, dosyada bulunan verilere göre otomatik olarak tanımlanır.
DatabaseExport() ise tablonun veya sorgu sonuçlarının bir dosyaya kaydedilmesine olanak tanır. Sorgu sonuçları dışa aktarılırsa, SQL sorgusu "SELECT" veya "select" ile başlamalıdır. Başka bir deyişle, SQL sorgusu veritabanının durumunu değiştiremez, aksi takdirde DatabaseExport() başarısız olur.
MQL5 Dokümantasyonundan fonksiyonların tam açıklamasına bakabilirsiniz.
Optimizasyon sonuçlarını veritabanına kaydetme
Veritabanlarıyla çalışmak için fonksiyonlar, optimizasyon sonuçlarını işlemek için de kullanılabilir. Şimdi platformda standart olarak bulunan MACD Sample Uzman Danışmanını kullanarak framelerle test sonuçlarının nasıl alınacağını ve tüm optimizasyon kriterlerinin değerlerinin tek bir dosyaya nasıl kaydedileceğini göreceğiz. Bunu yapmak için, ticaret istatistiklerini göndermek adına OnTester() metodunu tanımladığımız CDatabaseFrames sınıfını oluşturalım:
//+------------------------------------------------------------------+ //| Tester function - sends trading statistics in a frame | //+------------------------------------------------------------------+ void CDatabaseFrames::OnTester(const double OnTesterValue) { //--- stats[] array to send data to a frame double stats[16]; //--- allocate separate variables for trade statistics to achieve more clarity int trades=(int)TesterStatistics(STAT_TRADES); double win_trades_percent=0; if(trades>0) win_trades_percent=TesterStatistics(STAT_PROFIT_TRADES)*100./trades; //--- fill in the array with test results stats[0]=trades; // number of trades stats[1]=win_trades_percent; // percentage of profitable trades stats[2]=TesterStatistics(STAT_PROFIT); // net profit stats[3]=TesterStatistics(STAT_GROSS_PROFIT); // gross profit stats[4]=TesterStatistics(STAT_GROSS_LOSS); // gross loss stats[5]=TesterStatistics(STAT_SHARPE_RATIO); // Sharpe Ratio stats[6]=TesterStatistics(STAT_PROFIT_FACTOR); // profit factor stats[7]=TesterStatistics(STAT_RECOVERY_FACTOR); // recovery factor stats[8]=TesterStatistics(STAT_EXPECTED_PAYOFF); // trade mathematical expectation stats[9]=OnTesterValue; // custom optimization criterion //--- calculate built-in standard optimization criteria double balance=AccountInfoDouble(ACCOUNT_BALANCE); double balance_plus_profitfactor=0; if(TesterStatistics(STAT_GROSS_LOSS)!=0) balance_plus_profitfactor=balance*TesterStatistics(STAT_PROFIT_FACTOR); double balance_plus_expectedpayoff=balance*TesterStatistics(STAT_EXPECTED_PAYOFF); double balance_plus_dd=balance/TesterStatistics(STAT_EQUITYDD_PERCENT); double balance_plus_recoveryfactor=balance*TesterStatistics(STAT_RECOVERY_FACTOR); double balance_plus_sharpe=balance*TesterStatistics(STAT_SHARPE_RATIO); //--- add the values of built-in optimization criteria stats[10]=balance; // Balance stats[11]=balance_plus_profitfactor; // Balance+ProfitFactor stats[12]=balance_plus_expectedpayoff; // Balance+ExpectedPayoff stats[13]=balance_plus_dd; // Balance+EquityDrawdown stats[14]=balance_plus_recoveryfactor; // Balance+RecoveryFactor stats[15]=balance_plus_sharpe; // Balance+Sharpe //--- create a data frame and send it to the terminal if(!FrameAdd(MQLInfoString(MQL_PROGRAM_NAME)+"_stats", STATS_FRAME, trades, stats)) Print("Frame add error: ", GetLastError()); else Print("Frame added, Ok"); }
Bu sınıfın ikinci önemli metodu OnTesterDeinit()'dir. Optimizasyondan sonra alınan tüm frameleri okur ve istatistikleri veritabanına kaydeder:
//+------------------------------------------------------------------+ //| TesterDeinit function - read data from frames | //+------------------------------------------------------------------+ void CDatabaseFrames::OnTesterDeinit(void) { //--- take the EA name and optimization end time string filename=MQLInfoString(MQL_PROGRAM_NAME)+" "+TimeToString(TimeCurrent())+".sqlite"; StringReplace(filename, ":", "."); // ":" character is not allowed in file names //--- open/create the database in the common terminal folder int db=DatabaseOpen(filename, DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE | DATABASE_OPEN_COMMON); if(db==INVALID_HANDLE) { Print("DB: ", filename, " open failed with code ", GetLastError()); return; } else Print("DB: ", filename, " opened successful"); //--- create the PASSES table if(!DatabaseExecute(db, "CREATE TABLE PASSES(" "PASS INT PRIMARY KEY NOT NULL," "TRADES INT," "WIN_TRADES INT," "PROFIT REAL," "GROSS_PROFIT REAL," "GROSS_LOSS REAL," "SHARPE_RATIO REAL," "PROFIT_FACTOR REAL," "RECOVERY_FACTOR REAL," "EXPECTED_PAYOFF REAL," "ON_TESTER REAL," "BL_BALANCE REAL," "BL_PROFITFACTOR REAL," "BL_EXPECTEDPAYOFF REAL," "BL_DD REAL," "BL_RECOVERYFACTOR REAL," "BL_SHARPE REAL );")) { Print("DB: ", filename, " create table failed with code ", GetLastError()); DatabaseClose(db); return; } //--- variables for reading frames string name; ulong pass; long id; double value; double stats[]; //--- move the frame pointer to the beginning FrameFirst(); FrameFilter("", STATS_FRAME); // select frames with trading statistics for further work //--- variables to get statistics from the frame int trades; double win_trades_percent; double profit, gross_profit, gross_loss; double sharpe_ratio, profit_factor, recovery_factor, expected_payoff; double ontester_value; // custom optimization criterion double balance; // Balance double balance_plus_profitfactor; // Balance+ProfitFactor double balance_plus_expectedpayoff; // Balance+ExpectedPayoff double balance_plus_dd; // Balance+EquityDrawdown double balance_plus_recoveryfactor; // Balance+RecoveryFactor double balance_plus_sharpe; // Balance+Sharpe //--- block the database for the period of bulk transactions DatabaseTransactionBegin(db); //--- go through frames and read data from them bool failed=false; while(FrameNext(pass, name, id, value, stats)) { Print("Got pass #", pass); trades=(int)stats[0]; win_trades_percent=stats[1]; profit=stats[2]; gross_profit=stats[3]; gross_loss=stats[4]; sharpe_ratio=stats[5]; profit_factor=stats[6]; recovery_factor=stats[7]; expected_payoff=stats[8]; stats[9]; balance=stats[10]; balance_plus_profitfactor=stats[11]; balance_plus_expectedpayoff=stats[12]; balance_plus_dd=stats[13]; balance_plus_recoveryfactor=stats[14]; balance_plus_sharpe=stats[15]; PrintFormat("VALUES (%d,%d,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%G,%.2f,%.2f,%2.f,%.2f,%.2f,%.2f,%.2f)", pass, trades, win_trades_percent, profit, gross_profit, gross_loss, sharpe_ratio, profit_factor, recovery_factor, expected_payoff, ontester_value, balance, balance_plus_profitfactor, balance_plus_expectedpayoff, balance_plus_dd, balance_plus_recoveryfactor, balance_plus_sharpe); //--- write data to the table string request=StringFormat("INSERT INTO PASSES (PASS,TRADES,WIN_TRADES, PROFIT,GROSS_PROFIT,GROSS_LOSS," "SHARPE_RATIO,PROFIT_FACTOR,RECOVERY_FACTOR,EXPECTED_PAYOFF,ON_TESTER," "BL_BALANCE,BL_PROFITFACTOR,BL_EXPECTEDPAYOFF,BL_DD,BL_RECOVERYFACTOR,BL_SHARPE) " "VALUES (%d, %d, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, %G, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f)", pass, trades, win_trades_percent, profit, gross_profit, gross_loss, sharpe_ratio, profit_factor, recovery_factor, expected_payoff, ontester_value, balance, balance_plus_profitfactor, balance_plus_expectedpayoff, balance_plus_dd, balance_plus_recoveryfactor, balance_plus_sharpe); //--- execute a query to add a pass to the PASSES table if(!DatabaseExecute(db, request)) { PrintFormat("Failed to insert pass %d with code %d", pass, GetLastError()); failed=true; break; } } //--- if an error occurred during a transaction, inform of that and complete the work if(failed) { Print("Transaction failed, error code=", GetLastError()); DatabaseTransactionRollback(db); DatabaseClose(db); return; } else { DatabaseTransactionCommit(db); Print("Transaction done successful"); } //--- close the database if(db!=INVALID_HANDLE) { Print("Close database with handle=", db); DatabaseClose(db); }
Ardından, MACD Sample Uzman Danışmanında DatabaseFrames.mqh dosyasını dahil edelim ve CDatabaseFrames sınıf değişkenini bildirelim:
#define MACD_MAGIC 1234502 //--- #include <Trade\Trade.mqh> #include <Trade\SymbolInfo.mqh> #include <Trade\PositionInfo.mqh> #include <Trade\AccountInfo.mqh> #include "DatabaseFrames.mqh" ... CDatabaseFrames DB_Frames;
Sonrasında da Uzman Danışmanın sonuna yalnızca optimizasyon sırasında çağrılacak üç fonksiyon ekliyoruz:
//+------------------------------------------------------------------+ //| TesterInit function | //+------------------------------------------------------------------+ int OnTesterInit() { return(DB_Frames.OnTesterInit()); } //+------------------------------------------------------------------+ //| TesterDeinit function | //+------------------------------------------------------------------+ void OnTesterDeinit() { DB_Frames.OnTesterDeinit(); } //+------------------------------------------------------------------+ //| Tester function | //+------------------------------------------------------------------+ double OnTester() { double ret=0; //--- create a custom optimization criterion as the ratio of a net profit to a relative balance drawdown if(TesterStatistics(STAT_BALANCE_DDREL_PERCENT)!=0) ret=TesterStatistics(STAT_PROFIT)/TesterStatistics(STAT_BALANCE_DDREL_PERCENT); DB_Frames.OnTester(ret); return(ret); } //+------------------------------------------------------------------+
Optimizasyonu başlatıyoruz ve ortak terminal klasöründe ticaret istatistiklerini içerecek bir veritabanı dosyası oluşturuyoruz:
CDatabaseFrames::OnTesterInit: optimization launched at 15:53:27
DB: MACD Sample Database 2020.01.20 15.53.sqlite opened successful
Transaction done successful
Close database with handle=65537
Database stored in file 'MACD Sample Database 2020.01.20 15.53.sqlite'
Oluşturulan veritabanı dosyası ileri çalışma için MetaEditor'da veya başka bir MQL5 uygulamasında açılabilir.
Böylece, herhangi bir veriyi istediğiniz biçimde hazırlayarak ileri analiz veya diğer yatırımcılarla paylaşmak amacıyla kullanabilirsiniz. Kaynak kodunu, optimizasyon parametrelerini içeren ini dosyasını ve yürütme sonucunu aşağıda ekli MACD.zip arşivinde bulabilirsiniz.
İndeksleri kullanarak sorgu yürütmeyi optimize etme
SQL kullanmanın güzelliği (sadece SQLite’ta değil, tüm uygulamalarında), yordamsal bir dil değil, bildirimsel bir dil olmasıdır. SQL'de programlama yaparken, sisteme NASIL hesaplaması gerektiğini değil NE hesaplamak istediğinizi söylersiniz. “Nasıl” sorusunu çözme görevi, SQL veritabanı motorundaki sorgu planlayıcı alt sistemine devredilir.
Belirli bir SQL sorgusunu yürütmek için yüzlerce veya binlerce farklı algoritma olabilir. Bazıları diğerlerinden daha hızlı çalışsa da, bu algoritmaların tümü doğru cevabı verecektir. SQLite'taki sorgu planlayıcı, her SQL ifadesi için en hızlı ve en verimli algoritmayı seçmeye çalışır.
SQLite'taki sorgu planlayıcı çoğu zaman doğru algoritmayı seçme konusunda iyi bir iş çıkarır. Ancak, sorgu planlayıcının elinden gelenin en iyisini yapması için indekslere ihtiyacı vardır. Bu indeksler genellikle programcılar tarafından eklenmelidir. Ancak, bazen sorgu planlayıcı, optimal olmayan bir algoritma seçecektir. Bu durumlarda, programcılar, sorgu planlayıcının daha iyi bir iş çıkarmasına yardımcı olmak adına ek ipuçları sağlamak isteyebilir.
İndeksler olmadan arama
Belirtilen 14 alanı içeren DEALS tablosuna sahip olduğumuzu varsayalım. Aşağıda bu tablonun ilk 10 girdisi bulunmaktadır.
rowid | ID | ORDER_ID | POSITION_ID | TIME | TYPE | ENTRY | SYMBOL | VOLUME | PRICE | PROFIT | SWAP | COMMISSION | MAGIC | REASON |
1 | 34429573 | 0 | 0 | 1567723199 | 2 | 0 | 0 | 0 | 2000 | 0 | 0 | 0 | 0 | |
2 | 34432127 | 51447238 | 51447238 | 1567749603 | 0 | 0 | USDCAD | 0.1 | 1.3232 | 0 | 0 | 0.16 | 500 | 3 |
3 | 34432128 | 51447239 | 51447239 | 1567749603 | 1 | 0 | USDCHF | 0.1 | 0.98697 | 0 | 0 | 0.16 | 500 | 3 |
4 | 34432450 | 51447565 | 51447565 | 1567753200 | 0 | 0 | EURUSD | 0.1 | 1.10348 | 0 | 0 | 0.18 | 400 | 3 |
5 | 34432456 | 51447571 | 51447571 | 1567753200 | 1 | 0 | AUDUSD | 0.1 | 0.68203 | 0 | 0 | 0.11 | 400 | 3 |
6 | 34432879 | 51448053 | 51448053 | 1567756800 | 1 | 0 | USDCHF | 0.1 | 0.98701 | 0 | 0 | 0.16 | 600 | 3 |
7 | 34432888 | 51448064 | 51448064 | 1567756800 | 0 | 0 | USDJPY | 0.1 | 106.962 | 0 | 0 | 0.16 | 600 | 3 |
8 | 34435147 | 51450470 | 51450470 | 1567765800 | 1 | 0 | EURUSD | 0.1 | 1.10399 | 0 | 0 | 0.18 | 100 | 3 |
9 | 34435152 | 51450476 | 51450476 | 1567765800 | 0 | 0 | GBPUSD | 0.1 | 1.23038 | 0 | 0 | 0.2 | 100 | 3 |
10 | 34435154 | 51450479 | 51450479 | 1567765800 | 1 | 0 | EURJPY | 0.1 | 118.12 | 0 | 0 | 0.18 | 200 | 3 |
Tablo işlem geçmişinin analizi için işlem özellikleri bölümünden (DEAL_TIME_MSC, DEAL_COMMENT ve DEAL_EXTERNAL_ID hariç) gerekli verileri içermektedir. Her tablo her zaman ilk olarak int tipi rowid anahtarını ve devamında girdi alanlarını içerir. rowid anahtarları otomatik olarak oluşturulurlar, tablo içerisinde benzersizdirler ve yeni girdiler eklendikçe artarlar. Girdilerin silinmesi numaralandırma boşluklarına neden olabilir, ancak tablo satırları her zaman artan rowid düzeninde depolanır.
Belirli bir pozisyonla ilgili işlemleri bulmamız gerekirse, ID=51447571 için, aşağıdaki sorguyu yazmalıyız:
SELECT * FROM deals WHERE position_id=51447571
Bu durumda, tam bir tablo taraması gerçekleştirilir - tüm satırlar taranır ve POSITION_ID alanının her satırda 51447571 değerine eşit olup olmadığı kontrol edilir. Bu koşulu sağlayan satırlar, sorgu sonuçlarında görüntülenecektir. Tablo milyonlarca girdi içeriyorsa, arama önemli miktarda uzun sürebilir. Aramayı position_id=51447571 yerine rowid=5 koşuluna göre yapsaydık, arama süresi (tablonun boyutuna bağlı olarak) binlerce hatta milyonlarca kez azalabilirdi.
SELECT * FROM deals WHERE rowid=5
rowid=5 olan satır position_id=51447571 olduğundan, sorguyu yürütmenin sonucu aynı olacaktır. rowid değerlerinin artan şekilde sıralanması ve sonucu almak için ikili arama kullanılması nedeniyle hızlanma sağlanır. Ancak ne yazık ki rowid değeriyle arama yapmak bizim için uygun değildir, çünkü biz istediğimiz position_id değerine sahip girdilerle ilgileniyoruz.
İndekse göre arama
Sorgunun yürütülmesini daha verimli hale getirmek için, aşağıdaki sorguyu kullanarak POSITION_ID alanı için bir indeks eklememiz gerekir:
CREATE INDEX Idx1 ON deals(position_id)
Bu durumda, iki sütunlu ayrı bir tablo oluşturulur. İlk sütun artan şekilde sıralanmış POSITION_ID değerlerinden, ikinci sütun ise rowid değerlerinden oluşur.
POSITION_ID | rowid |
0 | 1 |
51447238 | 2 |
51447239 | 3 |
51447565 | 4 |
51447571 | 5 |
51448053 | 6 |
51448064 | 7 |
51450470 | 8 |
51450476 | 9 |
51450479 | 10 |
rowid’nin sırası, örneğimizde korunmuş olmasına rağmen, sizin durumunuzda halihazırda ihlal edilmiş olabilir. Çünkü zamanla pozisyonlar açıldıkça POSITION_ID de artacaktır.
Artık POSITION_ID alanı için indekse sahip olduğumuza göre, sorgumuz
SELECT * FROM deals WHERE position_id=51447571
farklı çalışacaktır. İlk olarak, POSITION_ID sütunundaki Idx1 indeksinde ikili arama yapılır ve koşulla eşleşen tüm rowid’ler bulunur. Ardından, orijinal DEALS tablosunda ikinci ikili arama yapılır. Bu arama da bilinen rowid değerlerine göre tüm girdileri arar. Böylece, büyük tablonun tek tam taraması iki ardışık arama ile değiştirilmiş olur - önce indekse, ardından da tablo satır numaralarına göre. Bu, tabloda çok sayıda satır olması durumunda, bu tür sorguların yürütme süresinin binlerce kez azaltılmasına olanak tanır.
Genel kural: Tablo alanlarından bazıları genellikle arama/karşılaştırma/sıralama için kullanılıyorsa, bu alanlar için indeksler oluşturulması önerilir.
DEALS tablosu ayrıca SYMBOL, MAGIC ve ENTRY alanlarını da içermektedir. Bu alanlar üzerinde seçim yapılması gerekiyorsa, onlar için uygun indeksler oluşturulması mantıklıdır. Örneğin:
CREATE INDEX Idx2 ON deals(symbol) CREATE INDEX Idx3 ON deals(magic) CREATE INDEX Idx4 ON deals(entry)
İndeksler oluşturmanın ek bellek gerektirdiği ve her girdi ekleme/silme ile yeniden indeksleme gerçekleştirildiği akılda tutulmalıdır. Ayrıca, birden çok alana dayalı çoklu indeksler de oluşturulabilir. Örneğin, USDCAD sembolünde MAGIC=500 olan Uzman Danışman tarafından gerçekleştirilen tüm işlemleri seçmek istiyorsak aşağıdaki sorguyu oluşturabiliriz:
SELECT * FROM deals WHERE magic=500 AND symbol='USDCAD'
Bu durumda MAGIC ve SYMBOL alanları için çoklu indeks oluşturabiliriz:
CREATE INDEX Idx5 ON deals(magic, symbol)
Böylece, aşağıdaki indeks tablosu oluşturulur (şematik olarak ilk 10 satır gösterilmiştir):
MAGIC | SYMBOL | rowid |
100 | EURUSD | 4 |
100 | EURUSD | 10 |
100 | EURUSD | 20 |
100 | GBPUSD | 5 |
100 | GBPUSD | 11 |
200 | EURJPY | 6 |
200 | EURJPY | 12 |
200 | EURJPY | 22 |
200 | GBPJPY | 7 |
200 | GBPJPY | 13 |
Oluşturulan çoklu indekste girdiler önce MAGIC alanına göre, ardından da SYMBOL alanına göre bloklar halinde sıralanır. Dolayısıyla, AND sorguları varlığında, indekste arama ilk olarak MAGIC sütununda gerçekleştirilecek, sonrasında ise SYMBOL sütunu kontrol edilecektir. Her iki koşul da karşılanırsa, rowid, orijinal tablo aramasında kullanılacak sonuç kümesine eklenir. Genel olarak konuşursak, böyle bir çoklu indeks, önce SYMBOL alanının, ardından MAGIC alanının kontrol edildiği sorgular için uygun değildir:
SELECT * FROM deals WHERE symbol='USDCAD' AND magic=500
Her ne kadar sorgu planlayıcı bu gibi durumlarda nasıl doğru hareket edeceğini anlayıp aramayı doğru sırada yapsa da, tablo ve sorgu tasarımındaki hataları her zaman otomatik olarak düzelteceğini ummak akıllıca olmaz.
OR sorguları
Çoklu indeksler yalnızca AND sorguları için uygundur. Örneğin, MAGIC=100 olan Uzman Danışman tarafından veya EURUSD üzerinde gerçekleştirilen tüm işlemleri bulmak istediğimizi varsayalım:
SELECT * FROM deals WHERE magic=100 OR symbol='EURUSD'
Bu durumda, iki ayrı arama yapılır. Ardından bulunan rowid’ler, orijinal tablodaki satır numaralarına göre son arama için ortak bir seçimde birleştirilir.
SELECT * FROM deals WHERE magic=100 SELECT * FROM deals WHERE symbol='EURUSD'
Burada da OR sorgusunun her iki alanının da indekse sahip olması gerekir, aksi takdirde arama tam tablo taramasına neden olur.
Sıralama
Sıralamayı hızlandırmak adına, sorgu sonuçlarının sıralanacağı alanlar için de indekslerin var olması akıllıcadır. Örneğin, tüm EURUSD işlemlerini işlem zamanına göre sıralanmış olarak seçmemiz gerektiğini varsayalım:
SELECT * FROM deals symbol='EURUSD' ORDER BY time
Bu durumda, TIME alanı için bir indeks oluşturmalıyız. İndeks ihtiyacı tablonun boyutuna bağlıdır. Tabloda girdi sayısı azsa, indeksleme gözle görülür bir zaman kazandırmayacaktır.
Burada sorgu optimizasyonunun yalnızca temellerini ele aldık. Daha iyi anlamak adına SQLite geliştiricilerinin web sitesindeki Query Planning bölümünden başlayarak bu konuyu incelemenizi öneririz.
Veritabanlarıyla çalışmanın MetaEditor'a entegrasyonu
MetaTrader 5 platformu sürekli geliştirilmektedir. MQL5 diline SQL sorguları için yerel destek ekledik. Veritabanı oluşturma, veriler ekleme ve silme ve toplu işlemler gerçekleştirme dahil olmak üzere veritabanlarıyla çalışmak için MetaEditor'a yeni işlevsellikler entegre ettik. Veritabanı oluşturma, MQL5 Sihirbazı kullanılarak standart bir şekilde gerçekleştirilir. Dosya ve tablo adını belirtmeniz ve gerekli tüm alanları türleriyle birlikte eklemeniz yeterlidir.
Ardından, tabloyu verilerle doldurabilir, arama ve seçim yapabilir, SQL sorgularını tanıtabilirsiniz... Böylece veritabanlarıyla yalnızca MQL5 programlarından değil manuel olarak da çalışabilirsiniz. Bunun için üçüncü taraf uygulamalara başvurmanıza gerek yoktur.
SQLite'ın MetaTrader'a girişi, yatırımcılara büyük miktarda veriyi hem programlı hem de manuel olarak işlemek için yeni fırsatlar sunmaktadır. Ayrıca, bu işlevselliklerin kullanımının mümkün olduğu kadar pratik hale gelmesine ve hız açısından diğer çözümlerden daha düşük olmamasını sağlamak adına elimizden gelenin en iyisini yaptık. SQL sorgu dilini öğrenin ve onu çalışmalarınızda uygulayın.
MetaQuotes Ltd tarafından Rusçadan çevrilmiştir.
Orijinal makale: https://www.mql5.com/ru/articles/7463





- Ücretsiz alım-satım uygulamaları
- İşlem kopyalama için 8.000'den fazla sinyal
- Finansal piyasaları keşfetmek için ekonomik haberler
Gizlilik ve Veri Koruma Politikasını ve MQL5.com Kullanım Şartlarını kabul edersiniz