【FX】Dockerを使ってPythonでoanda-APIを叩く

## 概要
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だと以下の記事なんかがわかりやすい。

- https://qiita.com/THERE2/items/f716565c884e7750c6c1

↑を見ればoandaの登録や決済方法がだいたいわかる

## いい感じのサンプルコードを探す
上の記事を見れば単発でAPIを叩くことはできるんだけど、今後にいろいろ実装していくことを考えるとclassや関数としてまとまっていた方がやりやすい。 自分で実装するのめんどくさいなーと思ってたんだけど、探してみるとGithub上にいい感じのサンプルコードがあった。 

- https://github.com/oanda/v20-python-samples

こいつを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には以下が必要と書いてある

```bash

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の中で

```bash

env/bin/pip install -r requirements/base.txt
```

とかやって必要なパッケージをインストールしている。このへんの設定を全部dockerにやってもらおう。 
以下の感じで書く。

``` bash:Dockerfile
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などの認証情報を読み込む。
以下のように書く。

``` bash:.v20.conf
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`を以下のように書けば良い。

```yaml: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
```

ファイルの構造は以下

```bash
$ tree -a
.
├── .v20.conf
├── docker-compose.yaml
└── oanda
    └── Dockerfile

```

`docker-compose`の仕組みはここではあまり触れないが、複数のコンテナをまとめて扱える。
今回は`./oanda/Dockerfile`をもとにdocker imageが作られる。
secretsの情報はコンテナ内で`/run/secrets`以下にマウントされる。
以下を参考にした

- https://qiita.com/KEINOS/items/518610bc2fdf5999acf2

PASSの情報などを環境変数として渡す方法は他にもいくつかあるみたいだが、今回は`.20.conf`を読み取るという仕様をできるだけそのまま使いたかったのでこの方法を使った。

## docker-composeの実行
これで準備は整ったのでコンテナを生成してみる。

```bash

$ 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イメージに入ってコマンドを実行してみる

```bash

$ 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以下にマウントされているため)

あとはコマンドを実行するだけで売買が可能。試しに自分のアカウント情報を表示してみよう。

```bash

# 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分足を表示した場合は

```bash
$ 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）は１。なおプラスの値であれば買い、マイナスであれば売りとなる。

```bash
$ 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して使っている人も多く、そちらは微妙に書き方違うので注意。

```python:details.py

#!/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()
```

 - 成行注文の場合

```python:market.py

#!/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の~/以下にコピーしようとしていた。認証情報をどうやって安全渡せばいいのかちゃんとちゃんと考えないとダメですね。。