無料で使えるシステムトレードフレームワーク「Jiji」 をリリースしました!

・OANDA Trade APIを利用した、オープンソースのシステムトレードフレームワークです。
・自分だけの取引アルゴリズムで、誰でも、いますぐ、かんたんに、自動取引を開始できます。

レンジブレイク手法でのトレードをアシストするBotのサンプル

FXシステムトレードフレームワーク「Jiji」 のサンプルその3。
レンジブレイク手法を使ったトレードをアシストするBotを作ってみました。

FX Wroks さんのサイト に掲載されていた「レンジブレイクを狙うシンプルな順張り」手法を、そのままJijiに移植してみたものです。

動作

以下のような動作をします。

f:id:unageanu:20160123200914p:plain

  • 1) Botがレートを監視し、レンジブレイクをチェック
    • 条件は、サイトの内容と同等、8時間レートが100pips内で推移したあと、上or下に抜ける、としました。
    • 待つ期間やpipsは、パラメータで調整できるようにしています。
  • 2) レンジブレイクを検出したら、スマホに通知を送信します
    • ダマしが多いので、今回は通知を送って判断する形に。
  • 3) 通知を受けて最終判断を行い、トレードを実行。
    • 通知にあるボタンを押すことで、売or買で成行注文を実行できるようにしています。
    • 決済は、トレーリングストップで。

軽く動かしてみた感想

軽くテストしてみましたが、思ったよりもダマしに引っかかる感じですね。

f:id:unageanu:20160123200915p:plain

これは、まぁまぁ。

f:id:unageanu:20160123200917p:plain

これは、ブレイクと判定された時点で下げが終わっている・・。

f:id:unageanu:20160123200916p:plain

これは、一度上にブレイクしたあと、逆方向に進んでいます・・・。

ブレイクの条件を調整してみる、移動平均でのトレンドチェックと組み合わせるなど、カスタマイズして使ってみてください。

コード

# === レンジブレイクでトレードを行うエージェント
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)の登録商標です。

トラップリピートイフダンとは

指値/逆指値の注文と決済を複数組み合わせて行い、その中でレートが上下することで利益を出すことを狙う、発注ロジックです。 具体的にどういった動きをするのかは、マネースクウェアジャパン のサイトがとてもわかりやすいので、そちらをご覧ください。

www.toraripifx.com

特徴

FX研究日記さんの評価記事が参考になります。

tasfx.net

  • レンジ相場では、利益を出しやすい
  • ×レートが逆行すると損失を貯めこんでしまう

仕組みからして、いわゆるコツコツドカンなシステムという印象です。 レンジ相場なら利益を積み上げやすいので、トレンドを判定するロジックと組み合わせて、レートが一定のレンジで動作しそうになったら稼働させる、などすれば使えるかも。

エージェントのコード

  • 実装は、こちらのサイトで配布されているEAを参考にさせていただきました。
  • TrapRepeatIfDoneAgentが、エージェントの本体です。これをバックテストやリアルトレードで動作させればOK。
  • 機能の再利用ができるように、発注処理は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 でトレーリングストップする場合、

f:id:unageanu:20151228130735p:plain

  • 建玉作成直後は、120.00 で逆指値決済される状態になる
  • レートが 120.30 になった場合、逆指値の決済価格が高値に合わせて上昇し、120.20に切り上がる
  • その後、レートが120.20 になると、逆指値で決済される

トレンドに乗っている間はそのまま利益を増やし、トレンドが変わって下げ始めたら決済する、という動きをする決済ロジックですね。

インタラクティブにしてみる

単純なトレーリングストップだけなら証券会社が提供している機能で実現できるので、少し手を加えてインタラクティブにしてみました。

トレーリングストップでは、以下のようなパターンがありがち。

  • すこし大きなドローダウンがきて、トレンド変わってないのに決済されてしまい、利益を逃した・・
  • レートが急落した時に、決済が遅れて損失が広がった・・・

これを回避できるように、Botでの強制決済に加えて、人が状況をみて決済するかどうか判断できる仕組みをいれてみます。

仕様

以下のような動作をします。

f:id:unageanu:20151228130736p:plain

  • トレーリングストップの閾値を2段階で指定できるようにして、1つ目の閾値を超えたタイミングでは警告通知を送信。

    • 通知を確認して、即時決済するか、保留するか判断できる。
    • 決済をスムーズに行えるよう、通知から1タップで決済を実行できるようにする。 f:id:unageanu:20151228105949p:plain
  • 2つ目の閾値を超えた場合、Bot建玉を決済。

    • 夜間など通知を受けとっても対処できない場合を考慮して、2つ目の閾値を超えたら、強制決済するようにしておきます。
    • なお、決済時にはOANDA JAPANから通知が送信されるので、Jijiからの通知は省略しました。

Bot(エージェント)のコード

  • TrailingStopAgentが、Botの本体。これをバックテストやリアルトレードで動作させればOKです。
  • 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: ユニットテストを書く
  • まとめ

作ったもの

jiji2.unageanu.net

自分だけの取引アルゴリズムで、誰でも、いますぐ、かんたんにFX自動取引を開始できる、システムトレードフレームワークです。

スクリーンショットをいくつか。

Web UI:

f:id:unageanu:20151209113018p:plain f:id:unageanu:20151209113019p:plain f:id:unageanu:20151209113020p:plain

スマホアプリ:

f:id:unageanu:20151209113043p:plain f:id:unageanu:20151209113044p:plain f:id:unageanu:20151209113045p:plain

コード規模

UI側のコード規模は以下の通り。テストケースを含めた合計で、31000行くらいです。

WebUI アプリ 合計
ソースコード 15222 4454 19676
テストケース 9676 1574 11250


また、これとは別に、サーバーのコード(ruby)が、ソースコード/テストケースあわせて、27000行くらいあります。

構成

  • UIは、いわゆるシングルページアプリケーションで、ECMAScript2015 で書いています。
  • マテリアルデザインを採用していて、Reactで動くUIライブラリの Material UI を利用しています。

前置きは以上。

最大の課題:作業量

開発にあたっての最大の課題は、なんといっても作業量でした。
初期の検討段階で、「Webアプリ or モバイルアプリのどちらかに注力できないか?」と考えましたが、

  • Push通知やモバイルでのシステム管理に対応した、スマホ時代のFXシステムトレードフレームワークにしたかった。

    • 同様の機能を提供するソフトはすでにあるのですが、モバイルでの取引状況の確認やアルゴリズムの管理に難があったのが、開発のきっかけにもなっています。
  • とはいえ、スマホで取引アルゴリズムを作成したり、取引結果を分析するのはツラい

    • Rubyのコードを作成するのは、やはりPCからになると思われる。(外出先からスマホアプリで緊急修正、というのはあるにしても)
    • 取引結果の分析も、広い画面で行いたい。

ということで、両方必要という結論に至りました。そうと決まれば、後は、如何にして作りきるかです。

一人で作りきるために意識したこと

以下の2点を、特に意識しました。

  • コードの共通化/再利用
    • Web UI/スマホアプリのコードを可能な限り共通化して、開発コストを削減する。
  • ユニットテスト
    • ユニットテストで個々のモジュールの動作を保障。
    • 機能追加や変更を、低コストで素早く行えるようにする。

そして、↑のための具体的な取り組みとして、以下を行いました。

  1. Cordovaを使って、Web UI/スマホアプリのコードを共通化する
  2. レイヤードアーキテクチャを採用し、共有できるコードを最大化する
  3. ユニットテストを書く

取り組み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も、変更の度合いに応じて最適なレイヤーでカスタマイズすることで、共有できるコードを最大化しました。

f:id:unageanu:20151209145903p:plain

  • Model/View/ViewModel + 通信などのInfrastructureで構成。
  • Model(UIに依存しない、アプリ共通のコアドメイン)は、Web UI/スマホアプリでそのまま共有。
  • スタイルの変更だけで済む場合は、CSSのレベルでカスタマイズ。
  • DOM構造を変える必要がある場合は、View(Reactコンポーネント)のコードを差し替えて対応。
  • Viewで管理するデータや機能がそもそも異なる場合は、ViewModel(Modelをラップして、Viewに依存するデータや操作を提供するクラス群)のレベルで、カスタマイズして再利用。
    • 例えば、WebUIでは、"通知の一覧を表示する機能"と"選択した通知の詳細を表示する機能"を同じ画面で提供していますが、スマホアプリでは別画面にしています。 このため、WebUI版では、通知の一覧画面のViewModelで通知一覧と選択状態を管理していますが、スマホアプリ版では2つのViewModelで管理する形に変更しています。

このほか、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システムトレードフレームワーク「Jiji」 のBeta版をリリースしました

f:id:unageanu:20151201143338p:plain

お待たせしました。本日、Beta版をリリースしました!
使ってみて、不具合報告やご意見など頂ければ嬉しいです!

jiji2.unageanu.net

FXシステムトレードフレームワーク「Jiji2」の開発状況 その2

追記(2015-12-01):

FXシステムトレードフレームワーク「Jiji」、リリースしました!

jiji2.unageanu.net

使ってみて、ご意見など頂けるとうれしいです。



前回の更新からだいぶ時間が空いてしまいました・・・。少しずつ実装は進んでいます。

github.com

9月10月でスマホアプリの実装とUIデザインがだいたいできたので、スクリーンショットを公開してみます。 例によって、コメントなど頂ければ嬉しいです。

ホーム

  • 最初に表示される画面です。
  • 以下のような情報を、1画面でさっと確認できます。
    • 口座残高/直近の勝率などのサマリ
    • 現在のレートと値動きをチャートで
    • トレードシステムからの新着の通知
    • 最新の建玉

f:id:unageanu:20151031090006p:plain

取引状況

  • 合計損益や勝率などシステムトレードの取引状況を確認する画面です。
  • 左上のメニューから集計期間を変えて分析することができます。

f:id:unageanu:20151031085323p:plain

チャート

  • 詳細チャートです。最大10年前までさかのぼってレートを閲覧できます。
  • ローソク足のほか、エージェントで描画した移動平均線などのグラフを表示できます。
  • 真ん中あたりにある緑と赤のバーが建玉の保有期間を示していて、チャートの値動きにあわせて期待通りトレードができているかチェックできるようになっています。

f:id:unageanu:20151031085316p:plain

取引ロジック(エージェント)エディタ

  • 取引ロジックを作成/編集する画面です。取引ロジックはRubyで記述します。
  • 外出先から緊急に編集が必要な場合もあるかな、ということでスマホアプリにも簡易な編集機能を用意しました。

f:id:unageanu:20151031085314p:plain

通知一覧

  • 取引ロジック(エージェントと呼んでいます)から送られてきた通知の一覧画面。
  • クリックで詳細が閲覧可能です。

f:id:unageanu:20151031090213p:plain

ポジション一覧

  • 取引ロジックが行った取引の一覧です。
  • こちらもクリックで詳細が表示されます。

f:id:unageanu:20151031090212p:plain

バックテストの作成

  • 任意の期間とエージェントを指定してバックテストを実行できます。

f:id:unageanu:20151031085319p:plain

バックテスト詳細

  • バックテストの結果を確認できます。

f:id:unageanu:20151031090305p:plain

ログ

  • エージェントが出力したログを確認できます。

f:id:unageanu:20151031085318p:plain

設定

  • 使用する証券会社やアクセスパスワードを設定できます。

f:id:unageanu:20151031085322p:plain

残タスク

残っているタスクは以下です。

  • ロゴの作成とデザインの最終調整
  • デバッグと使い込みテスト
  • 使い方などのドキュメント準備
  • 導入手順の整備

1か月でこれらを消化して、11月末にはなんとかリリースしたいところ。もうしばらくお待ちください。

Jiji2 - ホーム画面のスクリーンショット

追記(2015-12-01):

FXシステムトレードフレームワーク「Jiji」、リリースしました!

jiji2.unageanu.net

使ってみて、ご意見など頂けるとうれしいです。



ホーム画面のデザインができてきたので、現状のスクリーンショットを公開します。
ご意見などいただければ嬉しいです。

PC版:

f:id:unageanu:20150829130640p:plain

スマホ版:

f:id:unageanu:20150829130717p:plain

ホームはアプリ利用時のエントリーポイントになる画面です。
以下のような、システムトレードの主要な情報を1画面でさっと確認できるようにしました。

  • 口座残高/直近の勝率などのサマリ
  • 現在のレートと値動きをチャートで
  • トレードシステムからの新着の通知
  • 最新の建玉

まずはこの画面で状況を把握した後、エージェント設定の変更などのアクションを実行していくイメージです。

ソースコード

コードはGitHubで公開しているので、気になる方はこちらをご覧ください。

github.com