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/>.
20 class TransactionTest < ActiveSupport::TestCase
21 # User.id for transaction tests
24 # Security.id to buy/sell during test
25 ID_SECURITY = 7 # 'Siemens AG' - has no position in fixtures data
28 # make a new and empty AssetPosition
29 def make_position options={:cash_position=>false}
31 pf = Portfolio.create(
32 :id_user=>ID_USER, :name=>'TransactionTest', :type=>'', :date_created=>Time.now.to_i
35 :id_security=>(options[:cash_position] ? Smr::ID_CASH_SECURITY : ID_SECURITY),
37 :comment=>(options[:cash_position] ? 'make cash booking' : 'transaction test')
39 assert p.save, 'could not create new position'
44 # see if the database is populated with the things needed here
45 test 'database setup properly' do
47 assert Security.where(:id=>Smr::ID_CASH_SECURITY).first, 'Smr::ID_CASH_SECURITY not present in database'
48 assert Security.where(:id=>ID_SECURITY).first, 'ID_SECURITY not present in database'
49 assert User.where(:id=>ID_USER).first, 'ID_USER not present in database'
53 # purchase security and sell it again
54 test 'purchase and sell' do
58 t1 = Smr::Transaction.new(p, User.find(ID_USER))
59 assert t1.is_a?(Smr::Transaction)
60 assert t1.buy(100, 11.0, 'TST')
61 assert t1.set_charges(1, 2, 3)
62 assert t1.set_accrued_interest(100)
63 assert t1.set_comment('transaction test: purchase')
64 assert_equal 1, t1.execute(10.50)
65 assert_equal 100, t1.order.PositionRevision.first.shares
68 t2 = Smr::Transaction.new(p, User.find(ID_USER))
69 assert t2.sell(80, 15.0, 'TST')
70 assert t2.set_charges(3, 2, 1)
71 assert t2.set_accrued_interest(150)
72 assert t2.set_comment('transaction test: sell')
73 assert_equal 1, t2.execute(15.00, Time.now + 2.days)
74 assert_equal 20, t2.order.PositionRevision.first.shares
76 # investigate position
77 ap = Smr::AssetPosition.new(p.id, ID_USER, Time.now + 3.days)
78 assert_equal 20, ap.shares, 'Number of shares in AssetPosition wrong'
79 assert_not ap.is_closed?
81 # investigate the bookings
82 # - must have two orders on one cash position
83 # - the purchase security order created a sale of cash
84 # - the sell security order created a purchase of cash
85 assert_not_equal t1.order, t2.order
86 assert_equal t1.cash_position, t2.cash_position
87 rev1 = ap.revisions.first
88 rev2 = ap.revisions.second
89 booking1 = Order.where(
90 :type=>'sale', :triggered_by_order=>rev1.id_order,
91 :shares=>(t1.order.volume + t1.order.charges + t1.order.accrued_interest)
93 booking2 = Order.where(
94 :type=>'buy', :triggered_by_order=>rev2.id_order,
95 :shares=>(t2.order.volume - t2.order.charges + t2.order.accrued_interest)
97 assert booking1.is_a?(Order), 'booking for t1 failed, got: %s' % booking1.inspect
98 assert booking2.is_a?(Order), 'booking for t2 failed, got: %s' % booking2.inspect
100 # Cash Position should be shown in AssetPosition list
101 assets = Smr::Asset.new(ID_USER, Time.now, :id_portfolio=>p.id_portfolio)
102 cp = assets.open_positions.find{|p| p.id_security == ID_CASH_SECURITY }
103 assert cp.is_a?(Smr::AssetPosition), 'cash position not found among Smr::Asset open positions'
107 # purchase security and sell it again using a Smr::AssetPosition
108 test 'purchase and sell on AssetPosition' do
110 ap = Smr::AssetPosition.new(p.id, ID_USER, Time.now - 3.days)
111 t1 = Smr::Transaction.new(ap, User.find(ID_USER))
112 assert t1.is_a?(Smr::Transaction)
113 assert t1.buy(200, 20.0, 'TST')
114 assert t1.set_charges(1, 2, 3)
115 assert t1.set_comment('transaction test: purchase')
116 assert_equal 1, t1.execute(20, Time.now - 2.days)
117 assert_equal 200, t1.order.PositionRevision.first.shares
120 t2 = Smr::Transaction.new(ap, User.find(ID_USER))
121 assert t2.sell(100, 25.0, 'TST')
122 assert t2.set_charges(3, 2, 1)
123 assert t2.set_comment('transaction test: sell')
124 assert_equal 1, t2.execute(25.00, Time.now - 1.days)
125 assert_equal 100, t2.order.PositionRevision.first.shares
127 # investigate position
128 ap_after_transactions = Smr::AssetPosition.new(p.id, ID_USER, Time.now)
129 assert_equal 100, ap_after_transactions.shares, 'Number of shares in AssetPosition wrong'
130 assert_not ap_after_transactions.is_closed?
132 # investigate the bookings (see test above)
133 assert_not_equal t1.order, t2.order
134 assert_equal t1.cash_position, t2.cash_position
135 rev1 = ap_after_transactions.revisions.first
136 rev2 = ap_after_transactions.revisions.second
137 booking1 = Order.where(:type=>'sale', :triggered_by_order=>rev1.id_order, :shares=>(t1.order.volume+t1.order.charges)).first
138 booking2 = Order.where(:type=>'buy', :triggered_by_order=>rev2.id_order, :shares=>(t2.order.volume-t2.order.charges)).first
139 assert booking1.is_a?(Order), 'booking for t1 failed, got: %s' % booking1.inspect
140 assert booking2.is_a?(Order), 'booking for t2 failed, got: %s' % booking2.inspect
144 # cancel a transaction
145 test 'cancel transaction' do
147 t1 = Smr::Transaction.new(p, User.find(ID_USER))
148 assert t1.buy(99.99, 2.0, 'TST')
149 assert t1.set_comment('transaction test: cancel transaction')
151 assert t1.order.is_canceled
153 # doing things on a canceled Transaction should not work
154 assert_not t1.set_charges(1, 2, 3)
155 assert_not t1.set_comment('transaction test: change after cancel()')
156 assert_not t1.buy(2, 1, 'TST')
157 assert_not t1.sell(2, 1, 'TST')
158 assert_not t1.execute(3)
159 assert t1.error.is_a?(String)
163 # If a cash position is closed, the corresponding period is settled. No
164 # further transactions should be possible then.
165 test 'transaction in settled period' do
167 t = Smr::Transaction.new(p, User.find(ID_USER))
169 id_cp = t.cash_position.id
170 cp = Smr::AssetPosition.new(id_cp, ID_USER)
172 # book a small amount in case we got a new empty (!) cashposition
173 assert_equal 2, Smr::Transaction.new(cp, User.find(ID_USER)).book(1, :comment=>'small cash booking'), 'wrong return code for cash booking'
175 # re-read the cash position and settle
176 cp = Smr::AssetPosition.new(id_cp, ID_USER)
179 # make another transaction that should get the same cashposition, but re-read after #settle
180 t = Smr::Transaction.new(p, User.find(ID_USER))
181 assert_equal id_cp, t.cash_position.id, 'second transaction got another cashposition'
182 cp = Smr::AssetPosition.new(t.cash_position.id, ID_USER)
185 retval = t.buy(2, 1, 'TST')
186 assert_not retval, '#buy returned %s object, expect FalseClass' % retval.class
187 assert t.error.is_a?(String)
188 assert t.error.include?('settled'), t.error
192 # Issues a transaction in current year that happend in the year before.
193 # Both, the transaction /and/ the cash booking must happen in the
195 test 'transaction in previous year' do
196 current_year = Time.now
197 prev_year = current_year - 1.year
198 ap = Smr::AssetPosition.new(make_position.id, ID_USER, current_year)
200 # purchase shares in past year while the assetposition is at current year
201 t = Smr::Transaction.new(ap, User.find(ID_USER), :issue_time=>prev_year )
202 assert t.buy 80, 12.5, 'some exchange'
203 assert_equal 1, t.execute(12.5, prev_year), '#execute should return 1 to indicate successful booking'
205 # where is the cash booking?
206 cp_current_year = Smr::Transaction.find_cashposition ap.portfolio.id, ID_USER, current_year
207 assert cp_current_year.PositionRevision.all.empty?, 'current year not supposed to have cash bookings'
209 cp_prev_year = Smr::Transaction.find_cashposition ap.portfolio.id, ID_USER, prev_year
210 assert_not cp_prev_year.PositionRevision.all.empty?, 'past year must have cash bookings'
211 found_booking = false
212 cp_prev_year.PositionRevision.all.each do |b|
213 found_booking=true if b.shares == (80*12.5*-1) and b.date_created == prev_year.to_i
215 assert found_booking, 'cash booking not found in previous year'
219 # Cash bookings are done on a cash position only. There is no security
220 # Order involved here.
221 test 'make a cash booking' do
223 cp = make_position :cash_position=>true
225 t1_exectime = Time.now - 3.days
226 t1 = Smr::Transaction.new(cp, User.find(ID_USER), :issue_time=>t1_exectime - 1.days)
227 t1.set_comment('booking positive cashflow')
228 assert t1.book(100, :time=>t1_exectime), t1.error
230 t2_exectime = Time.now - 1.days
231 t2 = Smr::Transaction.new(cp, User.find(ID_USER), :issue_time=>t2_exectime - 1.days)
232 t2.set_comment('booking negative cashflow')
233 assert t2.book(-50, :time=>t2_exectime), t2.error
235 # investigate cash position and bookings
236 assert_equal t1.cash_position, t1.cash_position
237 assert_equal t1_exectime.to_i, cp.PositionRevision.order(:date_created).first.date_created
238 assert_equal t2_exectime.to_i, cp.PositionRevision.order(:date_created).second.date_created
239 assert_equal 50, cp.PositionRevision.order(:date_created).second.shares, 'booking calculation wrong'
243 # the booking must happen in the previous year, *not* in the current one
244 test 'make a cash booking after year changed' do
248 cp_past_year =Smr::Transaction.find_cashposition(p.id_portfolio, User.find(ID_USER).id, Time.now - 1.year)
249 initial_balance_past_year = (cp_past_year.PositionRevision.count > 0 ? cp_past_year.PositionRevision.order(:date_created).last.shares : 0)
251 t1_exectime = Time.now - 1.year + 1.day
252 t1 = Smr::Transaction.new(cp_past_year, User.find(ID_USER), :issue_time=>t1_exectime)
253 t1.set_comment('booking positive cashflow in past year')
254 assert t1.book(100, :time=>t1_exectime), t1.error
256 cp_current_year =Smr::Transaction.find_cashposition(p.id_portfolio, User.find(ID_USER).id, Time.now)
257 initial_balance_current_year = (cp_current_year.PositionRevision.count > 0 ? cp_current_year.PositionRevision.order(:date_created).last.shares : 0)
259 t2_exectime = Time.now + 1.second
260 t2 = Smr::Transaction.new(cp_current_year, User.find(ID_USER), :issue_time=>t2_exectime)
261 t2.set_comment('booking negative cashflow in current year')
262 assert t2.book(-50, :time=>t2_exectime), t2.error
264 # investigate cash position and bookings
267 (t1.cash_position.PositionRevision.order(:date_created).last.shares - initial_balance_past_year),
268 'wrong balance in past years cash_position'
272 (t2.cash_position.PositionRevision.order(:date_created).last.shares - initial_balance_current_year),
273 'wrong balance in current cash_position'
275 assert_not_equal t1.cash_position, t2.cash_position, 'transaction2 should have created a new cashposition'
279 # Do excessive cash bookings and check balance
280 test 'cash position balance correct' do
284 cp = make_position :cash_position=>true
289 amount *= -1 if offset.odd?
290 exectime = Time.now - offset.days
292 t = Smr::Transaction.new(cp, User.find(ID_USER), :issue_time=>exectime - 1.days)
293 assert t.book(amount, :time=>exectime), t.error
297 # investigate AssetPosition showing the cash position properly
298 ap = Smr::AssetPosition.new(cp.id, ID_USER)
299 assert_equal balance, ap.invested, 'wrong balance on AssetPosition'
303 # Bookings in past years must create/use cash position of correct (past) year.
304 test "make transaction with booking in past year" do
307 t1 = Smr::Transaction.new(p, User.find(ID_USER), :issue_time=>Time.now)
308 t2 = Smr::Transaction.new(p, User.find(ID_USER), :issue_time=>Time.now - 1.year)
309 assert_not_equal t1.cash_position.id, t2.cash_position.id, 't1 got same cashposition as t2, should have differend ones'
311 # execute both transactions, then inspect what happened in the cashpositions
312 # - its crucial to first execute both and second look for errors in both
313 cpt1_oldbalance = Smr::AssetPosition.new(t1.cash_position.id, ID_USER).shares
315 t1.execute 5, Time.now
316 cpt1 = Smr::AssetPosition.new(t1.cash_position.id, ID_USER)
318 cpt2_oldbalance = Smr::AssetPosition.new(t2.cash_position.id, ID_USER).shares
320 t2.execute 5, Time.now-1.year
321 cpt2 = Smr::AssetPosition.new(t2.cash_position.id, ID_USER)
323 assert t1.error.empty?, t1.to_s
324 assert t2.error.empty?, t2.to_s
325 assert_equal cpt1_oldbalance - 50, cpt1.shares, 'unexpected cashposition balance in current year'
326 assert_equal cpt2_oldbalance - 100, cpt2.shares, 'unexpected cashposition balance in past year'
330 # All charges must be deducted from the cash bookings.
331 test 'deduct charges from PURCHASE cash booking' do
333 t = Smr::Transaction.new(p, User.find(ID_USER))
334 initial_balance = (t.cash_position.PositionRevision.count > 0 ? t.cash_position.PositionRevision.order(:date_created).last.shares : 0)
335 t.set_charges(1, 2, 3)
337 assert t.execute(5), t.error
338 assert_equal initial_balance-506, t.cash_position.PositionRevision.order(:date_created).last.shares
342 # All charges must be deducted from the cash bookings.
343 test 'deduct charges from SALE cash booking' do
345 t = Smr::Transaction.new(p, User.find(ID_USER))
346 initial_balance = (t.cash_position.PositionRevision.count > 0 ? t.cash_position.PositionRevision.order(:date_created).last.shares : 0)
347 t.set_charges(10, 20, 30)
349 assert t.execute(10), t.error
350 assert_equal initial_balance+440, t.cash_position.PositionRevision.order(:date_created).last.shares
354 # Transactions can operate only with Order in pending state
355 test 'transaction using canceled or executed order' do
358 :date_issued=>Time.now.to_i,
359 :date_expire=>(Time.now + 1.month).to_i,
361 :expense=>0, :courtage=>0, :provision=>0,
362 :shares=>20, :limit=>1, :is_canceled=>true
365 t = Smr::Transaction.new(p, User.find(ID_USER), :order=>o)
366 assert_not t.buy(100, 1), "purchase accepted on canceled order"
367 assert t.error.include?('canceled')
369 o.is_canceled = false
372 t = Smr::Transaction.new(p, User.find(ID_USER), :order=>o)
373 assert_not t.buy(100, 1), "purchase accepted on executed order"
374 assert t.error.include?('executed')
378 # test disabled for now, rounding issues exist on multiple levels
379 # - involved columns are FLOAT still, thus #settle /must/ run into a remaining blance
380 # - http://stackoverflow.com/questions/23120584/why-does-mysql-round-floats-way-more-than-expected
382 # test 'rounding-off difference while booking cash' do
384 # # book amounts that can be roundet at every digit, each into a new year/cash position
385 # [ 5.6789, 78.6789, 678.6789, 5678.6789, 456789.6789 ].each do |amount|
386 # cp = make_position :cash_position=>true
387 # ct = Smr::Transaction.new(cp, User.find(ID_USER))
390 # ct.book(amount, :comment=>'uneven booking'),
391 # 'cash booking failed with: %s' % ct.error
395 # ct.order.PositionRevision.first.shares,
396 # 'spottet rounding-off difference between Order and PositionRevision'
400 # # re-read the cash position: settle should not raise after no rounding-offs were spottet
401 # cp = Smr::AssetPosition.new(id_cp, ID_USER)
402 #p '=settle=> %s' % cp.shares.inspect
403 # assert_nil cp.settle
405 # # re-read at early next year, should be closed with (almost) no balance
406 # cp = Smr::AssetPosition.new(id_cp, ID_USER, Time.now.end_of_year + 1.day)
407 # assert cp.is_closed?, 'cash position not closed, perhaps #settle failed'
408 # assert cp.shares < 0.01, 'some unusual balance remains, perhaps #settle failed'
409 #p '==> %s' % cp.shares.inspect