many fixes after test with lots of data
[smr.git] / gui / lib / smr_position.rb
blob316ee3e12f98093113692d134642e4b43b3148f5
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
7 # version.
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:
27 # <b>open</b>:: 
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.
37 class SmrPosition
38     ##
39     # initialize with Position id and User id
40     #
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)
48         @id = id_position
49         @date = date
50         @orders = Array.new
51         @orders_pending = Array.new
52         @quotes = Array.new
53         @dividend = nil
54         @sum_buy_orders = 0.0
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
63         end
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
74                 @orders << r.Order
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
78                 end
79             end
80         end
81         @orders_pending=Order.where(:id_position=>@id).where(:quote=>0).where(:is_canceled=>nil).order(issued: :desc)
82     end
84     public
86     def id
87         @id
88     end
90     def date
91         @date
92     end
94     def comment
95         @position.comment
96     end
98     def time_closed
99         @position.time_closed
100     end
102     ##
103     # return Stock object held by this position
104     def stock
105         @stock
106     end
108     ##
109     # return Portfolio object this position is in
110     def portfolio
111         @portfolio
112     end
114     ##
115     # return array of PositionRevision objects with respect to the point in time we are at
116     def revisions
117         @revisions
118     end
120     ##
121     # return array of Order (s) in pending state, ie those not executed
122     # FIXME: test expiry and canceled state, see query above
123     def pending_orders
124         @orders_pending
125     end
127     ##
128     # tell whether there are orders waiting for execute or expiry
129     def has_pending_orders?
130         not @orders_pending.empty?
131     end
133     ##
134     # return SmrDividend object providing dividend information as of self.date
135     def dividend
136         if not @dividend then @dividend = SmrDividend.new(self) end
137         @dividend
138     end
140     def is_closed?
141         @position.closed > 0    
142     end
144     def is_new?
145         @revisions.first.id_order == 0 and @revisions.count == 1 and not self.is_closed?
146     end
148     def is_viewed_before_closure?
149         @viewed_before_closure
150     end
152     ##
153     # total money invested here at date or false if settled at given date
154     def invested
155         if self.is_closed? and not self.is_viewed_before_closure? then return false end
156         self.revisions.last.invested
157     end
159     ##
160     # total number of shares held at this point in time
161     def shares
162         shares=0
163         self.revisions.each do |pr|
164             if not pr.Order.nil? then
165                 if pr.Order.type == 'buy'
166                     shares += pr.Order.shares
167                 else
168                     shares -= pr.Order.shares
169                 end
170             end
171         end
172         return shares
173     end
175     ##
176     # price this position has cost per share
177     def cost_price
178         if self.shares.zero? then return 0.0 end
179         self.invested / self.shares
180     end
182     ##
183     # returns position status as human readable string
184     def status
185         if self.is_closed?
186             return 'closed'
187         elsif self.is_new?
188             return 'new'
189         else
190             return 'open'
191         end
192     end
194     ##
195     # returns last Quote of the Stock held, false if there is no quote data
196     def last_quote
197         if not @last_quote.nil? then return @last_quote.quote else false end
198     end
200     ##
201     # returns time of last Quote
202     def quote_time
203         if not @last_quote.nil? then return @last_quote.time else false end
204     end
206     ##
207     # returns market value of position based on last price or false if there is
208     # no price
209     def market_value
210         if self.last_quote then self.shares * self.last_quote else false end
211     end
213     ##
214     # returns volume purchased or false if position is in open state
215     def purchase_volume
216         if self.is_closed? and not self.is_viewed_before_closure? then @sum_buy_orders else false end
217     end
219     ##
220     # returns amount settled of false if position is in open state
221     def settled_volume
222         if self.is_closed? and not self.is_viewed_before_closure? then @sum_sell_orders else false end
223     end
225     ##
226     # returns profit/loss based on cost of position and market value or
227     # settlement or false if it cant be calculated
228     def profit_loss
229         if self.is_closed? and not self.is_viewed_before_closure? then
230             @sum_sell_orders - @sum_buy_orders 
231         else
232             if not self.market_value then return false
233             else self.market_value - self.invested end
234         end
235     end
237     ##
238     # returns gain based on market value adjusted by dividend received and
239     # charges payed
240     def gain
241         self.profit_loss + self.dividend.received + self.charges
242     end
244     ##
245     # returns dirty_value composed of market value, dividend received and 
246     # charges paid or false if position is closed.
247     #
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.
251     def dirty_value
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
254     end
256     ##
257     # returns total charges paid on this position
258     # Note: this is always a negative number
259     def charges
260         charges = 0.0
261         @orders.each do |o|
262             charges += o.provision + o.courtage + o.expense
263         end
264         charges * -1
265     end
269 # Collection of SmrPosition objects as found by an SQL statement at some
270 # point in time
272 # ATTENTION::
273 #  the statement should only select position.id, this class takes care of
274 #  everything else
275 class SmrPositions
276     ##
277     # provide +Time+ date and SQL statement as string.
278     def initialize(date, sql)
279         @date = date
280         @smrpositions = Array.new
281         @positions = Position.find_by_sql(sql)
282     end
284     ##
285     # returns array of SmrPosition objects
286     def get_all
287         @positions
288     end