From 76cc967c42c38ce8ec852e8ca4c927427066cfd8 Mon Sep 17 00:00:00 2001 From: Tianyu Liu Date: Fri, 19 Sep 2025 23:04:17 +0200 Subject: [PATCH] cycle and trade add exchange field --- backend/tests/test_crud.py | 18 ++++++++++++++++++ backend/trading_journal/crud.py | 5 +++++ backend/trading_journal/dto.py | 21 +++++++++++++++++++++ backend/trading_journal/models.py | 2 ++ backend/trading_journal/models_v1.py | 2 ++ 5 files changed, 48 insertions(+) create mode 100644 backend/trading_journal/dto.py diff --git a/backend/tests/test_crud.py b/backend/tests/test_crud.py index e951665..2ba27f7 100644 --- a/backend/tests/test_crud.py +++ b/backend/tests/test_crud.py @@ -50,6 +50,7 @@ def make_cycle(session: Session, user_id: int, friendly_name: str = "Test Cycle" user_id=user_id, friendly_name=friendly_name, symbol="AAPL", + exchange="NASDAQ", underlying_currency=models.UnderlyingCurrency.USD, status=models.CycleStatus.OPEN, start_date=datetime.now(timezone.utc).date(), @@ -65,6 +66,7 @@ def make_trade(session: Session, user_id: int, cycle_id: int, friendly_name: str user_id=user_id, friendly_name=friendly_name, symbol="AAPL", + exchange="NASDAQ", underlying_currency=models.UnderlyingCurrency.USD, trade_type=models.TradeType.LONG_SPOT, trade_strategy=models.TradeStrategy.SPOT, @@ -129,6 +131,7 @@ def test_create_trade_success_with_cycle(session: Session) -> None: "user_id": user_id, "friendly_name": "Test Trade", "symbol": "AAPL", + "exchange": "NASDAQ", "underlying_currency": models.UnderlyingCurrency.USD, "trade_type": models.TradeType.LONG_SPOT, "trade_strategy": models.TradeStrategy.SPOT, @@ -169,6 +172,7 @@ def test_create_trade_with_auto_created_cycle(session: Session) -> None: "user_id": user_id, "friendly_name": "Test Trade with Auto Cycle", "symbol": "AAPL", + "exchange": "NASDAQ", "underlying_currency": models.UnderlyingCurrency.USD, "trade_type": models.TradeType.LONG_SPOT, "trade_strategy": models.TradeStrategy.SPOT, @@ -211,6 +215,7 @@ def test_create_trade_missing_required_fields(session: Session) -> None: "user_id": user_id, "friendly_name": "Incomplete Trade", "symbol": "AAPL", + "exchange": "NASDAQ", "underlying_currency": models.UnderlyingCurrency.USD, "trade_type": models.TradeType.LONG_SPOT, "trade_strategy": models.TradeStrategy.SPOT, @@ -226,6 +231,13 @@ def test_create_trade_missing_required_fields(session: Session) -> None: crud.create_trade(session, trade_data) assert "symbol is required" in str(excinfo.value) + # Missing exchange + trade_data = base_trade_data.copy() + trade_data.pop("exchange", None) + with pytest.raises(ValueError) as excinfo: + crud.create_trade(session, trade_data) + assert "exchange is required" in str(excinfo.value) + # Missing underlying_currency trade_data = base_trade_data.copy() trade_data.pop("underlying_currency", None) @@ -269,6 +281,7 @@ def test_get_trade_by_id(session: Session) -> None: "user_id": user_id, "friendly_name": "Test Trade for Get", "symbol": "AAPL", + "exchange": "NASDAQ", "underlying_currency": models.UnderlyingCurrency.USD, "trade_type": models.TradeType.LONG_SPOT, "trade_strategy": models.TradeStrategy.SPOT, @@ -307,6 +320,7 @@ def test_get_trade_by_user_id_and_friendly_name(session: Session) -> None: "user_id": user_id, "friendly_name": friendly_name, "symbol": "AAPL", + "exchange": "NASDAQ", "underlying_currency": models.UnderlyingCurrency.USD, "trade_type": models.TradeType.LONG_SPOT, "trade_strategy": models.TradeStrategy.SPOT, @@ -333,6 +347,7 @@ def test_get_trades_by_user_id(session: Session) -> None: "user_id": user_id, "friendly_name": "Trade One", "symbol": "AAPL", + "exchange": "NASDAQ", "underlying_currency": models.UnderlyingCurrency.USD, "trade_type": models.TradeType.LONG_SPOT, "trade_strategy": models.TradeStrategy.SPOT, @@ -349,6 +364,7 @@ def test_get_trades_by_user_id(session: Session) -> None: "user_id": user_id, "friendly_name": "Trade Two", "symbol": "GOOGL", + "exchange": "NASDAQ", "underlying_currency": models.UnderlyingCurrency.USD, "trade_type": models.TradeType.SHORT_SPOT, "trade_strategy": models.TradeStrategy.SPOT, @@ -412,6 +428,7 @@ def test_replace_trade(session: Session) -> None: "user_id": user_id, "friendly_name": "Replaced Trade", "symbol": "MSFT", + "exchange": "NASDAQ", "underlying_currency": models.UnderlyingCurrency.USD, "trade_type": models.TradeType.LONG_SPOT, "trade_strategy": models.TradeStrategy.SPOT, @@ -452,6 +469,7 @@ def test_create_cycle(session: Session) -> None: "user_id": user_id, "friendly_name": "My First Cycle", "symbol": "GOOGL", + "exchange": "NASDAQ", "underlying_currency": models.UnderlyingCurrency.USD, "status": models.CycleStatus.OPEN, "start_date": datetime.now(timezone.utc).date(), diff --git a/backend/trading_journal/crud.py b/backend/trading_journal/crud.py index 0e83bed..37973ce 100644 --- a/backend/trading_journal/crud.py +++ b/backend/trading_journal/crud.py @@ -37,6 +37,8 @@ def create_trade(session: Session, trade_data: Mapping) -> models.Trades: payload = {k: v for k, v in data.items() if k in allowed} if "symbol" not in payload: raise ValueError("symbol is required") + if "exchange" not in payload: + raise ValueError("exchange is required") if "underlying_currency" not in payload: raise ValueError("underlying_currency is required") payload["underlying_currency"] = _check_enum(models.UnderlyingCurrency, payload["underlying_currency"], "underlying_currency") @@ -74,6 +76,7 @@ def create_trade(session: Session, trade_data: Mapping) -> models.Trades: c_payload = { "user_id": user_id, "symbol": payload["symbol"], + "exchange": payload["exchange"], "underlying_currency": payload["underlying_currency"], "friendly_name": "Auto-created Cycle by trade " + payload.get("friendly_name", ""), "status": models.CycleStatus.OPEN, @@ -184,6 +187,8 @@ def create_cycle(session: Session, cycle_data: Mapping) -> models.Cycles: raise ValueError("user_id is required") if "symbol" not in payload: raise ValueError("symbol is required") + if "exchange" not in payload: + raise ValueError("exchange is required") if "underlying_currency" not in payload: raise ValueError("underlying_currency is required") payload["underlying_currency"] = _check_enum(models.UnderlyingCurrency, payload["underlying_currency"], "underlying_currency") diff --git a/backend/trading_journal/dto.py b/backend/trading_journal/dto.py new file mode 100644 index 0000000..b372474 --- /dev/null +++ b/backend/trading_journal/dto.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from sqlmodel import SQLModel + +if TYPE_CHECKING: + from datetime import date, datetime + + from trading_journal.models import TradeStrategy, TradeType, UnderlyingCurrency + + +class TradeBase(SQLModel): + user_id: int + friendly_name: str | None + symbol: str + underlying_currency: UnderlyingCurrency + trade_type: TradeType + trade_strategy: TradeStrategy + trade_date: date + trade_time_utc: datetime diff --git a/backend/trading_journal/models.py b/backend/trading_journal/models.py index 259ea3d..6659361 100644 --- a/backend/trading_journal/models.py +++ b/backend/trading_journal/models.py @@ -72,6 +72,7 @@ class Trades(SQLModel, table=True): # allow null while user may omit friendly_name; uniqueness enforced per-user by constraint friendly_name: str | None = Field(default=None, sa_column=Column(Text, nullable=True)) symbol: str = Field(sa_column=Column(Text, nullable=False)) + exchange: str = Field(sa_column=Column(Text, nullable=False)) underlying_currency: UnderlyingCurrency = Field(sa_column=Column(Text, nullable=False)) trade_type: TradeType = Field(sa_column=Column(Text, nullable=False)) trade_strategy: TradeStrategy = Field(sa_column=Column(Text, nullable=False)) @@ -100,6 +101,7 @@ class Cycles(SQLModel, table=True): user_id: int = Field(foreign_key="users.id", nullable=False, index=True) friendly_name: str | None = Field(default=None, sa_column=Column(Text, nullable=True)) symbol: str = Field(sa_column=Column(Text, nullable=False)) + exchange: str = Field(sa_column=Column(Text, nullable=False)) underlying_currency: UnderlyingCurrency = Field(sa_column=Column(Text, nullable=False)) status: CycleStatus = Field(sa_column=Column(Text, nullable=False)) funding_source: FundingSource = Field(sa_column=Column(Text, nullable=True)) diff --git a/backend/trading_journal/models_v1.py b/backend/trading_journal/models_v1.py index 259ea3d..6659361 100644 --- a/backend/trading_journal/models_v1.py +++ b/backend/trading_journal/models_v1.py @@ -72,6 +72,7 @@ class Trades(SQLModel, table=True): # allow null while user may omit friendly_name; uniqueness enforced per-user by constraint friendly_name: str | None = Field(default=None, sa_column=Column(Text, nullable=True)) symbol: str = Field(sa_column=Column(Text, nullable=False)) + exchange: str = Field(sa_column=Column(Text, nullable=False)) underlying_currency: UnderlyingCurrency = Field(sa_column=Column(Text, nullable=False)) trade_type: TradeType = Field(sa_column=Column(Text, nullable=False)) trade_strategy: TradeStrategy = Field(sa_column=Column(Text, nullable=False)) @@ -100,6 +101,7 @@ class Cycles(SQLModel, table=True): user_id: int = Field(foreign_key="users.id", nullable=False, index=True) friendly_name: str | None = Field(default=None, sa_column=Column(Text, nullable=True)) symbol: str = Field(sa_column=Column(Text, nullable=False)) + exchange: str = Field(sa_column=Column(Text, nullable=False)) underlying_currency: UnderlyingCurrency = Field(sa_column=Column(Text, nullable=False)) status: CycleStatus = Field(sa_column=Column(Text, nullable=False)) funding_source: FundingSource = Field(sa_column=Column(Text, nullable=True))