unageanu/jiji2

View on GitHub
sample_agents/src/bollinger_bands_agent.rb

Summary

Maintainability
A
1 hr
Test Coverage
# frozen_string_literal: true

require 'quandl'
require 'lru_redux'

# === ボリンジャーバンドエージェント
class BollingerBandsAgent

  include Jiji::Model::Agents::Agent

  def self.description
    <<-STR
      ボリンジャーバンドエージェント
    STR
  end

  # UIから設定可能なプロパティの一覧
  def self.property_infos
    [
      Property.new('pair',           '通貨ペア', 'USDJPY'),
      Property.new('trade_units',    '取引数量', 10_000),
      Property.new('distance',       '取引を仕掛ける間隔(sdに対する倍率)', 1),
      Property.new('quandl_api_key', 'Quandl API KEY', '')
    ]
  end

  def post_create
    calculator =
      BollingerBands::BollingerBandsCalculator.new(@quandl_api_key)
    @trader = BollingerBands::Trader.new(
      @pair.to_sym, @trade_units.to_i, @distance.to_f,
      broker, create_index_graph, create_bands_graph, logger, calculator)
  end

  def next_tick(tick)
    @trader.process_tick(tick)
  end

  def create_index_graph
    graph_factory.create('index', :line, :last, ['#779999'])
  end

  def create_bands_graph
    graph_factory.create('rate', :rate, :last, [
      '#FF6633', '#FFAA22', '#FFCC11', '#FFEE00',
      '#FFCC11', '#FFAA22', '#FF6633'
    ])
  end

end

module BollingerBands
  module Utils
    def bd(v)
      BigDecimal(v.to_f, 10)
    end

    def sum(array)
      array.inject(0) { |a, e| a + e }
    end

    def mean(array)
      array.sum / array.length.to_f
    end

    def sample_variance(array)
      m = mean(array)
      sum = array.inject(0) { |a, e| a + (e - m)**2 }
      sum / (array.length - 1).to_f
    end

    def standard_deviation(array)
      Math.sqrt(sample_variance(array))
    end
  end

  class BollingerBandsCalculator

    include Utils

    def initialize(api_key)
      Quandl::ApiConfig.api_key = api_key if api_key
      Quandl::ApiConfig.api_version = '2015-04-09'

      @cache = LruRedux::ThreadSafeCache.new(10)
    end

    def calculate(price, timestamp, pair)
      key = timestamp.strftime('%Y-%m-%d')
      @cache[key] \
      || (@cache[key] = calculate_bollinger_bands(price, timestamp, pair))
    end

    def calculate_bollinger_bands(price, timestamp, pair)
      rates = retrieve_rates(timestamp, pair)
      {
        mean: mean(rates).to_f.round(9),
        sd:   standard_deviation(rates).to_f.round(9)
      }
    end

    def retrieve_rates(time, pair)
      retrieve_rates_from_quandl(time, pair).map do |e|
        e['rate']
      end
    end

    def retrieve_rates_from_quandl(time, pair)
      Quandl::Dataset.get("CURRFX/#{pair}")
        .data(params: {
          rows:     20,
          end_date: time.strftime('%Y-%m-%d')
        })
    end

  end

  class Trader

    include Utils

    attr_reader :positions
    attr_writer :cointegration_resolver

    def initialize(pair, units, distance, broker,
      index_graph = nil, bands_graph = nil, logger = nil,
      calculator = BollingerBandsCalculator.new)
      @pair          = pair
      @units         = units
      @distance      = distance
      @broker        = broker
      @logger        = logger
      @bands_graph   = bands_graph
      @index_graph   = index_graph
      init
    end

    def init
      @bollinger_bands_calculator = calculator
      @positions = {}
      restore_state
    end

    def process_tick(tick)
      price = bd(tick[@pair].bid)
      bands = calculate_bollinger_bands(price, tick.timestamp, @pair)
      index = calculate_index(price, bands)

      do_takeprofit(index)
      do_trade(tick, bands, index)
    end

    def do_trade(tick, bands, index)
      register_graph_data(bands, index)
      log(tick, bands, index)

      if index.nonzero? && index != -1 && !@positions.include?(index.to_s)
        @positions[index.to_s] = create_position(index)
      end
    end

    def register_graph_data(bands, index)
      @index_graph << [index] if @index_graph
      @bands_graph << create_bands(bands) if @bands_graph
    end

    def create_bands(bands)
      [-3, -2, -1, 0, 1, 2, 3].map do |i|
        bands[:mean] + i * bands[:sd]
      end
    end

    def log(tick, bands, index)
      return unless @logger

      @logger.info(
        "#{tick.timestamp} #{tick[@pair].bid}" \
      + " #{bands[:sd]} #{bands[:mean]} #{index}")
    end

    def do_takeprofit(index)
      @positions.keys.each do |key|
        @positions.delete(key) if @positions[key].close_if_required(index)
      end
    end

    def create_position(index)
      index < 0 ? buy(index) : sell(index)
    end

    def buy(index)
      position = @broker.buy(@pair, @units).trade_opened
      Position.new(index, @broker.positions[position.internal_id])
    end

    def sell(index)
      position = @broker.sell(@pair, @units).trade_opened
      Position.new(index, @broker.positions[position.internal_id])
    end

    def calculate_bollinger_bands(price, timestamp, pair)
      @bollinger_bands_calculator.calculate(price, timestamp, pair)
    end

    def calculate_index(price, bands)
      ((bd(price) - bands[:mean]) / (bands[:sd] * @distance)).floor.to_i
    end

    def restore_state
      @broker.positions.each do |p|
        next unless p.pair_name == @pair

        register_position(p)
      end
    end

    def register_position(position)
      bands = calculate_bollinger_bands(
        position.entry_price, position.entered_at, @pair)
      index = calculate_index(position.entry_price, bands)

      @positions[index.to_s] = Position.new(index, position)
    end

  end

  class Position

    include Utils
    attr_reader :index, :position

    def initialize(index, position)
      @index     = index
      @position  = position
    end

    def close_if_required(index)
      return false unless take_profit?(index)

      close_positions
      true
    end

    def take_profit?(index)
      if @position.sell_or_buy == :buy
        @index + 1 < index
      else
        @index - 1 > index
      end
    end

    def close_positions
      @position.close
    end

  end
end