English Русский 中文 Español Deutsch 日本語 Português Français Italiano Türkçe
소셜 테크 스타트업 만들기 Pt. 2: MQL5 REST 클라이언트 프로그래밍

소셜 테크 스타트업 만들기 Pt. 2: MQL5 REST 클라이언트 프로그래밍

MetaTrader 5트레이딩 | 12 10월 2021, 14:53
97 0
laplacianlab
[삭제]

개요

이전 글에서는 '사회적 의사결정 지원 시스템(SDSS)'의 구조를 알아봤습니다. 한 쪽은 MetaTrader5 터미널에서 서버 측으로 EA의 자동 결정을 전달하죠. 반대쪽에서는 Slim을 기반으로 하는 트위터 애플리케이션이 거래 시그널을 수신하고 MySQL 데이터베이스에 저장한 후 트윗을 작성합니다. 해당 SDSS의 목표는 로봇의 신호를 기반으로 하는 인간의 행동을 기록하고 그에 따른 인간의 의사결정을 하는 것인데요. 로봇의 신호가 매우 방대한 전문가 집단에게 노출되기 때문에 가능한 일입니다.

이번에는 MQL5 언어를 이용해 SDSS의 클라이언트 사이드를 개발해 보겠습니다. 여러 방법을 살펴보면서 각각의 장단점도 논의할 겁니다. 끝으로는 액스퍼트 어드바이저에서 거래 시그널을 수신하는 PHP REST API를 만들 거고요. 이에 앞서 우선 클라이언트 사이드 프로그래밍에 필요한 것들을 알아보죠.

MQL5 거래 시그널을 트위터에 올려 보세요!

MQL5 거래 시그널을 트위터에 올려 보세요!


1. SDSS 클라이언트 사이드

1.1. OnTimer 이벤트로 거래 시그널 트윗하기

OnTimer 이벤트를 통한 거래 시그널 전송 방법은 매우 간단합니다. 간단한 예제를 살펴본 후 그 핵심을 일반 EA에 적용해 보겠습니다.

dummy_ontimer.mq5

#property copyright     "Author: laplacianlab, CC Attribution-Noncommercial-No Derivate 3.0"
#property link          "https://www.mql5.com/en/users/laplacianlab"
#property version       "1.00"
#property description   "Simple REST client built on the OnTimer event for learning purposes."

int OnInit()
  {          
   EventSetTimer(10);    
   return(0);  
  }
  
void OnDeinit(const int reason)
  {  
  }
  
void OnTimer()
  {
//--- REST client's HTTP vars
   string uri="http://api.laplacianlab.com/signal/add";
   char post[];
   char result[];
   string headers;
   int res;
   string signal = "id_ea=1&symbol=AUDUSD&operation=BUY&value=0.9281&";
   StringToCharArray(signal,post);
//--- reset last error
   ResetLastError();
//--- post data to REST API
   res=WebRequest("POST",uri,NULL,NULL,50,post,ArraySize(post),result,headers);
//--- check errors
   if(res==-1)
     {
      Print("Error code =",GetLastError());
      //--- maybe the URL is not added, show message to add it
      MessageBox("Add address '"+uri+"' in Expert Advisors tab of the Options window","Error",MB_ICONINFORMATION);
     }
   else
     {
      //--- successful
      Print("REST client's POST: ",signal);
      Print("Server response: ",CharArrayToString(result,0,-1));
     }         
  }
-->

MQL5의 WebRequest 함수가 해당 클라이언트 애플리케이션의 중심이 됩니다.

HTTP를 이용하는 커스텀 MQL5 컴포넌트를 만드는 것도 한 방법입니다만 이 새로운 기능을 이용하는 것이 더 안전합니다.

위의 MQL5 프로그램은 다음의 아웃풋을 생산합니다.

OR      0       15:43:45.363    RESTClient (EURUSD,H1)  REST client's POST: id_ea=1&symbol=AUDUSD&operation=BUY&value=0.9281&
KK      0       15:43:45.365    RESTClient (EURUSD,H1)  Server response: {"id_ea":"1","symbol":"AUDUSD","operation":"BUY","value":"0.9281","id":77}
PD      0       15:43:54.579    RESTClient (EURUSD,H1)  REST client's POST: id_ea=1&symbol=AUDUSD&operation=BUY&value=0.9281&
CE      0       15:43:54.579    RESTClient (EURUSD,H1)  Server response: {"status": "ok", "message": {"text": "Please wait until the time window has elapsed."}}
ME      0       15:44:04.172    RESTClient (EURUSD,H1)  REST client's POST: id_ea=1&symbol=AUDUSD&operation=BUY&value=0.9281&
JD      0       15:44:04.172    RESTClient (EURUSD,H1)  Server response: {"status": "ok", "message": {"text": "Please wait until the time window has elapsed."}}
NE      0       15:44:14.129    RESTClient (EURUSD,H1)  REST client's POST: id_ea=1&symbol=AUDUSD&operation=BUY&value=0.9281&
ID      0       15:44:14.129    RESTClient (EURUSD,H1)  Server response: {"status": "ok", "message": {"text": "Please wait until the time window has elapsed."}}
NR      0       15:44:24.175    RESTClient (EURUSD,H1)  REST client's POST: id_ea=1&symbol=AUDUSD&operation=BUY&value=0.9281&
IG      0       15:44:24.175    RESTClient (EURUSD,H1)  Server response: {"status": "ok", "message": {"text": "Please wait until the time window has elapsed."}}
MR      0       15:44:34.162    RESTClient (EURUSD,H1)  REST client's POST: id_ea=1&symbol=AUDUSD&operation=BUY&value=0.9281&
JG      0       15:44:34.162    RESTClient (EURUSD,H1)  Server response: {"status": "ok", "message": {"text": "Please wait until the time window has elapsed."}}
PR      0       15:44:44.179    RESTClient (EURUSD,H1)  REST client's POST: id_ea=1&symbol=AUDUSD&operation=BUY&value=0.9281&
CG      0       15:44:44.179    RESTClient (EURUSD,H1)  Server response: {"status": "ok", "message": {"text": "Please wait until the time window has elapsed."}}
HS      0       15:44:54.787    RESTClient (EURUSD,H1)  REST client's POST: id_ea=1&symbol=AUDUSD&operation=BUY&value=0.9281&
KJ      0       15:44:54.787    RESTClient (EURUSD,H1)  Server response: {"id_ea":"1","symbol":"AUDUSD","operation":"BUY","value":"0.9281","id":78}
DE      0       15:45:04.163    RESTClient (EURUSD,H1)  REST client's POST: id_ea=1&symbol=AUDUSD&operation=BUY&value=0.9281&
OD      0       15:45:04.163    RESTClient (EURUSD,H1)  Server response: {"status": "ok", "message": {"text": "Please wait until the time window has elapsed."}}
-->

이 경우 서버는 다음의 메세지로 응답하게 되죠.

{"status": "ok", "message": {"text": "Please wait until the time window has elapsed."}}
-->

간단한 보안 메커니즘이 비정상적으로 활동량이 많은 스캘핑 로봇으로부터 API를 보호하기 때문입니다.

/**
 * REST method.
 * Adds and tweets a new trading signal.
 */
$app->post('/signal/add', function() {
    $tweeterer = new Tweeterer();
    // This condition is a simple mechanism to prevent hyperactive scalpers
    if ($tweeterer->canTweet($tweeterer->getLastSignal(1)->created_at, '1 minute'))
    {
        $signal = (object)($_POST);
        $signal->id = $tweeterer->addSignal(1, $signal);
        $tokens = $tweeterer->getTokens(1);
        $connection = new TwitterOAuth(
            API_KEY, 
            API_SECRET, 
            $tokens->access_token, 
            $tokens->access_token_secret);
        $connection->host = "https://api.twitter.com/1.1/";
        $ea = new EA();
        $message = "{$ea->get($signal->id_ea)->name} on $signal->symbol. $signal->operation at $signal->value";
        $connection->post('statuses/update', array('status' => $message));           
        echo '{"status": "ok", "message": {"text": "Signal processed."}}';
    }   
});
-->

해당 메커니즘은 웹 서버가 HTTP 요청에 대한 악성 여부를 확인한 후 실행됩니다.

다시 말해 웹 서버가 이런 공격으로부터 보호해 주는 것이죠. 예를 들어, 아파치 HTTP서버는 evasive 모듈과 security 모듈을 결합해 공격을 막습니다.

이게 바로 잘 알려진 mod_evasive 보안 모듈이죠. 서버 관리자가 애플리케이션의 초당 허용 HTTP 요청 개수 등을 컨트롤 할 수 있게 됩니다.

<IfModule mod_evasive20.c>
DOSHashTableSize    3097
DOSPageCount        2
DOSSiteCount        50
DOSPageInterval     1
DOSSiteInterval     1
DOSBlockingPeriod   60
DOSEmailNotify someone@somewhere.com
</IfModule>
-->

따라서 canTweet PHP 메소드의 목적은 SDSS가 HTTP 공격으로 분류하지 않는 스캘핑 로봇을 차단하는 것이죠. canTweet 메소드는 후술할 Twetterer 클래스를 통해 구현됩니다.

/**
 * Checks if it's been long enough so that the tweeterer can tweet again
 * @param string $timeLastTweet e.g. 2014-07-05 15:26:49
 * @param string $timeWindow A time window, e.g. 1 hour
 * @return boolean
 */
public function canTweet($timeLastTweet=null, $timeWindow=null)
{
    if(!isset($timeLastTweet)) return true;
    $diff = time() - strtotime($timeLastTweet);
    switch($timeWindow)
    {
        case '1 minute';                
            $diff <= 60 ? $canTweet = false : $canTweet = true;                
            break;                
        case '1 hour';                
            $diff <= 3600 ? $canTweet = false : $canTweet = true;                
            break;                
        case '1 day':                                
            $diff <= 86400 ? $canTweet = false : $canTweet = true;                
            break;
        default:                
            $canTweet = false;                
            break;                
    } 
    if($canTweet)
    {
        return true;
    }
    else 
    {
        throw new Exception('Please wait until the time window has elapsed.');
    }
}
-->

우선 WebRequest가 자동으로 생성한 HTTP 요청 헤더 필드를 살펴보겠습니다.

Content-Type: application/x-www-form-urlencoded
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, */*
-->

WebRequest의 POST는 프로그래머가 HTML 형식 데이터를 전송할 것으로 가정합니다. 우리의 경우 다음의 HTTP 요청 헤드 필드를 보내고자 합니다.

Content-Type: application/json
Accept: application/json
-->

개발자에게 은빛 총알은 없잖아요? 결정한 대로 WebRequest를 꼼꼼히 살펴보겠습니다. 어떤 장단점이 있는지도요.

기술적 관점에서 따져 보면 REST를 이용하는 게 더 맞겠지만 앞서 말했다시피 HTTP 대신 MetaQuotes 사가 제공하는 기능을 사용하는 편이 안전합니다. WebRequest()가 웹 서비스가 아닌 웹 페이지 용으로 개발된 것 같긴 하지만요. 바로 이런 이유 때문에 클라이언트의 거래 시그널에 URL 인코딩을 사용할 겁니다. URL 인코딩이 된 시그널을 API가 전달 받아 PHP stdClass 형식으로 변환하게 됩니다.

WebRequest() 함수를 이용하는 대신 wininet.dll 라이브러리를 이용해 운영 체제와 비슷한 수준에서 작동하는 커스텀 MQL5 컴포넌트를 작성할 수도 있습니다. WinInet.dll로 온라인 터미널 데이터 교환하기MQL5 Winlnet 적용법 Pt 2: POST 요청 및 파일에서 해당 접근법의 원리를 찾아볼 수 있습니다. 하지만 MQL5 개발자나 MQL5 이용자들의 경험에 따르면 위의 방법은 보기보다 어려운 것 같다는 군요. MetaTrader의 버전이 업그레이드될 경우 WinINet 함수 호출이 종료될 수 있기 때문이죠.

1.2. EA 거래 시그널 트윗하기

이제 위에서 설명한 내용을 확대 적용해 볼게요. 스캘핑과 서비스 거부 공격 관련 문제를 보여드리기 위해 다음의 더미 로봇을 작성했습니다.

Dummy.mq5

//+------------------------------------------------------------------+
//|                                                        Dummy.mq5 |
//|                               Copyright © 2014, Jordi Bassagañas |
//+------------------------------------------------------------------+
#property copyright     "Author: laplacianlab, CC Attribution-Noncommercial-No Derivate 3.0"
#property link          "https://www.mql5.com/en/users/laplacianlab"
#property version       "1.00"
#property description   "Dummy REST client (for learning purposes)."
//+------------------------------------------------------------------+
//| Trade class                                                      |
//+------------------------------------------------------------------+
#include <Trade\Trade.mqh>
//+------------------------------------------------------------------+
//| Declaration of variables                                         |
//+------------------------------------------------------------------+
CPositionInfo PositionInfo;
CTrade trade;
MqlTick tick;
int stopLoss = 20;
int takeProfit = 20;
double size = 0.1;
//+------------------------------------------------------------------+
//| Tweet trading signal                                             |
//+------------------------------------------------------------------+   
void Tweet(string uri, string signal)
  {
   char post[];
   char result[];
   string headers;
   int res;
   StringToCharArray(signal,post);
//--- reset last error
   ResetLastError();
//--- post data to REST API
   res=WebRequest("POST",uri,NULL,NULL,50,post,ArraySize(post),result,headers);
//--- check errors
   if(res==-1)
     {
      //--- error
      Print("Error code =",GetLastError());
     }
   else
     {
      //--- successful
      Print("REST client's POST: ",signal);
      Print("Server response: ",CharArrayToString(result,0,-1));
     }         
  }
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {                
   return(0);  
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+  
void OnDeinit(const int reason)
  {  
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+  
void OnTick()
  {
//--- update tick
   SymbolInfoTick(_Symbol, tick);
//--- calculate Take Profit and Stop Loss levels
   double tp;
   double sl;   
   sl = tick.ask + stopLoss * _Point;
   tp = tick.bid - takeProfit * _Point;
//--- open position
   trade.PositionOpen(_Symbol,ORDER_TYPE_SELL,size,tick.bid,sl,tp);
//--- trade URL-encoded signal "id_ea=1&symbol=AUDUSD&operation=BUY&value=0.9281&";
   string signal = "id_ea=1&symbol=" + _Symbol + "&operation=SELL&value=" + (string)tick.bid + "&";
   Tweet("http://api.laplacianlab.com/signal/add",signal);
}
-->

정말 쉬운 코드죠. 이 엑스퍼트 어드바이저는 틱이 발생할 때마다 하나의 숏 포지션을 생성합니다. 따라서 짧은 시간 내에 많은 포지션을 생성할 가능성이 매우 높죠. 특히 유동성이 높은 시간에 해당 로봇을 실행하는 경우에는 더더욱이요. 걱정할 필요는 없습니다. 서버 사이드가 웹 서버가 DoS 공격을 막을 수 있도록 설정하고 PHP 애플리케이션에 특정한 시간대를 정해주어 트윗 작성 간격을 조정하거든요.

이제 여러분의 액스퍼트 어드바이저에 트윗 기능을 적용해 볼 차례입니다.

1.3. 유저들이 트윗된 거래 시그널을 어떻게 보나요?

다음의 예시에서 @laplacianlab은 SDSS의 더미 EA 시그널 트윗을 허용합니다.

그림 1. SDSS에게 트윗 작성을 허용하는 @laplacianlab

그림 1. SDSS에게 트윗 작성을 허용한 @laplacianlab

참고로 볼린저 밴드가 나타나는 이유는 우리가 이전에 MySQL 데이터베이스에 저장했던 예제를 사용하기 때문입니다. id_ea=1가 '볼린저 밴드' 값을 가지니까요. '더미'같은 걸로 바꾸는 게 더 나았겠네요. 크게 상관은 없지만 혹시 헷갈렸다면 미안합니다.

최종 MySQL 데이터베이스는 다음과 같습니다.

# MySQL database creation...

CREATE DATABASE IF NOT EXISTS laplacianlab_com_sdss;

use laplacianlab_com_sdss;

CREATE TABLE IF NOT EXISTS twitterers (
    id mediumint UNSIGNED NOT NULL AUTO_INCREMENT, 
    twitter_id VARCHAR(255),
    access_token TEXT,
    access_token_secret TEXT,
    created_at TIMESTAMP NOT NULL DEFAULT NOW(),
    PRIMARY KEY (id)
) ENGINE=InnoDB;

CREATE TABLE IF NOT EXISTS eas (
    id mediumint UNSIGNED NOT NULL AUTO_INCREMENT, 
    name VARCHAR(32),
    description TEXT,
    created_at TIMESTAMP NOT NULL DEFAULT NOW(),    
    PRIMARY KEY (id)
) ENGINE=InnoDB;

CREATE TABLE IF NOT EXISTS signals (
    id int UNSIGNED NOT NULL AUTO_INCREMENT,
    id_ea mediumint UNSIGNED NOT NULL,
    id_twitterer mediumint UNSIGNED NOT NULL,
    symbol VARCHAR(10) NOT NULL,
    operation VARCHAR(6) NOT NULL,
    value DECIMAL(9,5) NOT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT NOW(),
    PRIMARY KEY (id),
    FOREIGN KEY (id_ea) REFERENCES eas(id),
    FOREIGN KEY (id_twitterer) REFERENCES twitterers(id)
) ENGINE=InnoDB;

# Dump some sample data...

# As explained in Part I, there's one single twitterer

INSERT INTO eas(name, description) VALUES
('Bollinger Bands', '<p>Robot based on Bollinger Bands. Works with H4 charts.</p>'),
('Two EMA', '<p>Robot based on the crossing of two MA. Works with H4 charts.</p>');
-->


2. SDSS 서버 사이드

SDSS 서버 사이드 구축에 앞서 다음의 디렉토리 구조를 다시 한번 살피겠습니다.

그림 2. Slim 기반 PHP API 디렉토리 구조

그림 2. Slim 기반 PHP API 디렉토리 구조

2.1. PHP API 코드

앞서 설명한 대로 index.php 파일은 다음과 같은 모습일 겁니다.

<?php
/**
 * Laplacianlab's SDSS - A REST API for tweeting MQL5 trading signals
 *
 * @author      Jordi Bassagañas
 * @copyright   2014 Jordi Bassagañas
 * @link        https://www.mql5.com/en/users/laplacianlab
 */

/* Bootstrap logic */
require_once 'config/config.php';
set_include_path(get_include_path() . PATH_SEPARATOR . APPLICATION_PATH . '/vendor/');
set_include_path(get_include_path() . PATH_SEPARATOR . APPLICATION_PATH . '/model/');
require_once 'slim/slim/Slim/Slim.php';
require_once 'abraham/twitteroauth/twitteroauth/twitteroauth.php';
require_once 'Tweeterer.php';
require_once 'EA.php';
session_start();

/* Init Slim */
use \Slim\Slim;
Slim::registerAutoloader();
$app = new Slim(array('debug' => false));
$app->response->headers->set('Content-Type', 'application/json');

/**
 * Slim's exception handler
 */
$app->error(function(Exception $e) use ($app) {
    echo '{"status": "error", "message": {"text": "' . $e->getMessage() . '"}}';
});

/**
 * REST method.
 * Custom 404 error.
 */
$app->notFound(function () use ($app) {
    echo '{"status": "error 404", "message": {"text": "Not found."}}';
});

/**
 * REST method.
 * Home page.
 */
$app->get('/', function () {
    echo '{"status": "ok", "message": {"text": "Service available, please check API."}}';
});

/**
 * REST method.
 * Adds and tweets a new trading signal.
 */
$app->post('/signal/add', function() {
    $tweeterer = new Tweeterer();
    // This condition is a simple mechanism to prevent hyperactive scalpers
    if ($tweeterer->canTweet($tweeterer->getLastSignal(1)->created_at, '1 minute'))
    {
        $signal = (object)($_POST);
        $signal->id = $tweeterer->addSignal(1, $signal);
        $tokens = $tweeterer->getTokens(1);
        $connection = new TwitterOAuth(
            API_KEY, 
            API_SECRET, 
            $tokens->access_token, 
            $tokens->access_token_secret);
        $connection->host = "https://api.twitter.com/1.1/";
        $ea = new EA();
        $message = "{$ea->get($signal->id_ea)->name} on $signal->symbol. $signal->operation at $signal->value";
        $connection->post('statuses/update', array('status' => $message));           
        echo '{"status": "ok", "message": {"text": "Signal processed."}}';
    }   
});

/**
 * REST implementation with TwitterOAuth.
 * Gives permissions to Laplacianlab's SDSS to tweet on the user's behalf.
 * Please, visit https://github.com/abraham/twitteroauth
 */
$app->get('/tweet-signals', function() use ($app) {   
    if (empty($_SESSION['twitter']['access_token']) || empty($_SESSION['twitter']['access_token_secret']))
    {
        $connection = new TwitterOAuth(API_KEY, API_SECRET);
        $request_token = $connection->getRequestToken(OAUTH_CALLBACK);
        if ($request_token)
        {
            $_SESSION['twitter'] = array(
                'request_token' => $request_token['oauth_token'],
                'request_token_secret' => $request_token['oauth_token_secret']
            );
            switch ($connection->http_code) 
            {
                case 200:
                    $url = $connection->getAuthorizeURL($request_token['oauth_token']);                    
                    // redirect to Twitter
                    $app->redirect($url);
                    break;
                default:
                    throw new Exception('Connection with Twitter failed.');
                break;
            }
        }
        else 
        {
            throw new Exception('Error Receiving Request Token.');
        }
    } 
    else 
    {    
        echo '{"status": "ok", "message": {"text": "Laplacianlab\'s SDSS can '
        . 'now access your Twitter account on your behalf. Please, if you no '
        . 'longer want this, log in your Twitter account and revoke access."}}';
    }    
});

/**
 * REST implementation with TwitterOAuth.
 * This is the OAuth callback of the method above. 
 * Stores the access tokens into the database.
 * Please, visit https://github.com/abraham/twitteroauth
 */
$app->get('/twitter/oauth_callback', function() use ($app) {
    if(isset($_GET['oauth_token']))
    {
        $connection = new TwitterOAuth(
            API_KEY, 
            API_SECRET, 
            $_SESSION['twitter']['request_token'], 
            $_SESSION['twitter']['request_token_secret']);
        $access_token = $connection->getAccessToken($_REQUEST['oauth_verifier']);
        if($access_token)
        {       
            $connection = new TwitterOAuth(
                API_KEY, 
                API_SECRET, 
                $access_token['oauth_token'], 
                $access_token['oauth_token_secret']);
            // Set Twitter API version to 1.1.
            $connection->host = "https://api.twitter.com/1.1/";
            $params = array('include_entities' => 'false');
            $content = $connection->get('account/verify_credentials', $params);    
            if($content && isset($content->screen_name) && isset($content->name))
            {
                $tweeterer = new Tweeterer();                
                $data = (object)array(
                    'twitter_id' => $content->id, 
                    'access_token' => $access_token['oauth_token'],
                    'access_token_secret' => $access_token['oauth_token_secret']);                
                $tweeterer->exists($content->id) 
                        ? $tweeterer->update($data) 
                        : $tweeterer->create($data);
                echo '{"status": "ok", "message": {"text": "Laplacianlab\'s SDSS can '
                . 'now access your Twitter account on your behalf. Please, if you no '
                . 'longer want this, log in your Twitter account and revoke access."}}';        
                session_destroy();
            }
            else
            {
                throw new Exception('Login error.');                
            }
        }
    } 
    else
    {
        throw new Exception('Login error.');
    }
});

/**
 * Run Slim!
 */
$app->run();
-->

2.2. MySQL OOP 랩퍼

이제 Slim의 모델 디렉토리에 PHP 클래스인 Tweeterer.php와 EA.php 파일을 생성할 차례입니다. 별도의 레이어를 개발하는 대신에 간단한 객체 지향 클래스를 이용해 MySQL 테이블을 랩핑할 겁니다.

model\Tweeterer.php

<?php
require_once 'DBConnection.php';
/**
 * Tweeterer's simple OOP wrapper
 *
 * @author      Jordi Bassagañas
 * @copyright   2014 Jordi Bassagañas
 * @link        https://www.mql5.com/en/users/laplacianlab
 */
class Tweeterer
{   
    /**
     * @var string MySQL table
     */
    protected $table = 'twitterers';
    /**
     * Gets the user's OAuth tokens
     * @param integer $id
     * @return stdClass OAuth tokens: access_token and access_token_secret
     */
    public function getTokens($id)
    {
        $sql = "SELECT access_token, access_token_secret FROM $this->table WHERE id=$id";
        return DBConnection::getInstance()->query($sql)->fetch_object();        
    }    
    /**
     * Checks if it's been long enough so that the tweeterer can tweet again
     * @param string $timeLastTweet e.g. 2014-07-05 15:26:49
     * @param string $timeWindow A time window, e.g. 1 hour
     * @return boolean
     */
    public function canTweet($timeLastTweet=null, $timeWindow=null)
    {
        if(!isset($timeLastTweet)) return true;
        $diff = time() - strtotime($timeLastTweet);
        switch($timeWindow)
        {
            case '1 minute';                
                $diff <= 60 ? $canTweet = false : $canTweet = true;                
                break;                
            case '1 hour';                
                $diff <= 3600 ? $canTweet = false : $canTweet = true;                
                break;                
            case '1 day':                                
                $diff <= 86400 ? $canTweet = false : $canTweet = true;                
                break;
            default:                
                $canTweet = false;                
                break;                
        } 
        if($canTweet)
        {
            return true;
        }
        else 
        {
            throw new Exception('Please wait until the time window has elapsed.');
        }
    }
    /**
     * Adds a new signal
     * @param type $id_twitterer
     * @param stdClass $data
     * @return integer The new row id
     */
    public function addSignal($id_twitterer, stdClass $data)
    {
        $sql = 'INSERT INTO signals(id_ea, id_twitterer, symbol, operation, value) VALUES (' 
            . $data->id_ea . ","
            . $id_twitterer . ",'"    
            . $data->symbol . "','"
            . $data->operation . "',"
            . $data->value . ')'; 
        DBConnection::getInstance()->query($sql);        
        return DBConnection::getInstance()->getHandler()->insert_id;  
    }
    /**
     * Checks whether the given twitterer exists
     * @param string $id
     * @return boolean
     */
    public function exists($id)
    {
        $sql = "SELECT * FROM $this->table WHERE twitter_id='$id'";        
        $result = DBConnection::getInstance()->query($sql);        
        return (boolean)$result->num_rows;
    }    
    /**
     * Creates a new twitterer
     * @param stdClass $data
     * @return integer The new row id
     */
    public function create(stdClass $data)
    {
        $sql = "INSERT INTO $this->table(twitter_id, access_token, access_token_secret) "
            . "VALUES ('"
            . $data->twitter_id . "','"
            . $data->access_token . "','"
            . $data->access_token_secret . "')";        
        DBConnection::getInstance()->query($sql);
        return DBConnection::getInstance()->getHandler()->insert_id;
    }    
    /**
     * Updates the twitterer's data
     * @param stdClass $data
     * @return Mysqli object
     */
    public function update(stdClass $data)
    {
        $sql = "UPDATE $this->table SET "
            . "access_token = '" . $data->access_token . "', "
            . "access_token_secret = '" . $data->access_token_secret . "' "
            . "WHERE twitter_id ='" . $data->twitter_id . "'";        
        return DBConnection::getInstance()->query($sql);
    }    
    /**
     * Gets the last trading signal sent by the twitterer
     * @param type $id The twitterer id
     * @return mixed The last trading signal
     */
    public function getLastSignal($id)
    {
        $sql = "SELECT * FROM signals WHERE id_twitterer=$id ORDER BY id DESC LIMIT 1";
        $result = DBConnection::getInstance()->query($sql);
        if($result->num_rows == 1)
        {
            return $result->fetch_object();
        }
        else
        {
            $signal = new stdClass;
            $signal->created_at = null;
            return $signal;
        }
    }
}
-->

model\EA.php

<?php
require_once 'DBConnection.php';
/**
 * EA's simple OOP wrapper
 *
 * @author      Jordi Bassagañas
 * @copyright   2014 Jordi Bassagañas
 * @link        https://www.mql5.com/en/users/laplacianlab
 */
class EA
{   
    /**
     * @var string MySQL table
     */
    protected $table = 'eas';
    /**
     * Gets an EA by id
     * @param integer $id
     * @return stdClass
     */
    public function get($id)
    {
        $sql = "SELECT * FROM $this->table WHERE id=$id";     
        return DBConnection::getInstance()->query($sql)->fetch_object();
    }
}
-->

model\DBConnection.php

<?php
/**
 * DBConnection class
 * 
 * @author      Jordi Bassagañas
 * @copyright   2014 Jordi Bassagañas
 * @link        https://www.mql5.com/en/users/laplacianlab
 */
class DBConnection 
{ 
    /**
     * @var DBConnection Singleton instance
     */
    private static $instance;
    /**
     * @var mysqli Database handler
     */
    private $mysqli;
    /**
     *  Opens a new connection to the MySQL server
     */
    private function __construct()
    { 
        mysqli_report(MYSQLI_REPORT_STRICT);
        try {
            $this->mysqli = new MySQLI(DB_SERVER, DB_USER, DB_PASSWORD, DB_NAME); 
        } catch (Exception $e) {        
            throw new Exception('Unable to connect to the database, please, try again later.');
        }
    } 
    /**
     * Gets the singleton instance
     * @return type
     */
    public static function getInstance()
    {
        if (!self::$instance instanceof self) self::$instance = new self; 
        return self::$instance;
    } 
    /**
     * Gets the database handler
     * @return mysqli
     */
    public function getHandler()
    { 
        return $this->mysqli; 
    } 
    /**
     * Runs the given query
     * @param string $sql
     * @return mixed
     */
    public function query($sql)
    {
        $result = $this->mysqli->query($sql);
        if ($result === false)
        {       
            throw new Exception('Unable to run query, please, try again later.');
        }
        else
        {
            return $result;
        }
    } 
}
-->

결론

이번에는 저번에 소개한 SDSS 클라이언트 사이드를 개발했습니다. 그에 따른 서버 사이드도 만들어 봤죠. 그리고 MQL5 네이티브 함수인 WebRequest()도 드디어 이용해 봤네요. WebRequest() 함수는 웹 서비스에 적용되도록 고안된 함수가 아닙니다. 웹 페이지에 GET과 POST 요청을 하기 위해 만들어진 함수죠. 하지만 완전히 새로운 커스텀 컴포넌트를 만드는 것보다는 안전하기 때문에 본문의 예제에서 사용하게 되었습니다.

MQL5 클라이언트와 PHP 서버 사이에 REST 다이얼로그를 생성할 수 있었다면 훨씬 깔끔했겠지만 WebRequest()를 잘 이용한 덕분에 훨씬 쉬워졌죠. 대신 웹 서비스가 URL 인코딩이 된 시그널을 전달 받아 PHP에 적합한 형식으로 변환합니다.

지금 이 시스템을 테스트 중인데요. 제 개인 거래 시그널 트윗까지는 성공했습니다. 실용적이고, 단일 유저에게는 잘 맞지만 실제 환경에 적용하려면 개선해야 할 부분이 있습니다. 예를 들어, Slim은 데이터베이스-애그노스틱 프레임워크이기 때문에 SQL 삽입에 주의해야 합니다. MetaTrader5 터미널과 PHP 애플리케이션 간 커뮤니케이션 보안에 대해서도 아직 설명하지 않았으니 실제 환경에서는 적용하지 마시길 바랍니다.

MetaQuotes 소프트웨어 사를 통해 영어가 번역됨
원본 기고글: https://www.mql5.com/en/articles/1044

파일 첨부됨 |
database.txt (1.32 KB)
dummy_ontimer.mq5 (1.36 KB)
Dummy.mq5 (3.27 KB)
index.txt (6.08 KB)
Tweeterer.txt (4.59 KB)
EA.txt (0.58 KB)
DBConnection.txt (1.54 KB)
Johnpaul77 시그널 프로바이더: "3년 째 수익을 올리고 있는 전략을 왜 바꿔야 하나요?" Johnpaul77 시그널 프로바이더: "3년 째 수익을 올리고 있는 전략을 왜 바꿔야 하나요?"
MQL5.com 이용자들이 대부분의 시간을 Johnpaul77의 시그널 페이지를 보며 보낸다는 걸 아셨나요? 약 900명의 구독자가 총 570만 달러에 달하는 자금을 투자 중입니다. 그래서 시그널 프로바이더를 인터뷰해봤습니다. 총 네 분이시더라고요! 팀원 간 업무 배분은 어떻게 하는지? 어떤 기술적 도구를 이용하는지? 왜 John Paul이라는 이름을 지었는지? 그리고 마지막으로, 어떻게 인도네시아의 일반 게이머들이 MQL5.com의 최상위 시그널 프로바이더가 되었는지? 본문에서 모두 알아보겠습니다.
MQL5.com 프리랜서 서비스: 개발자 수익(인포그래픽) MQL5.com 프리랜서 서비스: 개발자 수익(인포그래픽)
MQL5 프리랜서 서비스 런칭 4주년을 맞아 지난 4년 간의 서비스 결과를 나타내는 인포그래픽을 준비했습니다. 약 60만 달러에 달하는 1만 개의 주문이 처리되었고, 3천 여 명의 고객과 약 3백 명의 개발자가 프리랜서 서비스를 이용했죠.
최적화 몇 가지 아이디어 최적화 몇 가지 아이디어
최적화 프로세스는 여러분의 컴퓨터 리소스의 상당 부분을 필요로 합니다. MQL5 클라우드 네트워크 테스트 에이전트의 리소스까지 필요로 하는 경우도 있죠. 이번 글에서는 제가 이 과정을 용이하게 하기 위해, 또 MT5 전략 테스터르 개선하기 위해 이용하는 몇 가지 간단한 아이디어를 공유해 보겠습니다. 관련 자료 및 포럼 등을 통해 얻은 아이디어입니다.
개발자들이 사랑하는 MQL5.com 프리랜서 서비스 개발자들이 사랑하는 MQL5.com 프리랜서 서비스
더이상 엑스퍼트 어드바이저 개발 광고를 하지 않아도 됩니다. 이제 투자자들이 직접 찾아오니까요! 이미 수천 명의 투자자들이 MQL5.com의 MQL5 프리랜서 서비스를 이용하고 있습니다. 지난 4년 간 총 3천 여명의 투자자들이 1만 건 이상의 프로젝트를 주문했고요. 게다가 점점 더 많은 투자자와 개발자들이 모여들고 있답니다!