def time_correct_verlet(state, t, accn, mass=1): """ This low-level function implements a single step of Time-corrected Verlet position integration. See Jonathan Dummer's `article on TCV`_ for a full description of the algorithm. :param state: a 2-tuple of :py:class:`Impulses <turberfield.utils.travel.Impulse>` . Element 0 is the most recent in time. :param t: a time quantity. :param accn: an acceleration quantity. :returns: a new state 2-tuple. :requires: `acceleration` type to support multiplication over `time` type. .. _article on TCV: http://lonesock.net/article/verlet.html """ imp0, imp_1 = state dt0, dt_1 = (Dl(imp0.tBegin - imp0.tEnd), Dl(imp_1.tBegin - imp_1.tEnd)) leap = imp0.accn * dt0 * dt0 try: pos = imp0.pos + (imp0.pos - imp_1.pos) * dt0 / dt_1 + leap except (ZeroDivisionError, decimal.InvalidOperation): pos = imp0.pos + (imp0.pos - imp_1.pos) + leap rv = Impulse(imp0.tEnd, t, accn, pos) return (rv, imp0)
def add_column(self, ref, role, *, label="{}", currency=None): assert role is not Role.trading crncy = currency or self.ref rv = Column(ref, crncy, role, label) self._tally[rv] = Dl(0) self._rates[rv] = Exchange({}) if crncy not in self._tradingAccounts: tA = Column(crncy.name, crncy, Role.trading, "{} trading account") self._tradingAccounts[crncy] = tA self._tally[tA] = Dl(0) return rv
def infer_rate(self, key): try: return self[key] except KeyError: key = tuple(reversed(key)) try: return Dl(1) / self[key] except KeyError as err: if key[0] == key[1]: return Dl(1) else: raise err
def test_output_sequential_journals(self): ldgr = Ledger( Column("Canadian cash", Cy.CAD, Role.asset, "{}"), Column("US cash", Cy.USD, Role.asset, "{}"), Column("Capital", Cy.CAD, Role.capital, "{}"), Column("Expense", Cy.CAD, Role.expense, "{}"), ref=Cy.CAD) cols = ldgr.columns for amount, col in zip( (Dl(200), Dl(0), Dl(200), Dl(0)), ldgr.columns.values() ): ldgr.commit(amount, col) out = io.StringIO() print(metadata(ldgr), file=out) print(journal( ldgr, ts=datetime.date(2013, 1, 1), note="Opening balance"), file=out) exchange = Exchange({(Cy.USD, Cy.CAD): Dl("1.2")}) for args in ldgr.adjustments(exchange): ldgr.commit(*args) print(journal( ldgr, ts=datetime.date(2013, 1, 2), note="1 USD -> 1.20 CAD"), file=out) ldgr.commit(-20, cols["Canadian cash"]) ldgr.commit(20, cols["Expense"]) print(journal( ldgr, ts=datetime.date(2013, 1, 3), note="Buy food"), file=out) objs = rson.loads(out.getvalue()) self.assertEqual(8, len(objs)) self.assertEqual([180., 0., 200., 20., 0., 0.], objs[-1])
def equation(self): """ The `Fundamental Accounting Equation`_ is this:: Assets - Liabilities = Capital + Income - Expenses - Dividends Currency trading gains are counted as income. For practical purposes, the `FAE` is often rearranged to be:: Assets + Expenses + Dividends = Capital + Income + Liabilities This property evaluates both sides of this second equation, and determines if they are equal or not. :returns: A tuple of `lhs`, `rhs`, `status` .. _Fundamental Accounting Equation: \ http://en.wikipedia.org/wiki/Accounting_equation """ st = Status.failed lhCols = set(i for i in self._tally if i.role in (Role.asset, Role.expense, Role.dividend)) trCols = set(i for i in self._tally if i.role is Role.trading) rhCols = set(self._tally.keys()) - lhCols - trCols try: lhs = sum(self._rates[col].convert( self._tally[col], TradePath(col.currency, self.ref, self.ref)) for col in lhCols) rhs = sum(self._rates[col].convert( self._tally.get(col, Dl(0)), TradePath(col.currency, self.ref, self.ref)) for col in rhCols) + sum( self._tally.get(col, Dl(0)) for col in trCols) except KeyError: lhs = None rhs = None else: if lhs.quantize(Dl("0.01")) == rhs.quantize(Dl("0.01")): st = Status.ok return FAE(lhs, rhs, st)
def test_track_exchange_gain_with_fixed_assets(self): """ From Selinger table 4.1 date asset asset capital gain Jan 1 Balance (1 USD = 1.20 CAD) CAD 60 USD 100 CAD 180 CAD 0 Jan 2 Balance (1 USD = 1.30 CAD) CAD 60 USD 100 CAD 180 CAD 10 Jan 3 Balance (1 USD = 1.25 CAD) CAD 60 USD 100 CAD 180 CAD 5 Jan 4 Balance (1 USD = 1.15 CAD) CAD 60 USD 100 CAD 180 – CAD 5 """ ldgr = Ledger(Column("Canadian cash", Cy.CAD, Role.asset, "{}"), Column("US cash", Cy.USD, Role.asset, "{}"), Column("Capital", Cy.CAD, Role.capital, "{}"), ref=Cy.CAD) usC = ldgr.columns["US cash"] for args in ldgr.adjustments(Exchange({(Cy.USD, Cy.CAD): Dl("1.2")})): ldgr.commit(*args, ts=datetime.date(2013, 1, 1), note="1 USD = 1.20 CAD") lhs, rhs, st = ldgr.equation self.assertEqual(lhs, rhs) self.assertIs(st, Status.ok) for deposit, col in zip((Dl(60), Dl(100), Dl(180)), ldgr.columns.values()): ldgr.commit(deposit, col, ts=datetime.date(2013, 1, 1), note="Initial balance") lhs, rhs, st = ldgr.equation self.assertEqual(lhs, rhs) self.assertIs(st, Status.ok) trade, col, exchange = next( ldgr.adjustments(Exchange({(Cy.USD, Cy.CAD): Dl("1.3")}), [usC])) self.assertIs(col, usC) self.assertEqual(10, trade.gain) trade, col, exchange = next( ldgr.adjustments(Exchange({(Cy.USD, Cy.CAD): Dl("1.25")}), [usC])) self.assertIs(col, usC) self.assertEqual(5, trade.gain) trade, col, exchange = next( ldgr.adjustments(Exchange({(Cy.USD, Cy.CAD): Dl("1.15")}), [usC])) self.assertIs(col, usC) self.assertEqual(-5, trade.gain) lhs, rhs, st = ldgr.equation self.assertEqual(lhs, rhs) self.assertIs(st, Status.ok)
def test_output_journal(self): ldgr = Ledger( Column("Canadian cash", Cy.CAD, Role.asset, "{}"), Column("US cash", Cy.USD, Role.asset, "{}"), Column("Capital", Cy.CAD, Role.capital, "{}"), Column("Expense", Cy.CAD, Role.expense, "{}"), ref=Cy.CAD) for amount, col in zip( (Dl(200), Dl(0), Dl(200), Dl(0)), ldgr.columns.values() ): (_, _, _, _, st) = ldgr.commit(amount, col) self.assertIs(Status.ok, st) t = journal( ldgr, ts=datetime.date(2013, 1, 1), note="Opening balance") out = rson.loads(t) self.assertEqual(2, len(out)) self.assertEqual( {"ts": "2013-01-01", "note": "Opening balance"}, out[0]) self.assertEqual(6, len(out[1])) self.assertEqual(400, sum(out[1]))
def __init__(self, *args, ref=Currency.XTW): """ :param ref: (optional) the base Currency_ type for the Ledger :param args: One or more Column objects """ self.ref = ref cols = list(args) cols.extend( Column(c.name, c, Role.trading, "{} trading account") for c in set(i.currency for i in args)) self._tradingAccounts = { i.currency: i for i in cols if i.role is Role.trading } self._rates = {i: Exchange({}) for i in args} self._tally = OrderedDict((i, Dl(0)) for i in cols) self.transaction = singledispatch(transaction)
def test_point_calculation(self): expected = [point(i, 0, 0) for i in [ Dl("0"), Dl("29.41800004"), Dl("56.38450008"), Dl("80.89950012"), Dl("102.96300016"), Dl("122.5750002"), Dl("139.73550024"), Dl("154.44450028"), Dl("166.70200032"), Dl("176.50800036"), Dl("183.8625004"), Dl("188.76550044"), Dl("191.21700048"), Dl("191.21700052"), Dl("188.76550056"), Dl("183.8625006"), Dl("176.50800064"), Dl("166.70200068"), Dl("154.44450072"), Dl("139.73550076"), Dl("122.5750008"), Dl("102.96300084"), Dl("80.89950088"), Dl("56.38450092"), Dl("29.41800096"), Dl("0.000001")] ] dt = Dl("0.5") vel = vector(Dl("61.28750008"), 0, 0) accn = vector(Dl("-9.806"), 0, 0) proc = trajectory() proc.send(None) for n, x in enumerate(expected): if n == 0: imp = proc.send(Impulse( Dl(0), Dl("0.5"), accn, point(0, 0, 0))) elif n == 1: p = ( point(0, 0, 0) + vel * dt + Dl("0.5") * accn * dt * dt ) imp = proc.send(Impulse( imp.tEnd, imp.tEnd + dt, accn, p)) else: imp = proc.send(Impulse( imp.tEnd, imp.tEnd + dt, accn, imp.pos)) with self.subTest(n=n): self.assertEqual(x, imp.pos)
def test_point_calculation(self): expected = [point(i, 0, 0) for i in [ Dl("0"), Dl("0.502656"), Dl("0.824448"), Dl("0.986112"), Dl("1.008384"), Dl("0.912"), Dl("0.717696"), Dl("0.446208"), Dl("0.118272"), Dl("-0.245376"), Dl("-0.624"), Dl("-0.996864"), Dl("-1.343232"), Dl("-1.642368"), Dl("-1.873536"), Dl("-2.016"), Dl("-2.049024"), Dl("-1.951872"), Dl("-1.703808"), Dl("-1.284096"), Dl("-0.672"), Dl("0.153216"), Dl("1.212288"), Dl("2.525952"), Dl("4.114944"), Dl("6")] ] accns = [vector(i, 0, 0) for i in [ Dl("-14"), Dl("-12.56"), Dl("-11.12"), Dl("-9.68"), Dl("-8.24"), Dl("-6.8"), Dl("-5.36"), Dl("-3.92"), Dl("-2.48"), Dl("-1.04"), Dl("0.4"), Dl("1.84"), Dl("3.28"), Dl("4.72"), Dl("6.16"), Dl("7.6"), Dl("9.04"), Dl("10.48"), Dl("11.92"), Dl("13.36"), Dl("14.8"), Dl("16.24"), Dl("17.68"), Dl("19.12"), Dl("20.56"), Dl("22")] ] dt = Dl("0.12") proc = trajectory() proc.send(None) for n, x in enumerate(expected): accn = accns[n] if n == 0: p = point(Dl(0), Dl(0), Dl(0)) imp = proc.send(Impulse(Dl(0), dt, accn, p)) elif n == 1: p = point(Dl("0.502656"), Dl(0), Dl(0)) imp = proc.send(Impulse( imp.tEnd, imp.tEnd + dt, accn, p)) else: imp = proc.send(Impulse( imp.tEnd, imp.tEnd + dt, accn, imp.pos)) with self.subTest(n=n): self.assertEqual(x, imp.pos)
def test_commit_exchange_gain_via_expenses(self): """ From Selinger table 4.4 date asset asset capital expense trading Jan 1 Opening balance CAD 200 USD 0 CAD 200 CAD 0 USD 0 CAD 0 Jan 2 1 USD==1.20CAD CAD-120 USD 100 USD 100 CAD 120 Balance CAD 80 USD 100 CAD 200 CAD 0 USD 100 CAD 120 Jan 3 1 USD==1.30CAD USD-40 CAD 52 USD-40 CAD 52 Balance CAD 80 USD 60 CAD 200 CAD 52 USD 60 CAD-68 Jan 5 1 USD==1.25CAD CAD 75 USD-60 USD-60 CAD 75 Balance CAD 155 USD 0 CAD 200 CAD 52 USD 0 CAD 07 Jan 7 Buy food CAD-20 CAD 20 Balance CAD 135 USD 0 CAD 200 CAD 72 USD 0 CAD 07 """ ldgr = Ledger(Column("Canadian cash", Cy.CAD, Role.asset, "{}"), Column("US cash", Cy.USD, Role.asset, "{}"), Column("Capital", Cy.CAD, Role.capital, "{}"), Column("Expense", Cy.CAD, Role.expense, "{}"), ref=Cy.CAD) cols = ldgr.columns self.assertIs(Status.failed, ldgr.equation.status) # row one for amount, col in zip((Dl(200), Dl(0), Dl(200), Dl(0)), ldgr.columns.values()): ldgr.commit(amount, col, ts=datetime.date(2013, 1, 1), note="Opening balance") self.assertEqual(200, ldgr.value("Canadian cash")) self.assertEqual(200, ldgr.value("Capital")) self.assertEqual(0, ldgr.value("USD trading account")) # whitebox test self.assertIs(Status.failed, ldgr.equation.status) # row two exchange = Exchange({(Cy.USD, Cy.CAD): Dl("1.2")}) for args in ldgr.adjustments(exchange): ldgr.commit(*args, ts=datetime.date(2013, 1, 2), note="1 USD = 1.20 CAD") lhs, rhs, st = ldgr.equation self.assertEqual(lhs, rhs) self.assertIs(st, Status.ok) usd = exchange.convert(120, TradePath(Cy.CAD, Cy.CAD, Cy.USD)) self.assertEqual(100, usd) ldgr.commit(-120, cols["Canadian cash"]) self.assertIs(ldgr.equation.status, Status.failed) ldgr.commit(usd, cols["US cash"]) self.assertIs(ldgr.equation.status, Status.ok) # row three exchange = Exchange({(Cy.USD, Cy.CAD): Dl("1.3")}) for args in ldgr.adjustments(exchange): ldgr.commit(*args, ts=datetime.date(2013, 1, 3), note="1 USD = 1.30 CAD") cad = exchange.convert(40, TradePath(Cy.USD, Cy.CAD, Cy.CAD)) self.assertEqual(52, cad) self.assertIs(ldgr.equation.status, Status.ok) ldgr.commit(-40, cols["US cash"]) self.assertIs(ldgr.equation.status, Status.failed) ldgr.commit(cad, cols["Expense"]) self.assertIs(ldgr.equation.status, Status.ok) # row four exchange = Exchange({(Cy.USD, Cy.CAD): Dl("1.25")}) for args in ldgr.adjustments(exchange): ldgr.commit(*args, ts=datetime.date(2013, 1, 5), note="1 USD = 1.25 CAD") cad = exchange.convert(60, TradePath(Cy.USD, Cy.CAD, Cy.CAD)) self.assertEqual(75, cad) self.assertIs(ldgr.equation.status, Status.ok) ldgr.commit(-60, cols["US cash"]) self.assertIs(ldgr.equation.status, Status.failed) ldgr.commit(cad, cols["Canadian cash"]) self.assertIs(ldgr.equation.status, Status.ok) self.assertEqual(155, ldgr.value("Canadian cash")) # row five ldgr.commit(-20, cols["Canadian cash"], note="Buy food") self.assertIs(ldgr.equation.status, Status.failed) ldgr.commit(20, cols["Expense"], note="Buy food") self.assertIs(ldgr.equation.status, Status.ok) # final balance self.assertEqual(135, ldgr.value("Canadian cash")) self.assertEqual(0, ldgr.value("US cash")) self.assertEqual(200, ldgr.value("Capital")) self.assertEqual(72, ldgr.value("Expense")) self.assertEqual(7, ldgr.value("USD trading account"))