From 458a24d09f95eefb161728d9676ecdfca30e3f5e Mon Sep 17 00:00:00 2001 From: xengineering Date: Sun, 8 Sep 2024 17:02:33 +0200 Subject: Split flow.py to simulate.py and model.py model.py should be a file containing only dataclasses to model finance. simulate.py should take care of the simulation of that finance data to create a financial forecast. --- finance/flow.py | 66 ------------------------------------------------ finance/model.py | 12 +++++++++ finance/simulate.py | 59 +++++++++++++++++++++++++++++++++++++++++++ finance/test_flow.py | 47 ---------------------------------- finance/test_simulate.py | 48 +++++++++++++++++++++++++++++++++++ 5 files changed, 119 insertions(+), 113 deletions(-) delete mode 100644 finance/flow.py create mode 100644 finance/model.py create mode 100644 finance/simulate.py delete mode 100644 finance/test_flow.py create mode 100644 finance/test_simulate.py (limited to 'finance') diff --git a/finance/flow.py b/finance/flow.py deleted file mode 100644 index c6b28c0..0000000 --- a/finance/flow.py +++ /dev/null @@ -1,66 +0,0 @@ -import dataclasses -from datetime import datetime -from decimal import Decimal -from typing import Generator - - -@dataclasses.dataclass(kw_only=True, frozen=True) -class Flow: - """Time-discrete flow of money paid on the first day of a month""" - - amount: Decimal - since: None | datetime - until: None | datetime - - def integrate(self, start: datetime, end: datetime) -> Decimal: - """Integrate the flow between two dates to an amount of money""" - - payments: int = 0 - - if self.since is not None: - if start < self.since: - start = self.since - - if self.until is not None: - if end > self.until: - end = self.until - - for candidate in monthly_candidates(start): - if start <= candidate <= end: - payments += 1 - if candidate > end: - break - - return self.amount * Decimal(payments) - - -def monthly_candidates(start: datetime) -> Generator[datetime, None, None]: - current = datetime(start.year, start.month, 1) - while True: - yield current - if current.month == 12: - current = datetime(current.year + 1, 1, 1) - else: - current = datetime(current.year, current.month + 1, 1) - - -def simulate( - start: datetime, end: datetime, flows: tuple[Flow, ...] -) -> list[tuple[datetime, Decimal]]: - dates: list[datetime] = [] - values: list[Decimal] = [] - - for candidate in monthly_candidates(start): - if start <= candidate <= end: - dates.append(candidate) - - if candidate > end: - break - - for date in dates: - value = Decimal(0.0) - for flow in flows: - value += flow.integrate(start, date) - values.append(value) - - return [(date, values[index]) for index, date in enumerate(dates)] diff --git a/finance/model.py b/finance/model.py new file mode 100644 index 0000000..8bf2e3d --- /dev/null +++ b/finance/model.py @@ -0,0 +1,12 @@ +import dataclasses +import datetime +import decimal + + +@dataclasses.dataclass(kw_only=True, frozen=True) +class Flow: + """Time-discrete flow of money paid on the first day of a month""" + + amount: decimal.Decimal + since: None | datetime.datetime + until: None | datetime.datetime diff --git a/finance/simulate.py b/finance/simulate.py new file mode 100644 index 0000000..3f8e3ad --- /dev/null +++ b/finance/simulate.py @@ -0,0 +1,59 @@ +from datetime import datetime +from decimal import Decimal +from typing import Generator + +from finance.model import Flow + + +def integrate(flow: Flow, start: datetime, end: datetime) -> Decimal: + """Integrate the flow between two dates to an amount of money""" + + payments: int = 0 + + if flow.since is not None: + if start < flow.since: + start = flow.since + + if flow.until is not None: + if end > flow.until: + end = flow.until + + for candidate in monthly_candidates(start): + if start <= candidate <= end: + payments += 1 + if candidate > end: + break + + return flow.amount * Decimal(payments) + + +def monthly_candidates(start: datetime) -> Generator[datetime, None, None]: + current = datetime(start.year, start.month, 1) + while True: + yield current + if current.month == 12: + current = datetime(current.year + 1, 1, 1) + else: + current = datetime(current.year, current.month + 1, 1) + + +def simulate( + start: datetime, end: datetime, flows: tuple[Flow, ...] +) -> list[tuple[datetime, Decimal]]: + dates: list[datetime] = [] + values: list[Decimal] = [] + + for candidate in monthly_candidates(start): + if start <= candidate <= end: + dates.append(candidate) + + if candidate > end: + break + + for date in dates: + value = Decimal(0.0) + for flow in flows: + value += integrate(flow, start, date) + values.append(value) + + return [(date, values[index]) for index, date in enumerate(dates)] diff --git a/finance/test_flow.py b/finance/test_flow.py deleted file mode 100644 index 91d189b..0000000 --- a/finance/test_flow.py +++ /dev/null @@ -1,47 +0,0 @@ -from datetime import datetime -from decimal import Decimal - -from finance.flow import Flow, simulate - - -def test_flow_integration() -> None: - fl = Flow( - amount=Decimal(3000.0), - since=datetime(2023, 1, 1), - until=datetime(2026, 1, 5), - ) - - tests = ( - (datetime(2024, 3, 12), datetime(2024, 4, 2), Decimal(3000.0)), - (datetime(2024, 3, 1), datetime(2024, 3, 15), Decimal(3000.0)), - (datetime(2024, 2, 25), datetime(2024, 3, 1), Decimal(3000.0)), - (datetime(2024, 2, 25), datetime(2024, 6, 12), Decimal(12000.0)), - (datetime(2022, 1, 5), datetime(2022, 9, 14), Decimal(0.0)), - (datetime(2022, 1, 5), datetime(2023, 1, 1), Decimal(3000.0)), - (datetime(2022, 7, 4), datetime(2024, 12, 8), Decimal(72000.0)), - (datetime(2025, 11, 4), datetime(2026, 5, 9), Decimal(6000.0)), - (datetime(2026, 1, 5), datetime(2027, 1, 1), Decimal(0.0)), - (datetime(2027, 5, 28), datetime(2028, 7, 7), Decimal(0.0)), - ) - - for test in tests: - assert fl.integrate(start=test[0], end=test[1]) == test[2] - - -def test_simulate() -> None: - flows = (Flow(amount=Decimal(100.0), since=None, until=None),) - - simulated = simulate( - start=datetime(2024, 1, 1), - end=datetime(2024, 4, 1), - flows=flows, - ) - - expected = [ - (datetime(2024, 1, 1), Decimal(100.0)), - (datetime(2024, 2, 1), Decimal(200.0)), - (datetime(2024, 3, 1), Decimal(300.0)), - (datetime(2024, 4, 1), Decimal(400.0)), - ] - - assert simulated == expected diff --git a/finance/test_simulate.py b/finance/test_simulate.py new file mode 100644 index 0000000..3b79c99 --- /dev/null +++ b/finance/test_simulate.py @@ -0,0 +1,48 @@ +from datetime import datetime +from decimal import Decimal + +from finance.model import Flow +from finance.simulate import simulate, integrate + + +def test_flow_integration() -> None: + fl = Flow( + amount=Decimal(3000.0), + since=datetime(2023, 1, 1), + until=datetime(2026, 1, 5), + ) + + tests = ( + (datetime(2024, 3, 12), datetime(2024, 4, 2), Decimal(3000.0)), + (datetime(2024, 3, 1), datetime(2024, 3, 15), Decimal(3000.0)), + (datetime(2024, 2, 25), datetime(2024, 3, 1), Decimal(3000.0)), + (datetime(2024, 2, 25), datetime(2024, 6, 12), Decimal(12000.0)), + (datetime(2022, 1, 5), datetime(2022, 9, 14), Decimal(0.0)), + (datetime(2022, 1, 5), datetime(2023, 1, 1), Decimal(3000.0)), + (datetime(2022, 7, 4), datetime(2024, 12, 8), Decimal(72000.0)), + (datetime(2025, 11, 4), datetime(2026, 5, 9), Decimal(6000.0)), + (datetime(2026, 1, 5), datetime(2027, 1, 1), Decimal(0.0)), + (datetime(2027, 5, 28), datetime(2028, 7, 7), Decimal(0.0)), + ) + + for test in tests: + assert integrate(fl, start=test[0], end=test[1]) == test[2] + + +def test_simulate() -> None: + flows = (Flow(amount=Decimal(100.0), since=None, until=None),) + + simulated = simulate( + start=datetime(2024, 1, 1), + end=datetime(2024, 4, 1), + flows=flows, + ) + + expected = [ + (datetime(2024, 1, 1), Decimal(100.0)), + (datetime(2024, 2, 1), Decimal(200.0)), + (datetime(2024, 3, 1), Decimal(300.0)), + (datetime(2024, 4, 1), Decimal(400.0)), + ] + + assert simulated == expected -- cgit v1.2.3-70-g09d2