シストレでも複利運用を開始します! たっきん(Twitter)です
僕が今まで作成してきたシストレ・アプリケーションのバリエーションも「単純移動平均線(SMA)」、「ボリンジャーバンド」、「窓埋め」、「仲値&ゴトー」と4つまで増え、中でも「単純移動平均線(SMA)」、「ボリンジャーバンド」がそれなりにパフォーマンスを発揮している状態です。
なので今回はシステムトレードで資産を効率よく増やす方法として、複利で資産を増やせる運用アルゴリズムを考えていこうと思います。
レバレッジ
複利運用アルゴリズムを考えるにあたり、まずはレバレッジについて改めて理解していきましょう!
レバレッジとは預けた証拠金以上の取引ができる仕組みのことで、例えば証拠金が10万円しかなかったとしても100万円分の取引が可能になります。
このとき、証拠金10万円に対して10倍の100万円分の取引をしているので、レバレッジは10倍となります。
レバレッジのメリットは証拠金以上の取引ができるため、うまく活用すれば効率よく資産を増やすことができる反面、取引で損失を出した場合はレバレッジの大きさ分の損失を出すことになります。
レバレッジを上げ過ぎてしまうとギャンブル性が高く、ハイリスクハイリターン取引になってしまうことから国内FXの最大レバレッジは金融庁により25倍(個人の場合)と定められています。
以上のことからシステムトレード運用する際は常にレバレッジが25倍以下となるようにポジション管理をすることが大前提となります。
式で表現すると常に下式の条件を満たすようにする必要があります。
\begin{align*}
\frac{\mbox{約定レート} \times \mbox{取引数量}}{\mbox{証拠金}} = レバレッジ ≦ 25
\end{align*}
複利運用アルゴリズム
複利運用アルゴリズムを考える上でレバレッジ管理は必須であることを前章で説明しました。
よってレバレッジを管理できるように下記2つのパラメータを設定します。
- 最大レバレッジ
- 最大同時エントリー数
「最大レバレッジ」はシストレ運用する上での最大レバレッジになります。
最大レバレッジは25倍なのでは?と疑問に思った方もいると思いますが、基本的に上限の25倍に合わせて運用をすることはしません。
レバレッジは証拠金の変動によって値も変わっていくのですが、証拠金は含み損益も加味されるため、例えば最大レバレッジの25倍でエントリーした後に少しでも含み損になった瞬間にレバレッジが25倍を超え、強制ロスカットとなってしまうからです。
なので運用する上での最大レバレッジには余裕を持たせ、20倍、15倍程度にしようと考えています。
「最大同時エントリー数」は同時にエントリー可能な上限数になります。
この2つパラメータにより、レバレッジとポジションを管理していくことになりますが、例として各パラメータに下記の値が設定された場合を考えてみます。
- 最大レバレッジ = 20
- 最大同時エントリー数 = 2
上記のパラメータ例ですと、レバレッジ10倍のポジションを最大2つまで保持する運用形態になります。
レバレッジが決定すると後は下式のように証拠金と約定レートから取引数量が決まります。
\begin{align*}
\mbox{取引数量} &=\mbox{レバレッジ} \times \frac{ \mbox{証拠金}}{\mbox{約定レート}}\\
&= \frac{\mbox{[Param]最大レバレッジ}}{\mbox{[Param]最大同時エントリー数}}\times \frac{ \mbox{証拠金}}{\mbox{約定レート}}
\end{align*}
しかし、上記でも説明したように証拠金は含み損益によって値が常時変動しているため、式を簡単にするために「証拠金 = 口座残高」に置き換えることにします。
\begin{align*}
\mbox{取引数量} &= \frac{\mbox{[Param]最大レバレッジ}}{\mbox{[Param]最大同時エントリー数}}\times \frac{\bf{\mbox{口座残高}}}{\mbox{約定レート}}
\end{align*}
上式を見ればわかるように、取引数量は口座残高に比例した式になっています。
取引は利確すると口座残高が増えていくため、上式で算出した取引数量でトレードするようにしていくことで自然と複利運用になるというわけです。
口座残高の取得方法
取引数量を算出するには口座残高を知る必要があります。
OANDAには口座情報を取得するAPIが提供されているのでこれを使うことで口座残高を知ることができそうですね。
<サンプルコード>
import json
from oandapyV20 import API
import oandapyV20.endpoints.accounts as accounts
from oandapyV20.exceptions import V20Error
account_number = "***-***-*******-***"
access_token = "********************************"
api = API(access_token=access_token)
r = accounts.AccountSummary(account_number)
try:
rsp = api.request(r)
print(json.dumps(rsp, indent=2))
except V20Error as e:
print("Error: {}".format(e))
<実行結果>
{
"account": {
"guaranteedStopLossOrderMode": "DISABLED",
"hedgingEnabled": true,
"id": <your id>,
"createdTime": "2022-02-28T13:37:39.792737223Z",
"currency": "JPY",
"createdByUserID": <user id>,
"alias": "SystemTradeTrial",
"marginRate": "0.02",
"lastTransactionID": "127",
"balance": "2997378.6660",
"openTradeCount": 0,
"openPositionCount": 0,
"pendingOrderCount": 0,
"pl": "-2621.3340",
"resettablePL": "-2621.3340",
"resettablePLTime": "0",
"financing": "0.0000",
"commission": "0.0000",
"dividendAdjustment": "0",
"guaranteedExecutionFees": "0.0000",
"unrealizedPL": "0.0000",
"NAV": "2997378.6660",
"marginUsed": "0.0000",
"marginAvailable": "2997378.6660",
"positionValue": "0.0000",
"marginCloseoutUnrealizedPL": "0.0000",
"marginCloseoutNAV": "2997378.6660",
"marginCloseoutMarginUsed": "0.0000",
"marginCloseoutPositionValue": "0.0000",
"marginCloseoutPercent": "0.00000",
"withdrawalLimit": "2997378.6660"
},
"lastTransactionID": "127"
}
口座情報がたくさん出てきていますが、口座残高はbalanceになります。
なのでプログラムする上ではbalanceの値だけを抽出すればOKですね!
シストレ・アーキテクチャ
では、さっそく複利運用のためのアルゴリズムをシストレ・アーキテクチャ設計に落とし込んでいきます。
今回修正するノードは下記の2つになります。
- fetcherノード:account_queryサービス追加
- order_schedulerノード:取引数量算出(複利運用)追加
<アーキテクチャ設計>
①account_queryサービス追加
oanda_apiパッケージ内のFetcherノードに口座残高取得用のサービスを追加しました。
サービスメッセージとコードの全貌はGitHubを参照してほしいのですが、追加部分のコードを下記の載せておきます。
レスポンスで必要な情報は「balance(口座残高)」のみとなりますが、今後の機能拡張を踏まえて他の情報も取得できるようにしておきました。
・サービスメッセージ:[GitHub]AccountQuerySrv.srv
・コード:[GitHub]fetcher.py
追加部分を抜粋したコードは下記となります。
class Fetcher(Node):
def __init__(self) -> None:
super().__init__("fetcher")
# Create service server "AccountQuery"
srv_type = AccountQuerySrv
srv_name = "account_query"
callback = self._on_recv_account_query
self._aq_srv = self.create_service(srv_type,
srv_name,
callback=callback,
callback_group=ReentrantCallbackGroup())
async def _on_recv_account_query(self,
req: SrvTypeRequest,
rsp: SrvTypeResponse
) -> SrvTypeResponse:
self.logger.debug("{:=^50}".format(" Service[account_query]:Start "))
self.logger.debug("<Request>")
self.logger.debug(" - None")
dbg_tm_start = dt.datetime.now()
rsp.result = False
rsp.frc_msg.reason_code = frc.REASON_UNSET
apirsp = None
try:
apirsp = self._api.request(self._acc)
except V20Error as err:
self.logger.error("{:!^50}".format(" V20Error "))
self.logger.error("{}".format(err))
rsp.frc_msg.reason_code = frc.REASON_OANDA_V20_ERROR
except ConnectionError as err:
self.logger.error("{:!^50}".format(" ConnectionError "))
self.logger.error("{}".format(err))
rsp.frc_msg.reason_code = frc.REASON_CONNECTION_ERROR
except ReadTimeout as err:
self.logger.error("{:!^50}".format(" ReadTimeout "))
self.logger.error("{}".format(err))
rsp.frc_msg.reason_code = frc.REASON_CONNECTION_ERROR
except Exception as err:
self.logger.error("{:!^50}".format(" OthersError "))
self.logger.error("{}".format(err))
rsp.frc_msg.reason_code = frc.REASON_OTHERS
else:
rsp.result = True
acc = apirsp["account"]
rsp.margin_rate = float(acc["marginRate"])
rsp.balance = int(float(acc["balance"]))
rsp.open_trade_count = int(acc["openTradeCount"])
rsp.open_position_count = int(acc["openPositionCount"])
rsp.pending_order_count = int(acc["pendingOrderCount"])
rsp.pl = int(float(acc["pl"]))
rsp.resettable_pl = int(float(acc["resettablePL"]))
rsp.financing = int(float(acc["financing"]))
rsp.unrealized_pl = int(float(acc["unrealizedPL"]))
rsp.nav = int(float(acc["NAV"]))
rsp.margin_used = int(float(acc["marginUsed"]))
rsp.margin_available = int(float(acc["marginAvailable"]))
rsp.position_value = int(float(acc["positionValue"]))
dbg_tm_end = dt.datetime.now()
self.logger.debug("<Response>")
self.logger.debug(" - margin_rate:[{}]".format(rsp.margin_rate))
self.logger.debug(" - balance:[{}]".format(rsp.balance))
self.logger.debug(" - open_trade_count:[{}]".format(rsp.open_trade_count))
self.logger.debug(" - open_position_count:[{}]".format(rsp.open_position_count))
self.logger.debug(" - pending_order_count:[{}]".format(rsp.pending_order_count))
self.logger.debug(" - pl:[{}]".format(rsp.pl))
self.logger.debug(" - resettable_pl:[{}]".format(rsp.resettable_pl))
self.logger.debug(" - financing:[{}]".format(rsp.financing))
self.logger.debug(" - unrealized_pl:[{}]".format(rsp.unrealized_pl))
self.logger.debug(" - nav:[{}]".format(rsp.nav))
self.logger.debug(" - margin_used:[{}]".format(rsp.margin_used))
self.logger.debug(" - margin_available:[{}]".format(rsp.margin_available))
self.logger.debug(" - position_value:[{}]".format(rsp.position_value))
self.logger.debug("[Performance]")
self.logger.debug(" - Response Time:[{}]".format(dbg_tm_end - dbg_tm_start))
self.logger.debug("{:=^50}".format(" Service[account_query]:End "))
return rsp
②取引数量算出(複利運用)追加
修正ノードはtrade_managerパッケージ内のorder_schedulerノードになります。
既存機能との互換性を保つために、修正方法はシンプルにorder_registerサービスメッセージ内のunits(数量)に指定された値で条件分けすることにしました。
■order_registerサービスメッセージ:units(数量)
・units == 0:複利運用モード、
・units > 0:units数量でトレード(従来モード)
・units < 0:異常値(トレード中止)
・コード:[GitHub]order_scheduler.py
条件分け部分を抜粋したコードは下記となります。
if req.units == 0:
# ---------- 複利運用モード ----------
if self._order_type == OrderType.MARKET:
if req.orddir_msg.order_dir == OrderDir.LONG:
price = tick_price.tick_ask
else:
price = tick_price.tick_bid
else:
price = req.entry_price
# 取引数量の算出
units = int(self._C_CI_CONST * self._c_balance / price)
if req.orddir_msg.order_dir == OrderDir.LONG:
self._units = units
else:
self._units = -units
else:
# ---------- 従来モード ----------
if req.orddir_msg.order_dir == OrderDir.LONG:
self._units = req.units
else:
self._units = -req.units
複利運用シミュレーション
参考までに単純移動平均線(SMA)のバックテストで良好なパフォーマンスが得られたパラメータで5年間複利運用した場合のシミュレーション結果も算出してみましたので紹介したいと思います。
まずは単純移動平均線(SMA)のバックテスト結果ですが、サンプルとして下記の結果を使用します。
バックテスト条件は下記となります。
- 対象通貨ペア:ドル円
- 過去チャート期間:2017年1月1日~2021年12月31日(5年間)
この結果では過去5年間で30,000pips(30円)、1ヵ月では平均500pips(0.5円)の利益となります。
では1ヵ月平均500pips(0.5円)の利益が上げられると仮定して複利運用シミュレーションしてみましょう!
シミュレーションする上での前提条件とパラメータは下記となります。
<前提条件>
・初期資産:100万円
・USD/JPY為替レート:110円(シミュレーションでは常時固定の値とする)
・月間利益:0.5円
<パラメータ>
・最大レバレッジ(max_leverage):20
・最大同時エントリー数(max_position_count):2
そしてシミュレーション結果ですが、下記のようになりました。
初期資産100万円から始め、
- 1年後(12ヵ月後) → 170万
- 2年後(24ヵ月後) → 291万
- 3年後(36ヵ月後) → 495万
- 4年後(48ヵ月後) → 845万
- 5年後(60ヵ月後) → 1440万
のように資産が増えていくのがわかります。
利回りは月間で4.55%です。(年間だと70.48%・・・)
かなりパフォーマンスが良いように見えますが、これはあくまでもシミュレーション結果なので本当にこの利回りで資産が増えていくのかはわかりません。
少なくともバックテスト結果のパフォーマンスが今後も継続していくとなると、上記グラフのように資産が増えていくことになります。
さいごに
複利運用のアルゴリズムの実装も完了し、いよいよ本格的にシステムトレードによる複利運用型の完全自動売買を実行していこうと思います。
シミュレーションではかなり良好なパフォーマンスが得られる結果となりましたが、今後の運用がシミュレーション通りとなる保証もないため、正直どのような結果になるかはやってみないとわかりません。
もうここからはトライあるのみですね!
また、シストレ開発もこれで完了ではなく、まだまだやらなければならないことがたくさんあります。
追加したいアプリケーションもありますし、シストレのエントリー/エグジット時にツイッターと連携して自動ツイート機能の追加も実装していきたいと考えています。
また、準備かできたら月単位でのシストレ運用結果ブログも書いていく予定です。
ある意味、シストレ開発の本当のスタートはここからなのかもしれませんね(笑)
コメント