2 # This file is part of SMR.
4 # SMR is free software: you can redistribute it and/or modify it under the
5 # terms of the GNU General Public License as published by the Free Software
6 # Foundation, either version 3 of the License, or (at your option) any later
9 # SMR is distributed in the hope that it will be useful, but WITHOUT ANY
10 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
11 # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License along with
14 # SMR. If not, see <http://www.gnu.org/licenses/>.
16 require 'smr_dividend'
19 # A position at some point in time.
21 # It provides all information from a positions point of view. Like the Stock
22 # held, Order (s) that were issued and the PositionRevision (s) it created.
24 # A SmrPosition is in one of three logical states of valuation, depending on
25 # the date given to #new:
28 # does valuation with market value at date
29 # <b>closed, date before closure</b>::
30 # does valuation with market value at date with securities held at date
31 # <b>closed, date after closure</b>::
32 # does final gain calculation
34 # Use #is_closed? and #is_viewed_before_closure? to check for the state as a
35 # number of methods will just return false when called in the wrong state.
39 # initialize with Position id and User id
41 # initilization will fail if +id_position+ is not part of a Portfolio owned
42 # by the User specified by +id_user+.
43 def initialize(id_position, id_user, date=Time.now)
45 @position = Position.where(:id=>id_position).joins(:Portfolio).where('portfolio.id_user=%i'% id_user).first
46 raise 'possible security violation: id_position=%i does not belong to id_user=%i, aborting SmrPosition.initialize()' % [id_position, id_user] if not @position.is_a?(Position)
51 @orders_pending = Array.new
55 @sum_sell_orders = 0.0
57 @viewed_before_closure = false
59 # adjust date in case we are closed in the future
60 # - subsequent gain calculations produce bogus results otherwise
61 if @position.closed > 0 and @date < @position.time_closed then
62 @viewed_before_closure = true
65 # preload things to SELECT them only once
66 # - even ActiveRecords CACHE takes time and we can be more intelligent
67 @stock = @position.Stock
68 @portfolio = Portfolio.where(:id=>@position.id_portfolio, :id_user=>id_user).first
69 @last_quote = Quote.order(date: :desc).where(id_stock: self.stock.id).where('date <= %i' % @date).limit(1).first
70 # @revisions = @position.PositionRevision.order(:date).where('date <= %i' % @date).where.not(:id_order=>0).to_a
71 @revisions = @position.PositionRevision.order(:date).where('date <= %i' % @date).to_a
72 @revisions.each do |r|
73 if r.id_order != 0 then
75 if r.Order.type.eql?('buy') then
76 @sum_buy_orders += r.Order.shares * r.Order.quote
77 else @sum_sell_orders += r.Order.shares * r.Order.quote
81 @orders_pending=Order.where(:id_position=>@id).where(:quote=>0).where(:is_canceled=>nil).order(issued: :desc)
103 # return Stock object held by this position
109 # return Portfolio object this position is in
115 # return array of PositionRevision objects with respect to the point in time we are at
121 # return array of Order (s) in pending state, ie those not executed
122 # FIXME: test expiry and canceled state, see query above
128 # tell whether there are orders waiting for execute or expiry
129 def has_pending_orders?
130 not @orders_pending.empty?
134 # return SmrDividend object providing dividend information as of self.date
136 if not @dividend then @dividend = SmrDividend.new(self) end
145 @revisions.first.id_order == 0 and @revisions.count == 1 and not self.is_closed?
148 def is_viewed_before_closure?
149 @viewed_before_closure
153 # total money invested here at date or false if settled at given date
155 if self.is_closed? and not self.is_viewed_before_closure? then return false end
156 self.revisions.last.invested
160 # total number of shares held at this point in time
163 self.revisions.each do |pr|
164 if not pr.Order.nil? then
165 if pr.Order.type == 'buy'
166 shares += pr.Order.shares
168 shares -= pr.Order.shares
176 # price this position has cost per share
178 if self.shares.zero? then return 0.0 end
179 self.invested / self.shares
183 # returns position status as human readable string
195 # returns last Quote of the Stock held, false if there is no quote data
197 if not @last_quote.nil? then return @last_quote.quote else false end
201 # returns time of last Quote
203 if not @last_quote.nil? then return @last_quote.time else false end
207 # returns market value of position based on last price or false if there is
210 if self.last_quote then self.shares * self.last_quote else false end
214 # returns volume purchased or false if position is in open state
216 if self.is_closed? and not self.is_viewed_before_closure? then @sum_buy_orders else false end
220 # returns amount settled of false if position is in open state
222 if self.is_closed? and not self.is_viewed_before_closure? then @sum_sell_orders else false end
226 # returns profit/loss based on cost of position and market value or
227 # settlement or false if it cant be calculated
229 if self.is_closed? and not self.is_viewed_before_closure? then
230 @sum_sell_orders - @sum_buy_orders
232 if not self.market_value then return false
233 else self.market_value - self.invested end
238 # returns gain based on market value adjusted by dividend received and
241 self.profit_loss + self.dividend.received + self.charges
245 # returns dirty_value composed of market value, dividend received and
246 # charges paid or false if position is closed.
248 # Its what one made on the position in total. Its dirty since we do not
249 # know whether dividend was re-invested in some other position. So do
250 # _not_ use this to calculate asset totals.
252 if self.is_closed? and not self.is_viewed_before_closure? then return false end
253 self.market_value + self.dividend.received + self.charges
257 # returns total charges paid on this position
258 # Note: this is always a negative number
262 charges += o.provision + o.courtage + o.expense
269 # Collection of SmrPosition objects as found by an SQL statement at some
273 # the statement should only select position.id, this class takes care of
277 # provide +Time+ date and SQL statement as string.
278 def initialize(date, sql)
280 @smrpositions = Array.new
281 @positions = Position.find_by_sql(sql)
285 # returns array of SmrPosition objects