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_income'
22 # A position at some point in time.
24 # It provides all information from a positions point of view. Like the Security
25 # held, Order (s) that were issued and the PositionRevision (s) it created.
27 # A AssetPosition is in one of three logical states of valuation, depending on
28 # the date given to #new:
31 # does valuation with market value at date
32 # <b>closed, date before closure</b>::
33 # does valuation with market value at date with securities held at date
34 # <b>closed, date after closure</b>::
35 # does final gain calculation
37 # Use #is_closed? and #is_viewed_before_closure? to check for the state as a
38 # number of methods will just return false when called in the wrong state.
43 # accessors to underlying Position for compatibility
45 attr_reader :id_portfolio
46 attr_reader :id_security
50 # Time at which we are looking at the Position.
53 # Security held by this position.
56 # Portfolio this position is in.
57 attr_reader :portfolio
59 # User holding this position.
62 # Quote found for Security, its the most recent one before given :date
63 attr_reader :last_quote
65 # Collection of PositionRevision with respect to the point in time we are
67 attr_reader :revisions
69 # Collection of Order (s) in pending state, ie those not executed yet.
70 # FIXME: test expiry and canceled state, see #has_pending_orders?
71 attr_reader :pending_orders
74 # Charges paid on all orders
78 # Accrued interest balance booked on executed orders
79 # - a BUY order is considered having paid interest
80 # - a SELL order is considered having received interest
81 attr_reader :accrued_interest
84 # initialize with Position id and User id
86 # initilization will fail if +id_position+ is not part of a Portfolio owned
87 # by the User specified by +id_user+.
88 def initialize(id_position, id_user, date=Time.now)
90 @user = User.where(:id=>id_user).first
91 @position = Position.where(:id=>id_position).joins(:Portfolio).where('portfolio.id_user=%i'% id_user).first
92 raise 'possible security violation: id_position=%i does not belong to id_user=%i.' % [id_position, id_user] if not @position.is_a?(Position)
95 @id_portfolio = @position.id_portfolio
96 @id_security = @position.id_security
97 @closed = @position.closed
98 @comment = @position.comment
101 @pending_orders = Array.new
103 @sum_buy_orders = 0.0
104 @sum_sell_orders = 0.0
105 @accrued_interest = 0.0
108 @viewed_before_closure = false
110 # adjust date in case we are closed in the future
111 # - subsequent gain calculations produce bogus results otherwise
112 if @position.closed > 0 and @date < @position.time_closed then
113 @viewed_before_closure = true
116 # preload things to SELECT them only once
117 # - even ActiveRecords CACHE takes time and we can be more intelligent
118 @security = @position.Security
119 @portfolio = Portfolio.where(:id=>@position.id_portfolio, :id_user=>id_user).first
120 @last_quote = @security.last_quote(@date)
121 @revisions = @position.PositionRevision.order(:date).where('date <= %i' % @date).to_a
122 @revisions.each do |r|
123 if r.id_order != 0 then
126 @charges += o.charges
127 if o.is_purchase? then
128 @sum_buy_orders += o.volume
129 @accrued_interest -= o.accrued_interest
131 @sum_sell_orders += o.volume
132 @accrued_interest += o.accrued_interest
136 @pending_orders=Order.where(:id_position=>@id).pending.order(issued: :desc)
142 # Descriptive String of the position and its contents, human readable.
143 def to_s(options={:with_details=>false})
144 if options[:with_details] then
145 '%s (%i shares held at %s)' % [@security, shares, @portfolio]
146 else @security.to_s end
150 # Currency code this position is held in.
151 # - note: currencies are not implemented yet, so this returns the default
153 Smr::DEFAULT_CURRENCY
157 # Time when this position was closed, if #closed?
159 @position.time_closed
163 # Tell whether there are orders waiting for execute, expiry or cancelation.
164 def has_pending_orders?
165 not @pending_orders.empty?
169 # Smr::Dividend object providing Dividend information as of #date.
171 if not @dividend then @dividend = Smr::DividendIncome.new(self) end
176 # Tell if Position is closed, regardless when that happened.
185 # Tell whether this is a cash deposit, also see Transaction.
186 def is_cash_position?
187 @position.is_cash_position?
191 # Close the Position.
193 # This will close the position right away in case it is empty. +True+ is
194 # returned in that case.
196 # In case it is not empty a new Order is returned, that will sell it off
197 # and close it if executed. That Order may be presented for editing first,
198 # ie to add charges, execution price, etc...
200 # +False+ is returned in case the position has been closed already, even if
201 # that happened 'in the future' (given date > smr_browse_date or Time.now).
203 # Closing a Smr::AssetPosition is a one-time-only operation. New orders are
204 # not possible once this has been done. Closing is only possible when the
205 # number of shares held is zero.
206 def close(date=@date)
207 return false if is_closed?
209 if shares.between? -0.01, 0.01
210 # at 0 or when a very tiny amount remains, we close.
212 # - see :precision and :scale on float type columns, it rounds to a scale of 4 digits
213 # - http://stackoverflow.com/questions/23120584/why-does-mysql-round-floats-way-more-than-expected
214 @position.closed = date.to_i
217 elsif not is_new? and date > @date
218 # need to look ahead in time
219 p = Smr::AssetPosition.new(@position.id, @user.id, date)
220 return true if p.close(date).is_a?(TrueClass)
223 :id_position=>@id, :type=>'sale', :issued=>date.to_i,
224 :shares=>shares, :limit=>(last_quote.quote or 0),
225 :comment=>'Selling off to close position.'
231 # Settle position in case its a cash position.
233 # Settlements happens at the end of year of given :date. Usually
236 raise 'only cashpositions can be settled' unless is_cash_position?
237 this_year = @revisions.first.time
238 next_year = this_year + 1.year
240 # re-read self at end of year (the year it was opened)
241 position_eoy = Smr::AssetPosition.new(@position.id, @user.id, this_year.end_of_year)
243 # put current balance into cashposition of next year
244 forward_position = Smr::Transaction::find_cashposition(@portfolio.id, @user.id, next_year)
245 forward_transaction = Smr::Transaction.new(forward_position, @user, :type=>:cash_booking)
246 forward_transaction.book(
248 :time=>next_year.beginning_of_year + 1.second,
249 :comment=>'balance carried over from %s' % this_year.year
252 # make settlement booking to zero balance
253 # - settling time is right at midnight, end of year to have the last balance shown
254 # on december 31st, while it disappears on january 1st
255 settle_transaction = Smr::Transaction.new(@position, @user, :type=>:cash_booking)
256 settle_transaction.book(
257 position_eoy.shares * -1,
258 :time=>this_year.end_of_year + 1.second,
259 :comment=>'settlement of %s, balance has been carried over into %s' % [this_year.year, next_year.year]
262 # re-read self at end of year and close
263 position_eoy = Smr::AssetPosition.new(@position.id, @user.id, this_year.end_of_year + 1.second)
264 raise 'cashposition not empty after #settle transaction' unless position_eoy.close.is_a?(TrueClass)
268 # A Position without Order is considered new.
270 @revisions.empty? and not is_closed? or (@revisions.count == 1 and @revisions.first.id_order == 0)
274 # Tell if this is a short position.
280 # Tell whether this Position has been closed in the future.
281 def is_viewed_before_closure?
282 @viewed_before_closure
286 # Total money invested here at date or false if settled at given date.
288 return false if is_closed? and not is_viewed_before_closure?
289 return 0 if revisions.count==0
290 revisions.last.invested
294 # Total number of shares held at this point in time.
297 revisions.each do |pr|
298 if not pr.Order.nil? then
299 if pr.Order.is_purchase?
300 shares += pr.Order.shares
302 shares -= pr.Order.shares
310 # charges per 1k of purchase volume
312 charges / (purchase_volume / 1000)
316 # Price this position has cost per share.
318 return 0.0 if shares.zero? or is_closed?
323 # Position status as human readable string.
325 if is_closed? then 'closed'
326 elsif is_new? then 'new'
331 # Time of last Quote.
333 if not @last_quote then return @last_quote.time else false end
337 # Market value of position based on last price or false if there is
340 if last_quote.quote != 0 then shares * last_quote.quote else false end
344 # Volume spend to purchase this position.
350 # Amount settled or false if position is open
352 if is_closed? and not is_viewed_before_closure? then @sum_sell_orders else false end
356 # Profit/loss based on total cost of position and market value or
357 # settlement or false if it cant be calculated.
359 if is_closed? and not is_viewed_before_closure? then
360 @sum_sell_orders - @sum_buy_orders
362 return false unless market_value
363 market_value - invested
368 # Gain based on market value adjusted by dividend received and charges
369 # payed or false if it cant be calculated
371 return false unless profit_loss
372 profit_loss + dividend.received - charges + accrued_interest
376 # Dirty value composed of market value, dividend received and charges paid
377 # or false if position is closed or it cant be calculated.
379 # Its what one made on the position in total. Its dirty since we do not
380 # know whether dividend was re-invested in some other position. So do
381 # _not_ use this to calculate asset totals.
383 return false if is_closed? and not is_viewed_before_closure?
384 return false unless market_value
385 market_value + dividend.received - charges + accrued_interest
389 # Absolute value this position provides as collateral. Its composed usint
390 # the #collateral_coverage_ratio of Security.
391 def collateral_coverage_value
392 return 0.0 if not market_value or is_cash_position?
393 ( market_value * @security.collateral_coverage_ratio).to_f
397 # Smr::Link to this position
400 Smr::Link.new :cashposition, id
401 else Smr::Link.new :position, id end
405 # compares by profit_loss and invested, where losses have priority
407 return 0 unless profit_loss
410 both_have_loss = false
412 # a value weights more than false (which means we could not calculate
414 return 1 if profit_loss!=false and other.profit_loss==false
415 return -1 if profit_loss==false and other.profit_loss!=false
417 # compare losses first...
420 if profit_loss < other.profit_loss
422 else profit_loss > other.profit_loss
427 if other.profit_loss < 0
428 if have_loss then both_have_loss = true end
431 if profit_loss > other.profit_loss
433 else profit_loss < other.profit_loss
438 # ... then investment, but only both have a loss or there is none at all
439 # - compare by absolute values so short positions are inline
440 if both_have_loss or not have_loss
441 if invested.abs > other.invested.abs
443 elsif invested.abs < other.invested.abs
452 # Upcoming / expected cashflows. Collection, ordered by date.
454 # The :type option can filter cashflows of a specific type. See
455 # PositionRevision#types to learn what types of cashflow SMR knows. Note,
456 # however, that a Security typemodel is not required to implement all of
457 # them. If a filter is unknown to the typemodel it will not return any
460 # Its empty if the Security typemodel does not support cashflow predictions
461 # or just has no cashflow coming up.
462 def cashflow(options={:end_date=>false, :type=>:none})
463 return Array.new unless @security.has_type_model?
465 when :bond then amount = shares * 100
468 @security.get_type.cashflow(
470 :start_date=>@date, :end_date=>options[:end_date],
471 :type=>options[:type], :item_link=>Smr::Link.new(:position, @position.id)
478 # Collection of AssetPosition objects as found by an SQL statement at some
482 # the statement should only select position.id, this class takes care of
486 # Provide Time date and SQL statement as string.
487 def initialize(date, sql)
489 @smrpositions = Array.new
490 @positions = Position.find_by_sql(sql)
494 # Collection of AssetPosition objects.