シストレどうですか

  Algorithmic Trading for Dummies

OANDA API 解説編 第14回 決済注文 その2 - 分割決済

あっという間にクリスマスも過ぎて、2021年になってしまいました。
先日結構な雨が降ったのですが、新しく取り換えた窓と壁の間にどうやら隙間があったらしく、窓枠から家の中に水が入ってきて取り換えたばかりのカーペットが水浸しになってしまいました。 運よくコーキング材があったので急いで雨の中塗りたくりなんとかおさまりましたが、漏れ出したのが夜中の12時すぎだったので気が付いたら午前2時になっていてその後も興奮してか寝つきが悪く、次の日のは寝不足な1日でした。
どうやら窓を取り換えた際に塗ったコーキング材が薄かったらしく数か月でひび割れがおきて久しぶりの強い雨でそこから水が浸入してきたようです。 もともと築50年超の古い家でメインテナンスを怠っていたこともあり、雨漏りなどがひどかったのでこの夏にようやくいろいろな所を改修したのですが、なおしたらなおしたで新たな問題がおきるというのはアメリカならではという事でしょうか?
それはさておき、今年は健康で普段通りの生活ができる日が1日でも早く戻ってくれる事を只々祈るばかりです。

さて、前回のタイトルに”その1”とつけてしまったのですが、”その2"を書こうと思ったらだいたい説明してしまったのでネタ切れになってしまいましたので、分割決済の方法について考えてみました。

分割決済の仕方を考えてみた

一定の利益が出たところで、一部のロットを決済していき実現損益を出しながら手仕舞いしていくやり方です。
利確注文やトレイリングストップ注文をいれておく方法もありますが、いったん引っかかると全ロット決済されてしまいます。
分割決済のほうが、裁量で行おうとすると複雑な手続き(要は面倒くさい)になりますので、シストレで実行するほうが向いていそうですね。
どちらかの方法が利益が残るかどうかを検証するわけではなく、あくまでも決済注文の応用例の一つとして試してみます。

考えをまとめる

コーディングを始める前に分割決済のルールをざっくりと考えてみたいと思います。

  • 複数回で均等に決済
  • 前回決済したプライスを基準に、次の決済を行うプライスを決定
  • 分割決済の最終回は全部クローズ
  • その他
おおざっぱに言えばこんなところでしょうか。

分割決済のルールを作成

とりあえず考えてみたルールをもう少し細かく定義していきます。

複数回で均等に決済

せっかくプログラムで行いますので2回とか3回等の固定ではなく複数回で分割できるようにします。また決済するロットサイズも均等にかつ最小単位未満にならないようにします。 方法を探したところ以下のような記事がありましたので これ を参考にしました。 qiita.com

x = 6  #ロット数
n = 4  #分割回数
   
l = [(x + i) // n for i in range(n)]
    
print(l)  #結果確認
[3, 3, 3, 4]

すごくシンプルですね。とりあえず使えそうです。
初めに大きめのロットから決済したい場合は、

l.sort(reverse=True)

で並べ替えできます。

[4, 3, 3, 3]

ただし、売りポジションの場合はロットがマイナスなので、その場合は[-1, -1, -1, -2]のようにサイズからみれば並び順が逆になってしまいますので、 絶対値で計算することにします。(1ユニットの差だけですが・・・)

また、ロット数を分割回数以下にセットした場合はリストに0が入りますので、リストから除外しておくか、0を読み込んだ時点で決済処理を終了させるようにします。

x = 6
n = 10
l = [(abs(x) + i) // n for i in range(n)]
l.sort(reverse=True)
print(l)
l = [ y for y in l if y != 0 ]
print(l)

#一行で書くと
# l = [(abs(x) + i) // n for i in range(n) if (x + i) // n != 0]な感じ
[1, 1, 1, 1, 1, 1, 0, 0, 0, 0]
[1, 1, 1, 1, 1, 1]

前回決済したプライスを基準に、次の決済を行うプライスを決定

予め分割決済する値幅を指定しておき、そのプライスに到達したら決済する。決済が成功したらそのプライスと値幅をもとに次に決済するプライスを決定する。
今回は、値幅を定数としてセットしておき、決済する度に決済できたプライスとその値幅を使って次の決済用のプライスを決定します。

分割決済の最終回は全ロットクローズ

初めに分割決済するサイズをリストにセットしましたが、念のため何らかの原因で途中予定とは違う数量で決済された場合を考えて、ポジションが残らないように分割決済の最終回は残りのポジション全てをクローズするようにしました。

その他

あまりいろいろ詰め込みすぎると複雑になりすぎてなにがなんだかわからなくなってきますので、

  • 損切ラインの変更は行わない
  • 新規のオーダー注文部分は記述せずにパラメータで設定する。

という前提にします。

分割決済の仕方を作ってみた

サンプル例を作成してみました。分割決済の部分のみを検証するために発注部分を省略してありますので、トレードIDや最初のプライスはあらかじめ自分で設定するようにしてあります。 また、本来は値動きをストリーミング配信でキャッチしたいところですが、シンプルにするためにsleep関数で1分置きに最新レートを取得する形にしました。

サンプル

あくまでも検証用ですので、本格的に使うならばもう少し細かい設定を追加する必要がありますが、とりあえずこんな感じで作ってみました。
具体的には、予めエントリーしておいたトレード(ID:1439、通貨:USD/JPY、サイズ:ショート7ロット、プライス:103.234円)を0.005円の値幅で6回に分けて決済する流れになります。

#--------------#
# 初期設定     #
#--------------#
#外部モジュール
import requests
import json
import time

  
#口座情報
API_Token = '9999999999999999999999999999999999999999999999999999999999999999'
API_AccountID = '999-999-99999999-999'
  
# URLの設定 (デモ口座用非ストリーミングURL)
API_URL =  "https://api-fxpractice.oanda.com"
  
# ヘッダー情報の変数の設定
headers = {
               "Content-Type" : "application/json", 
               "Authorization" : "Bearer " + API_TOKEN
        }  
  
#test用データのセット
CURRENCY_PAIR = "USD_JPY"
TRADE_NO = "1439"
  
current_units = -7
last_price = 103.234
  
#urlの設定
# プライス取得用
url_get_rate = API_URL + "/v3/accounts/%s/pricing?instruments=%s" %(str(API_ACCOUNTID), CURRENCY_PAIR)
# 分割決済用
url_close = API_URL + "/v3/accounts/%s/trades/%s/close" %(str(API_ACCOUNTID), TRADE_NO)
  

#----------------------#
# 分割決済処理スタート #
#----------------------#
  
#決済用値幅の指定 (例:0.005円)
DISTANCE = 0.005
  
#分割回数の指定
NUNMBER_CLOSE = 6
  
#分割決済用リストの作成
units_partial_close = [(abs(current_units) + i) // NUNMBER_CLOSE for i in range(NUNMBER_CLOSE) \
                                                if (current_units + i) // NUNMBER_CLOSE != 0]
units_partial_close.sort(reverse=True)
print("分割サイズリスト:%s" %str(units_partial_close))
  
try:
    
        j = 0
        j_last = len(units_partial_close)-1
  
        while True: 
                
                #最新レートの取得
                response_body = requests.get(url_get_rate, headers=headers)
                response_body.raise_for_status()
  
                #決済シグナルの計算
                if units_partial_close[j] == 0:
                        print("中止(0)")
                        break
                #Long        
                elif current_units > 0:
                        #Long
                        current_price = float(response_body.json()["prices"][0]["bids"][0]["price"]) 
                        signal_close = round(current_price - (last_price + DISTANCE), 3)
                #Short
                elif current_units < 0:
                        #Short
                        current_price = float(response_body.json()["prices"][0]["asks"][0]["price"])
                        signal_close = round((last_price - DISTANCE) - current_price, 3) 
                else:
                        print("中止(0)")
                        break
  
                print("最新レート:%s" %str(current_price))
  
                #決済処理                
                if signal_close >= 0:
                       
                        #分割決済最終回の時はすべてクローズ
                        if j == j_last:
                                close_units = "ALL"
                        else:
                                close_units = str(units_partial_close[j])
                          
                        #データ引数の設定
                        data_close = { 
                                        "units":  close_units
                                }  
                        data = json.dumps(data_close)
        
                        # サーバーへの要求
                        response_body = requests.put(url_close, headers=headers, data=data)
                        print("クローズ処理結果:")
                        print(json.dumps(response_body.json(), indent=2))
                        response_body.raise_for_status()                
                        
                        #クローズ処理の確認
                        if 'tradeReduced' in response_body.json()['orderFillTransaction']:
                                last_price = float(response_body.json()["orderFillTransaction"]["tradeReduced"]["price"]) 
                                print("決済(続く)closed price: %s" %str(last_price))
                        elif 'tradesClosed' in response_body.json()['orderFillTransaction']:
                                print("決済(終了)")
                                break                       
                        else:
                                print("中止")
                                print("Status Error from Close : %s" %response_body.text)
                                break
                                                
                        #結果の表示
                        print(json.dumps(response_body.json(), indent=2))
                        if j >= j_last: #最後か?
                                break
                        else:
                                j = j + 1
    
                print("last_price: %s" %str(last_price))                
                print("待機")
                time.sleep(10)                                            
        #loop
  
#例外処理
except Exception as e:
                if "response_body" in locals(): # or vars()
                        print("Status Error from Server(raise) : %s" %response_body.text)
                print("Error(e) : %s" %e)
                print("中止")
  
print("End!")

テストする環境を作るのが大変でしたので、極小の値幅(0.005円)の設定でテストしてみました。
結果としては以下のようになりましたが、長いので途中省略してあります。

分割サイズリスト:[2, 1, 1, 1, 1, 1]
最新レート:103.155
クローズ処理結果:
{
  "orderCreateTransaction": {
    "id": "1445",
    "accountID": "999-999-99999999-999",
    "userID": 99999999,
    "batchID": "1445",
    "requestID": "78807818267795740",        
    "time": "2020-12-31T01:41:09.699110672Z",
    "type": "MARKET_ORDER",
    "instrument": "USD_JPY",
    "units": "2",
    "timeInForce": "FOK",
    "positionFill": "REDUCE_ONLY",
    "reason": "TRADE_CLOSE",
    "tradeClose": {
      "units": "2",
      "tradeID": "1439"
    }
  },
  "orderFillTransaction": {
    "id": "1446",
    "accountID": "999-999-99999999-999",
    "userID": 99999999,
    "batchID": "1445",
    "requestID": "78807818267795740",
    "time": "2020-12-31T01:41:09.699110672Z",
    "type": "ORDER_FILL",
    "orderID": "1445",
    "instrument": "USD_JPY",
    "units": "2",
    "requestedUnits": "2",
    "price": "103.155",
    "pl": "0.0015",
    "quotePL": "0.158",
    "financing": "0.0000",
    "baseFinancing": "0.00000000000000",
    "commission": "0.0000",
    "accountBalance": "98891.6865",
    "gainQuoteHomeConversionFactor": "0.009646333468",
    "lossQuoteHomeConversionFactor": "0.009743281543",
    "guaranteedExecutionFee": "0.0000",
    "quoteGuaranteedExecutionFee": "0",
    "halfSpreadCost": "0.0001",
    "fullVWAP": "103.155",
    "reason": "MARKET_ORDER_TRADE_CLOSE",
    "tradeReduced": {              👈
      "tradeID": "1439",
      "units": "2",
      "realizedPL": "0.0015",
      "financing": "0.0000",
      "baseFinancing": "0.00000000000000",
      "price": "103.155",
      "guaranteedExecutionFee": "0.0000",
      "quoteGuaranteedExecutionFee": "0",
      "halfSpreadCost": "0.0001"
    },
    "fullPrice": {
      "closeoutBid": "103.141",
      "closeoutAsk": "103.155",
      "timestamp": "2020-12-31T01:41:06.702226035Z",
      "bids": [
        {
          "price": "103.141",
          "liquidity": "10000000"
        }
      ],
      "asks": [
        {
          "price": "103.155",
          "liquidity": "10000000"
        }
      ]
    },
    "homeConversionFactors": {
      "gainQuoteHome": {
        "factor": "0.00964633346825"
      },
      "lossQuoteHome": {
        "factor": "0.00974328154331"
      },
      "gainBaseHome": {
        "factor": "1"
      },
      "lossBaseHome": {
        "factor": "1"
      }
    }
  },
  "relatedTransactionIDs": [
    "1445",
    "1446"
  ],
  "lastTransactionID": "1446"
}
決済(続く)closed price: 103.155
last_price: 103.155
待機
  ≀
(省略)
  ≀
最新レート:103.125
クローズ処理結果:
{
  "orderCreateTransaction": {
    "id": "1455",
    "accountID": "999-999-99999999-999",
    "userID": 99999999,
    "batchID": "1455",
    "requestID": "78807820957593981",
    "time": "2020-12-31T01:51:50.330164149Z",
    "type": "MARKET_ORDER",
    "instrument": "USD_JPY",
    "units": "1",
    "timeInForce": "FOK",
    "positionFill": "REDUCE_ONLY",
    "reason": "TRADE_CLOSE",
    "tradeClose": {
      "units": "ALL",
      "tradeID": "1439"
    }
  },
  "orderFillTransaction": {
    "id": "1456",
    "accountID": "999-999-99999999-999",
    "userID": 99999999,
    "batchID": "1455",
    "requestID": "78807820957593981",
    "time": "2020-12-31T01:51:50.330164149Z",
    "type": "ORDER_FILL",
    "orderID": "1455",
    "instrument": "USD_JPY",
    "units": "1",
    "requestedUnits": "1",
    "price": "103.125",
    "pl": "0.0011",
    "quotePL": "0.109",
    "financing": "0.0000",
    "baseFinancing": "0.00000000000000",
    "commission": "0.0000",
    "accountBalance": "98891.6912",
    "gainQuoteHomeConversionFactor": "0.009648999489",
    "lossQuoteHomeConversionFactor": "0.009745974358",
    "guaranteedExecutionFee": "0.0000",
    "quoteGuaranteedExecutionFee": "0",
    "halfSpreadCost": "0.0001",
    "fullVWAP": "103.125",
    "reason": "MARKET_ORDER_TRADE_CLOSE",
    "tradesClosed": [             👈
      {
        "tradeID": "1439",
        "units": "1",
        "realizedPL": "0.0011",
        "financing": "0.0000",
        "baseFinancing": "0.00000000000000",
        "price": "103.125",
        "guaranteedExecutionFee": "0.0000",
        "quoteGuaranteedExecutionFee": "0",
        "halfSpreadCost": "0.0001"
      }
    ],
    "fullPrice": {
      "closeoutBid": "103.114",
      "closeoutAsk": "103.125",
      "timestamp": "2020-12-31T01:51:49.527833592Z",
      "bids": [
        {
          "price": "103.114",
          "liquidity": "10000000"
        }
      ],
      "asks": [
        {
          "price": "103.125",
          "liquidity": "10000000"
        }
      ]
    },
    "homeConversionFactors": {
      "gainQuoteHome": {
        "factor": "0.00964899948924"
      },
      "lossQuoteHome": {
        "factor": "0.00974597435848"
      },
      "gainBaseHome": {
        "factor": "1"
      },
      "lossBaseHome": {
        "factor": "1"
      }
    }
  },
  "relatedTransactionIDs": [
    "1455",
    "1456"
  ],
  "lastTransactionID": "1456"
}
決済(終了)
End!

f:id:jantzen:20210101061234p:plain:w300:h600

初めに分割決済用のサイズを決定するリストを作り、値幅に合わせて決済することが出来ました。 ちなみに "tradesClosed"キーのところは"tradeReduced"キーと違い配列[]になっています。複数に分けて決済される可能性があるという事ですね。


まとめ

分割決済の方法を試してみました。
分割決済の注文の出し方は、今回のトレードのクローズ("PUT /v3/accounts/{accountID}/trades/{tradeSpecifier}/close")だけでなく新規オーダー("POST /v3/accounts/{accountID}/orders")でも可能 です。
値幅を一定ではなく段々狭くしたり広くしても面白そうですし、今回は損切ラインの変更は試しませんでしたが、自分で自由に損切の設定もできますのである程度利益を確保したら最後のほうは更にリスクを取って利益をのばしていくという使い方もできそうです。

最後に皆様、いや世界中の人たちが安心して暮らせる2021年であることを切に願っております。
Happy New Year!