manage bookmarks, bugfixes
[smr.git] / gui / test / unit / smr_transaction_test.rb
blobdfec17d9bdf4a20a25c160875711bd4118078176
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 'test_helper'
18 module Smr  #:nodoc:
20     class TransactionTest < ActiveSupport::TestCase
21         # User.id for transaction tests
22         ID_USER = 1
24         # Security.id to buy/sell during test
25         ID_SECURITY = 7     # 'Siemens AG' - has no position in fixtures data
27         ##
28         # make a new and empty AssetPosition
29         def make_position options={:cash_position=>false}
30             smr_seed
31             pf = Portfolio.create(
32                 :id_user=>ID_USER, :name=>'TransactionTest', :type=>'', :date_created=>Time.now.to_i
33             )
34             p = Position.new(
35                 :id_security=>(options[:cash_position] ? Smr::ID_CASH_SECURITY : ID_SECURITY),
36                 :id_portfolio=>pf.id,
37                 :comment=>(options[:cash_position] ? 'make cash booking' : 'transaction test')
38             )
39             assert p.save, 'could not create new position'
40             p
41         end
43         ##
44         # see if the database is populated with the things needed here
45         test 'database setup properly' do
46             smr_seed
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'
50         end
52         ##
53         # purchase security and sell it again
54         test 'purchase and sell' do
55             p = make_position
57             # BUY security
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
67             # SELL again
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)
92             ).first
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)
96             ).first
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'
104         end
106         ##
107         # purchase security and sell it again using a Smr::AssetPosition
108         test 'purchase and sell on AssetPosition' do
109             p = make_position
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
119             # sell again
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
141         end
143         ##
144         # cancel a transaction
145         test 'cancel transaction' do
146             p = make_position
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')
150             t1.cancel
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)
160         end
162         ##
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
166             p = make_position
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)
177             cp.settle
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)
184             assert cp.is_closed?
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
189         end
191         ##
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
194         # previous year.
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
214             end
215             assert found_booking, 'cash booking not found in previous year'
216         end
218         ##
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
222             smr_seed
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'
240         end
242         ##
243         # the booking must happen in the previous year, *not* in the current one
244         test 'make a cash booking after year changed' do
245             smr_seed
246             p = make_position
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
265             assert_equal(
266                 100,
267                 (t1.cash_position.PositionRevision.order(:date_created).last.shares - initial_balance_past_year),
268                 'wrong balance in past years cash_position'
269             )
270             assert_equal(
271                 -50,
272                 (t2.cash_position.PositionRevision.order(:date_created).last.shares - initial_balance_current_year),
273                 'wrong balance in current cash_position'
274             )
275             assert_not_equal t1.cash_position, t2.cash_position, 'transaction2 should have created a new cashposition'
276         end
278         ##
279         # Do excessive cash bookings and check balance
280         test 'cash position balance correct' do
281             smr_seed
282             balance = 0
283             srand(Time.now.to_i)
284             cp = make_position :cash_position=>true
286             for i in 1...50 do
287                 offset = rand(100)
288                 amount = rand(10000)
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
294                 balance += amount
295             end
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'
300         end
302         ##
303         # Bookings in past years must create/use cash position of correct (past) year.
304         test "make transaction with booking in past year" do
305             p = make_position
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
314             t1.buy 10, 5
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
319             t2.buy 20, 5
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'
327         end
329         ##
330         # All charges must be deducted from the cash bookings.
331         test 'deduct charges from PURCHASE cash booking' do
332             p = make_position
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)
336             t.buy(100, 5)
337             assert t.execute(5), t.error
338             assert_equal initial_balance-506, t.cash_position.PositionRevision.order(:date_created).last.shares
339         end
341         ##
342         # All charges must be deducted from the cash bookings.
343         test 'deduct charges from SALE cash booking' do
344             p = make_position
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)
348             t.sell(50, 10)
349             assert t.execute(10), t.error
350             assert_equal initial_balance+440, t.cash_position.PositionRevision.order(:date_created).last.shares
351         end
353         ##
354         # Transactions can operate only with Order in pending state
355         test 'transaction using canceled or executed order' do
356             p = make_position
357             o = Order.new(
358                 :date_issued=>Time.now.to_i,
359                 :date_expire=>(Time.now + 1.month).to_i,
360                 :id_position=>p.id,
361                 :expense=>0, :courtage=>0, :provision=>0,
362                 :shares=>20, :limit=>1, :is_canceled=>true
363             )
364             o.save!
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
370             o.quote = 1
371             o.save!
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')
375         end
377         ##
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
381         #
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))
388 #                assert_equal(
389 #                   2,
390 #                   ct.book(amount, :comment=>'uneven booking'),
391 #                   'cash booking failed with: %s' % ct.error
392 #                )
393 #                assert_equal(
394 #                   ct.order.shares,
395 #                   ct.order.PositionRevision.first.shares,
396 #                   'spottet rounding-off difference between Order and PositionRevision'
397 #                )
398 #            end
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
410 #        end
412    end
414 end # module