LoginSignup
326

More than 5 years have passed since last update.

遺伝的アルゴリズムでFX自動売買 その3 OandaAPIで実際に取引

Last updated at Posted at 2015-10-13

前回の記事

pythonと遺伝的アルゴリズムで作るFX自動売買システム その1
遺伝的アルゴリズムでFX自動売買 その2 進化する売買AIの実装

今回作るモノ

自動売買システムを作ったときに使ったOandaAPIの紹介と、売買注文の発注機能について書いていこうと思います。

自動売買で使ったOandaAPI

5種類のAPIを使って構築しました。詳細はOandaのAPIDocument参照
OAuth2のTokenは、口座開いてログインするとWebで発行できます。
トークンはcurlコマンドHeaderのAuthorization: Bearer ********に設定します。

"""
01.アカウント情報API
curl -H "Authorization: Bearer ********" https://api-
fxtrade.oanda.com/v1/accounts/12345
"""
{
    "accountId" : 12345,
    "accountName" : "Primary",
    "balance" : 123456.5765,
    "unrealizedPl" : 36.8816,
    "realizedPl" : 235.5839,
    "marginUsed" : 123456.154,
    "marginAvail" : 123456.3041,
    "openTrades" : 20,
    "openOrders" : 0,
    "marginRate" : 0.04,
    "accountCurrency" : "JPY"
}


"""
02.発注API
curl -H "Authorization: Bearer ********" -X POST -d "instrument=EUR_USD&units=2&side=sell&type=market" "https://api-fxtrade.oanda.com/v1/accounts/12345/orders"
"""
{

  "instrument" : "EUR_USD",
  "time" : "2013-12-06T20:36:06Z", // Time that order was executed
  "price" : 1.37041,               // Trigger price of the order
  "tradeOpened" : {
    "id" : 175517237,              // Order id
    "units" : 1000,                // Number of units
    "side" : "buy",                // Direction of the order
    "takeProfit" : 0,              // The take-profit associated with the Order, if any
    "stopLoss" : 0,                // The stop-loss associated with the Order, if any
    "trailingStop" : 0             // The trailing stop associated with the rrder, if any
  },
  "tradesClosed" : [],
  "tradeReduced" : {}
}

"""
03.現在のポジション一覧を取得
curl -H "Authorization: Bearer ####" https://api-fxtrade.oanda.com/v1/accounts/######/positions
"""
{
    "positions" : [
        {
            "instrument" : "AUD_USD",
            "units" : 100,
            "side" : "sell",
            "avgPrice" : 0.78216
        },
        {
            "instrument" : "GBP_USD",
            "units" : 100,
            "side" : "sell",
            "avgPrice" : 1.49128
        },
        {
            "instrument" : "USD_JPY",
            "units" : 850,
            "side" : "buy",
            "avgPrice" : 119.221
        }
}

"""
04.マーケットのレート取得
curl -H "Authorization: Bearer ********" -X GET "https://api-fxtrade.oanda.com/v1/prices?instruments=EUR_USD%2CUSD_JPY%2CEUR_CAD"
"""
{
    "prices" : [
        {
            "instrument" : "EUR_USD",
            "time" : "2015-03-13T20:59:58.165668Z",
            "bid" : 1.04927,
            "ask" : 1.04993,
            "status" : "halted"
        },
        {
            "instrument" : "USD_JPY",
            "time" : "2015-03-13T20:59:58.167818Z",
            "bid" : 121.336,
            "ask" : 121.433,
            "status" : "halted"
        },
        {
            "instrument" : "EUR_CAD",
            "time" : "2015-03-13T20:59:58.165465Z",
            "bid" : 1.34099,
            "ask" : 1.34225,
            "status" : "halted"
        }
    ]
}

"""
05.売買注文の発注状況を調べるAPI
curl -H "Authorization: Bearer ************" https://api-fxtrade.oanda.com/v1/accounts/****/transactions
"""
{
     "id" : 1789536248,
     "accountId" : *****,
     "time" : "2015-03-20T12:17:06.000000Z",
     "type" : "TAKE_PROFIT_FILLED",
     "tradeId" : 1789531428,
     "instrument" : "USD_JPY",
     "units" : 1,
     "side" : "sell",
     "price" : 121.017,
     "pl" : 0.02,
     "interest" : 0,
     "accountBalance" : 33299999.1173
},

          {
     "id" : 1789560748,
     "accountId" : *****,
     "time" : "2015-03-20T12:49:38.000000Z",
     "type" : "STOP_LOSS_FILLED",
     "tradeId" : 1789500620,
     "instrument" : "USD_JPY",
     "units" : 100,
     "side" : "sell",
     "price" : 120.899,
     "pl" : -18.6,
     "interest" : 0,
     "accountBalance" : 33299980.3023
}


APIにアクセスするサンプルコード

価格取得APIの実装例です。urllibのrequestでWebアクセスします。応答はJsonで返ってくるので頑張ってパーサclassを書きます。スプレッドが開いたとき(相場が荒れて買値と売値の差が開くこと)不利なレートのときは発注しない機能を組み込んでみました。

■ サンドボックス用APIにアクセスしているサンプルコードです
http://api-sandbox.oanda.com/v1/prices?instruments=EUR_USD%2CUSD_JPY%2CGBP_USD%2CAUD_USD

price_api.py
# -*- coding: utf-8 -*-
from __future__ import absolute_import
from __future__ import unicode_literals
import urllib
import requests
import time
import ujson
import pytz
from enum import Enum


class CurrencyPair(Enum):
    EUR_USD = 1
    USD_JPY = 2
    GBP_USD = 3
    AUD_USD = 4


class OandaAPIMode(Enum):
    PRODUCTION = 1
    DEVELOP = 2
    SANDBOX = 3
    DUMMY = 4

    @property
    def url_base(self):
        api_base_url_dict = {
            OandaAPIMode.PRODUCTION: 'https://api-fxtrade.oanda.com/',
            OandaAPIMode.DEVELOP: 'https://api-fxpractice.oanda.com/',
            OandaAPIMode.SANDBOX: 'http://api-sandbox.oanda.com/',
        }
        return api_base_url_dict.get(self)

    @property
    def headers(self):
        """
        :rtype : dict
        """
        _base = {
            'Accept-Encoding': 'identity, deflate, compress, gzip',
            'Accept': '*/*', 'User-Agent': 'python-requests/1.2.0',
            'Content-type': 'application/x-www-form-urlencoded',
        }

        if self == OandaAPIMode.SANDBOX:
            return _base
        if self == OandaAPIMode.DEVELOP:
            _base['Authorization'] = 'Bearer {}'.format(get_password('OandaRestAPITokenDemo'))
            return _base
        if self == OandaAPIMode.PRODUCTION:
            _base['Authorization'] = 'Bearer {}'.format(get_password('OandaRestAPIToken'))
            return _base
        raise ValueError


class OandaServiceUnavailableError(Exception):
    """
    サービス停止中のエラー
    """
    pass


class OandaInternalServerError(Exception):
    """
    サービス停止中のエラー
    """
    pass


class OandaAPIBase(object):
    mode = None

    class Meta(object):
        abstract = True

    def __init__(self, mode):
        """
        :param mode: OandaAPIMode
        """
        self.mode = mode

    def requests_api(self, url, payload=None):
        # 通信に失敗したら3回までretryする
        for x in xrange(3):
            response = None
            try:
                if payload:
                    response = requests.post(url, headers=self.mode.headers, data=payload)
                else:
                    response = requests.get(url, headers=self.mode.headers)

                assert response.status_code == 200, response.status_code
                print 'API_Access: {}'.format(url)
                data = ujson.loads(response.text)
                self.check_json(data)
                return data
            except Exception as e:
                if response is None:
                    raise
                if response.text:
                    if str("Service Unavailable") in str(response.text):
                        raise OandaServiceUnavailableError
                    if str("An internal server error occurred") in str(response.text):
                        raise OandaInternalServerError
                time.sleep(3)
                if x >= 2:
                    raise ValueError, response.text
        raise

    def check_json(self, data):
        raise NotImplementedError


class PriceAPI(OandaAPIBase):
    """
    レート確認API

    %2C はカンマ

    curl -H "Authorization: Bearer ********" -X GET "https://api-fxtrade.oanda.com/v1/prices?instruments=EUR_USD%2CUSD_JPY%2CEUR_CAD"

    {
        "prices" : [
            {
                "instrument" : "EUR_USD",
                "time" : "2015-03-13T20:59:58.165668Z",
                "bid" : 1.04927,
                "ask" : 1.04993,
                "status" : "halted"
            },
            {
                "instrument" : "USD_JPY",
                "time" : "2015-03-13T20:59:58.167818Z",
                "bid" : 121.336,
                "ask" : 121.433,
                "status" : "halted"
            },
            {
                "instrument" : "EUR_CAD",
                "time" : "2015-03-13T20:59:58.165465Z",
                "bid" : 1.34099,
                "ask" : 1.34225,
                "status" : "halted"
            }
        ]
    }

    :rtype : dict of PriceAPIModel
    """
    url_base = '{}v1/prices?'

    def get_all(self):
        instruments = ','.join([x.name for x in CurrencyPair])
        instruments = urllib.quote(instruments, '')
        url = self.url_base.format(self.mode.url_base)
        url += 'instruments={}'.format(instruments)
        data = self.requests_api(url)
        d = {}
        for price in data.get('prices'):
            price_model = PriceAPIModel(price)
            d[price_model.currency_pair] = price_model
        return d

    def check_json(self, data):
        assert 'prices' in data


class PriceAPIModel(object):
    """
    レート確認APIをパースするクラス
    """
    instrument = None
    time = None
    bid = None  # SELL時のtick
    ask = None  # BUYの時のtick
    status = None

    def __init__(self, price):
        self.ask = float(price.get('ask'))
        self.bid = float(price.get('bid'))
        self.time = parse_time(price.get('time'))
        self.instrument = str(price.get('instrument'))
        if 'status' in price:
            self.status = str(price.get('status'))
        self._check(price)

    def _check(self, price):
        if 'ask' not in price:
            raise ValueError
        if 'bid' not in price:
            raise ValueError
        if 'time' not in price:
            raise ValueError
        if 'instrument' not in price:
            raise ValueError
        self.currency_pair

    @property
    def currency_pair(self):
        """
        :rtype : CurrencyPair
        """
        for pair in CurrencyPair:
            if str(pair.name) == str(self.instrument):
                return pair
        raise ValueError

    @property
    def is_maintenance(self):
        """
        メンテ中ならTrue
        :rtype : bool
        """
        if self.status is None:
            return False
        if str(self.status) == str('halted'):
            return True
        return False

    def is_active(self):
        """
        有効なレートならTrue
        :rtype : bool
        """
        # メンテ中じゃない
        if self.is_maintenance:
            return False

        # レートが正常 4tickまで許容する
        if self.cost_tick > 4:
            return False
        return True


def get_password(key):
    """
    パスワードを応答する
    """
    return "12345"


def parse_time(time_text):
    """
    文字列をUTCへ変換する。
    例)
    2015-02-22T15:00:00.000000Z
    :param time_text: char
    :rtype : datetime
    """
    import datetime
    t = time_text
    utc = datetime.datetime(int(t[0:4]),
                            int(t[5:7]),
                            int(t[8:10]),
                            int(t[11:13]),
                            int(t[14:16]),
                            int(t[17:19]),
                            int(t[20:26]), tzinfo=pytz.utc)
    return utc


def utc_to_jst(utc):
    return utc.astimezone(pytz.timezone('Asia/Tokyo'))


print "START"
# API_Access
price_group_dict = PriceAPI(OandaAPIMode.SANDBOX).get_all()

# 結果をprint
for key in price_group_dict:
    pair = key
    price = price_group_dict[key]
    print "Pair:{} bid:{} ask:{} time:{}".format(pair.name, price.bid, price.ask, price.time)
print "FINISH"

実行結果
>python price_api.py 
START
API_Access: http://api-sandbox.oanda.com/v1/prices?instruments=EUR_USD%2CUSD_JPY%2CGBP_USD%2CAUD_USD
Pair:USD_JPY bid:120.243 ask:120.293 time:2015-09-23 21:07:30.363800+00:00
Pair:AUD_USD bid:0.70049 ask:0.70066 time:2015-09-23 21:07:30.367431+00:00
Pair:EUR_USD bid:1.13916 ask:1.1393 time:2015-10-13 07:16:53.931425+00:00
Pair:GBP_USD bid:1.52455 ask:1.5248 time:2015-09-23 21:07:30.362069+00:00
FINISH

本番システムでは思いがけないエラーがよく起こります。OandaAPIは単純なので自分でライブラリ書いた方が、後々楽できると思います。もしライブラリ利用する場合は、土日APIの応答が停止することを考慮したライブラリを利用するとよいです。

100本のAIが売買すると割と悪夢(両建て問題)

5分足が更新されたタイミングで100のAIがドル円を売買するとき、buyが60、sellが40とすると重複している40部分が両建てとなり手数料で損してしまいます。両建て問題の対策として売買を一括注文にして最適化していきます。

■サンプル事例
AI1〜6が次の通り発注したときの売買手数料を最小にする。
1回の売買手数料100円と仮定する。

AI Name Order bid Limit StopLimit
AI.001 Buy 120.00 120.50 119.50
AI.002 Buy 120.00 120.70 119.50
AI.003 Buy 120.00 120.50 119.50
AI.004 Buy 120.00 120.50 119.00
AI.005 Sell 120.00 119.00 120.50
AI.006 Sell 120.00 118.50 120.50

■ 方式1.
120.00円の時、注文をBuy4本、Sell2本発注(売買手数料600円)
その後相場が変動し6本のオーダーが全て消化される(売買手数料600円)
計1200円

■ 方式2.
120.00円の時、AI1〜6の注文をとりまとめドル円 120.00 Buy 2本発注(売買手数料200円)
その後相場が変動し6本のオーダーが全て消化される(売買手数料600円)
計800円

Oandaは両建てでポジションを持てないシステムなので、解2の方式で売買を行うことにしました。

売買注文を一括注文方式にするとLimit(利確)とStopLimit(指値で損切り)発注が出来なくなる副作用が発生する

解1の方式では注文毎にLimitとStopLimitを設定して発注可能なので、発注さえ行ってしまえばOanda側で自動決済してもらえましたが、解2の方式に倒すと注文単位が異なるため注文の決済も自身のプログラムから発注する必要があります。

supervisordを導入して可用性を上げる

もし決済プログラムがエラー停止したらどうなるでしょうか?システムは発注を続けますが、ポジションはクローズされません。口座のレバレッジは上がり続けます。そうならないためにも売買プログラムには高い可用性が求められます。またエラー時に電話通知する機能を組み込んでみました。

高可用性実現にはsupervisordでプログラムをプロセス化してしまうと楽ちんです。ポジション(売買注文)のopenとcloseコマンドをPythonで作成し、supervisordを使ってプロセスを立ち上げていきます。Cronやwhile Trueでも同様の機能を実現できますがデメリットが多いため採用しませんでした。

方式 デメリット
cron コマンド終了後に即時再実行が出来ない
while True メモリリーク起きる、エラー落ちしたとき自動復旧できない
supervisord 導入が手間、deployが手間
order_open.py
# -*- coding: utf-8 -*-
# 力つきたので、疑似言語です。多分動かないです。ごめんなさい
from __future__ import absolute_import
from __future__ import unicode_literals
import datetime
from django.core.management import BaseCommand
import time
import sys
import pytz
from requests import ConnectionError

ACCOUNT_ID = 12345


class CustomBaseCommand(BaseCommand):
    class Meta(object):
        abstract = True

    def echo(self, txt):
        d = datetime.datetime.today()
        print d.strftime("%Y-%m-%d %H:%M:%S") + ':' + txt


class Command(CustomBaseCommand):
    """
    BoardAIを利用して発注する
    """
    CACHE_PREV_RATES = {}

    def handle(self, *args, **options):
        try:
            self.run()
        except OandaServiceUnavailableError:
            # 土日メンテ中のとき
            self.echo("ServiceUnavailableError")
            time.sleep(60)
        except OandaInternalServerError:
            # 土日メンテ中のとき
            self.echo("OandaInternalServerError")
            time.sleep(60)
        except ConnectionError:
            time.sleep(60)

        # 3秒停止
        time.sleep(3)

    def run(self):
        # 証拠金チェック
        account = AccountAPI(OandaAPIMode.PRODUCTION, ACCOUNT_ID).get_all()
        if int(account.get('marginAvail')) < 10000:
            self.echo('MARGIN EMPTY!! marginAvail:{}'.format(int(account.get('marginAvail'))))
            time.sleep(3000)

        # price取る
        price_group = PriceAPI(OandaAPIMode.PRODUCTION).get_all()

        # AIインスタンス生成
        order_group = []
        ai_board_group = AIBoard.get_all()

        # 仮発注
        now = datetime.datetime.now(tz=pytz.utc)
        for ai_board in ai_board_group:
            order = self.pre_order(ai_board, price_group, now)
            if order:
                order_group.append(order)

        # 発注
        self.order(order_group, price_group)

    def pre_order(self, ai_board, price_group, now):
        """
        発注したらTrueを返却
        :param ai_board: AIBoard
        :param price_group: dict of PriceAPIModel
        :param now: datetime
        :rtype : bool
        """
        ai = ai_board.get_ai_instance()
        price = price_group.get(ai.currency_pair, None)

        # 価格は正常?
        if price is None:
            return None
        if not price.is_active():
            # print 'price not active'
            return None

        # レートが正常ではない
        prev_rates = self.get_prev_rates(ai.currency_pair, Granularity.H1)
        if not prev_rates:
            return None
        prev_rate = prev_rates[-1]

        # 前回レートから乖離が3分以内
        if now - prev_rate.start_at > datetime.timedelta(seconds=60 * 3):
            # print 'ORDER TIME IS OVER'
            return None

        # ポジション数による購入制限と時間による購入制限
        if not ai_board.can_order(prev_rate):
            # print 'TIME OR POSITION LIMIT'
            return None

        # 購入判断 AIのシミュレーションと同じ様に、midのみを対象にして売買を決定する
        order_ai = ai.get_order_ai(prev_rates, price.mid, price.time, is_production=True)

        if order_ai is None:
            return None
        if order_ai.order_type == OrderType.WAIT:
            return None

        # 仮注文発砲
        order = Order.pre_open(ai_board, order_ai, price, prev_rate.start_at)
        return order

    def order(self, order_group, price_group):
        """
        プレオーダーをサマリーとって実際に発注する
        :param order_group: list of Order
        :param price_group: dict of PriceAPIModel
        :return:
        """
        # サマリー取る
        order_dict = {x: 0 for x in CurrencyPair}
        for order in order_group:
            if order.buy:
                order_dict[order.currency_pair] += order.units
            else:
                order_dict[order.currency_pair] -= order.units
        print order_dict

        # 発注
        api_response_dict = {}
        for key in order_dict:
            if order_dict[key] == 0:
                print '{}:NO ORDER'.format(key)
                continue
            print '{}:ORDER!'.format(key)
            units = order_dict[key]
            api_response_dict[key] = OrdersAPI(OandaAPIMode.PRODUCTION, ACCOUNT_ID).post(key, units, tag='open')

        # DB更新
        for order in order_group:
            order.open(price_group.get(order.currency_pair))

    def get_prev_rates(self, currency_pair, granularity):
        key = self._get_key(currency_pair, granularity)
        print key
        r = self.CACHE_PREV_RATES.get(key)
        if r:
            return r
        prev_rates = CurrencyPairToTable.get_table(currency_pair, granularity).get_new_record_by_count(10000)
        self.CACHE_PREV_RATES[key] = prev_rates
        return prev_rates

    def _get_key(self, currency_pair, granularity):
        return 'RATE:{}:{}'.format(currency_pair.value, granularity.value)

order_close.py
# -*- coding: utf-8 -*-
# 力つきたので、疑似言語です。多分動かないです。ごめんなさい
from __future__ import absolute_import
from __future__ import unicode_literals
import datetime
from django.core.management import BaseCommand
import time
import pytz
from requests import ConnectionError

class Command(CustomBaseCommand):
    """
    account historyを利用してクローズする
    """
    CACHE_PREV_RATES = {}

    def handle(self, *args, **options):
        print '********************************'
        self.echo('close start')
        self.check_kill_switch()

        try:
            self.run()
        except OandaServiceUnavailableError:
            # 土日メンテ中のとき
            self.echo("ServiceUnavailableError")
            time.sleep(60)
        except OandaInternalServerError:
            # 土日メンテ中のとき
            self.echo("OandaInternalServerError")
            time.sleep(60)
        except ConnectionError:
            time.sleep(60)
        except Exception as e:
            self.critical_error('close', e)
        time.sleep(3)

    def run(self):
        orders = Order.get_open()

        # price取る
        price_group = PriceAPI(OandaAPIMode.PRODUCTION).get_all()

        # サマリー取る
        order_dict = {x: 0 for x in CurrencyPair}
        for order in orders:
            if order.can_close(price_group[order.currency_pair]):
                if order.buy:
                    # 反転売買する
                    print order.currency_pair, order.units
                    order_dict[order.currency_pair] -= order.units
                    print order_dict[order.currency_pair]
                else:
                    # 反転売買する
                    print order.currency_pair, order.units
                    order_dict[order.currency_pair] += order.units
                    print order_dict[order.currency_pair]
        print order_dict

        # 発注
        api_response_dict = {}
        for key in order_dict:
            if order_dict[key] == 0:
                print '{}:NO ORDER'.format(key)
                continue
            print '{}:ORDER'.format(key)
            units = order_dict[key]
            api_response_dict[key] = OrdersAPI(OandaAPIMode.PRODUCTION, 12345).post(key, units, tag='close')

        # 記録
        for order in orders:
            if order.can_close(price_group[order.currency_pair]):
                order.close(price_group.get(order.currency_pair))

遺伝的アルゴリズムの計算クラスタを構築する

オレオレ仕様で遺伝的アルゴリズムの計算クラスタを構築してみました。ワーカーはpythonで遺伝的アルゴリズムで計算するコマンドを作ってsupervisordで起動しています。
スクリーンショット 2015-10-13 17.37.46.png

ネタバレ:取引履歴

結局自動売買で儲かったのか?

売買利益 サーバ維持費
2015年3月 +5万 1万
2015年4月 +2万 1万
2015年5月 -7万 1万
2015年6月 -5万 1万
2015年7月 自動売買システムを止めてETF:1557に全額ぶっこむ -

最後までお付き合い頂きありがとうございました。
自動売買システム開発はとっても面白かったです。是非挑戦してみてください。

関連記事

pythonと遺伝的アルゴリズムで作るFX自動売買システム その1
遺伝的アルゴリズムでFX自動売買 その2 進化する売買AIの実装
遺伝的アルゴリズムでFX自動売買 その3 OandaAPIで実際に取引

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
326