Files
tradon/modules/purchase_trade/financing_tools.py
2026-02-15 14:33:14 +01:00

260 lines
7.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from decimal import Decimal, getcontext
from datetime import datetime, date
from calendar import isleap
from typing import Callable, Dict
import uuid
getcontext().prec = 28
# {
# "computation_type": "INTEREST_ACCRUAL",
# "input": {
# "start_date": "2026-01-01",
# "end_date": "2026-06-30",
# "notional": "1000000",
# "rate": {
# "value": "0.08",
# "type": "ANNUAL"
# },
# "day_count_convention": "ACT/360",
# "compounding_method": "SIMPLE"
# }
# }
# result = FinancialComputationService.execute(payload)
# interest = Decimal(result["result"]["interest_amount"])
# interest = currency.round(interest)
# ============================================================
# VERSIONING
# ============================================================
ENGINE_VERSION = "1.0.0"
# ============================================================
# REGISTRY (PLUGIN SYSTEM)
# ============================================================
DAY_COUNT_REGISTRY: Dict[str, Callable] = {}
COMPOUNDING_REGISTRY: Dict[str, Callable] = {}
def register_day_count(name: str):
def decorator(func):
DAY_COUNT_REGISTRY[name] = func
return func
return decorator
def register_compounding(name: str):
def decorator(func):
COMPOUNDING_REGISTRY[name] = func
return func
return decorator
# ============================================================
# DOMAIN DAY COUNT CONVENTIONS
# ============================================================
@register_day_count("ACT/360")
def act_360(start: date, end: date) -> Decimal:
return Decimal((end - start).days) / Decimal(360)
@register_day_count("ACT/365F")
def act_365f(start: date, end: date) -> Decimal:
return Decimal((end - start).days) / Decimal(365)
@register_day_count("ACT/ACT_ISDA")
def act_act_isda(start: date, end: date) -> Decimal:
total = Decimal("0")
current = start
while current < end:
year_end = date(current.year, 12, 31)
next_year = date(current.year + 1, 1, 1)
period_end = min(next_year, end)
days = (period_end - current).days
year_days = 366 if isleap(current.year) else 365
total += Decimal(days) / Decimal(year_days)
current = period_end
return total
@register_day_count("30E/360")
def thirty_e_360(start: date, end: date) -> Decimal:
d1 = min(start.day, 30)
d2 = min(end.day, 30)
days = (
(end.year - start.year) * 360 +
(end.month - start.month) * 30 +
(d2 - d1)
)
return Decimal(days) / Decimal(360)
# ============================================================
# DOMAIN COMPOUNDING STRATEGIES
# ============================================================
@register_compounding("SIMPLE")
def simple(rate: Decimal, yf: Decimal) -> Decimal:
return rate * yf
@register_compounding("ANNUAL")
def annual(rate: Decimal, yf: Decimal) -> Decimal:
return (Decimal(1) + rate) ** yf - Decimal(1)
@register_compounding("MONTHLY")
def monthly(rate: Decimal, yf: Decimal) -> Decimal:
monthly_rate = rate / Decimal(12)
months = yf * Decimal(12)
return (Decimal(1) + monthly_rate) ** months - Decimal(1)
@register_compounding("CONTINUOUS")
def continuous(rate: Decimal, yf: Decimal) -> Decimal:
from math import exp
return Decimal(exp(float(rate * yf))) - Decimal(1)
# ============================================================
# DOMAIN INTEREST COMPUTATION OBJECT
# ============================================================
class InterestComputation:
def __init__(
self,
start_date: date,
end_date: date,
notional: Decimal,
rate_value: Decimal,
rate_type: str,
day_count: str,
compounding: str,
):
self.start_date = start_date
self.end_date = end_date
self.notional = notional
self.rate_value = rate_value
self.rate_type = rate_type
self.day_count = day_count
self.compounding = compounding
def compute(self):
if self.end_date <= self.start_date:
raise ValueError("end_date must be after start_date")
if self.day_count not in DAY_COUNT_REGISTRY:
raise ValueError("Unsupported day count convention")
if self.compounding not in COMPOUNDING_REGISTRY:
raise ValueError("Unsupported compounding method")
yf = DAY_COUNT_REGISTRY[self.day_count](
self.start_date,
self.end_date
)
# Normalize rate to annual
if self.rate_type == "MONTHLY":
annual_rate = self.rate_value * Decimal(12)
else:
annual_rate = self.rate_value
factor = COMPOUNDING_REGISTRY[self.compounding](
annual_rate,
yf
)
interest_amount = self.notional * factor
return {
"year_fraction": yf,
"interest_factor": factor,
"interest_amount": interest_amount,
}
# ============================================================
# APPLICATION LAYER JSON SERVICE (Camunda Ready)
# ============================================================
class FinancialComputationService:
@staticmethod
def execute(payload: dict) -> dict:
"""
Stateless JSON entrypoint.
Compatible Camunda / REST / Tryton bridge.
"""
try:
request_id = str(uuid.uuid4())
input_data = payload["input"]
start_date = datetime.strptime(
input_data["start_date"], "%Y-%m-%d"
).date()
end_date = datetime.strptime(
input_data["end_date"], "%Y-%m-%d"
).date()
notional = Decimal(input_data["notional"])
rate_value = Decimal(input_data["rate"]["value"])
rate_type = input_data["rate"]["type"].upper()
day_count = input_data["day_count_convention"]
compounding = input_data["compounding_method"]
computation = InterestComputation(
start_date=start_date,
end_date=end_date,
notional=notional,
rate_value=rate_value,
rate_type=rate_type,
day_count=day_count,
compounding=compounding,
)
result = computation.compute()
return {
"metadata": {
"engine_version": ENGINE_VERSION,
"request_id": request_id,
"timestamp": datetime.utcnow().isoformat() + "Z"
},
"result": {
"year_fraction": str(result["year_fraction"]),
"interest_factor": str(result["interest_factor"]),
"interest_amount": str(result["interest_amount"]),
},
"explainability": {
"formula": "Interest = Notional × Factor",
"factor_definition": f"{compounding} compounding applied to annualized rate",
"day_count_used": day_count
},
"status": "SUCCESS"
}
except Exception as e:
return {
"status": "ERROR",
"message": str(e),
"engine_version": ENGINE_VERSION
}