app/models/iro/position.rb



class Iro::Position
  include Mongoid::Document
  include Mongoid::Timestamps
  include Mongoid::Paranoia
  store_in collection: 'iro_positions'

  field :prev_gain_loss_amount, type: :float
  attr_accessor :next_gain_loss_amount
  def prev_gain_loss_amount
    out  = autoprev.outer.end_price - autoprev.inner.end_price
    out += inner.begin_price - outer.begin_price
  end


  STATUS_ACTIVE   = 'active'
  STATUS_CLOSED   = 'closed'
  STATUS_PROPOSED = 'proposed'
  ## one more, 'selected' after proposed?
  STATUS_PENDING  = 'pending' ## 'working'
  STATUSES = [ nil, STATUS_CLOSED, STATUS_ACTIVE, STATUS_PROPOSED, STATUS_PENDING ]
  field :status
  validates :status, presence: true
  scope :active, ->{ where( status: 'active' ) }

  belongs_to :purse,    class_name: 'Iro::Purse',    inverse_of: :positions
  index({ purse_id: 1, ticker: 1 })

  belongs_to :stock,   class_name: 'Iro::Stock',    inverse_of: :positions
  delegate :ticker, to: :stock

  belongs_to :strategy, class_name: 'Iro::Strategy', inverse_of: :positions
  delegate :put_call,        to: :strategy
  delegate :long_or_short,   to: :strategy
  delegate :credit_or_debit, to: :strategy

  belongs_to :next_strategy, class_name: 'Iro::Strategy', inverse_of: :next_position, optional: true


  belongs_to :prev, class_name: 'Iro::Position', inverse_of: :nxts, optional: true
  belongs_to :autoprev, class_name: 'Iro::Position', inverse_of: :autonxt, optional: true
  ## there are many of these, for viewing on the 'roll' view
  has_many :nxts,     class_name: 'Iro::Position', inverse_of: :prev
  has_one :autonxt, class_name: 'Iro::Position', inverse_of: :autoprev

  ## Options

  belongs_to :inner, class_name: 'Iro::Option', inverse_of: :inner
  validates_associated :inner

  belongs_to :outer, class_name: 'Iro::Option', inverse_of: :outer
  validates_associated :outer

  accepts_nested_attributes_for :inner, :outer

  field     :outer_strike, type: :float
  # validates :outer_strike, presence: true

  field     :inner_strike, type: :float
  # validates :inner_strike, presence: true

  field :expires_on
  validates :expires_on, presence: true

  field :quantity, type: :integer
  validates :quantity, presence: true
  def q; quantity; end

  field :begin_on

  field :end_on

  def begin_delta
    strategy.send("begin_delta_#{strategy.kind}", self)
  end
  def end_delta
    strategy.send("end_delta_#{strategy.kind}", self)
  end

  def breakeven
    strategy.send("breakeven_#{strategy.kind}", self)
  end

  def current_underlying_strike
    Iro::Stock.find_by( ticker: ticker ).last
  end

  def refresh
    out = Tda::Option.get_quote({
      contractType:   'CALL',
      strike:         strike,
      expirationDate: expires_on,
      ticker:         ticker,
    })
    update({
      end_delta: out[:delta],
      end_price: out[:last],
    })
    print '^'
  end

  def net_percent
    net_amount / max_gain
  end
  def net_amount # each
    strategy.send("net_amount_#{strategy.kind}", self)
  end
  def max_gain # each
    strategy.send("max_gain_#{strategy.kind}", self)
  end
  def max_loss # each
    strategy.send("max_loss_#{strategy.kind}", self)
  end


  def sync
    inner.sync
    outer.sync
  end


  ##
  ## decisions
  ##

  field :next_reasons, type: :array, default: []
  field :rollp, type: :float

  ## should_roll?
  def calc_rollp
    self.next_reasons = []
    # self.next_symbol  = nil
    # self.next_delta   = nil

    out = strategy.send( "calc_rollp_#{strategy.kind}", self )

    self.rollp = out[0]
    self.next_reasons.push out[1]
    save
  end

  def calc_nxt
    pos = self

    ## 7 days ahead - not configurable so far
    outs = Tda::Option.get_quotes({
      contractType: pos.put_call,
      expirationDate: next_expires_on,
      ticker: ticker,
    })
    outs_bk = outs.dup

    outs = outs.select do |out|
      out[:bidSize] + out[:askSize] > 0
    end

    if 'CALL' == pos.put_call
      ;
    elsif 'PUT' == pos.put_call
      outs = outs.reverse
    end

    ## next_inner_strike
    outs = outs.select do |out|
      if Iro::Strategy::CREDIT == pos.credit_or_debit
        if Iro::Strategy::SHORT == pos.long_or_short
          ## short credit call
          out[:strikePrice] >= strategy.next_inner_strike
        elsif Iro::Strategy::LONG == pos.long_or_short
          ## long credit put
          out[:strikePrice] <= strategy.next_inner_strike
        end
      else
        raise 'zz3 - @TODO: implement, debit spreads'
      end
    end
    puts! outs[0][:strikePrice], 'after calc next_inner_strike'
    puts! outs, 'outs'

    ## next_buffer_above_water
    outs = outs.select do |out|
      if Iro::Strategy::SHORT == pos.long_or_short
        out[:strikePrice] > strategy.next_buffer_above_water + strategy.stock.last
      elsif Iro::Strategy::LONG == pos.long_or_short
        out[:strikePrice] < strategy.stock.last - strategy.next_buffer_above_water
      else
        raise 'zz4 - this cannot happen'
      end
    end
    puts! outs[0][:strikePrice], 'after calc next_buffer_above_water'
    puts! outs, 'outs'

    ## next_inner_delta
    outs = outs.select do |out|
      if 'CALL' == pos.put_call
        out_delta  = out[:delta] rescue 1
        out_delta <= strategy.next_inner_delta
      elsif 'PUT' == pos.put_call
        out_delta  = out[:delta] rescue 0
        out_delta <= strategy.next_inner_delta
      else
        raise 'zz5 - this cannot happen'
      end
    end
    puts! outs[0][:strikePrice], 'after calc next_inner_delta'
    puts! outs, 'outs'

    inner = outs[0]
    outs = outs.select do |out|
      if 'CALL' == pos.put_call
        out[:strikePrice] >= inner[:strikePrice].to_f + strategy.next_spread_amount
      elsif 'PUT' == pos.put_call
        out[:strikePrice] <= inner[:strikePrice].to_f - strategy.next_spread_amount
      end
    end
    outer = outs[0]

    if inner && outer
      o_attrs = {
        expires_on: next_expires_on,
        put_call:   pos.put_call,
        stock_id:   pos.stock_id,
      }
      inner_ = Iro::Option.new(o_attrs.merge({
        strike:        inner[:strikePrice],
        begin_price: ( inner[:bid] + inner[:ask] )/2,
        begin_delta:   inner[:delta],
        end_price:   ( inner[:bid] + inner[:ask] )/2,
        end_delta:     inner[:delta],
      }))
      outer_ = Iro::Option.new(o_attrs.merge({
        strike:        outer[:strikePrice],
        begin_price: ( outer[:bid] + outer[:ask] )/2,
        begin_delta:   outer[:delta],
        end_price:   ( outer[:bid] + outer[:ask] )/2,
        end_delta:     outer[:delta],
      }))
      pos.autonxt ||= Iro::Position.new
      pos.autonxt.update({
        prev_gain_loss_amount: 'a',
        status:       'proposed',
        stock:        strategy.stock,
        inner:        inner_,
        outer:        outer_,
        inner_strike: inner_.strike,
        outer_strike: outer_.strike,
        begin_on:     Time.now.to_date,
        expires_on:   next_expires_on,
        purse:        purse,
        strategy:     strategy,
        quantity:     1,
        autoprev:     pos,
      })

      pos.autonxt.sync
      pos.autonxt.save!
      pos.save
      return pos

    else
      throw 'zmq - should not happen'
    end
  end



  ## ok
  def next_expires_on
    out = expires_on.to_datetime.next_occurring(:monday).next_occurring(:friday)
    if !out.workday?
      out = Time.previous_business_day(out)
    end
    return out
  end

  ## ok
  def self.long
    where( long_or_short: Iro::Strategy::LONG )
  end

  ## ok
  def self.short
    where( long_or_short: Iro::Strategy::SHORT )
  end

  def to_s
    out = "#{stock} (#{q}) #{expires_on.to_datetime.strftime('%b %d')} #{strategy.long_or_short} ["
    if Iro::Strategy::LONG == long_or_short
      if outer.strike
        out = out + "$#{outer.strike}->"
      end
      out = out + "$#{inner.strike}"
    else
      out = out + "$#{inner.strike}"
      if outer.strike
        out = out + "<-$#{outer.strike}"
      end
    end
    out += "] "
    return out
  end
end