diff --git a/backend/tests/test_db_migration.py b/backend/tests/test_db_migration.py index 042bb54..f14b2da 100644 --- a/backend/tests/test_db_migration.py +++ b/backend/tests/test_db_migration.py @@ -46,6 +46,23 @@ def test_run_migrations_0_to_1(monkeypatch: pytest.MonkeyPatch) -> None: "start_date": ("DATE", 1, 0), "end_date": ("DATE", 0, 0), }, + "cycle_loan_change_events": { + "id": ("INTEGER", 1, 1), + "cycle_id": ("INTEGER", 1, 0), + "effective_date": ("DATE", 1, 0), + "loan_amount_cents": ("INTEGER", 0, 0), + "loan_interest_rate_tenth_bps": ("INTEGER", 0, 0), + "related_trade_id": ("INTEGER", 0, 0), + "notes": ("TEXT", 0, 0), + "created_at": ("DATETIME", 1, 0), + }, + "cycle_daily_accrual": { + "id": ("INTEGER", 1, 1), + "cycle_id": ("INTEGER", 1, 0), + "accrual_date": ("DATE", 1, 0), + "accrual_amount_cents": ("INTEGER", 1, 0), + "created_at": ("DATETIME", 1, 0), + }, "trades": { "id": ("INTEGER", 1, 1), "user_id": ("INTEGER", 1, 0), @@ -100,6 +117,13 @@ def test_run_migrations_0_to_1(monkeypatch: pytest.MonkeyPatch) -> None: {"table": "users", "from": "user_id", "to": "id"}, {"table": "exchanges", "from": "exchange_id", "to": "id"}, ], + "cycle_loan_change_events": [ + {"table": "cycles", "from": "cycle_id", "to": "id"}, + {"table": "trades", "from": "related_trade_id", "to": "id"}, + ], + "cycle_daily_accrual": [ + {"table": "cycles", "from": "cycle_id", "to": "id"}, + ], "sessions": [ {"table": "users", "from": "user_id", "to": "id"}, ], diff --git a/backend/tests/test_service.py b/backend/tests/test_service.py new file mode 100644 index 0000000..b23e2be --- /dev/null +++ b/backend/tests/test_service.py @@ -0,0 +1,5 @@ +import pytest + +from trading_journal import crud, service + +monkeypatch = pytest.MonkeyPatch() diff --git a/backend/trading_journal/db_migration.py b/backend/trading_journal/db_migration.py index 8a63250..b6a78ea 100644 --- a/backend/trading_journal/db_migration.py +++ b/backend/trading_journal/db_migration.py @@ -28,6 +28,8 @@ def _mig_0_1(engine: Engine) -> None: models_v1.Users.__table__, # type: ignore[attr-defined] models_v1.Sessions.__table__, # type: ignore[attr-defined] models_v1.Exchanges.__table__, # type: ignore[attr-defined] + models_v1.CycleLoanChangeEvents.__table__, # type: ignore[attr-defined] + models_v1.CycleDailyAccrual.__table__, # type: ignore[attr-defined] ], ) diff --git a/backend/trading_journal/models.py b/backend/trading_journal/models.py index e8dc281..81d98e1 100644 --- a/backend/trading_journal/models.py +++ b/backend/trading_journal/models.py @@ -1,11 +1,13 @@ from datetime import date, datetime from enum import Enum +from typing import Optional from sqlmodel import ( Column, Date, DateTime, Field, + ForeignKey, Integer, Relationship, SQLModel, @@ -92,8 +94,14 @@ class Trades(SQLModel, table=True): replaced_by_trade_id: int | None = Field(default=None, foreign_key="trades.id", nullable=True) notes: str | None = Field(default=None, sa_column=Column(Text, nullable=True)) cycle_id: int | None = Field(default=None, foreign_key="cycles.id", nullable=True, index=True) + cycle: "Cycles" = Relationship(back_populates="trades") + related_loan_change_event: Optional["CycleLoanChangeEvents"] = Relationship( + back_populates="trade", + sa_relationship_kwargs={"uselist": False}, + ) + class Cycles(SQLModel, table=True): __tablename__ = "cycles" # type: ignore[attr-defined] @@ -113,8 +121,40 @@ class Cycles(SQLModel, table=True): loan_interest_rate_tenth_bps: int | None = Field(default=None, nullable=True) start_date: date = Field(sa_column=Column(Date, nullable=False)) end_date: date | None = Field(default=None, sa_column=Column(Date, nullable=True)) + trades: list["Trades"] = Relationship(back_populates="cycle") + loan_change_events: list["CycleLoanChangeEvents"] = Relationship(back_populates="cycle") + daily_accruals: list["CycleDailyAccrual"] = Relationship(back_populates="cycle") + + +class CycleLoanChangeEvents(SQLModel, table=True): + __tablename__ = "cycle_loan_change_events" # type: ignore[attr-defined] + id: int | None = Field(default=None, primary_key=True) + cycle_id: int = Field(foreign_key="cycles.id", nullable=False, index=True) + effective_date: date = Field(sa_column=Column(Date, nullable=False)) + loan_amount_cents: int | None = Field(default=None, sa_column=Column(Integer, nullable=True)) + loan_interest_rate_tenth_bps: int | None = Field(default=None, sa_column=Column(Integer, nullable=True)) + related_trade_id: int | None = Field(default=None, sa_column=Column(Integer, ForeignKey("trades.id"), nullable=True, unique=True)) + notes: str | None = Field(default=None, sa_column=Column(Text, nullable=True)) + created_at: datetime = Field(sa_column=Column(DateTime(timezone=True), nullable=False)) + + cycle: "Cycles" = Relationship(back_populates="loan_change_events") + trade: Optional["Trades"] = Relationship(back_populates="related_loan_change_event") + + +class CycleDailyAccrual(SQLModel, table=True): + __tablename__ = "cycle_daily_accrual" # type: ignore[attr-defined] + __table_args__ = (UniqueConstraint("cycle_id", "accrual_date", name="uq_cycle_daily_accruals_cycle_date"),) + + id: int | None = Field(default=None, primary_key=True) + cycle_id: int = Field(foreign_key="cycles.id", nullable=False, index=True) + accrual_date: date = Field(sa_column=Column(Date, nullable=False)) + accrual_amount_cents: int = Field(sa_column=Column(Integer, nullable=False)) + created_at: datetime = Field(sa_column=Column(DateTime(timezone=True), nullable=False)) + + cycle: "Cycles" = Relationship(back_populates="daily_accruals") + class Exchanges(SQLModel, table=True): __tablename__ = "exchanges" # type: ignore[attr-defined] diff --git a/backend/trading_journal/models_v1.py b/backend/trading_journal/models_v1.py index e8dc281..81d98e1 100644 --- a/backend/trading_journal/models_v1.py +++ b/backend/trading_journal/models_v1.py @@ -1,11 +1,13 @@ from datetime import date, datetime from enum import Enum +from typing import Optional from sqlmodel import ( Column, Date, DateTime, Field, + ForeignKey, Integer, Relationship, SQLModel, @@ -92,8 +94,14 @@ class Trades(SQLModel, table=True): replaced_by_trade_id: int | None = Field(default=None, foreign_key="trades.id", nullable=True) notes: str | None = Field(default=None, sa_column=Column(Text, nullable=True)) cycle_id: int | None = Field(default=None, foreign_key="cycles.id", nullable=True, index=True) + cycle: "Cycles" = Relationship(back_populates="trades") + related_loan_change_event: Optional["CycleLoanChangeEvents"] = Relationship( + back_populates="trade", + sa_relationship_kwargs={"uselist": False}, + ) + class Cycles(SQLModel, table=True): __tablename__ = "cycles" # type: ignore[attr-defined] @@ -113,8 +121,40 @@ class Cycles(SQLModel, table=True): loan_interest_rate_tenth_bps: int | None = Field(default=None, nullable=True) start_date: date = Field(sa_column=Column(Date, nullable=False)) end_date: date | None = Field(default=None, sa_column=Column(Date, nullable=True)) + trades: list["Trades"] = Relationship(back_populates="cycle") + loan_change_events: list["CycleLoanChangeEvents"] = Relationship(back_populates="cycle") + daily_accruals: list["CycleDailyAccrual"] = Relationship(back_populates="cycle") + + +class CycleLoanChangeEvents(SQLModel, table=True): + __tablename__ = "cycle_loan_change_events" # type: ignore[attr-defined] + id: int | None = Field(default=None, primary_key=True) + cycle_id: int = Field(foreign_key="cycles.id", nullable=False, index=True) + effective_date: date = Field(sa_column=Column(Date, nullable=False)) + loan_amount_cents: int | None = Field(default=None, sa_column=Column(Integer, nullable=True)) + loan_interest_rate_tenth_bps: int | None = Field(default=None, sa_column=Column(Integer, nullable=True)) + related_trade_id: int | None = Field(default=None, sa_column=Column(Integer, ForeignKey("trades.id"), nullable=True, unique=True)) + notes: str | None = Field(default=None, sa_column=Column(Text, nullable=True)) + created_at: datetime = Field(sa_column=Column(DateTime(timezone=True), nullable=False)) + + cycle: "Cycles" = Relationship(back_populates="loan_change_events") + trade: Optional["Trades"] = Relationship(back_populates="related_loan_change_event") + + +class CycleDailyAccrual(SQLModel, table=True): + __tablename__ = "cycle_daily_accrual" # type: ignore[attr-defined] + __table_args__ = (UniqueConstraint("cycle_id", "accrual_date", name="uq_cycle_daily_accruals_cycle_date"),) + + id: int | None = Field(default=None, primary_key=True) + cycle_id: int = Field(foreign_key="cycles.id", nullable=False, index=True) + accrual_date: date = Field(sa_column=Column(Date, nullable=False)) + accrual_amount_cents: int = Field(sa_column=Column(Integer, nullable=False)) + created_at: datetime = Field(sa_column=Column(DateTime(timezone=True), nullable=False)) + + cycle: "Cycles" = Relationship(back_populates="daily_accruals") + class Exchanges(SQLModel, table=True): __tablename__ = "exchanges" # type: ignore[attr-defined]