English Русский 中文 Español Deutsch 日本語 Português Français Italiano Türkçe
시장 분석을 위한 데이터베이스의 실용적 활용

시장 분석을 위한 데이터베이스의 실용적 활용

MetaTrader 5통합 | 5 7월 2021, 13:51
59 0
Alexander
Alexander

들어가며

데이터를 다루는 것이야말로 스탠드얼론이나 네트워크 앱을 가리지 않고, 오늘날 개발되는 소프트웨어들의 주 임무가 되었습니다. 이런 문제를 해결하기위해 특화된 소프트웨어가 탄생했습니다. 이들은 데이터베이스 매니지먼트 시스템(Database Management Systems, DBMS)이라고 불리며, 컴퓨터 저장 및 처리를 위해 구조화, 시스템화, 자료 정리가 가능합니다. 이러한 소프트웨어는 제조업에서 금융 및 통신까지 모든 분야의 정보 활동의 기반이죠. 

거래와 관련해서는 대부분의 분석가들이 업무에서 데이터베이스를 사용하지 않습니다. 그러나 이러한 솔루션을 쓰는 편이 편리한 작업도 있습니다. 

이 문서에서는 그런 작업 하나를 다루겠습니다. 데이터베이스에서 데이터를 저장하고 로드하는 틱 인디케이터입니다.

BuySellVolume 알고리즘 

BuySellVolume - 제가 이 인디케이터에 붙인 단순한 이름보다도 알고리즘쪽이 더 단순합니다: 두개의 연속적인 틱(tick1 tick2)의 시간(t)와 가격(p)를 받아오기. 차이량을 계산해봅시다:

Δt = t2 - t1     (초)
Δp = p2 - p1    (포인트)

볼륨은 이 계산식을 써서 계산합니다:

v2 = Δp / Δt

따라서 이 볼륨은 가격이 이동한 포인트 수에 정비례하며 소요 시간과 반비례합니다. 만약 Δt = 0 이면, 그것 대신 0.5가 값입니다. 이를 통해 시장에서의 매수자와 매도자의 행동을 얻을 수 있습니다. 

1. 데이터베이스를 사용하지않는 인디케이터 구현

지정된 기능이 있지만 데이터베이스와의 상호 작용이 없는 인디케이터를 먼저 고려하는 것이 논리적일 것이라고 생각합니다. 제 생각에 최선의 해결책은 적절한 계산을 할 수 있는 기본 클래스를 만드는 것이며 데이터베이스와의 상호 작용을 실현하는 것이 파생 모델입니다. 이걸 구현하기 위해 AdoSuite 라이브러리가 필요합니다. 링크를 누르고 다운로드합시다.

먼저 BsvEngine.mqh 파일을 만들고 AdoSuite 데이터 클래스를 연결하세요:

#include <Ado\Data.mqh>

그런 다음 데이터베이스 작업을 제외한 필요한 모든 기능을 구현하는 기본 인디케이터 클래스를 만듭니다. 다음과 같이 보입니다:

항목 1.1

//+------------------------------------------------------------------+
// BuySellVolume indicator class (without storing to database)       |
//+------------------------------------------------------------------+
class CBsvEngine
  {
private:
   MqlTick           TickBuffer[];     // ticks buffer
   double            VolumeBuffer[];   // volume buffer
   int               TicksInBuffer;    // number of ticks in the buffer

   bool              DbAvailable;      // indicates, whether it's possible to work with the database

   long              FindIndexByTime(const datetime &time[],datetime barTime,long left,long right);

protected:

   virtual string DbConnectionString() { return NULL; }
   virtual bool DbCheckAvailable() { return false; }
   virtual CAdoTable *DbLoadData(const datetime startTime,const datetime endTime) { return NULL; }
   virtual void DbSaveData(CAdoTable *table) { return; }

public:
                     CBsvEngine();

   void              Init();
   void              ProcessTick(double &buyBuffer[],double &sellBuffer[]);
   void              LoadData(const datetime startTime,const datetime &time[],double &buyBuffer[],double &sellBuffer[]);
   void              SaveData();
  };

솔루션 생산성을 높이기 위해 데이터가 특수 버퍼(TickBuffer 및 VolumeBuffer)에 저장되고 일정 시간이 지나면 데이터베이스에 업로드된다는 점에 주목해주시기 바랍니다. 

클래스 구현 순서를 봅시다. 생성자로 시작하도록 하죠.

항목 1.2

//+------------------------------------------------------------------+
// Constructor                                                       |
//+------------------------------------------------------------------+
CBsvEngine::CBsvEngine(void)
  {
// Initially, can be placed up to 500 ticks in a buffer
   ArrayResize(TickBuffer,500);
   ArrayResize(VolumeBuffer,500);
   TicksInBuffer=0;
   DbAvailable=false;
  } 

변수가 초기화되고 버퍼의 초기 크기가 설정된다는 점을 분명히 해야 한다고 생각합니다.

Init() 메소드의 구현입니다:

 항목 1.3

//+-------------------------------------------------------------------+
// Function, called in the OnInit event                               |
//+-------------------------------------------------------------------+
CBsvEngine::Init(void)
  {
   DbAvailable=DbCheckAvailable();
   if(!DbAvailable)
      Alert("Unable to work with database. Working offline.");
  }

여기에서 우선 데이터베이스를 이용하는 것이 가능한지 체크합니다 기본 클래스 DbCheckAvailable() 는 언제나 false를 반환하는데, DB관리는 다른 클래스에서 처리되기 때문입니다. 어쩌면 눈치채셨는지도 모르겠지만 DbConnectionString(), DbCheckAvailable(), DbLoadData(), DbSaveData() 함수들은 아직 아무런 의미가 없습니다. 이 함수들은 특정 DB에 바인딩할때 필요한 오버라이딩 함수입니다.

항목 1.4은 새 틱이 도달할 때 호출되고 버퍼에 틱을 넣고 인디케이터 값을 계산하는 ProcessTick() 함수 구현을 보여줍니다. 이를 위해 두 인디케이터 버퍼는 기능으로 전달됩니다. 하나는 구매자 활동을 저장하는 데 사용되고 다른 하나는 판매자 활동을 저장하는 데 사용됩니다. 

  항목 1.4

//+------------------------------------------------------------------+
// Processing incoming tick and updating indicator data              |
//+------------------------------------------------------------------+
CBsvEngine::ProcessTick(double &buyBuffer[],double &sellBuffer[])
  {
// if it's not enough of allocated buffer for ticks, let's increase it
   int bufSize=ArraySize(TickBuffer);
   if(TicksInBuffer>=bufSize)
     {
      ArrayResize(TickBuffer,bufSize+500);
      ArrayResize(VolumeBuffer,bufSize+500);
     }

// getting the last tick and writing it to the buffer
   SymbolInfoTick(Symbol(),TickBuffer[TicksInBuffer]);

   if(TicksInBuffer>0)
     {
      // calculating the time difference
      int span=(int)(TickBuffer[TicksInBuffer].time-TickBuffer[TicksInBuffer-1].time);
      // calculating the price difference
      int diff=(int)MathRound((TickBuffer[TicksInBuffer].bid-TickBuffer[TicksInBuffer-1].bid)*MathPow(10,_Digits));

      // calculating the volume. If the tick came in the same second as the previous one, we consider the time equal to 0.5 seconds
      VolumeBuffer[TicksInBuffer]=span>0 ?(double)diff/(double)span :(double)diff/0.5;

      // filling the indicator buffers with data
      int index=ArraySize(buyBuffer)-1;
      if(diff>0) buyBuffer[index]+=VolumeBuffer[TicksInBuffer];
      else sellBuffer[index]+=VolumeBuffer[TicksInBuffer];
     }

   TicksInBuffer++;
  }

Load Data() 함수는 지정된 기간 동안 현재 시간 동안 데이터베이스에서 데이터를 로드합니다. 

  항목 1.5

//+------------------------------------------------------------------+
// Loading historical data from the database                         |
//+------------------------------------------------------------------+
CBsvEngine::LoadData(const datetime startTime,const datetime &time[],double &buyBuffer[],double &sellBuffer[])
  {
// if the database is inaccessible, then does not load the data
   if(!DbAvailable) return;

// getting data from the database
   CAdoTable *table=DbLoadData(startTime,TimeCurrent());

   if(CheckPointer(table)==POINTER_INVALID) return;

// filling buffers with received data
   for(int i=0; i<table.Records().Total(); i++)
     {
      // get the record with data
      CAdoRecord *row=table.Records().GetRecord(i);

      // getting the index of corresponding bar
      MqlDateTime mdt;
      mdt=row.GetValue(0).ToDatetime();
      long index=FindIndexByTime(time,StructToTime(mdt));

      // filling buffers with data
      if(index!=-1)
        {
         buyBuffer[index]+=row.GetValue(1).ToDouble();
         sellBuffer[index]+=row.GetValue(2).ToDouble();
        }
     }
   delete table;
  }

LoadData()는 DbLoadData() 함수를 호출하며, DbLoadData()는 bar time, buyers buffer value, seller buffer value 등 3개의 열이 있는 테이블을 반환합니다.

여기선 다른 함수 FindIndexByTime()가 쓰입니다. 이 글을 쓸 당시 표준 라이브러리에서 타임시리즈 이진수 검색 기능을 찾지 못해 직접 짰습니다.

마지막으로 데이터를 저장하는 데에 쓰일 SaveData() 함수: 

항목 1.6

//+---------------------------------------------------------------------+
// Saving data from the TickBuffer and VolumeBuffer buffers to database |
//+---------------------------------------------------------------------+
CBsvEngine::SaveData(void)
  {
   if(DbAvailable)
     {
      // creating a table for passing data to SaveDataToDb
      CAdoTable *table=new CAdoTable();
      table.Columns().AddColumn("Time", ADOTYPE_DATETIME);
      table.Columns().AddColumn("Price", ADOTYPE_DOUBLE);
      table.Columns().AddColumn("Volume", ADOTYPE_DOUBLE);

      // filling table with data from buffers
      for(int i=1; i<TicksInBuffer; i++)
        {
         CAdoRecord *row=table.CreateRecord();
         row.Values().GetValue(0).SetValue(TickBuffer[i].time);
         row.Values().GetValue(1).SetValue(TickBuffer[i].bid);
         row.Values().GetValue(2).SetValue(VolumeBuffer[i]);

         table.Records().Add(row);
        }

      // saving data to database
      DbSaveData(table);

      if(CheckPointer(table)!=POINTER_INVALID)
         delete table;
     }

// writing last tick to the beginning, to have something to compare
   TickBuffer[0] = TickBuffer[TicksInBuffer - 1];
   TicksInBuffer = 1;
  }

보시는 바와 같이 인디케이터에 필요한 정보를 표로 작성하여 DbSaveData() 함수로 전달하는 메소드에서, 데이터를 데이터베이스에 저장하며 저장이 완료되면 버퍼를 비웁니다.

이제 프레임워크가 준비되었습니다. 이제 항목 1.7에서 BuySellVolume.mq5 인디케이터가 어떻게 짜였는지 볼 수 있습니다: 

항목 1.7

// including file with the indicator class
#include "BsvEngine.mqh"

//+------------------------------------------------------------------+
//| Indicator Properties                                             |
//+------------------------------------------------------------------+
#property indicator_separate_window

#property indicator_buffers 2
#property indicator_plots   2

#property indicator_type1   DRAW_HISTOGRAM
#property indicator_color1  Red
#property indicator_width1  2

#property indicator_type2   DRAW_HISTOGRAM
#property indicator_color2  SteelBlue
#property indicator_width2  2

//+------------------------------------------------------------------+
//| Data Buffers                                                     |
//+------------------------------------------------------------------+
double ExtBuyBuffer[];
double ExtSellBuffer[];

//+------------------------------------------------------------------+
//| Variables                                                        |
//+------------------------------------------------------------------+
// declaring indicator class
CBsvEngine bsv;
//+------------------------------------------------------------------+
//| OnInit
//+------------------------------------------------------------------+
int OnInit()
  {
// setting indicator properties
   IndicatorSetString(INDICATOR_SHORTNAME,"BuySellVolume");
   IndicatorSetInteger(INDICATOR_DIGITS,2);
// buffer for 'buy'
   SetIndexBuffer(0,ExtBuyBuffer,INDICATOR_DATA);
   PlotIndexSetString(0,PLOT_LABEL,"Buy");
// buffer for 'sell'
   SetIndexBuffer(1,ExtSellBuffer,INDICATOR_DATA);
   PlotIndexSetString(1,PLOT_LABEL,"Sell");

// setting the timer to clear buffers with ticks
   EventSetTimer(60);

   return(0);
  }
//+------------------------------------------------------------------+
//| OnDeinit
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   EventKillTimer();
  }
//+------------------------------------------------------------------+
//| OnCalculate
//+------------------------------------------------------------------+
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[])
  {
// processing incoming tick
   bsv.ProcessTick(ExtBuyBuffer,ExtSellBuffer);

   return(rates_total);
  }
//+------------------------------------------------------------------+
//| OnTimer
//+------------------------------------------------------------------+
void OnTimer()
  {
// saving data
   bsv.SaveData();
  }

제 생각에는 몹시 간결하네요. 인디케이터 내에서는 클래스의 함수 중 단 둘 만 호출됩니다: ProcessTick() and SaveData(). ProcessTick() 함수는 계산에 사용되며, 데이터를 저장하지 않긴하지만 tics로 버퍼를 재설정하려면 SaveData() 함수가 필요합니다.

컴파일 해보고, "voila" - 인디케이터가 값을 보여주기 시작합니다:

 

 1번 그림. 데이터베이스로의 링크가 없는 BuySellVolume 인디케이터의 GBPUSD M1 처리

완벽하네요! 틱은 제대로 카운트 되고 인디케이터는 계산이 굴러갑니다. 이 솔루션의 장점은 - 인디케이터 자체 (ex5) 만 필요하지 그 외엔 딱히 필요 없다는 점입니다. 하지만 만약 타임프레임, 기구를 바꾸거나 터미널을 닫으면 데이터는 비가역적으로 소실됩니다. 이를 피하기 위해 인디케이터에서 데이터를 저장하고 불러오는지 알아보겠습니다.

2. SQL Server 2008에 연결하기

현 시점에 저는 두개의 DBMSd를 컴퓨터에 설치해 두었습니다 - SQL Server 2008 그리고 Db2 9.7. 제가 보기엔 독자분들은 Db2 보다는 SQL Server에 더 친숙하실거 같아서 SQL Server를 선택했습니다.

먼저 SQL Server 2008용 BuySellVolume과 BsvMsSql.mqh 파일을 새로 생성하여 (SQL Server Management Studio 또는 다른 방법을 통해) CBSvEngine 클래스와 함께 포함하도록 하겠습니다.

#include "BsvEngine.mqh"

SQL Server에는 OLE DB 드라이버가 장착되어 있으므로 AdoSuite 라이브러리에 포함된 OleDb 프로바이더를 통해 작업할 수 있습니다. 이를 위해 필수 클래스들을 포함시키십시오:

#include <Ado\Providers\OleDb.mqh>

그리고 파생 클래스를 생성합니다:

항목 2.1

//+------------------------------------------------------------------+
// Class of the BuySellVolume indicator, linked with MsSql database  |
//+------------------------------------------------------------------+
class CBsvSqlServer : public CBsvEngine
  {
protected:

   virtual string    DbConnectionString();
   virtual bool      DbCheckAvailable();
   virtual CAdoTable *DbLoadData(const datetime startTime,const datetime endTime);
   virtual void      DbSaveData(CAdoTable *table);

  };

데이터베이스를 직접 다루는 4개의 함수를 재정의하는 것으로 충분합니다. 시작해봅시다. DbConnectionString() 메소드는 데이터베이스에 접속하기 위한 스트링을 반환합니다.

제 쪽에서는 이렇게 보여요:

항목 2.2

//+------------------------------------------------------------------+
// Returns the string for connection to database                     |
//+------------------------------------------------------------------+
string CBsvSqlServer::DbConnectionString(void)
  {
   return "Provider=SQLOLEDB;Server=.\SQLEXPRESS;Database=BuySellVolume;Trusted_Connection=yes;";
  }

연결 스트링에서 로컬 시스템에 위치한 SQL OLE-DB 드라이버와 SQLEXPRESS 서버를 써서 작업할 수 있다는 것을 알 수 있습니다.Windows 인증(기타 옵션 - 별도로 로그인 및 암호 입력) 을 사용하여 BuySellVolume 데이터베이스에 연결합니다. .

다음 단계는 DbCheckAvailable() 함수를 구현하는 것입니다. 하지만 먼저 이 함수로 뭘 해야하는가 보도록 하죠.

해당 데이터베이스로 작업할 수 있는가 여부를 체크한다고 했었죠. 일부에서는 사실입니다. 이 함수의 주 목적은 현 기구를 위해 데이터를 저장할 테이블이 있는가를 확인하고, 만약 없으면 하나 만드는 것입니다.이러한 작업이 오류로 끝나면 False가 반환됩니다. 즉, 표의 인디케이터 데이터를 읽고 쓰는 것이 무시되고 인디케이터도 이와 유사하게 작동합니다(항목 1.7 참조). .

SQL Server의 stored procedures(SP)를 쓰는 것을 추천하겠습니다. 왜 그걸 쓰는가? 그냥 그러고 싶어서요.물론 취향에 따라 다르겠지만, 동적 쿼리가 사용되므로 코드에 쿼리를 작성하는 것보다 SP를 사용하는 것이 더 우아한 솔루션이라고 생각합니다(이 경우에는 해당되지 않지만 컴파일하는 데 더 많은 시간이 필요합니다) :).

DbCheckAvailable()의 stored procedure 는 다음과 같습니다:

항목 2.3

CREATE PROCEDURE [dbo].[CheckAvailable]
        @symbol NVARCHAR(30)
AS
BEGIN
        SET NOCOUNT ON;
        
        -- If there is no table for instrument, we create it
        IF OBJECT_ID(@symbol, N'U') IS NULL
        EXEC ('
                -- Creating table for the instrument
                CREATE TABLE ' + @symbol + ' (Time DATETIME NOT NULL,
                        Price REAL NOT NULL, 
                        Volume REAL NOT NULL);
                
                -- Creating index for the tick time
                CREATE INDEX Ind' + @symbol + '
                ON  ' + @symbol + '(Time);
        ');
END

데이터베이스에 원하는 테이블이 없으면 테이블을 생성하는 동적 쿼리(스트링으로)가 생성되고 실행됩니다. 그 stored procedure가 생성되면 DbCheckAvailable() 함수로 다룰 순간이 온 것입니다: 

  항목 2.4

//+------------------------------------------------------------------+
// Checks whether it's possible to connect to database               |
//+------------------------------------------------------------------+
bool CBsvSqlServer::DbCheckAvailable(void)
  {
// working with ms sql via Oledb provider
   COleDbConnection *conn=new COleDbConnection();
   conn.ConnectionString(DbConnectionString());

// using stored procedure to create a table
   COleDbCommand *cmd=new COleDbCommand();
   cmd.CommandText("CheckAvailable");
   cmd.CommandType(CMDTYPE_STOREDPROCEDURE);
   cmd.Connection(conn);

// passing parameters to stored procedure
   CAdoValue *vSymbol=new CAdoValue();
   vSymbol.SetValue(Symbol());
   cmd.Parameters().Add("@symbol",vSymbol);

   conn.Open();

// executing stored procedure
   cmd.ExecuteNonQuery();

   conn.Close();

   delete cmd;
   delete conn;

   if(CheckAdoError())
     {
      ResetAdoError();
      return false;
     }

   return true;
  }

보시는 것처럼 서버의 stored procedure로 작업할 수 있습니다. CommandType 속성을 CMDTYPE_STOREDPROCEDURE로 바꾸고 필요한 패러미터만 패싱하면 됩니다. 오류가 발생하면 DbCheck Available 함수가 false를 반환합니다. 

DbLoadData 함수를 위해 stored procedure를 짜 봅시다. 데이터베이스는 각 체크 표시에 대한 데이터를 저장하므로 필요한 기간의 각 표시줄에 대한 데이터를 생성해야 합니다. .다음 procedure를 만들었습니다:

  항목 2.5

CREATE PROCEDURE [dbo].[LoadData]
        @symbol NVARCHAR(30),   -- instrument
        @startTime DATETIME,    -- beginning of calculation
        @endTime DATETIME,      -- end of calculation
        @period INT             -- chart period (in minutes)
AS
BEGIN
        SET NOCOUNT ON;
        
        -- converting inputs to strings for passing to a dynamic query
        DECLARE @sTime NVARCHAR(20) = CONVERT(NVARCHAR, @startTime, 112) + ' ' + CONVERT(NVARCHAR, @startTime, 114),
                @eTime NVARCHAR(20) = CONVERT(NVARCHAR, @endTime, 112) + ' ' + CONVERT(NVARCHAR, @endTime, 114),
                @p NVARCHAR(10) = CONVERT(NVARCHAR, @period);
                
        EXEC('        
                SELECT DATEADD(minute, Bar * ' + @p + ', ''' + @sTime + ''') AS BarTime, 
                        SUM(CASE WHEN Volume > 0 THEN Volume ELSE 0 END) as Buy,
                        SUM(CASE WHEN Volume < 0 THEN Volume ELSE 0 END) as Sell 
                FROM 
                (
                        SELECT DATEDIFF(minute, ''' + @sTime + ''', TIME) / ' + @p + ' AS Bar,
                                Volume 
                        FROM ' + @symbol + '
                        WHERE Time >= ''' + @sTime + ''' AND Time <= ''' + @eTime + '''
                ) x 
                GROUP BY Bar 
                ORDER BY 1;
        ');
END 

참고로 첫 번째 채워진 막대의 열림 시간은 @startTime으로 전달되어야 합니다. 그렇지 않으면 오프셋을 얻을 수 있습니다.

다음 항목에서 DbLoadData() 구현을 봅시다:

항목 2.6

//+------------------------------------------------------------------+
// Loading data from the database                                    |
//+------------------------------------------------------------------+
CAdoTable *CBsvSqlServer::DbLoadData(const datetime startTime,const datetime endTime)
  {
// working with ms sql via Oledb provider
   COleDbConnection *conn=new COleDbConnection();
   conn.ConnectionString(DbConnectionString());

// using stored procedure to calculate data
   COleDbCommand *cmd=new COleDbCommand();
   cmd.CommandText("LoadData");
   cmd.CommandType(CMDTYPE_STOREDPROCEDURE);
   cmd.Connection(conn);

// passing parameters to stored procedure
   CAdoValue *vSymbol=new CAdoValue();
   vSymbol.SetValue(Symbol());
   cmd.Parameters().Add("@symbol",vSymbol);

   CAdoValue *vStartTime=new CAdoValue();
   vStartTime.SetValue(startTime);
   cmd.Parameters().Add("@startTime",vStartTime);

   CAdoValue *vEndTime=new CAdoValue();
   vEndTime.SetValue(endTime);
   cmd.Parameters().Add("@endTime",vEndTime);

   CAdoValue *vPeriod=new CAdoValue();
   vPeriod.SetValue(PeriodSeconds()/60);
   cmd.Parameters().Add("@period",vPeriod);

   COleDbDataAdapter *adapter=new COleDbDataAdapter();
   adapter.SelectCommand(cmd);

// creating table and filling it with data, that were returned by stored procedure
   CAdoTable *table=new CAdoTable();
   adapter.Fill(table);

   delete adapter;
   delete conn;

   if(CheckAdoError())
     {
      delete table;
      ResetAdoError();
      return NULL;
     }

   return table;
  }

여기서는 stored procedure, 전달 도구, 계산 시작 날짜, 계산 종료 날짜 및 현재 차트 기간(분)을 호출합니다. 그런 다음 COleDbDataAdapter 클래스를 사용하여 표에서 결과를 읽습니다. 이 표에서 인디케이터의 버퍼가 채워집니다.

DbSaveData()를 구현하는 최종 단계는:

  항목 2.7

CREATE PROCEDURE [dbo].[SaveData]
        @symbol NVARCHAR(30),
        @ticks NVARCHAR(MAX)
AS
BEGIN
        EXEC('
                DECLARE @xmlId INT,
                        @xmlTicks XML = ''' + @ticks + ''';

                EXEC sp_xml_preparedocument 
                        @xmlId OUTPUT, 
                        @xmlTicks;
                
                -- read data from xml to table
                INSERT INTO ' + @symbol + '
                SELECT *
                FROM OPENXML( @xmlId, N''*/*'', 0)
                WITH
                (
                        Time DATETIME N''Time'', 
                        Price REAL N''Price'',
                        Volume REAL N''Volume'' 
                );

                EXEC sp_xml_removedocument @xmlId;
        ');
END

저장된 틱 데이터가 있는 xml은 @ticks 패러미터로 프로시저에 전달해야 합니다.성능상의 이유로 이 결정을 내렸습니다. 절차를 한 번 호출하고 20개의 틱을 보내는 것이 20번 호출하고 하나의 틱을 전달하는 것보다 더 쉽기 때문입니다. 다음 항목에서 xml 스트링이 어떻게 짜여야하는지 한 번 봅시다: 

항목 2.8

//+------------------------------------------------------------------+
// Saving data to database                                           |
//+------------------------------------------------------------------+
CBsvSqlServer::DbSaveData(CAdoTable *table)
  {
// if there is nothing to write, then return
   if(table.Records().Total()==0) return;

// forming the xml with data to pass into the stored procedure
   string xml;
   StringAdd(xml,"<Ticks>");

   for(int i=0; i<table.Records().Total(); i++)
     {
      CAdoRecord *row=table.Records().GetRecord(i);

      StringAdd(xml,"<Tick>");

      StringAdd(xml,"<Time>");
      MqlDateTime mdt;
      mdt=row.GetValue(0).ToDatetime();
      StringAdd(xml,StringFormat("%04u%02u%02u %02u:%02u:%02u",mdt.year,mdt.mon,mdt.day,mdt.hour,mdt.min,mdt.sec));
      StringAdd(xml,"</Time>");

      StringAdd(xml,"<Price>");
      StringAdd(xml,DoubleToString(row.GetValue(1).ToDouble()));
      StringAdd(xml,"</Price>");

      StringAdd(xml,"<Volume>");
      StringAdd(xml,DoubleToString(row.GetValue(2).ToDouble()));
      StringAdd(xml,"</Volume>");

      StringAdd(xml,"</Tick>");
     }

   StringAdd(xml,"</Ticks>");

// working with ms sql via Oledb provider
   COleDbConnection *conn=new COleDbConnection();
   conn.ConnectionString(DbConnectionString());

// using stored procedure to write data
   COleDbCommand *cmd=new COleDbCommand();
   cmd.CommandText("SaveData");
   cmd.CommandType(CMDTYPE_STOREDPROCEDURE);
   cmd.Connection(conn);

   CAdoValue *vSymbol=new CAdoValue();
   vSymbol.SetValue(Symbol());
   cmd.Parameters().Add("@symbol",vSymbol);

   CAdoValue *vTicks=new CAdoValue();
   vTicks.SetValue(xml);
   cmd.Parameters().Add("@ticks",vTicks);

   conn.Open();

// executing stored procedure
   cmd.ExecuteNonQuery();

   conn.Close();

   delete cmd;
   delete conn;

   ResetAdoError();
  }

이 함수의 절반정도는 스트링과 xml 생성에 할애되었습니다. 또한 이 스트링은 stored procedure 로 전달되어 구문 분석됩니다.

이것으로 SQL Server 2008 인터랙션 구현은 끝났으니 BuySellVolume SqlServer.mq5 인디케이터를 구현할 수 있습니다.

보시다시피 이 버전의 구현은 조금 더 자세히 설명하는 일부 변경 사항을 제외하고는 마지막 버전의 구현과 유사합니다.

  항목 2.9

// including file with the indicator class
#include "BsvSqlServer.mqh"

//+------------------------------------------------------------------+
//| Indicator Properties                                             |
//+------------------------------------------------------------------+
#property indicator_separate_window

#property indicator_buffers 2
#property indicator_plots   2

#property indicator_type1   DRAW_HISTOGRAM
#property indicator_color1  Red
#property indicator_width1  2

#property indicator_type2   DRAW_HISTOGRAM
#property indicator_color2  SteelBlue
#property indicator_width2  2

//+------------------------------------------------------------------+
//| Input parameters of indicator                                    |
//+------------------------------------------------------------------+
input datetime StartTime=D'2010.04.04'; // start calculations from this date

//+------------------------------------------------------------------+
//| Data Buffers                                                     |
//+------------------------------------------------------------------+
double ExtBuyBuffer[];
double ExtSellBuffer[];

//+------------------------------------------------------------------+
//| Variables                                                        |
//+------------------------------------------------------------------+
// declaring indicator class
CBsvSqlServer bsv;
//+------------------------------------------------------------------+
//| OnInit
//+------------------------------------------------------------------+
int OnInit()
  {
// setting indicator properties
   IndicatorSetString(INDICATOR_SHORTNAME,"BuySellVolume");
   IndicatorSetInteger(INDICATOR_DIGITS,2);
// buffer for 'buy'
   SetIndexBuffer(0,ExtBuyBuffer,INDICATOR_DATA);
   PlotIndexSetString(0,PLOT_LABEL,"Buy");
// buffer for 'sell'
   SetIndexBuffer(1,ExtSellBuffer,INDICATOR_DATA);
   PlotIndexSetString(1,PLOT_LABEL,"Sell");

// calling the Init function of indicator class
   bsv.Init();

// setting the timer to load ticks into database
   EventSetTimer(60);

   return(0);
  }
//+------------------------------------------------------------------+
//| OnDeinit
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   EventKillTimer();
// if there are unsaved data left, then save them
   bsv.SaveData();
  }
//+------------------------------------------------------------------+
//| OnCalculate
//+------------------------------------------------------------------+
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[])
  {
   if(prev_calculated==0)
     {
      // calculating the time of the nearest bar
      datetime st[];
      CopyTime(Symbol(),Period(),StartTime,1,st);
      // loading data
      bsv.LoadData(st[0],time,ExtBuyBuffer,ExtSellBuffer);
     }

// processing incoming tick
   bsv.ProcessTick(ExtBuyBuffer,ExtSellBuffer);

   return(rates_total);
  }
//+------------------------------------------------------------------+
//| OnTimer
//+------------------------------------------------------------------+
void OnTimer()
  {
// saving data
   bsv.SaveData();
  }

먼저 눈에 띄는 것은 StartTime 입력 변수의 존재입니다. 이 패러미터는 인디케이터용 데이터를 로딩하는 인터벌을 제한하기 위하여 만들어졌습니다. 실제로 많은 양의 데이터로 인해 계산시간이 많이 걸리는데 오래된 데이터는 의미가 없습니다.

두번째로 주목할만한 것은 bsv 변수의 타입이 바뀌었다는 겁니다.

세번째 차이는 인디케이터의 첫 계산에서 데이터 로딩하는 것이 추가되었단 것과 OnInit() 함수 안의 Init() 함수나, OnDeinit() 안의 SaveData() 함수같은.

이제 인디케이터를 컴파일하고 결과를 보겠습니다: 

 

2번 그림. SQL Server 2008 데이터베이스에 연결된 BuySellVolume 인디케이터에서의 EURUSD M15

끝났습니다! 이걸로 데이터는 저장되었고 타임프레임간 자유자재로 옮겨도 되게 되었습니다.

3. SQLite 3.6에 연결하기

"소 잡는 칼로 닭을 잡지 말라" - 제가 무슨 말을 하고싶은지 아실겁니다. 이걸 하자고 SQL Server를 도입하는건 좀 미친 짓이죠. 물론 이미 이 DBMS를 설치해놨고 쓰고 있다면 이걸 쓰고 싶으실 수는 있겠지만. 만약 컴공하고 조금 거리가 먼 사람도 별다른 노력 없이 쓸 수 있게 해주는 기술을 인디케이터에 접목시키고싶다면 어떡하시겠습니까?

다음은 표시기의 세 번째 버전으로, 이전 버전과 달리 파일-서버 아키텍처가 있는 데이터베이스에서 작동합니다.이 어프로치에서는 대부분의 경우 데이터베이스 커널하고 몇개의 DLL이 필요할 뿐입니다.

저는 이전에 SQLite를 다뤄본 적이 없습니다만, 구조적 간결함, 처리 속도, 그리고 경량이라는 점때문에 선택했습니다. 처음엔 C++나 TCL을 통해서 작동하는 API 외엔 찾아볼 수 없었지만 서드 파티 개발자들의 ODBC 드라이버와 ADO.NET 프로바이더를 찾을 수 있었습니다. AdoSuite는 ODBC를 통해 데이터 소스에 작업할 수도 있게 해주기 때문에, ODBC 드라이버를 다운받아 설치하는 편이 나아보입니다. 그러나 제가 알기로 1년도 더 전에 지원이 끊겼습니다. 게다가 이론적으로도 ADO.NET이 조금 더 빠를겁니다.

인디케이터의 ADO.NET 프로바이더를 통해 SQLite를 가지고 작업하기위해 무엇이 필요한지 봅시다.

목표 달성에 두가지 정도 필요합니다:

  • 우선 설치 제공자를 다운로드 받아야합니다. 이 공식홈페이지 http://sqlite.phxsoftware.com/에서 다운로드 링크를 찾으세요. 이 파일들 중에서System.Data.SQLite.dll. 어셈블리를 유념하시기바랍니다. 여기에는 SQLite 커널 자체와 ADO.NET 프로바이더가 포함됩니다. 편의상 이 라이브러리를 문서에 첨부했습니다. 다운로드 후, 윈도우즈 탐색기 (!) 로 Windows\assembly 폴더를 여십시오. 3번 그림에서 보다시피 많은 어셈블리가 보일 것입니다:

 

3번 그림. 탐색기는 GAC (글로벌 어셈블리 캐시, global assembly cache) 를 목록으로 열 수 있습니다


이제, System.Data.SQLite.dll을 이 폴더에 드래그 드랍 (!) 하세요.

따라서 어셈블리가 GAC(Global Assembly Cache)에 배치되므로 다음과 같이 작업할 수 있습니다.


4번 그림  System.Data.SQLite.dll이 GAC에 설치됨

이제 프로바이더 설치는 끝났습니다.

  • 두 번째 준비 작업은 SQLite와 함께 작동하도록 AdoSuite 프로바이더를 작성하는 것입니다. 빠르고 쉽게 짤 수 있습니다 (저는 15분 가량 걸렸습니다). 문서가 터 커지는건 바람직하지 못하니까 그 코드를 여기 넣진 않겠습니다. 이 문서에 달린 첨부 파일 내에서 확인하실 수 있습니다.

자 이제 인디케이터를 짜봅시다. SQLite 데이터베이스용으로 MQL5\Files 폴더에 빈 파일을 하나 만듭니다. SQLite는 딱히 파일 확장자를 가리지 않으니까 BuySellVolume.sqlite라고 명명합시다.

사실 파일을 만들 필요도 없습니다. 연결 문자열에 지정된 데이터베이스를 처음 쿼리할 때 파일이 자동으로 생성됩니다(항목 3.2 참조).. 우리가 여기에서 굳이 만드는 이유는 이 파일에 어디에서 튀어나온 파일인가 확실히 알고 넘어가기 위함입니다.

BsvSqlite.mqh 파일을 새로 만들고, SQLite용 기본 클래스와 프로바이더를 포함하십시오: 

#include "BsvEngine.mqh"
#include <Ado\Providers\SQLite.mqh>

 파생 클래스의 형식은 다음 이름을 제외하고 이전 클래스와 동일합니다:

   리스팅 3.1

//+------------------------------------------------------------------+
// Class of the BuySellVolume indicator, linked with SQLite database |
//+------------------------------------------------------------------+
class CBsvSqlite : public CBsvEngine
  {
protected:

   virtual string    DbConnectionString();
   virtual bool      DbCheckAvailable();
   virtual CAdoTable *DbLoadData(const datetime startTime,const datetime endTime);
   virtual void      DbSaveData(CAdoTable *table);

  };

이제 메소드를 구현합시다.

DbConnectionString() 은 다음과 같습니다:

    리스팅 3.2

//+------------------------------------------------------------------+
// Returns the string for connection to database                     |
//+------------------------------------------------------------------+
string CBsvSqlite::DbConnectionString(void)
  {
   return "Data Source=MQL5\Files\BuySellVolume.sqlite";
  }

보다시피 연결 스트링은 훨씬 단순해 보이고 기지 위치만 나타냅니다.

여기서는 상대경로를 썼지만 절대경로 또한 가능합니다: "Data Source = c:\Program Files\Metatrader 5\MQL 5\Files\BuySellVolume.sqlite".

항목 3.3이 DbCheckAvailable() 코드입니다. SQLite는 저장 프로시저와 같은 기능을 제공하지 않기 때문에 이제 모든 쿼리가 코드에 직접 기록됩니다.

   리스팅 3.3

//+------------------------------------------------------------------+
// Checks whether it's possible to connect to database               |
//+------------------------------------------------------------------+
bool CBsvSqlite::DbCheckAvailable(void)
  {
// working with SQLite via written SQLite provider
   CSQLiteConnection *conn=new CSQLiteConnection();
   conn.ConnectionString(DbConnectionString());

// command, that checks the availability of table for the instrument
   CSQLiteCommand *cmdCheck=new CSQLiteCommand();
   cmdCheck.Connection(conn);
   cmdCheck.CommandText(StringFormat("SELECT EXISTS(SELECT name FROM sqlite_master WHERE name = '%s')", Symbol()));

// command, that creates a table for the instrument
   CSQLiteCommand *cmdTable=new CSQLiteCommand();
   cmdTable.Connection(conn);
   cmdTable.CommandText(StringFormat("CREATE TABLE %s(Time DATETIME NOT NULL, " +
                        "Price DOUBLE NOT NULL, "+
                        "Volume DOUBLE NOT NULL)",Symbol()));

// command, that creates an index for the time
   CSQLiteCommand *cmdIndex=new CSQLiteCommand();
   cmdIndex.Connection(conn);
   cmdIndex.CommandText(StringFormat("CREATE INDEX Ind%s ON %s(Time)", Symbol(), Symbol()));

   conn.Open();

   if(CheckAdoError())
     {
      ResetAdoError();
      delete cmdCheck;
      delete cmdTable;
      delete cmdIndex;
      delete conn;
      return false;
     }

   CSQLiteTransaction *tran=conn.BeginTransaction();

   CAdoValue *vExists=cmdCheck.ExecuteScalar();

// if there is no table, we create it
   if(vExists.ToLong()==0)
     {
      cmdTable.ExecuteNonQuery();
      cmdIndex.ExecuteNonQuery();
     }

   if(!CheckAdoError()) tran.Commit();
   else tran.Rollback();

   conn.Close();

   delete vExists;
   delete cmdCheck;
   delete cmdTable;
   delete cmdIndex;
   delete tran;
   delete conn;

   if(CheckAdoError())
     {
      ResetAdoError();
      return false;
     }

   return true;
  }

이 함수의 결과물은 SQL Server 버전의 그것과 같습니다. 한가지 주의드리고싶은 것은 테이블 내 필드의 타입에 관한 건데요. 웃긴 점은 SQLite에서는 그런 필드 타입은 별로 의미가 없다는 것입니다. 또한 여기에는 DOUBLE 및 DATETIME 데이터 유형이 없습니다(최소한 표준 데이터 유형에는 포함되지 않음). 모든 값은 스트링 으로 저장된 후, 필요한 타입에 맞춰 동적으로 캐스팅됩니다.

그런데 DOUBLE 이나 DATETIME으로 열의 이름을 정하는 데에 의미가 있는가? 작업의 복잡성은 알 수 없지만 쿼리 시 ADO.NET은 그 값들을 자동으로 DOUBLE 및 DATETIME 타입으로 변환합니다. 하지만 다음 목록에는 그 중 하나가 나타나기 때문에 항상 그렇지는 않습니다.

DbLoadData() 함수의 항목을 고려해봅시다:

   항목 3.4

//+------------------------------------------------------------------+
// Loading data from the database                                    |
//+------------------------------------------------------------------+
CAdoTable *CBsvSqlite::DbLoadData(const datetime startTime,const datetime endTime)
  {
// working with SQLite via written SQLite provider
   CSQLiteConnection *conn=new CSQLiteConnection();
   conn.ConnectionString(DbConnectionString());

   CSQLiteCommand *cmd=new CSQLiteCommand();
   cmd.Connection(conn);
   cmd.CommandText(StringFormat(
                   "SELECT DATETIME(@startTime, '+' || CAST(Bar*@period AS TEXT) || ' minutes') AS BarTime, "+
                   "  SUM(CASE WHEN Volume > 0 THEN Volume ELSE 0 END) as Buy, "+
                   "  SUM(CASE WHEN Volume < 0 THEN Volume ELSE 0 END) as Sell "+
                   "FROM "+
                   "("+
                   "  SELECT CAST(strftime('%%s', julianday(Time)) - strftime('%%s', julianday(@startTime)) AS INTEGER)/ (60*@period) AS Bar, "+
                   "     Volume "+
                   "  FROM %s "+
                   "  WHERE Time >= @startTime AND Time <= @endTime "+
                   ") x "+
                   "GROUP BY Bar "+
                   "ORDER BY 1",Symbol()));

// substituting parameters
   CAdoValue *vStartTime=new CAdoValue();
   vStartTime.SetValue(startTime);
   cmd.Parameters().Add("@startTime",vStartTime);

   CAdoValue *vEndTime=new CAdoValue();
   vEndTime.SetValue(endTime);
   cmd.Parameters().Add("@endTime",vEndTime);

   CAdoValue *vPeriod=new CAdoValue();
   vPeriod.SetValue(PeriodSeconds()/60);
   cmd.Parameters().Add("@period",vPeriod);

   CSQLiteDataAdapter *adapter=new CSQLiteDataAdapter();
   adapter.SelectCommand(cmd);

// creating table and filling it with data
   CAdoTable *table=new CAdoTable();
   adapter.Fill(table);

   delete adapter;
   delete conn;

   if(CheckAdoError())
     {
      delete table;
      ResetAdoError();
      return NULL;
     }

// as we get the string with date, but not the date itself, it is necessary to convert it
   for(int i=0; i<table.Records().Total(); i++)
     {
      CAdoRecord* row= table.Records().GetRecord(i);
      string strDate = row.GetValue(0).AnyToString();
      StringSetCharacter(strDate,4,'.');
      StringSetCharacter(strDate,7,'.');
      row.GetValue(0).SetValue(StringToTime(strDate));
     }

   return table;
  }

MS SQL용 구현하고 똑같은 방식으로 돌아갑니다. 그런데 왜 함수 끝에 루프가 있는가? 예, 이 매직 쿼리에서는 DATETIME으로 반환하려는 제 시도가 전부 실패했습니다. SQLite에는 DATETIME 타입이 없습니다 - 대신 YYYY-MM-DD hh:mm:ss 포맷의 스트링이 반환됩니다. 그러나 StringToTime 기능으로 이해할 수 있는 형태로 쉽게 캐스트할 수 있으며, 이러한 장점을 활용했습니다.

마지막으로 DbSaveData() 함수:

  항목 3.5

//+------------------------------------------------------------------+
// Saving data to database                                           |
//+------------------------------------------------------------------+
CBsvSqlite::DbSaveData(CAdoTable *table)
  {
// if there is nothing to write, then return
   if(table.Records().Total()==0) return;

// working with SQLite via SQLite provider
   CSQLiteConnection *conn=new CSQLiteConnection();
   conn.ConnectionString(DbConnectionString());

// using stored procedure to write data
   CSQLiteCommand *cmd=new CSQLiteCommand();
   cmd.CommandText(StringFormat("INSERT INTO %s VALUES(@time, @price, @volume)", Symbol()));
   cmd.Connection(conn);

// adding parameters
   CSQLiteParameter *pTime=new CSQLiteParameter();
   pTime.ParameterName("@time");
   cmd.Parameters().Add(pTime);

   CSQLiteParameter *pPrice=new CSQLiteParameter();
   pPrice.ParameterName("@price");
   cmd.Parameters().Add(pPrice);

   CSQLiteParameter *pVolume=new CSQLiteParameter();
   pVolume.ParameterName("@volume");
   cmd.Parameters().Add(pVolume);

   conn.Open();

   if(CheckAdoError())
     {
      ResetAdoError();
      delete cmd;
      delete conn;
      return;
     }

// ! explicitly starting transaction
   CSQLiteTransaction *tran=conn.BeginTransaction();

   for(int i=0; i<table.Records().Total(); i++)
     {
      CAdoRecord *row=table.Records().GetRecord(i);

      // filling parameters with values
      CAdoValue *vTime=new CAdoValue();
      MqlDateTime mdt;
      mdt=row.GetValue(0).ToDatetime();
      vTime.SetValue(mdt);
      pTime.Value(vTime);

      CAdoValue *vPrice=new CAdoValue();
      vPrice.SetValue(row.GetValue(1).ToDouble());
      pPrice.Value(vPrice);

      CAdoValue *vVolume=new CAdoValue();
      vVolume.SetValue(row.GetValue(2).ToDouble());
      pVolume.Value(vVolume);

      // adding record
      cmd.ExecuteNonQuery();
     }

// completing transaction
   if(!CheckAdoError())
      tran.Commit();
   else tran.Rollback();

   conn.Close();

   delete tran;
   delete cmd;
   delete conn;

   ResetAdoError();
  }

이 함수 구현의 디테일을 다뤄보고싶네요.

먼저, 논리적이더라도 모든 것은 트랜잭션에서 이루어집니다. 그러나 이는 데이터 안전상의 이유로 이루어진 것이 아니라 성능상의 이유로 이루어졌습니다. 명시적인 트랜잭션 없이 항목을 추가하면 서버는 암시적으로 트랜잭션을 생성하고 테이블에 레코드를 삽입하고 트랜잭션을 제거합니다. 매 틱마다 이루어지죠! 이게 기록되는 중에는 데이터베이스 전체에 락이 걸려요! 커맨드는 트랜잭션이 필요 없다는 것에 주목할 가치가 있습니다. 저도 이해가 안되네요, 왜 저리 되는건지. 제 생각에는 여러 트랜잭션이 일어나지 않아서 그런 것 같습니다.

둘째, 커맨드를 한 번 생성한 다음, 패러미터를 할당하고 실행합니다. 이 역시 생산성 문제로, 커맨드가 한 번 컴파일(최적화)된 다음 컴파일된 버전으로 작업이 수행되기 때문입니다. 

결론으로 갑시다. BuySellVolume SQLite.mq5 인디케이터를 봅시다:

  항목 3.6

// including file with the indicator class
#include "BsvSqlite.mqh"

//+------------------------------------------------------------------+
//| Indicator Properties                                             |
//+------------------------------------------------------------------+
#property indicator_separate_window

#property indicator_buffers 2
#property indicator_plots   2

#property indicator_type1   DRAW_HISTOGRAM
#property indicator_color1  Red
#property indicator_width1  2

#property indicator_type2   DRAW_HISTOGRAM
#property indicator_color2  SteelBlue
#property indicator_width2  2

//+------------------------------------------------------------------+
//| Input parameters of indicator                                    |
//+------------------------------------------------------------------+
input datetime StartTime=D'2010.04.04';   // start calculations from this date

//+------------------------------------------------------------------+
//| Data Buffers
//+------------------------------------------------------------------+
double ExtBuyBuffer[];
double ExtSellBuffer[];

//+------------------------------------------------------------------+
//| Variables
//+------------------------------------------------------------------+
// declaring indicator class
CBsvSqlite bsv;
//+------------------------------------------------------------------+
//| OnInit
//+------------------------------------------------------------------+
int OnInit()
  {
// setting indicator properties
   IndicatorSetString(INDICATOR_SHORTNAME,"BuySellVolume");
   IndicatorSetInteger(INDICATOR_DIGITS,2);
// buffer for 'buy'
   SetIndexBuffer(0,ExtBuyBuffer,INDICATOR_DATA);
   PlotIndexSetString(0,PLOT_LABEL,"Buy");
// buffer for 'sell'
   SetIndexBuffer(1,ExtSellBuffer,INDICATOR_DATA);
   PlotIndexSetString(1,PLOT_LABEL,"Sell");

// calling the Init function of indicator class
   bsv.Init();

// setting the timer to load ticks into database
   EventSetTimer(60);

   return(0);
  }
//+------------------------------------------------------------------+
//| OnDeinit
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   EventKillTimer();
// if there are unsaved data left, then save them
   bsv.SaveData();
  }
//+------------------------------------------------------------------+
//| OnCalculate
//+------------------------------------------------------------------+
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[])
  {
   if(prev_calculated==0)
     {
      // calculating the time of the nearest bar
      datetime st[];
      CopyTime(Symbol(),Period(),StartTime,1,st);
      // loading data
      bsv.LoadData(st[0],time,ExtBuyBuffer,ExtSellBuffer);
     }

// processing incoming tick
   bsv.ProcessTick(ExtBuyBuffer,ExtSellBuffer);

   return(rates_total);
  }
//+------------------------------------------------------------------+
//| OnTimer
//+------------------------------------------------------------------+
void OnTimer()
  {
// saving data
   bsv.SaveData();
  }

함수 클래스만 변경되었으며 코드 내 나머지 부분은 그대로입니다.

이제 인디케이터의 3번째 버전이 구현되었습니다 - 결과를 보시죠.

 

5번 그림. The BuySellVolume indicator linked to SQLite 3.6 database on EURUSD M5

한편 Sql Server Management Studio와는 달리 SQLite에는 데이터베이스를 다루는 표준 유틸리티가 없습니다. 그러므로 "블랙 박스"를 처리해야하는 꼴을 보기 전에 서드 파티 개발자들로부터 적절한 유틸리티를 받아야합니다. 개인적으로 저는 SQLiteMan을 좋아합니다 - 쓰기 편할뿐더러 필요한 기능은 전부 다 담겨 때문입니다. 여기에서 다운로드 받으실 수 있습니다: http://sourceforge.net/projects/sqliteman/.

마치며

만약 이 줄을 읽고계시다면, 전부 끝났습니다 ;) 솔직히 말해서 이 문서가 이렇게 길어질 줄 예상하지 못했습니다. 그러니 대답해야할 질문들이 있기 마련이죠.

보시다시피 어떠한 솔루션이건 각각 장단점이 있습니다. 첫 번째 모델은 독립성에 따라, 두 번째 모델은 성능에 따라, 세 번째 모델은 휴대성에 따라 다릅니다. 어느걸 고를 것인가 - 당신에게 달려있습니다.

구현한 인디케이터는 과연 쓸만한가? 이 또한 독자분이 판단하실 내용입니다. 만약 제게 묻는다면 - 꽤나 흥미로운 표본이라고 하겠습니다.

일단 여기서 끝마치겠습니다. 나중에 봅시다!

아카이브 내용물 설명:

 # 파일명 설명
1
 Sources_en.zip
 모든 인디케이터의 소스 코드와 AdoSuite 라이브러리. 터미널 내의 올바른 폴더에 압축해제시켜야합니다 인디케이터의 용도: 데이터베이스 사용 없이 (BuySellVolume.mq5), SQL Server 2008 데이터베이스용 (BuySellVolume SqlServer.mq5) SQLite 데이터베이스용 (BuySellVolume SQLite.mq5).
2
 BuySellVolume-DB-SqlServer.zip
 SQL Server 2008 데이터베이스 아카이브*
3
 BuySellVolume-DB-SQLite.zip
 SQLite 데이터베이스 아카이브*
4
 System.Data.SQLite.zip
 System.Data.SQLite.dll 아카이브, SQLite 데이터베이스로 작업하는데 필요함
  5  Databases_MQL5_doc_en.zip  소스 코드, 인디케이터 및 AdoSuite 라이브러리 문서 아카이브

* 양쪽 데이터베이스 모두 4월 5일~9일 사이의 이하의 화폐쌍에 대한 틱 인디케이터 데이터를 담고있음: AUDNZD, EURUSD, GBPUSD, USDCAD, USDCHF, USDJPY.

MetaQuotes 소프트웨어 사를 통해 러시아어가 번역됨.
원본 기고글: https://www.mql5.com/ru/articles/69

MetaTrader 5: 이메일을 통해 거래 전망과 실시간 거래 명세서를 블로그, SNS, 지정된 웹사이트에 게시하기 MetaTrader 5: 이메일을 통해 거래 전망과 실시간 거래 명세서를 블로그, SNS, 지정된 웹사이트에 게시하기
이 문서는 MetaTrader 5를 통해 시장 예상을 게시할 수 있게 해주는 레디메이드 솔루션을 다룰 것입니다. MetaTrader 명세서를 게시하기 위해 전용 웹 사이트를 사용하는 것에서부터 웹 프로그래밍 경험이 거의 필요 없는 자체 웹 사이트를 구축하는 것, 그리고 마지막으로 많은 독자들이 예측에 참여하고 따를 수 있는 소셜 네트워크 마이크로블로깅 서비스와의 통합에 이르기까지 다양한 아이디어를 다루고 있습니다. 이 문서에서 다룰 솔루션들은 100% 무료이며, 이메일이나 ftp 서비스에 대한 기초적인 지식이 있는 독자라면 누구건 따라할 수 있습니다. 전문 호스팅 및 상업 거래 예측 서비스에 동일한 기술을 사용하는 데도 어렵지 않습니다.
MQL4에서 MQL5로 인디케이터 넘기기 MQL4에서 MQL5로 인디케이터 넘기기
이 문서에서는 MQL4로 쓰인 가격 생성을 MQL5로 넘길때의 특이성에 대해 다뤄보겠습니다. MQL4에서 인디케이터 계산을 MQL5로 보다 쉽게 이전하기 위해선 mql4_2_mql5.mqh 라이브러리 함수를 추천드립니다. 기본적으로 활용법은 MACD, Stochastic 그리고 RSI 인디케이터 이전의 기초입니다.
MQL4에서 MQL5로 이전하기 MQL4에서 MQL5로 이전하기
이 문서는 MQL4 언어 함수에 대한 간략한 가이드로, 프로그램을 MQL4에서 MQL5로 이전하는데에 도움을 드릴 것입니다. MQL4 함수마다 (거래 함수 제외) 설명과 대응하는 MQL5쪽의 구현이 적혀있어 이전하는데에 드는 시간을 눈에 띄게 줄여줄 것입니다.. 편의를 위해 MQL4 함수들은 MQL4 레퍼런스처럼 그룹별로 나뉘어있습니다.
MQL5으로 "스네이크" 게임 만들기 MQL5으로 "스네이크" 게임 만들기
본 문서에서는 "스네이크" 게임을 만드는 법에 대해서 설명하겠습니다. MQL5에서 게임 프로그래밍은 이벤트 핸들러 기능 덕분에 가능하게 되었다고 볼 수 있습니다. 객체 지향 프로그래밍이기에 이 프로세스가 크게 간소화됩니다. 이 문서에서는 이벤트 처리 기능, 표준 MQL5 라이브러리 클래스의 사용 예, 정기적 함수 호출에 대하여 살펴보겠습니다.