インタラクティブにトレーリングストップ決済を行うBot
JijiのPush通知機能を使って、インタラクティブにトレーリングストップ決済を行うBotのサンプルです。
トレーリングストップとは
建玉(ポジション)の決済方法の一つで、「最高値を更新するごとに、逆指値の決済価格を切り上げていく」決済ロジックです。
例) USDJPY/120.10で買建玉を作成。これを、10 pips でトレーリングストップする場合、
- 建玉作成直後は、120.00 で逆指値決済される状態になる
- レートが 120.30 になった場合、逆指値の決済価格が高値に合わせて上昇し、120.20に切り上がる
- その後、レートが120.20 になると、逆指値で決済される
トレンドに乗っている間はそのまま利益を増やし、トレンドが変わって下げ始めたら決済する、という動きをする決済ロジックですね。
インタラクティブにしてみる
単純なトレーリングストップだけなら証券会社が提供している機能で実現できるので、少し手を加えてインタラクティブにしてみました。
トレーリングストップでは、以下のようなパターンがありがち。
- すこし大きなドローダウンがきて、トレンド変わってないのに決済されてしまい、利益を逃した・・
- レートが急落した時に、決済が遅れて損失が広がった・・・
これを回避できるように、Botでの強制決済に加えて、人が状況をみて決済するかどうか判断できる仕組みをいれてみます。
仕様
以下のような動作をします。
1) トレーリングストップの閾値を2段階で指定できるようにして、1つ目の閾値を超えたタイミングでは警告通知を送信。
- 通知を確認して、即時決済するか、保留するか判断できる。
- 決済をスムーズに行えるよう、通知から1タップで決済を実行できるようにする。
2) 2つ目の閾値を超えた場合、Botが建玉を決済。
- 夜間など通知を受けとっても対処できない場合を考慮して、2つ目の閾値を超えたら、強制決済するようにしておきます。
- なお、決済時にはOANDA JAPANから通知が送信されるので、Jijiからの通知は省略しました。
Bot(エージェント)のコード
TrailingStopAgent
が、Botの本体。これをバックテストやリアルトレードで動作させればOKです。- エージェントファイルの追加の方法など、Jijiの基本的な使い方はこちらをご覧ください。
TrailingStopAgent
自体は、新規に建玉を作ることはしません。- 裁量トレードや他のエージェントが作成した建玉を自動で監視し、トレーリングストップを行います。
- バックテストで試す場合は、建玉を作成するエージェントと一緒に動作させてください。
- 機能の再利用ができるように、処理は
TrailingStopManager
に実装しています。 - ※GitHubにもコミットしています
# トレーリングストップで建玉を決済するエージェント
class TrailingStopAgent
include Jiji::Model::Agents::Agent
def self.description
<<-STR
トレーリングストップで建玉を決済するエージェント。
- 損益が警告を送る閾値を下回ったら、1度だけ警告をPush通知で送信。
- さらに決済する閾値も下回ったら、建玉を決済します。
STR
end
# UIから設定可能なプロパティの一覧
def self.property_infos
[
Property.new('warning_limit', '警告を送る閾値', 20),
Property.new('closing_limit', '決済する閾値', 40)
]
end
def post_create
@manager = TrailingStopManager.new(
@warning_limit.to_i, @closing_limit.to_i, notifier)
end
def next_tick(tick)
@manager.check(broker.positions, broker.pairs)
end
def execute_action(action)
@manager.process_action(action, broker.positions) || '???'
end
def state
{
trailing_stop_manager: @manager.state
}
end
def restore_state(state)
if state[:trailing_stop_manager]
@manager.restore_state(state[:trailing_stop_manager])
end
end
end
# 建玉を監視し、最新のレートに基づいてトレールストップを行う
class TrailingStopManager
# コンストラクタ
#
# warning_limit:: 警告を送信する閾値(pip)
# closing_limit:: 決済を行う閾値(pip)
# notifier:: notifier
def initialize(warning_limit, closing_limit, notifier)
@warning_limit = warning_limit
@closing_limit = closing_limit
@notifier = notifier
@states = {}
end
# 建玉がトレールストップの閾値に達していないかチェックする。
# warning_limit を超えている場合、警告通知を送信、
# closing_limit を超えた場合、強制的に決済する。
#
# positions:: 建て玉一覧(broker#positions)
# pairs:: 通貨ペア一覧(broker#pairs)
def check(positions, pairs)
@states = positions.each_with_object({}) do |position, r|
r[position.id.to_s] = check_position(position, pairs)
end
end
# アクションを処理する
#
# action:: アクション
# positions:: 建て玉一覧(broker#positions)
# 戻り値:: アクションを処理できた場合、レスポンスメッセージ。
# TrailingStopManagerが管轄するアクションでない場合、nil
def process_action(action, positions)
return nil unless action =~ /trailing\_stop\_\_([a-z]+)_(.*)$/
case $1
when "close" then
position = positions.find {|p| p.id.to_s == $2 }
return nil unless position
position.close
return "建玉を決済しました。"
end
end
# 永続化する状態。
def state
@states.each_with_object({}) {|s, r| r[s[0]] = s[1].state }
end
# 永続化された状態から、インスタンスを復元する
def restore_state(state)
@states = state.each_with_object({}) do |s, r|
state = PositionState.new( nil,
@warning_limit, @closing_limit )
state.restore_state(s[1])
r[s[0]] = state
end
end
private
# 建玉の状態を更新し、閾値を超えていたら対応するアクションを実行する。
def check_position(position, pairs)
state = get_and_update_state(position, pairs)
if state.under_closing_limit?
position.close
elsif state.under_warning_limit?
unless state.sent_warning # 通知は1度だけ送信する
send_notification(position, state)
state.sent_warning = true
end
end
return state
end
def get_and_update_state(position, pairs)
state = create_or_get_state(position, pairs)
state.update(position)
state
end
def create_or_get_state(position, pairs)
key = position.id.to_s
return @states[key] if @states.include? key
PositionState.new(
retrieve_pip_for(position.pair_name, pairs),
@warning_limit, @closing_limit )
end
def retrieve_pip_for(pair_name, pairs)
pairs.find {|p| p.name == pair_name }.pip
end
def send_notification(position, state)
message = "#{create_position_description(position)}" \
+ " がトレールストップの閾値を下回りました。決済しますか?"
@notifier.push_notification(message, [{
'label' => '決済する',
'action' => 'trailing_stop__close_' + position.id.to_s
}])
end
def create_position_description(position)
sell_or_buy = position.sell_or_buy == :sell ? "売" : "買"
"#{position.pair_name}/#{position.entry_price}/#{sell_or_buy}"
end
end
class PositionState
attr_reader :max_profit, :profit_or_loss, :max_profit_time, :last_update_time
attr_accessor :sent_warning
def initialize(pip, warning_limit, closing_limit)
@pip = pip
@warning_limit = warning_limit
@closing_limit = closing_limit
@sent_warning = false
end
def update(position)
@units = position.units
@profit_or_loss = position.profit_or_loss
@last_update_time = position.updated_at
if @max_profit.nil? || position.profit_or_loss > @max_profit
@max_profit = position.profit_or_loss
@max_profit_time = position.updated_at
@sent_warning = false
# 高値を更新したあと、 warning_limit を超えたら再度警告を送るようにする
end
end
def under_warning_limit?
return false if @max_profit.nil?
difference >= @warning_limit * @units * @pip
end
def under_closing_limit?
return false if @max_profit.nil?
difference >= @closing_limit * @units * @pip
end
def state
{
"max_profit" => @max_profit,
"max_profit_time" => @max_profit_time,
"pip" => @pip,
"sent_warning" => @sent_warning
}
end
def restore_state(state)
@max_profit = state["max_profit"]
@max_profit_time = state["max_profit_time"]
@pip = state["pip"]
@sent_warning = state["sent_warning"]
end
private
def difference
@max_profit - @profit_or_loss
end
end