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/>.
19 # Businesslogic to buy/sell securities in SMR.
21 # Works mostly on top of the Order model but interacts with Position and
24 # Methods return False in case #usage_constrains_ok is not happy. #error
25 # has a String desribing the error in that case.
28 # Quote.quote to use for executing cash bookings
29 # - defined as constant, though I do not see why this should ever change
30 CASHPOSITION_EXEC_QUOTE = 1.0
32 # cash Position used for booking this Transaction
33 attr_reader :cash_position
35 # Order used or issued by this Transaction
39 # Perform a transaction on given Position (or Smr::AssetPosition).
40 # - User is asked for the sake of security, it should be authenticated!
41 # - :order will use existing Order as basis for the transaction. The
42 # Order must belong to this Position and should be in pending state.
43 # - if given Position is a cash position, #buy and #sell will directly
44 # operate on it. See Smr::ID_CASH_SECURITY.
45 # - charges specified by #set_charges will be deducted from the cash
47 def initialize(position, user, options={:issue_time=>false, :order=>false, :triggered_by_order=>false})
50 @time = options[:issue_time] || Time.now
51 @order = prepare_order(options[:triggered_by_order], options[:order])
52 @cash_position = prepare_cash_position
57 # Tell whether this Transaction is a cash booking.
59 @position.is_cash_position?
63 # Human readable string describing this transaction
66 'Transaction in progress'
67 elsif @order.is_executed?
68 'Transaction complete, %s %.4f shares and booked against cash.' \
69 % [ @order.type, @order.shares ]
74 # Set charges to be booked with this order.
76 # All charges will be deducted from the cash booking.
77 def set_charges(provision, courtage, expense)
78 return false unless usage_constrains_ok
79 @order.provision = provision
80 @order.courtage = courtage
81 @order.expense = expense
85 # Set accrued interest to be booked with this order.
87 # Accrued interest is handled as follows:
88 # - on a BUY order :amount is considered being paid
89 # - on a SELL order :amount is considered being received
90 def set_accrued_interest(amount)
91 return false unless usage_constrains_ok
92 @order.accrued_interest = amount.abs
96 # Set a commentary text. It might be a verbose explanation.
98 return false unless usage_constrains_ok
103 # Make a PURCHASE on the Position.
104 def buy(shares, limit, exchange=nil)
105 return false unless usage_constrains_ok
107 @order.shares = shares.abs
109 @order.exchange = if is_cash_booking? then 'cash' else exchange end
114 # Make a SALE on the Position.
115 def sell(shares, limit, exchange=nil)
116 return false unless usage_constrains_ok
118 @order.shares = shares.abs
120 @order.exchange = if is_cash_booking? then 'cash' else exchange end
125 # Make a cash booking using :amount.
127 # The :amount can be positive or negative. Works only if given Position
128 # is a cash position.
130 # Specify optional :charges and :accrued_interest as absolute number. It
131 # will be added/deducted from :amount, depending its sign.
133 # There is no need to call #execute when #book is used.
134 def book(amount, options={:time=>false, :charges=>false, :accrued_interest=>false, :comment=>false})
135 return false unless usage_constrains_ok
136 if not is_cash_booking? then
137 @error << '#book works only if a cash position was given to ::new'
141 time = options[:time] || Time.now
142 charges = options[:charges] || 0.0
143 accrued_interest = options[:accrued_interest] || 0.0
144 set_comment(options[:comment]) if options[:comment]
147 sell(amount.abs + accrued_interest + charges, CASHPOSITION_EXEC_QUOTE)
149 buy(amount + accrued_interest - charges, CASHPOSITION_EXEC_QUOTE)
151 execute(CASHPOSITION_EXEC_QUOTE, time)
155 # Apply Order to Position at given quote and time.
157 # This is final and can not be reversed as it will also book the
158 # transaction against cash.
159 def execute(quote, time=Time.now)
160 return false unless usage_constrains_ok
162 @order.save! # trigger INSERT to have :id
164 PositionRevision.new(
165 :id_order=>@order.id,
166 :id_position=>@position.id,
171 book_order_against_cash(time)
175 # Cancel the Transaction. Can not be reversed.
176 def cancel(time=Time.now)
177 @order.is_canceled = 1
181 # returns String describing the error when some methode returned False
189 # usability is limited by conditions verified here
190 def usage_constrains_ok
191 if @order.is_canceled? then
192 @error << 'Order #%i has been canceled already' % @order.id
193 elsif @order.is_executed? then
194 @error << 'Order #%i has been executed already' % @order.id
195 elsif @position.closed? then
196 @error << 'Position #%i is closed. Orders can only be added to open positions.' % @position.id
197 elsif @cash_position.closed? then
198 @error << ('Cash position #%i has been settled. No ' \
199 + 'further Transactions are possible in this period.')
206 # create or use an Order
207 def prepare_order(triggered_by_order=false, order=false)
208 if order and order.is_a?(Order) and order.Position.Portfolio.id_user == @user.id then
213 :expire=>(@time+1.month).end_of_day.to_i,
214 :id_position=>@position.id,
215 :expense=>0, :courtage=>0, :provision=>0
218 o.triggered_by_order = triggered_by_order if triggered_by_order
223 # Create new or use existing cash Position, depends on time.
225 # Specification of cash type Position records:
226 # - a cash Position lives 1 year: Jan 1st to Dec 31st
227 # - when a cash position is closed, that year is settled! no more orders
228 # in that account / that year are possible
229 # - cash positions are created silently, if not present for that year
230 # - cash positions are to be closed manually (so it is still possible to
231 # enter order data of past years)
233 # - User.id is checked here to improve security
235 def prepare_cash_position
236 start_date=@time.beginning_of_year.to_i
237 end_date=@time.end_of_year.to_i
240 cps = Position.where(:id_portfolio=>@position.id_portfolio, :id_stock=>Smr::ID_CASH_SECURITY)
241 .joins(:Portfolio).where('portfolio.id_user=%i' % @user.id).all.to_a
244 cp.PositionRevision.count == 0 or cp.PositionRevision.first.time.year == @time.year
246 cp = cps.first # should we ever have multiple ones open the same year? multi currency?
250 :id_portfolio=>@position.id_portfolio,
251 :id_stock=>Smr::ID_CASH_SECURITY,
252 :comment=>'Jan - Dec %i' % @time.year)
259 # Re-calculate all PositionRevision records related to this Position.
260 def process_revisions
262 shares = invested = 0
263 PositionRevision.where(:id_position=>@position.id).order(date: :asc).each do |pr|
267 invested -= o.shares * o.quote
270 invested += o.shares * o.quote
274 pr.invested = invested
280 # Make cash booking on cash Position in same Portfolio.
283 # - 1 if cash booking was successfull
284 # - 2 if this Transaction is a cash booking itself
285 # - 3 if the Order in this Transaction has been booked already
287 def book_order_against_cash(time)
288 if @order.quote==0.0 or @order.shares==0.0 then
289 raise 'order not sufficiently prepared'
292 # do nothing if this is a cash booking or the order has already triggered a cash booking
293 return 2 if is_cash_booking?
294 return 3 if Order.where(:triggered_by_order=>@order.id).count > 0
296 # add new order onto it, execute right away
297 ct = Transaction.new(@cash_position, @user, :triggered_by_order=>@order.id)
299 vol *= -1 if @order.is_purchase?
300 ct.book(vol, :charges=>@order.charges, :accrued_interest=>@order.accrued_interest, :time=>time)