概要
FXの取引といえば基本手作業でぽちぽちやるものだ。 これをPythonを使ってプログラミングで自動で決済したりできると凄い楽しいことになる。 機械学習を用いて未来の価格を予測したり、決められた時間や決められたロジックで自動売買の設定だってできる。 ここではそのための最初の一歩として、 Dockerからapiを叩いてPythonを使った決済ができるようになる を目標とする。
前提となる環境
- oandaに口座(デモ口座でもよし)を作り、APIのtokenを発行する
- docker, docker-composeを使える環境
##oandaの設定
Pythonなどのプログラミング言語を用いてFXの売買をするためにはAPIを叩かなければいけない。
SBI証券とかDMMFXなどFXをできる業者はいっぱいあるが、国内でFX用のAPIを提供しているところはoandaくらいなので、まずはoandaで口座を作る。
バグとかがあったら大変なので、最初はデモ口座でいいだろう。15分くらいあればちゃちゃっとできる。
oanda APIを使ったFXの取引についてはQiitaだと以下の記事なんかがわかりやすい。
↑を見ればoandaの登録や決済方法がだいたいわかる
いい感じのサンプルコードを探す
上の記事を見れば単発でAPIを叩くことはできるんだけど、今後にいろいろ実装していくことを考えるとclassや関数としてまとまっていた方がやりやすい。 自分で実装するのめんどくさいなーと思ってたんだけど、探してみるとGithub上にいい感じのサンプルコードがあった。
こいつをgit cloneして書いてある通りにやれば良さそう。 詳しいやりかたは上のリポジトリのREAMDEを見ればわかるが、要点をまとめると以下
- Pythonの
virtualenv
を使って環境設定を行う - 決済に必要なoanda APIのtokenやaccount IDなどの情報は
~/.v20.conf
においといて、それを読み取る仕組み -
setup.py
にentrypointが指定してあり、v20-order-entry
とかv20-order-cancel
とか打って引数指定するだけで買ったり売ったりできる
便利そう。そのまま自分のPCで試してもいいんだけど、せっかくなので以下の改修を加えてみる。
- 元のリポジトリだと
virtualenv
を使ってPyhonの仮想環境みたいのを作っているが、Dockerを使えば任意の環境で使えるのでより便利になるのでは? - 今回は勉強もかねて
virtualenv
は使わずにAWSのサーバ上にDockerfile
を作ってコンテナ上で売買を試みる - 元々の仕様と同じようにアカウント情報を
~/.v20.conf
に置くのはセキュリティ上よろしくないので、docker-compose
を使ってsecrets
で渡す(他にもいい方法いっぱいあるかも)
AWSの設定
別にdockerさえ使えればなんでもいいのだけれど、自分はAWS
を使ったことがなかったので使ってみた。
今回はAWS
のAmazon EC2
から適当なOS(ubuntu
にした)でインスタンスを作り、その中でdockerを使ってみた。
お試し無料枠なのでメモリとかのスペックは一番しょぼいやつ。
デフォルトだと何も入ってないので、適当に調べて
dockerを入れた。
Dockerfileを書く
もともとのリポジトリをみると、setupには以下が必要と書いてある
user@host: ~/v20-python-samples$ make bootstrap #makefileからvirtualenvをする
user@host: ~/v20-python-samples$ source env/bin/activate
(env)user@host: ~/v20-python-samples$ python setup.py develop #entrypointの設定とか
上のmakefileの中で
env/bin/pip install -r requirements/base.txt
とかやって必要なパッケージをインストールしている。このへんの設定を全部dockerにやってもらおう。
以下の感じで書く。
FROM centos:7 #バージョンは適当
#githubから該当リポジトリを引っ張ってくる(master branch)
ARG BRANCH="master"
RUN yum install git -y \
&& git clone --depth 1 --single-branch -b ${BRANCH} https://github.com/oanda/v20-python-samples.git
#必要なパッケージを入れていく
RUN yum install python3 -y \
&& yum install python3-devel -y \
&& yum install gcc -y \
&& cd v20-python-samples/ \
&& pip3 install -r requirements/base.txt \
&& python3 setup.py develop
認証まわり
決済の際にはには毎回.v20.conf
からAPIのtokenなどの認証情報を読み込む。
以下のように書く。
hostname: api-fxpractice.oanda.com #デモなのでfxfractice
streaming_hostname: stream-fxpractice.oanda.com
port: 443
ssl: true
token: e6ab562b039325f12a026c6fdb7b71bb-b3d8721445817159410f01514acd19hbc #oandaで発行してもらったtokenの例
username: user
accounts:
- 101-001-100000-001
- 101-001-100000-002
active_account: 101-001-100000-002 #oandaのアカウントID例
問題はこいつの置き場所で、元々の想定のように自分のlocal環境から動かすなら~/.v20.conf
にぽいっと置いておけばいいのだが、docker image内にこのファイルそのままコピーするのはセキュリティ的にあまりよくないので、docker-compose
を使う。
docker-compose
を使うにはdocker-compose.yaml
を以下のように書けば良い。
services:
oanda: #
image: oanda #imageの名前
build: #./oanda #これで./oanda/Dockerfileを使ってbuildされる
tty: true #これがないと実行した後すぐにexitしてしまうのでつける
secrets:
- .v20.conf #渡したいファイル
secrets: #secretsの設定
.v20.conf:
file: .v20.conf
ファイルの構造は以下
$ tree -a
.
├── .v20.conf
├── docker-compose.yaml
└── oanda
└── Dockerfile
docker-compose
の仕組みはここではあまり触れないが、複数のコンテナをまとめて扱える。
今回は./oanda/Dockerfile
をもとにdocker imageが作られる。
secretsの情報はコンテナ内で/run/secrets
以下にマウントされる。
以下を参考にした
PASSの情報などを環境変数として渡す方法は他にもいくつかあるみたいだが、今回は.20.conf
を読み取るという仕様をできるだけそのまま使いたかったのでこの方法を使った。
docker-composeの実行
これで準備は整ったのでコンテナを生成してみる。
$ sudo docker-compose up -d #backgroupで実行したいので-dをつける
Creating network "test_default" with the default driver
Creating test_oanda_1 ... #今回は~/test/以下で実行したためこのような命名ルールになってるっぽい
Creating test_oanda_1 ... done
Attaching to test_oanda_1
#以下のコマンドでいろいろ確認できる
$ sudo docker-compose ps
Name Command State Ports
----------------------------------------
test_oanda_1 /bin/bash Up #StateがUpになってるのでちゃんとコンテナ動いてるっぽい
$ sudo docker-compose images #imageの確認
Container Repository Tag Image Id Size
----------------------------------------------------------
test_oanda_1 oanda latest c84xxxxxxxxx 489 MB
$ sudo docker ps #普通にdockerのコマンド使っても確認できる
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
d56xxxxxxxxx oanda "/bin/bash" 2 minutes ago Up 2 minutes test_oanda_1
今回はsecretsの情報を渡しただけだが、nginx
とmysql
連携したりとかするときも同じような方法でdocker-compose
が使えるっぽい。
ちなみに立ち上がってるコンテナはdocker-compose down
とかで消せる。
コンテナに入って実行してみる
作ったdockerイメージに入ってコマンドを実行してみる
$ sudo docker-compose exec oanda bash #oandaはDockerfileで設定したサービスの名前
無事入れたらgithubからcloneしたリポジトリが見れるはずなので、
v20-python-samples/src/common/config.py
のDEFAULT_PATH = "~/.v20.conf"
を"/run/secrets/.v20.conf"
に変更する。(認証ファイルが/run/secrets以下にマウントされているため)
あとはコマンドを実行するだけで売買が可能。試しに自分のアカウント情報を表示してみよう。
# cd v20-python-samples/
# v20-account-details
=============================== ==============================
Account ID 100-xxx-xxxxxxxx-xxx
Alias xxxxxxx
Home Currency JPY
Balance 2999999.978 #残高。デモ口座だから300万くらい入ってる
Created by User ID xxxxxxxx
Create Time 2020-06-03T12:45:17.366012787Z
Guaranteed Stop Loss Order Mode DISABLED
Profit/Loss -0.022
Resettable Profit/Loss -0.022
Profit/Loss Reset Time 0
Financing 0.0
Commission 0.0
Guaranteed Execution Fees 0.0
Margin Rate 0.04
Open Trade Count 0
Open Position Count 0
Pending Order Count 0
Hedging Enabled True
Unrealized Profit/Loss 0.0
Net Asset Value 2999999.978
Margin Used 0.0
Margin Available 2999999.978
Position Value 0.0
Closeout UPL 0.0
Closeout NAV 2999999.978
Closeout Margin Used 0.0
Margin Closeout Percentage 0.0
Margin Closeout Position Value 0.0
Withdrawal Limit 2999999.978
Last Transaction ID 10
=============================== ==============================
できた!
ローソク足の情報表示してみる
続いてローソク足の表示をしてみたい。v20-instrument-candles
を使う。オプションとしてinstrument(ドル円の場合はUSD_JPY)が必須。--granularity
で何秒足 or 分足 or時間足などの指定をする。例えばドル円の15分足を表示した場合は
$ v20-instrument-candles USD_JPY --granularity M15
Instrument: USD_JPY
Granularity: M15
Time Type Open High Low Close Volume
=================== ==== ======== ======== ======== ======== ======
2020-06-05 09:00:00 mid 109.246 109.279 109.23 109.272 1235
2020-06-05 09:15:00 mid 109.273 109.313 109.256 109.266 991
2020-06-05 09:30:00 mid 109.266 109.273 109.229 109.266 851
2020-06-05 09:45:00 mid 109.264 109.276 109.242 109.266 551
2020-06-05 10:00:00 mid 109.264 109.267 109.218 109.242 978
2020-06-05 10:15:00 mid 109.241 109.282 109.232 109.266 971
2020-06-05 10:30:00 mid 109.264 109.296 109.258 109.281 892
以下略
といった感じで出てくる。
実装はsrc/instrument/candles.py
に書いてある。
ちなみにv20-instrument-candles_poll
を使うと実際にチャートを見ているみたいにリアルタイムで値が更新される!(ちょっと感動)
売買してみる
やはりFXは売買してなんぼ。ということで成行注文でエントリーしてみる。
ドル円でロット数(units)は1。なおプラスの値であれば買い、マイナスであれば売りとなる。
$ v20-order-market USD_JPY 1
Response: 201 (Created)
Order Create
============
==================== ==============================
Transaction ID 14
Time 2020-06-12T15:07:41.298719851Z
User ID 1xxxxxxx
Account ID 101-00x-1xxxxxxx-0xx
Transaction Batch ID 14
Request ID 24xxxxxxxxxxxxxxx
Type MARKET_ORDER
Instrument USD_JPY
Amount 1.0
Time In Force FOK
Position Fill DEFAULT
Reason CLIENT_ORDER
==================== ==============================
Order Fill
==========
================================= ==============================
Transaction ID 15
Time 2020-06-12T15:07:41.298719851Z
User ID 1xxxxxxx
Account ID 101-009-15xxxxxx-002
Transaction Batch ID 14
Request ID 246xxxxxxxxxxxxxx
Type ORDER_FILL
Filled Order ID 14
Fill Instrument USD_JPY
Fill Units 1.0
Gain Quote Home Conversion Factor 1.0
Loss Quote Home Conversion Factor 1.0
Fill Price 107.393
Full VWAP 107.393
Price <pricing.ClientPrice>
Fill Reason MARKET_ORDER
Profit/Loss 0.0
Financing 0.0
Commission 0.0
Guranteed Execution Fee 0.0
Account Balance 2999999.978
Trade Opened <transaction.TradeOpen>
Half Spread Cost 0.002
================================= ==============================
# ポジションを確認する場合
$ v20-account-details
(略)
==== ======= ============================== ================ =====
ID State Summary Unrealized P/L P/L
==== ======= ============================== ================ =====
15 OPEN 1.0 (1.0) of USD_JPY @ 107.393 -0.04 0 #1lot買えている
#以下のコマンドでclose
$ v20-trade-close 15 --units 1
コードの中身
一応中身のコードについてもみてみる。
v20-account-details
は/src/account/details.py/
のmain()が呼ばれるためコードを追ってみよう。
ちなみに今回はv20.Config()からget()を使ってAPIにリクエストを飛ばしているが、パッとググったかんじサードパーティ製oandapyV20をimportして使っている人も多く、そちらは微妙に書き方違うので注意。
#!/usr/bin/env python
import sys
sys.path.append('..')
import argparse
import common.config
from account import Account
def main():
#認証情報を引数として受け取るためにArgumentParser()を呼び出す
parser = argparse.ArgumentParser()
#/src/common/config.pyのadd_argument(parser)を呼び出す
#中でparser.add_argument()を呼び出しており、指定したpathからconfigファイルをparserが読む
common.config.add_argument(parser)
#parserから引数をもってくる
args = parser.parse_args()
#読み取った認証情報はConfigクラスのオブジェクトであるconfigに格納されているので、そこからactive_accountを呼び出す
account_id = args.config.active_account
#create_contest()内でv20.Context()を呼び出しAPIの初期化を行う
api = args.config.create_context()
#getリクエストを送り、account情報を取得する
response = api.account.get(account_id)
#200が返ってくればok
account = Account(
response.get("account", "200")
)
account.dump()
if __name__ == "__main__":
main()
- 成行注文の場合
#!/usr/bin/env python
import argparse
import common.config
from .args import OrderArguments
from v20.order import MarketOrderRequest
from .view import print_order_create_response_transactions
def main():
#認証情報を引数として受け取るためにArgumentParser()を呼び出す
parser = argparse.ArgumentParser()
#/src/common/config.pyのadd_argument(parser)を呼び出す
#中でparser.add_argument()を呼び出しており、指定したpathからconfigファイルをparserが読む
common.config.add_argument(parser)
# 注文に必要な引数を設定する
marketOrderArgs = OrderArguments(parser)
marketOrderArgs.add_instrument()
marketOrderArgs.add_units()
marketOrderArgs.add_time_in_force(["FOK", "IOC"])
marketOrderArgs.add_price_bound()
marketOrderArgs.add_position_fill()
marketOrderArgs.add_take_profit_on_fill()
marketOrderArgs.add_stop_loss_on_fill()
marketOrderArgs.add_trailing_stop_loss_on_fill()
marketOrderArgs.add_client_order_extensions()
marketOrderArgs.add_client_trade_extensions()
#parserから引数をもってくる
args = parser.parse_args()
#create_contest()内でv20.Context()を呼び出しAPIの初期化を行う
api = args.config.create_context()
# 読み込んだ引数から注文に必要な引数を抽出する
marketOrderArgs.parse_arguments(args)
# 認証情報と引数を渡して注文する
response = api.order.market(
args.config.active_account,
**marketOrderArgs.parsed_args
)
# statusコードとその理由を表示
print("Response: {} ({})".format(response.status, response.reason))
print("")
#resposeの詳細情報を表示
print_order_create_response_transactions(response)
if __name__ == "__main__":
main()
まとめ
- dockerを使ってoanda-apiを叩くことができた!
- あとはこれをどんどん自分好みに改良していけば自動売買やら機械学習やらいろんなことができそう
- 気が向いたこれに続くような記事を書くかもしれない
その他、余談
- その他の売買コマンドの使い方はリポジトリの
README
に、oanda-apiを叩くためのより詳しい実行方法が出てくる。 - 最初はdocker-composeは使わずに脳死で
.v20.conf
をDocker imageの~/以下にコピーしようとしていた。認証情報をどうやって安全渡せばいいのかちゃんとちゃんと考えないとダメですね。。