レンジブレイク手法でのトレードをアシストするBotのサンプル
FXシステムトレードフレームワーク「Jiji」 のサンプルその3。
レンジブレイク手法を使ったトレードをアシストするBotを作ってみました。
FX Wroks さんのサイト に掲載されていた「レンジブレイクを狙うシンプルな順張り」手法を、そのままJijiに移植してみたものです。
動作
以下のような動作をします。
- 1) Botがレートを監視し、レンジブレイクをチェック
- 条件は、サイトの内容と同等、8時間レートが100pips内で推移したあと、上or下に抜ける、としました。
- 待つ期間やpipsは、パラメータで調整できるようにしています。
- 2) レンジブレイクを検出したら、スマホに通知を送信します
- ダマしが多いので、今回は通知を送って判断する形に。
- 3) 通知を受けて最終判断を行い、トレードを実行。
- 通知にあるボタンを押すことで、売or買で成行注文を実行できるようにしています。
- 決済は、トレーリングストップで。
軽く動かしてみた感想
軽くテストしてみましたが、思ったよりもダマしに引っかかる感じですね。
これは、まぁまぁ。
これは、ブレイクと判定された時点で下げが終わっている・・。
これは、一度上にブレイクしたあと、逆方向に進んでいます・・・。
ブレイクの条件を調整してみる、移動平均でのトレンドチェックと組み合わせるなど、カスタマイズして使ってみてください。
コード
# === レンジブレイクでトレードを行うエージェント class RangeBreakAgent include Jiji::Model::Agents::Agent def self.description <<-STR レンジブレイクでトレードを行うエージェント。 - 指定期間(デフォルトは8時間)のレートが一定のpipsに収まっている状態から、 レンジを抜けたタイミングで通知を送信。 - 通知からトレード可否を判断し、取引を実行できます。 - 決済はトレーリングストップで行います。 STR end # UIから設定可能なプロパティの一覧 def self.property_infos [ Property.new('target_pair', '対象とする通貨ペア', 'USDJPY'), Property.new('range_period', 'レンジを判定する期間(分)', 60 * 8), Property.new('range_pips', 'レンジ相場とみなす値幅(pips)', 100), Property.new('trailing_stop_pips', 'トレールストップで決済する値幅(pips)', 30), Property.new('trade_units', '取引数量', 1) ] end def post_create pair = broker.pairs.find { |p| p.name == @target_pair.to_sym } @checker = RangeBreakChecker.new( pair, @range_period.to_i, @range_pips.to_i) end def next_tick(tick) # レンジブレイクしたかどうかチェック result = @checker.check_range_break(tick) # ブレイクしていたら通知を送る send_notification(result) if result[:state] != :no end def execute_action(action) case action when 'range_break_buy' then buy when 'range_break_sell' then sell else '不明なアクションです' end end def state { checker: @checker.state } end def restore_state(state) @checker.restore_state(state[:checker]) if state[:checker] end private def sell broker.sell(@target_pair.to_sym, @trade_units.to_i, :market, { trailing_stop: @trailing_stop_pips.to_i }) '売注文を実行しました' end def buy broker.buy(@target_pair.to_sym, @trade_units.to_i, :market, { trailing_stop: @trailing_stop_pips.to_i }) '買注文を実行しました' end def send_notification(result) message = "#{@target_pair} #{result[:price]}" \ + ' がレンジブレイクしました。取引しますか?' @notifier.push_notification(message, [create_action(result)]) logger.info "#{message} #{result[:state]} #{result[:time]}" end def create_action(result) if result[:state] == :break_high { 'label' => '買注文を実行', 'action' => 'range_break_buy' } else { 'label' => '売注文を実行', 'action' => 'range_break_sell' } end end end class RangeBreakChecker def initialize(pair, period, range_pips) @pair = pair @range_pips = range_pips @candles = Candles.new(period * 60) end def check_range_break(tick) tick_value = tick[@pair.name] result = check_state(tick_value, tick.timestamp) @candles.reset unless result == :no # 一度ブレイクしたら、一旦状態をリセットして次のブレイクを待つ @candles.update(tick_value, tick.timestamp) { state: result, price: tick_value.bid, time: tick.timestamp } end def state @candles.state end def restore_state(state) @candles.restore_state(state) end private # レンジブレイクしているかどうか判定する def check_state(tick_value, time) highest = @candles.highest lowest = @candles.lowest return :no if highest.nil? || lowest.nil? return :no unless over_period?(time) diff = highest - lowest return :no if diff >= @range_pips * @pair.pip calculate_state( tick_value, highest, diff ) end def calculate_state( tick_value, highest, diff ) center = highest - diff / 2 pips = @range_pips / 2 * @pair.pip if tick_value.bid >= center + pips return :break_high elsif tick_value.bid <= center - pips return :break_low end :no end def over_period?(time) oldest_time = @candles.oldest_time return false unless oldest_time (time.to_i - oldest_time.to_i) >= @candles.period end end class Candles attr_reader :period def initialize(period) @candles = [] @period = period @next_update = nil end def update(tick_value, time) time = Candles.normalize_time(time) if @next_update.nil? || time > @next_update new_candle(tick_value, time) else @candles.last.update(tick_value, time) end end def highest high = @candles.max_by { |c| c.high } high.nil? ? nil : BigDecimal.new(high.high, 10) end def lowest low = @candles.min_by { |c| c.low } low.nil? ? nil : BigDecimal.new(low.low, 10) end def oldest_time oldest = @candles.min_by { |c| c.time } oldest.nil? ? nil : oldest.time end def reset @candles = [] @next_update = nil end def new_candle(tick_value, time) limit = time - period @candles = @candles.reject { |c| c.time < limit } @candles << Candle.new @candles.last.update(tick_value, time) @next_update = time + (60 * 5) end def state { candles: @candles.map { |c| c.to_h }, next_update: @next_update } end def restore_state(state) @candles = state[:candles].map { |s| Candle.from_h(s) } @next_update = state[:next_update] end def self.normalize_time(time) Time.at((time.to_i / (60 * 5)).floor * 60 * 5) end end class Candle attr_reader :high, :low, :time def initialize(high = nil, low = nil, time = nil) @high = high @low = low @time = time end def update(tick_value, time) price = extract_price(tick_value) @high = price if @high.nil? || @high < price @low = price if @low.nil? || @low > price @time = time if @time.nil? end def to_h { high: @high, low: @low, time: @time } end def self.from_h(hash) Candle.new(hash[:high], hash[:low], hash[:time]) end private def extract_price(tick_value) tick_value.bid end end
トラップリピートイフダンのような注文を発行するエージェントのサンプル
FXシステムトレードフレームワーク「Jiji」のサンプル その2、として、 トラップリピートイフダンのような注文を発行するエージェントを作ってみました。
※トラップリピートイフダン(トラリピ)は、マネースクウェアジャパン(M2J)の登録商標です。
トラップリピートイフダンとは
指値/逆指値の注文と決済を複数組み合わせて行い、その中でレートが上下することで利益を出すことを狙う、発注ロジックです。 具体的にどういった動きをするのかは、マネースクウェアジャパン のサイトがとてもわかりやすいので、そちらをご覧ください。
特徴
FX研究日記さんの評価記事が参考になります。
- レンジ相場では、利益を出しやすい
- ×レートが逆行すると損失を貯めこんでしまう
仕組みからして、いわゆるコツコツドカンなシステムという印象です。 レンジ相場なら利益を積み上げやすいので、トレンドを判定するロジックと組み合わせて、レートが一定のレンジで動作しそうになったら稼働させる、などすれば使えるかも。
エージェントのコード
- 実装は、こちらのサイトで配布されているEAを参考にさせていただきました。
TrapRepeatIfDoneAgent
が、エージェントの本体です。これをバックテストやリアルトレードで動作させればOK。- エージェントファイルの追加の方法など、Jijiの基本的な使い方はこちらをご覧ください。
- 機能の再利用ができるように、発注処理は
TrapRepeatIfDone
に実装しています。
# === トラップリピートイフダンのような注文を発行するエージェント class TrapRepeatIfDoneAgent include Jiji::Model::Agents::Agent def self.description <<-STR トラップリピートイフダンのような注文を発行するエージェント STR end # UIから設定可能なプロパティの一覧 def self.property_infos [ Property.new('trap_interval_pips', 'トラップを仕掛ける間隔(pips)', 50), Property.new('trade_units', '1注文あたりの取引数量', 1), Property.new('profit_pips', '利益を確定するpips', 100), Property.new('slippage', '許容スリッページ(pips)', 3) ] end def post_create @trap_repeat_if_done = TrapRepeatIfDone.new( broker.pairs.find {|p| p.name == :USDJPY }, :buy, @trap_interval_pips.to_i, @trade_units.to_i, @profit_pips.to_i, @slippage.to_i, logger) end def next_tick(tick) @trap_repeat_if_done.register_orders(broker) end def state @trap_repeat_if_done.state end def restore_state(state) @trap_repeat_if_done.restore_state(state) end end # トラップリピートイフダンのような注文を発行するクラス class TrapRepeatIfDone # コンストラクタ # # target_pair:: 現在の価格を格納するTick::Valueオブジェクト # sell_or_buy:: 取引モード。 :buy の場合、買い注文を発行する。 :sellの場合、売 # trap_interval_pips:: トラップを仕掛ける間隔(pips) # trade_units:: 1注文あたりの取引数量 # profit_pips:: 利益を確定するpips # slippage:: 許容スリッページ。nilの場合、指定しない def initialize(target_pair, sell_or_buy=:buy, trap_interval_pips=50, trade_units=1, profit_pips=100, slippage=3, logger=nil) @target_pair = target_pair @trap_interval_pips = trap_interval_pips @slippage = slippage @mode = if sell_or_buy == :sell Sell.new(target_pair, trade_units, profit_pips, slippage, logger) else Buy.new(target_pair, trade_units, profit_pips, slippage, logger) end @logger = logger @registerd_orders = {} end # 注文を登録する # # broker:: broker def register_orders(broker) broker.instance_variable_get(:@broker).refresh_positions # 常に最新の建玉を取得して利用するようにする # TODO 公開APIにする each_traps(broker.tick) do |trap_open_price| next if order_or_position_exists?(trap_open_price, broker) register_order(trap_open_price, broker) end end def state @registerd_orders end def restore_state(state) @registerd_orders = state unless state.nil? end private def each_traps(tick) current_price = @mode.resolve_current_price(tick[@target_pair.name]) base = resolve_base_price(current_price) 6.times do |n| # baseを基準に、上下3つのトラップを仕掛ける trap_open_price = BigDecimal.new(base, 10) \ + BigDecimal.new(@trap_interval_pips, 10) * (n-3) * @target_pair.pip yield trap_open_price end end # 現在価格をtrap_interval_pipsで丸めた価格を返す。 # # 例) trap_interval_pipsが50の場合、 # resolve_base_price(120.10) # -> 120.00 # resolve_base_price(120.49) # -> 120.00 # resolve_base_price(120.51) # -> 120.50 # def resolve_base_price(current_price) current_price = BigDecimal.new(current_price, 10) pip_precision = 1 / @target_pair.pip (current_price * pip_precision / @trap_interval_pips ).ceil \ * @trap_interval_pips / pip_precision end # trap_open_priceに対応するオーダーを登録する def register_order(trap_open_price, broker) result = @mode.register_order(trap_open_price, broker) unless result.order_opened.nil? @registerd_orders[key_for(trap_open_price)] \ = result.order_opened.internal_id end end # trap_open_priceに対応するオーダーを登録済みか評価する def order_or_position_exists?(trap_open_price, broker) order_exists?(trap_open_price, broker) \ || position_exists?(trap_open_price, broker) end def order_exists?(trap_open_price, broker) key = key_for(trap_open_price) return false unless @registerd_orders.include? key id = @registerd_orders[key] order = broker.orders.find {|o| o.internal_id == id } return !order.nil? end def position_exists?(trap_open_price, broker) # trapのリミット付近でレートが上下して注文が大量に発注されないよう、 # trapのリミット付近を開始値とする建玉が存在する間は、trapの注文を発行しない slipage_price = (@slippage.nil? ? 10 : @slippage) * @target_pair.pip position = broker.positions.find do |p| # 注文時に指定したpriceちょうどで約定しない場合を考慮して、 # 指定したslippage(指定なしの場合は10pips)の誤差を考慮して存在判定をする p.entry_price < trap_open_price + slipage_price \ && p.entry_price > trap_open_price - slipage_price end return !position.nil? end def key_for(trap_open_price) (trap_open_price * (1 / @target_pair.pip)).to_i.to_s end # 取引モード(売 or 買) # 買(Buy)の場合、買でオーダーを行う。売(Sell)の場合、売でオーダーを行う。 class Mode def initialize(target_pair, trade_units, profit_pips, slippage, logger) @target_pair = target_pair @trade_units = trade_units @profit_pips = profit_pips @slippage = slippage @logger = logger end # 現在価格を取得する(買の場合Askレート、売の場合Bidレートを使う) # # tick_value:: 現在の価格を格納するTick::Valueオブジェクト # 戻り値:: 現在価格 def resolve_current_price(tick_value) end # 注文を登録する def register_order(trap_open_price, broker) end def calculate_price(price, pips) price = BigDecimal.new(price, 10) pips = BigDecimal.new(pips, 10) * @target_pair.pip (price + pips).to_f end def pring_order_log(mode, options, timestamp) return unless @logger message = [ mode, timestamp, options[:price], options[:take_profit], options[:lower_bound], options[:upper_bound] ].map {|item| item.to_s }.join(" ") @logger.info message end end class Sell < Mode def resolve_current_price(tick_value) tick_value.bid end def register_order(trap_open_price, broker) timestamp = broker.tick.timestamp options = create_option(trap_open_price, timestamp) pring_order_log("sell", options, timestamp) broker.sell(@target_pair.name, @trade_units, :marketIfTouched, options) end def create_option(trap_open_price, timestamp) options = { price: trap_open_price.to_f, take_profit: calculate_price(trap_open_price, @profit_pips*-1), expiry: timestamp + 60*60*24*7 } unless @slippage.nil? options[:lower_bound] = calculate_price(trap_open_price, @slippage*-1) options[:upper_bound] = calculate_price(trap_open_price, @slippage) end options end end class Buy < Mode def resolve_current_price(tick_value) tick_value.ask end def register_order(trap_open_price, broker) timestamp = broker.tick.timestamp options = create_option(trap_open_price, timestamp) pring_order_log("buy", options, timestamp) broker.buy(@target_pair.name, @trade_units, :marketIfTouched, options) end def create_option(trap_open_price, timestamp) options = { price: trap_open_price.to_f, take_profit: calculate_price(trap_open_price, @profit_pips), expiry: timestamp + 60*60*24*7 } unless @slippage.nil? options[:lower_bound] = calculate_price(trap_open_price, @slippage*-1) options[:upper_bound] = calculate_price(trap_open_price, @slippage) end options end end end
インタラクティブにトレーリングストップ決済を行うBotを作ってみた
FXシステムトレードフレームワーク「Jiji」の使い方サンプル その1、ということで、 Jijiを使って、インタラクティブにトレーリングストップ決済を行うBotを作ってみました。
トレーリングストップとは
建玉(ポジション)の決済方法の一つで、「最高値を更新するごとに、逆指値の決済価格を切り上げていく」決済ロジックです。
例) USDJPY/120.10で買建玉を作成。これを、10 pips でトレーリングストップする場合、
- 建玉作成直後は、120.00 で逆指値決済される状態になる
- レートが 120.30 になった場合、逆指値の決済価格が高値に合わせて上昇し、120.20に切り上がる
- その後、レートが120.20 になると、逆指値で決済される
トレンドに乗っている間はそのまま利益を増やし、トレンドが変わって下げ始めたら決済する、という動きをする決済ロジックですね。
インタラクティブにしてみる
単純なトレーリングストップだけなら証券会社が提供している機能で実現できるので、少し手を加えてインタラクティブにしてみました。
トレーリングストップでは、以下のようなパターンがありがち。
- すこし大きなドローダウンがきて、トレンド変わってないのに決済されてしまい、利益を逃した・・
- レートが急落した時に、決済が遅れて損失が広がった・・・
これを回避できるように、Botでの強制決済に加えて、人が状況をみて決済するかどうか判断できる仕組みをいれてみます。
仕様
以下のような動作をします。
トレーリングストップの閾値を2段階で指定できるようにして、1つ目の閾値を超えたタイミングでは警告通知を送信。
- 通知を確認して、即時決済するか、保留するか判断できる。
- 決済をスムーズに行えるよう、通知から1タップで決済を実行できるようにする。
-
- 夜間など通知を受けとっても対処できない場合を考慮して、2つ目の閾値を超えたら、強制決済するようにしておきます。
- なお、決済時にはOANDA JAPANから通知が送信されるので、Jijiからの通知は省略しました。
Bot(エージェント)のコード
TrailingStopAgent
が、Botの本体。これをバックテストやリアルトレードで動作させればOKです。- エージェントファイルの追加の方法など、Jijiの基本的な使い方はこちらをご覧ください。
TrailingStopAgent
自体は、新規に建玉を作ることはしません。- 機能の再利用ができるように、処理は
TrailingStopManager
に実装しています。
# トレーリングストップで建玉を決済するエージェント 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
それでは、みなさま、良いお年を。
ReactとCordovaで、Web/モバイルのハイブリットアプリを作った話
ReactとCordovaを使って、ブラウザ向けのWebUI + Androidで動くスマホアプリ を提供するサービスを、一人で作ってみた話です。
サマリー
- 作ったもの
- 最大の課題:作業量
- 一人で作りきるために意識したこと
- 取り組み1: Cordovaを使って、Web UI/スマホアプリのコードを共通化する
- 取り組み2: レイヤードアーキテクチャを採用し、共有できるコードを最大化する
- 取り組み3: ユニットテストを書く
- まとめ
作ったもの
自分だけの取引アルゴリズムで、誰でも、いますぐ、かんたんにFX自動取引を開始できる、システムトレードフレームワークです。
- アルゴリズムの作成、バックテスト、リアル口座での自動取引まで、これ一つで可能。
- 取引アルゴリズムはRubyで記述。メール送信や取引タイミングのPush通知も、APIを呼び出すだけで実現できます。
- 取引の状況は、スマホアプリ/Web UIでいつでもどこでも確認が可能。アルゴリズムの管理もできるので、相場が急に動いても安心です。
- クラウドプラットフォームのHerokuに対応。運用も低コスト。
- オープンソース。ソースコードはGitHubで公開しています。
スクリーンショットをいくつか。
Web UI:
スマホアプリ:
コード規模
UI側のコード規模は以下の通り。テストケースを含めた合計で、31000行くらいです。
WebUI | アプリ | 合計 | |
---|---|---|---|
ソースコード | 15222 | 4454 | 19676 |
テストケース | 9676 | 1574 | 11250 |
また、これとは別に、サーバーのコード(ruby)が、ソースコード/テストケースあわせて、27000行くらいあります。
構成
- UIは、いわゆるシングルページアプリケーションで、ECMAScript2015 で書いています。
- Babelでコンパイルして、Webpackで統合する形。
- マテリアルデザインを採用していて、Reactで動くUIライブラリの Material UI を利用しています。
前置きは以上。
最大の課題:作業量
開発にあたっての最大の課題は、なんといっても作業量でした。
初期の検討段階で、「Webアプリ or モバイルアプリのどちらかに注力できないか?」と考えましたが、
Push通知やモバイルでのシステム管理に対応した、スマホ時代のFXシステムトレードフレームワークにしたかった。
- 同様の機能を提供するソフトはすでにあるのですが、モバイルでの取引状況の確認やアルゴリズムの管理に難があったのが、開発のきっかけにもなっています。
ということで、両方必要という結論に至りました。そうと決まれば、後は、如何にして作りきるかです。
一人で作りきるために意識したこと
以下の2点を、特に意識しました。
- コードの共通化/再利用
- Web UI/スマホアプリのコードを可能な限り共通化して、開発コストを削減する。
- ユニットテスト
- ユニットテストで個々のモジュールの動作を保障。
- 機能追加や変更を、低コストで素早く行えるようにする。
そして、↑のための具体的な取り組みとして、以下を行いました。
取り組み1: Cordovaを使って、Web UI/スマホアプリのコードを共通化する
スマホアプリをCordovaを使ったハイブリットアプリにして、Web UI/スマホアプリのコードを共通化しました。
- Cordovaを使うことで、JavaScript + HTML + CSSでスマホアプリを作ることができるので、同様の構成で作成するWeb UIのコードを再利用できるようになります。
- React Native という選択肢もありましたが、UI側の開発に着手した時点ではAndroidには対応していなかったため、見送りました。
- Cordovaは、Webでの情報が豊富で、実績も多くあった点がプラスでした。
- また、Push通知や課金決済関連のプラグインがあったことも大きいです。
- 採用に当たって懸念だった、UIのパフォーマンスについては、かんたんなプロトタイプを作って検証しました。
- やはり、ネイティブアプリよりはもっさりしていますが、ゲームのようなパフォーマンスがアプリの価値に直結する性質のソフトウェアではないので、問題ないレベルと判断。
- それよりも、リリースまでの開発工数を抑えること、また、リリース後の機能強化や改善をスピード感を持って行えること、を重視しました。 unageanu.hatenablog.com
「UIの反応速度とか使い勝手とか、実際どんなもんなの?」と気になる方は、ダウンロードしてお試し頂ければと思います。(まいど、ありがとうございます! / 30日の無料トライアル期間内に、定期利用を解除すれば請求は発生しないので大丈夫です)
なお、Crosswalkの効果は絶大でした。体感速度が劇的に向上したうえ、怪しい動きをしていた箇所も治ったり。 apkのサイズ/起動時間は増えますが、採用する価値はあります。
取り組み2: レイヤードアーキテクチャを採用し、共有できるコードを最大化する
MVVMのレイヤードアーキテクチャを採用して、Modelレイヤのコードを共通化。
ViewやViewModelも、変更の度合いに応じて最適なレイヤーでカスタマイズすることで、共有できるコードを最大化しました。
- Model/View/ViewModel + 通信などのInfrastructureで構成。
- Model(UIに依存しない、アプリ共通のコアドメイン)は、Web UI/スマホアプリでそのまま共有。
- スタイルの変更だけで済む場合は、CSSのレベルでカスタマイズ。
- DOM構造を変える必要がある場合は、View(Reactコンポーネント)のコードを差し替えて対応。
- Viewで管理するデータや機能がそもそも異なる場合は、ViewModel(Modelをラップして、Viewに依存するデータや操作を提供するクラス群)のレベルで、カスタマイズして再利用。
このほか、Infrastructureの通信部分やGoogle Analyticsでの利用状況解析も、WebUI/スマホアプリでカスタマイズして利用しています。
- 通信部分は接続先REST APIを差し替える必要があるため(Web UIは自ホストに、スマホアプリはUIで設定したサーバーに接続)、URLResolverとして機能を切り出しカスタマイズしています。
- 利用状況解析は、スマホアプリではJava APIを使うようカスタマイズしたクラスを用意して差し替え。
以下は、利用状況解析のコードの抜粋です。 インターフェイスを統一しつつ、実装を差し替える形で、別のクラスを用意しました。
WebUI版:
export default class GoogleAnalytics { // 略 // Google Analytics のJavaScript版APIを呼び出す sendEvent( action, label="", value={} ) { this.run((ga) => ga('send', 'event', this.category, action, label, value)); } sendTiming( category, timingVar, time, label ) { this.run((ga) => { ga('timing', category, timingVar, time, label); }); } // 略 }
スマホアプリ版:
export default class GoogleAnalytics { // 略 // Cordova google-analytics-plugin のAPIを呼び出す // https://github.com/danwilson/google-analytics-plugin sendEvent( action, label="", value={} ) { this.run((ga) => { ga.trackEvent(this.category, action, label, value, () => {}, (e) => console.log(e)); }); } sendTiming( category, timingVar, time, label ) { this.run((ga) => { ga.trackTiming(category, time, timingVar, label, () => {}, (e) => console.log(e.message)); }); } // 略 }
これら、コンポーネントの差し替えは、DI(Dependency Injection)コンテナを利用して、行うようにしました。 DIコンテナを使うことで、コンポーネントの外側で、環境ごとにどのクラスを使うのか宣言的に指定できるようになります。
WebUI版のコンポーネント定義:
binder.bind("googleAnalytics").to("utils.GoogleAnalytics") .withProperties({ category: "web-ui", version: "1.0" }).onInitialize("initialize");
// スマホアプリ用のクラスを使うように変更 binder.bind("googleAnalytics").to("app.utils.GoogleAnalytics") .withProperties({ category: "app", // category, versionもカスタマイズ。 version: "1.0.6" }).onInitialize("initialize");
利用側のコードは以下のような感じ。Inject
としてマークしておくと、コンポーネント定義で宣言されたコンポーネントが注入されるので、利用側はAPIを呼び出すだけです。
export default class RmtService extends AbstractService { constructor() { this.urlResolver = ContainerJS.Inject; this.xhrManager = ContainerJS.Inject; this.googleAnalytics = ContainerJS.Inject; // コンポーネント定義で宣言されたコンポーネントが注入される } // 略 putAgentSetting(settings) { this.googleAnalytics.sendEvent( "put rmt agent setting" ); // GAにイベントを送信 return this.xhrManager.xhr(this.serviceUrl("agents"), "PUT", settings); } }
取り組み3: ユニットテストを書く
MVVMアーキテクチャを採用したことで、View以外の部分はDOMに依存しなくなるため、容易にユニットテストができるようになります。 ユニットテストを意識的に用意したことで、一人でも何とか作りきることができたかな、と感じます。
- 一人で開発したとこもあり、サーバー側の開発で1か月くらいrubyのコードばかり書いていると、自分が書いたコードでも忘れてしまって、変なバグを埋め込んだりします。テストコードがあることでデグレードが検知され、バグに気付くことが多くありました。
- また、コードがどう動くか、についても、テストケースをみると思い出せるということもありました。
ユニットテストで確認された"動く"コードを少しずつ積み上げていく、という作り方は、心理的にも良かったと思います。 一人なので、開発期間はどうしても長くなりますし、やってもやってもタスクがなくならない、終わりが見えない感じになりますが、"動く"コードが積み上がっていくことで、毎日少しずつでもコードを書いていれば、いつか完成すると思えるようになります。
なお、初版リリースの段階ではViewのテストは省略しました。
- 初版リリースでは、UIの精緻化などでDOMの構造を変更する可能性が高く、テストを作成しても、効果が変更コストに見合わないと考えました。
- また、Viewの機能は「ViewModelの状態に応じて仮想DOMを生成する」だけで、テストすべきロジックが少ない && ViewModelのテストで、アプリケーションがこういう状態の時はこうなる、〇〇操作を実行するとこういう状態になる、といった機能は確認できている、というのも理由としてあります。
- 同じ理由で、E2Eテストも行っていません。このあたりは、今後、サービスが利用されるようになって、メンテナンスの比重が大きくなってからの課題と考えています。
まとめ
- Cordovaを使ってちゃんと作れば、一人でもサービスを作れる。
- プロトタイプや最初のMVPは、Cordovaで素早くコストをかけずに作る、そして、手ごたえがあったらネイティブで作り直す、という選択肢は割とアリなのではと思います。
- もちろん、パフォーマンスが重要なアプリの場合は、不相応な場合もありますが。
- ユニットテスト重要。テスタビリティは最初から考慮して作ろう。
- Crosswalk速い。使おう。
FXシステムトレードフレームワーク「Jiji2」の開発状況 その2
追記(2015-12-01):
FXシステムトレードフレームワーク「Jiji」、リリースしました!
使ってみて、ご意見など頂けるとうれしいです。
前回の更新からだいぶ時間が空いてしまいました・・・。少しずつ実装は進んでいます。
9月10月でスマホアプリの実装とUIデザインがだいたいできたので、スクリーンショットを公開してみます。 例によって、コメントなど頂ければ嬉しいです。
ホーム
- 最初に表示される画面です。
- 以下のような情報を、1画面でさっと確認できます。
- 口座残高/直近の勝率などのサマリ
- 現在のレートと値動きをチャートで
- トレードシステムからの新着の通知
- 最新の建玉
取引状況
- 合計損益や勝率などシステムトレードの取引状況を確認する画面です。
- 左上のメニューから集計期間を変えて分析することができます。
チャート
- 詳細チャートです。最大10年前までさかのぼってレートを閲覧できます。
- ローソク足のほか、エージェントで描画した移動平均線などのグラフを表示できます。
- 真ん中あたりにある緑と赤のバーが建玉の保有期間を示していて、チャートの値動きにあわせて期待通りトレードができているかチェックできるようになっています。
取引ロジック(エージェント)エディタ
通知一覧
- 取引ロジック(エージェントと呼んでいます)から送られてきた通知の一覧画面。
- クリックで詳細が閲覧可能です。
ポジション一覧
- 取引ロジックが行った取引の一覧です。
- こちらもクリックで詳細が表示されます。
バックテストの作成
- 任意の期間とエージェントを指定してバックテストを実行できます。
バックテスト詳細
- バックテストの結果を確認できます。
ログ
- エージェントが出力したログを確認できます。
設定
- 使用する証券会社やアクセスパスワードを設定できます。
残タスク
残っているタスクは以下です。
- ロゴの作成とデザインの最終調整
- デバッグと使い込みテスト
- 使い方などのドキュメント準備
- 導入手順の整備
1か月でこれらを消化して、11月末にはなんとかリリースしたいところ。もうしばらくお待ちください。
Jiji2 - ホーム画面のスクリーンショット
追記(2015-12-01):
FXシステムトレードフレームワーク「Jiji」、リリースしました!
使ってみて、ご意見など頂けるとうれしいです。
ホーム画面のデザインができてきたので、現状のスクリーンショットを公開します。
ご意見などいただければ嬉しいです。
PC版:
スマホ版:
ホームはアプリ利用時のエントリーポイントになる画面です。
以下のような、システムトレードの主要な情報を1画面でさっと確認できるようにしました。
- 口座残高/直近の勝率などのサマリ
- 現在のレートと値動きをチャートで
- トレードシステムからの新着の通知
- 最新の建玉
まずはこの画面で状況を把握した後、エージェント設定の変更などのアクションを実行していくイメージです。
ソースコード
コードはGitHubで公開しているので、気になる方はこちらをご覧ください。