前回の記事
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
# -*- 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が手間 |
# -*- 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)
# -*- 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年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で実際に取引