MQL5 Cookbook: 파일에 거래 내역 쓰기 및 Excel의 각 기호에 대한 대차 대조표 생성
소개
다양한 포럼에서 커뮤니케이션할 때 Microsoft Excel 차트의 스크린샷으로 표시되는 테스트 결과의 예를 자주 사용했습니다. 그러한 차트를 만드는 방법을 설명하라는 요청을 여러 번 받았습니다. Excel은 차트를 만들기 위한 충분한 기능을 제공하며 해당 주제에 대한 많은 책이 있습니다. 책에서 필요한 정보를 찾으려면 모두 읽어야 할 수도 있습니다. 이제 마지막으로 이 글에서 모든 것을 설명할 시간이 있습니다.
이전 두 글 MQL5 Cookbook: Multi-Currency Expert Advisor - Simple, Neat and Quick Approach 및 MQL5 Cookbook: 무제한 매개변수로 Multi-Currency Expert Advisor 개발 MQL5에서 다중 통화 EA의 개발을 다루었습니다. MetaTrader 5의 테스트 결과가 일반 균형/자본 곡선으로 표시된다는 것을 알고 있습니다. 즉, 각 기호에 대한 결과를 개별적으로 확인해야 하는 경우 Expert Advisor의 외부 매개변수로 계속 이동하여 모든 기호를 비활성화해야 합니다. 결과가 필요한 사람을 제외하고 테스트를 다시 실행하세요. 이것은 불편합니다.
그래서 오늘은 클릭 몇 번으로 단일 Excel 다이어그램에서 다중 통화 Expert Advisor의 누적 결과와 함께 모든 기호에 대한 대차 대조표를 얻을 수 있는 간단한 방법을 보여 드리겠습니다. 예를 재구성하기 위해 이전 글에서 다중 통화 Expert Advisor를 사용합니다. 테스트 완료 시 .csv 파일에 모든 기호에 대한 거래 내역 및 균형 곡선을 기록하는 기능으로 향상됩니다. 또한 보고서에 다른 열을 추가하여 모든 로컬 최대값의 감소를 표시합니다.
데이터 파일을 연결할 수 있도록 설정한 엑셀 북을 만들어 봅시다. 책은 항상 열 수 있으므로 다른 테스트를 실행하기 전에 책을 닫을 필요가 없습니다. 테스트가 완료되면 특정 키를 눌러 데이터를 새로 고치기만 하면 보고서와 차트에서 변경 사항을 볼 수 있습니다.
Expert Advisor 개발
EA에는 큰 변화가 없으며 몇 가지 기능만 추가할 것입니다. 기본 파일에 기호 균형에 대한 구조와 배열을 추가하는 것으로 시작하겠습니다.
//--- Arrays for balances struct Balance { double balance[]; }; //--- Array of balances for all symbols Balance symbol_balance[];
그런 다음 테스트 보고서를 생성하고 Expert Advisor의 기본 파일에 포함하는 별도의 Report.mqh 포함 파일을 만듭니다(아래 코드에서 강조 표시된 줄 참조).
//--- Include custom libraries #include "Include/Auxiliary.mqh" #include "Include/Enums.mqh" #include "Include/Errors.mqh" #include "Include/FileFunctions.mqh" #include "Include/InitializeArrays.mqh" #include "Include/Report.mqh" #include "Include/ToString.mqh" #include "Include/TradeFunctions.mqh" #include "Include/TradeSignals.mqh"
포지션 및 기호 속성에 대해 프로젝트에 이미 있는 것과 같은 거래 속성 구조를 먼저 생성해 보겠습니다. 이를 위해 속성 식별자 열거를 Enums.mqh 파일에 추가합니다.
//+------------------------------------------------------------------+ //| Enumeration of deal properties | //+------------------------------------------------------------------+ enum ENUM_DEAL_PROPERTIES { D_SYMBOL = 0, // Deal symbol D_COMMENT = 1, // Deal comment D_TYPE = 2, // Deal type D_ENTRY = 3, // Deal entry - entry in, entry out, reverse D_PRICE = 4, // Deal price D_PROFIT = 5, // Deal result (profit/loss) D_VOLUME = 6, // Deal volume D_SWAP = 7, // Cumulative swap on close D_COMMISSION = 8, // Deal commission D_TIME = 9, // Deal time D_ALL = 10 // All of the above mentioned deal properties };
또한 Report.mqh 파일 에서 거래 속성 구조와 거래 속성을 반환하는 GetHistoryDealProperties() 함수를 만듭니다. 이 함수는 거래 티켓과 속성 식별자라는 두 가지 매개변수를 허용합니다.
아래에서 구조 및 GetHistoryDealProperties() 함수의 코드를 볼 수 있습니다.
//--- Deal properties in the history struct HistoryDealProperties { string symbol; // Symbol string comment; // Comment ENUM_DEAL_TYPE type; // Deal type ENUM_DEAL_ENTRY entry; // Direction double price; // Price double profit; // Profit/Loss double volume; // Volume double swap; // Swap double commission; // Commission datetime time; // Time }; //--- Variable of deal properties HistoryDealProperties deal; //+------------------------------------------------------------------+ //| Gets deal properties by ticket | //+------------------------------------------------------------------+ void GetHistoryDealProperties(ulong ticket_number,ENUM_DEAL_PROPERTIES history_deal_property) { switch(history_deal_property) { case D_SYMBOL : deal.symbol=HistoryDealGetString(ticket_number,DEAL_SYMBOL); break; case D_COMMENT : deal.comment=HistoryDealGetString(ticket_number,DEAL_COMMENT); break; case D_TYPE : deal.type=(ENUM_DEAL_TYPE)HistoryDealGetInteger(ticket_number,DEAL_TYPE); break; case D_ENTRY : deal.entry=(ENUM_DEAL_ENTRY)HistoryDealGetInteger(ticket_number,DEAL_ENTRY); break; case D_PRICE : deal.price=HistoryDealGetDouble(ticket_number,DEAL_PRICE); break; case D_PROFIT : deal.profit=HistoryDealGetDouble(ticket_number,DEAL_PROFIT); break; case D_VOLUME : deal.volume=HistoryDealGetDouble(ticket_number,DEAL_VOLUME); break; case D_SWAP : deal.swap=HistoryDealGetDouble(ticket_number,DEAL_SWAP); break; case D_COMMISSION : deal.commission=HistoryDealGetDouble(ticket_number,DEAL_COMMISSION); break; case D_TIME : deal.time=(datetime)HistoryDealGetInteger(ticket_number,DEAL_TIME); break; case D_ALL : deal.symbol=HistoryDealGetString(ticket_number,DEAL_SYMBOL); deal.comment=HistoryDealGetString(ticket_number,DEAL_COMMENT); deal.type=(ENUM_DEAL_TYPE)HistoryDealGetInteger(ticket_number,DEAL_TYPE); deal.entry=(ENUM_DEAL_ENTRY)HistoryDealGetInteger(ticket_number,DEAL_ENTRY); deal.price=HistoryDealGetDouble(ticket_number,DEAL_PRICE); deal.profit=HistoryDealGetDouble(ticket_number,DEAL_PROFIT); deal.volume=HistoryDealGetDouble(ticket_number,DEAL_VOLUME); deal.swap=HistoryDealGetDouble(ticket_number,DEAL_SWAP); deal.commission=HistoryDealGetDouble(ticket_number,DEAL_COMMISSION); deal.time=(datetime)HistoryDealGetInteger(ticket_number,DEAL_TIME); break; //--- default: Print("The passed deal property is not listed in the enumeration!"); return; } }
또한 일부 거래 속성을 문자열 값으로 변환하는 여러 함수가 필요합니다. 이러한 단순 함수는 전달된 값이 비어 있거나 0이면 대시("-")를 반환합니다. ToString.mqh 파일에 작성해 보겠습니다.
//+------------------------------------------------------------------+ //| Returns the symbol name, otherwise - dash | //+------------------------------------------------------------------+ string DealSymbolToString(string deal_symbol) { return(deal_symbol=="" ? "-" : deal_symbol); } //+------------------------------------------------------------------+ //| Converts deal type to string | //+------------------------------------------------------------------+ string DealTypeToString(ENUM_DEAL_TYPE deal_type) { string str=""; //--- switch(deal_type) { case DEAL_TYPE_BUY : str="buy"; break; case DEAL_TYPE_SELL : str="sell"; break; case DEAL_TYPE_BALANCE : str="balance"; break; case DEAL_TYPE_CREDIT : str="credit"; break; case DEAL_TYPE_CHARGE : str="charge"; break; case DEAL_TYPE_CORRECTION : str="correction"; break; case DEAL_TYPE_BONUS : str="bonus"; break; case DEAL_TYPE_COMMISSION : str="commission"; break; case DEAL_TYPE_COMMISSION_DAILY : str="commission daily"; break; case DEAL_TYPE_COMMISSION_MONTHLY : str="commission monthly"; break; case DEAL_TYPE_COMMISSION_AGENT_DAILY : str="commission agent daily"; break; case DEAL_TYPE_COMMISSION_AGENT_MONTHLY : str="commission agent monthly"; break; case DEAL_TYPE_INTEREST : str="interest"; break; case DEAL_TYPE_BUY_CANCELED : str="buy canceled"; break; case DEAL_TYPE_SELL_CANCELED : str="sell canceled"; break; //--- Unknown deal type default : str="unknown"; } //--- return(str); } //+------------------------------------------------------------------+ //| Converts direction of deal to string | //+------------------------------------------------------------------+ string DealEntryToString(ENUM_DEAL_ENTRY deal_entry) { string str=""; //--- switch(deal_entry) { case DEAL_ENTRY_IN : str="in"; break; case DEAL_ENTRY_OUT : str="out"; break; case DEAL_ENTRY_INOUT : str="in/out"; break; case DEAL_ENTRY_STATE : str="status record"; break; //--- Unknown direction type default : str="unknown"; } //--- return(str); } //+------------------------------------------------------------------+ //| Converts volume to string | //+------------------------------------------------------------------+ string DealVolumeToString(double deal_volume) { return(deal_volume<=0 ? "-" : DoubleToString(deal_volume,2)); } //+------------------------------------------------------------------+ //| Converts price to string | //+------------------------------------------------------------------+ string DealPriceToString(double deal_price,int digits) { return(deal_price<=0 ? "-" : DoubleToString(deal_price,digits)); } //+------------------------------------------------------------------+ //| Converts deal result to string | //+------------------------------------------------------------------+ string DealProfitToString(string deal_symbol,double deal_profit) { return((deal_profit==0 || deal_symbol=="") ? "-" : DoubleToString(deal_profit,2)); } //+------------------------------------------------------------------+ //| Converts swap to string | //+------------------------------------------------------------------+ string DealSwapToString(double deal_swap) { return(deal_swap<=0 ? "-" : DoubleToString(deal_swap,2)); }
이제 보고서용 데이터를 준비하고 LastTest.csv 파일에 쓰는 CreateSymbolBalanceReport() 함수를 작성할 준비가 되었습니다. 매우 간단합니다. 먼저 헤더를 작성한 다음(테스트가 둘 이상의 기호에 대해 실행된 경우 문자열이 조정되는 방식에 유의) 보고서에 필요한 거래 속성이 파일에 추가로 기록되는 문자열로 연속적으로 연결됩니다.
다음은 CreateSymbolBalanceReport() 함수의 코드입니다.
//+------------------------------------------------------------------+ //| Creates the test report on deals in .csv format | //+------------------------------------------------------------------+ void CreateSymbolBalanceReport() { int file_handle =INVALID_HANDLE; // File handle string path =""; // File path //--- If an error occurred when creating/getting the folder, exit if((path=CreateInputParametersFolder())=="") return; //--- Create file to write data in the common folder of the terminal file_handle=FileOpen(path+"\\LastTest.csv",FILE_CSV|FILE_WRITE|FILE_ANSI|FILE_COMMON); //--- If the handle is valid (file created/opened) if(file_handle>0) { int digits =0; // Number of decimal places in the price int deals_total =0; // Number of deals in the specified history ulong ticket =0; // Deal ticket double drawdown_max =0.0; // Maximum drawdown double balance =0.0; // Balance //--- string delimeter =","; // Delimiter string string_to_write =""; // To generate the string for writing //--- Generate the header string string headers="TIME,SYMBOL,DEAL TYPE,ENTRY TYPE,VOLUME,PRICE,SWAP($),PROFIT($),DRAWDOWN(%),BALANCE"; //--- If more than one symbol is involved, modify the header string if(SYMBOLS_COUNT>1) { for(int s=0; s<SYMBOLS_COUNT; s++) StringAdd(headers,","+InputSymbols[s]); } //--- Write the report headers FileWrite(file_handle,headers); //--- Get the complete history HistorySelect(0,TimeCurrent()); //--- Get the number of deals deals_total=HistoryDealsTotal(); //--- Resize the array of balances according to the number of symbols ArrayResize(symbol_balance,SYMBOLS_COUNT); //--- Resize the array of deals for each symbol for(int s=0; s<SYMBOLS_COUNT; s++) ArrayResize(symbol_balance[s].balance,deals_total); //--- Iterate in a loop and write the data for(int i=0; i<deals_total; i++) { //--- Get the deal ticket ticket=HistoryDealGetTicket(i); //--- Get all the deal properties GetHistoryDealProperties(ticket,D_ALL); //--- Get the number of digits in the price digits=(int)SymbolInfoInteger(deal.symbol,SYMBOL_DIGITS); //--- Calculate the overall balance balance+=deal.profit+deal.swap+deal.commission; //--- Generate a string for writing via concatenation StringConcatenate(string_to_write, deal.time,delimeter, DealSymbolToString(deal.symbol),delimeter, DealTypeToString(deal.type),delimeter, DealEntryToString(deal.entry),delimeter, DealVolumeToString(deal.volume),delimeter, DealPriceToString(deal.price,digits),delimeter, DealSwapToString(deal.swap),delimeter, DealProfitToString(deal.symbol,deal.profit),delimeter, MaxDrawdownToString(i,balance,max_drawdown),delimeter, DoubleToString(balance,2)); //--- If more than one symbol is involved, write their balance values if(SYMBOLS_COUNT>1) { //--- Iterate over all symbols for(int s=0; s<SYMBOLS_COUNT; s++) { //--- If the symbols are equal and the deal result is non-zero if(deal.symbol==InputSymbols[s] && deal.profit!=0) { //--- Display the deal in the balance for the corresponding symbol // Take into consideration swap and commission symbol_balance[s].balance[i]=symbol_balance[s].balance[i-1]+ deal.profit+ deal.swap+ deal.commission; //--- Add to the string StringAdd(string_to_write,","+DoubleToString(symbol_balance[s].balance[i],2)); } //--- Otherwise write the previous value else { //--- If the deal type is "Balance" (the first deal) if(deal.type==DEAL_TYPE_BALANCE) { //--- the balance is the same for all symbols symbol_balance[s].balance[i]=balance; StringAdd(string_to_write,","+DoubleToString(symbol_balance[s].balance[i],2)); } //--- Otherwise write the previous value to the current index else { symbol_balance[s].balance[i]=symbol_balance[s].balance[i-1]; StringAdd(string_to_write,","+DoubleToString(symbol_balance[s].balance[i],2)); } } } } //--- Write the generated string FileWrite(file_handle,string_to_write); //--- Mandatory zeroing out of the variable for the next string string_to_write=""; } //--- Close the file FileClose(file_handle); } //--- If the file could not be created/opened, print the appropriate message else Print("Error creating file: "+IntegerToString(GetLastError())+""); }
위 코드에서 강조 표시된 MaxDrawdownToString() 함수는 로컬 최대값에서 모든 감소를 계산하고 새로운 로컬 최대값의 시간에 대한 문자열 표현을 반환합니다. 다른 모든 경우에 함수는 "-"(대시)를 포함하는 문자열을 반환합니다.
//+------------------------------------------------------------------+ //| Returns the maximum drawdown from the local maximum | //+------------------------------------------------------------------+ string MaxDrawdownToString(int deal_number,double balance,double &max_drawdown) { //--- The string to be displayed in the report string str=""; //--- To calculate the local maximum and drawdown static double max=0.0; static double min=0.0; //--- If this is the first deal if(deal_number==0) { //--- No drawdown yet max_drawdown=0.0; //--- Set the initial point as the local maximum max=balance; min=balance; } else { //--- If the current balance is greater than in the memory if(balance>max) { //--- calculate the drawdown using the previous values max_drawdown=100-((min/max)*100); //--- update the local maximum max=balance; min=balance; } else { //--- Return zero value of the drawdown max_drawdown=0.0; //--- Update the minimum min=fmin(min,balance); } } //--- Determine the string for the report if(max_drawdown==0) str="-"; else str=DoubleToString(max_drawdown,2); //--- Return result return(str); }
따라서 모든 보고서 생성 기능이 준비되었습니다. 우리는 위의 모든 것을 어떻게 사용해야 하는지 보기만 하면 됩니다. 테스트 완료 시 호출되는 OnTester() 함수가 필요합니다. MQL5 Reference에서 이 기능에 대한 자세한 설명을 확인하세요.
OnTester() 함수의 본문에 몇 줄의 코드를 작성하여 보고서가 생성되어야 하는 조건을 지정하기만 하면 됩니다. 해당 코드 조각은 아래에 제공됩니다.
//+------------------------------------------------------------------+ //| Handler of the event of testing completion | //+------------------------------------------------------------------+ double OnTester() { //--- Write the report only after testing if(IsTester() && !IsOptimization() && !IsVisualMode()) //--- Generate the report and write it to the file CreateSymbolBalanceReport(); //--- return(0.0); }
이제 Strategy Tester에서 Expert Advisor를 실행하면 테스트가 끝나면 공통 터미널 폴더 C:\ProgramData\MetaQuotes\Terminal\Common\Files에 생성된 Expert Advisor 폴더를 볼 수 있습니다. 그리고 Expert Advisor 폴더에 LastTest.csv 보고서 파일이 생성됩니다. 메모장으로 파일을 열면 다음과 같이 표시됩니다.
그림 1. .csv 형식의 보고서 파일입니다.
Excel에서 차트 만들기
생성된 파일을 Excel에서 열면 각 데이터 유형이 별도의 열에 있음을 확인할 수 있습니다. 이렇게 하면 데이터 보기가 훨씬 더 편리해집니다. 이 시점에서 기술적으로 차트를 만들고 파일을 *.xlsx 형식의 Excel 책으로 저장할 준비가 되었습니다. 그러나 나중에 테스트를 실행하고 책을 다시 열면 여전히 이전 데이터가 표시됩니다.
LastTest.csv 파일이 이미 Excel에서 사용되는 동안 데이터를 새로 고치려고 하면 Expert Advisor가 다른 응용 프로그램에서 사용 중인 동안 쓰기 위해 파일을 열 수 없기 때문에 파일이 업데이트되지 않습니다.
그림 2. Excel 2010의 .csv 형식 보고서 파일입니다.
우리의 경우에 사용할 수 있는 솔루션이 있습니다. 먼저 원하는 폴더에 *.xlsx 형식의 Excel 책을 만듭니다. 그런 다음 파일을 열고 데이터 탭으로 이동합니다.
그림 3. Excel 2010의 데이터 탭.
이 탭의 리본에서 텍스트에서 옵션을 선택합니다. "LastTest.csv" 파일을 선택해야 하는 위치에 텍스트 파일 가져오기 대화상자가 나타납니다. 파일을 선택하고 열기 버튼을 클릭하세요. 텍스트 가져오기 마법사 - 1/3단계 대화 상자가 아래와 같이 팝업됩니다.
그림 4. "텍스트 가져오기 마법사 - 1/3단계" 대화 상자.
위와 같이 설정을 조정하고 다음 >을 클릭합니다. 여기서(2/3단계) 데이터 파일에 사용되는 구분 기호를 지정해야 합니다. 우리 파일에서는 ","(쉼표)입니다.
그림 5. "텍스트 가져오기 마법사 - 2/3단계" 대화 상자.
다음 >을 클릭하여 텍스트 가져오기 마법사 - 3/3단계로 진행합니다. 여기에서 모든 열의 데이터 형식으로 일반을 그대로 둡니다. 나중에 형식을 변경할 수 있습니다.
그림 6. "텍스트 가져오기 마법사 - 3/3단계" 대화 상자.
마침 버튼을 클릭하면 데이터 가져오기를 위한 워크시트와 셀을 지정해야 하는 데이터 가져오기 창이 나타납니다.
그림 7. Excel 2010에서 데이터를 가져올 셀을 선택합니다.
일반적으로 왼쪽 상단 셀 A1을 선택합니다. 확인을 클릭하기 전에 속성... 버튼을 클릭하여 외부 데이터 범위 속성을 설정하세요. 아래와 같은 대화 상자가 표시됩니다.
그림 8. Excel 2010의 텍스트 파일에서 데이터를 가져올 때 외부 데이터 범위 속성.
위와 같이 정확하게 설정을 조정하고 현재 및 다음 창에서 확인을 클릭하세요.
결과적으로 데이터는 .csv 파일을 로드한 것과 동일하게 나타납니다. 그러나 이제 Excel 책을 닫지 않고도 MetaTrader 5에서 반복 테스트를 실행할 수 있습니다. 테스트를 실행한 후 Ctrl+Alt+F5 단축키를 사용하거나 데이터 탭 리본의 모두 새로고침 버튼을 사용하여 데이터를 새로고침하기만 하면 됩니다.
홈 탭 리본의 조건부 서식 옵션을 사용하여 데이터 표현에 필요한 시각적 속성을 설정할 수 있습니다.
그림 9. Excel 2010의 조건부 서식.
이제 Excel 차트에 데이터를 표시해야 합니다. 한 차트는 모든 균형 차트를 표시하고 다른 차트는 로컬 최대값의 모든 감소를 히스토그램으로 표시합니다.
먼저 대차 대조표에 대한 다이어그램을 만들어 보겠습니다. 모든 저울의 헤더와 전체 데이터 배열을 위에서 아래로 선택합니다(Shift 키를 누른 상태에서 End 키를 누른 다음 아래쪽 화살표 b2> 키). 이제 삽입 탭에서 원하는 차트 유형을 선택하세요.
그림 10. Excel 2010에서 차트 유형 선택.
결과적으로 편의를 위해 다른 워크시트로 이동할 수 있는 차트가 생성됩니다. 이렇게 하려면 간단히 선택하고 Ctrl+X(잘라내기)를 누르십시오. 그런 다음 새로 생성된 워크시트로 이동하여 A1 셀을 선택하고 Ctrl+V(붙여넣기)를 누릅니다.
기본 설정으로 생성된 차트는 아래 이미지와 같습니다.
그림 11. 기본 설정으로 차트의 모양과 느낌.
차트의 모든 요소를 사용자 정의할 수 있습니다. 크기, 색상, 스타일 등을 변경할 수 있습니다.
위 이미지에서 가로축은 거래 건수를 나타냅니다. 대신 날짜를 표시하도록 수정해 보겠습니다. 이를 위해 차트를 마우스 오른쪽 버튼으로 클릭하고 컨텍스트 메뉴에서 데이터 선택 옵션을 선택합니다. 데이터 소스 선택 대화 상자가 나타납니다. 수정 버튼을 클릭한 다음 시간 열에서 필요한 데이터 범위를 선택하고 확인을 클릭하세요.
그림 12. "데이터 소스 선택" 대화 상자.
직접 드로다운 차트를 만들어 첫 번째 차트 아래에 배치합니다. 이제 필요한 경우 시각적 속성을 사용자 지정할 수 있습니다. 개인적으로 저는 보통 이렇게 합니다.
그림 13. Excel 2010의 사용자 지정 차트.
결론
따라서 테스트 결과가 상당히 괜찮은 Excel 차트를 얻었습니다. 내 미래 글 중 하나에서 더 유익한 보고서를 만드는 방법을 보여 드리겠습니다. 글에 첨부된 파일은 귀하가 고려할 수 있도록 Expert Advisor의 파일과 함께 다운로드 가능한 아카이브입니다.
아카이브에서 파일을 추출한 후 ReportInExcel 폴더를 MetaTrader 5\MQL5\Experts 디렉토리에 넣습니다. 또한 EventsSpy.mq5 지표는 MetaTrader 5\MQL5\Indicators 디렉토리에 있어야 합니다.
MetaQuotes 소프트웨어 사를 통해 러시아어가 번역됨.
원본 기고글: https://www.mql5.com/ru/articles/651