feat: consolidate ledger trade writes into portfolio_ledger
- add ledger consolidation migration and remove ledger_trades usage - return portfolio state after v1 ledger trade writes - deprecate legacy buy/sell endpoints with deprecation headers - add BAT-016, DCY-006, and REQ-036~038 documentation - update repository models and tests for unified ledger entries
This commit is contained in:
+28
-2
@@ -1,9 +1,12 @@
|
||||
"""Portfolio API routes."""
|
||||
|
||||
from typing import NoReturn
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, HTTPException, Query, Request, Response, status
|
||||
|
||||
from app.core.auth import AuthContext, Permission, authorize_request
|
||||
from app.core.config import Settings, get_settings
|
||||
from app.core.errors import FeatureNotReadyError
|
||||
from app.repositories.factory import get_portfolio_repository_from_settings
|
||||
from app.schemas.portfolio import (
|
||||
BuyRequest,
|
||||
@@ -49,6 +52,21 @@ def _ownership_required(settings: Settings) -> bool:
|
||||
return settings.db_enabled
|
||||
|
||||
|
||||
def _raise_with_deprecation_header(exc: Exception) -> NoReturn:
|
||||
if isinstance(exc, HTTPException):
|
||||
headers = dict(exc.headers or {})
|
||||
headers["Deprecation"] = "true"
|
||||
exc.headers = headers
|
||||
raise exc
|
||||
if isinstance(exc, FeatureNotReadyError):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
||||
detail=str(exc) or "Feature not implemented yet",
|
||||
headers={"Deprecation": "true"},
|
||||
) from exc
|
||||
raise exc
|
||||
|
||||
|
||||
def _require_owner_context(auth_context: AuthContext) -> str:
|
||||
if auth_context.user_id is None:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Ownership context is required")
|
||||
@@ -155,38 +173,46 @@ def update_portfolio(
|
||||
_close_repository(repository)
|
||||
|
||||
|
||||
@router.post("/{portfolio_id}/buy", response_model=PortfolioDetailResponse)
|
||||
@router.post("/{portfolio_id}/buy", response_model=PortfolioDetailResponse, deprecated=True)
|
||||
def buy_portfolio_asset(
|
||||
portfolio_id: str,
|
||||
payload: BuyRequest,
|
||||
request: Request,
|
||||
response: Response,
|
||||
*,
|
||||
settings: Settings = Depends(get_settings),
|
||||
) -> PortfolioDetailResponse:
|
||||
response.headers["Deprecation"] = "true"
|
||||
auth_context = authorize_request(Permission.TRADE_WRITE, request, settings)
|
||||
service, repository = _get_service(settings)
|
||||
try:
|
||||
if _ownership_required(settings):
|
||||
_ensure_portfolio_owned(repository, portfolio_id, auth_context)
|
||||
return service.buy(portfolio_id, payload)
|
||||
except Exception as exc:
|
||||
_raise_with_deprecation_header(exc)
|
||||
finally:
|
||||
_close_repository(repository)
|
||||
|
||||
|
||||
@router.post("/{portfolio_id}/sell", response_model=PortfolioDetailResponse)
|
||||
@router.post("/{portfolio_id}/sell", response_model=PortfolioDetailResponse, deprecated=True)
|
||||
def sell_portfolio_asset(
|
||||
portfolio_id: str,
|
||||
payload: SellRequest,
|
||||
request: Request,
|
||||
response: Response,
|
||||
*,
|
||||
settings: Settings = Depends(get_settings),
|
||||
) -> PortfolioDetailResponse:
|
||||
response.headers["Deprecation"] = "true"
|
||||
auth_context = authorize_request(Permission.TRADE_WRITE, request, settings)
|
||||
service, repository = _get_service(settings)
|
||||
try:
|
||||
if _ownership_required(settings):
|
||||
_ensure_portfolio_owned(repository, portfolio_id, auth_context)
|
||||
return service.sell(portfolio_id, payload)
|
||||
except Exception as exc:
|
||||
_raise_with_deprecation_header(exc)
|
||||
finally:
|
||||
_close_repository(repository)
|
||||
|
||||
|
||||
@@ -583,5 +583,7 @@ def record_ledger_trade_v1(
|
||||
tax_amount=trade.tax_amount,
|
||||
executed_at=trade.executed_at,
|
||||
recorded_at=trade.recorded_at,
|
||||
cash_balance_after=trade.cash_balance_after,
|
||||
asset_quantity_after=trade.asset_quantity_after,
|
||||
status=trade_status,
|
||||
)
|
||||
|
||||
+19
-20
@@ -126,18 +126,31 @@ class Portfolio:
|
||||
@dataclass
|
||||
class LedgerEntry:
|
||||
portfolio_id: str
|
||||
recorded_at: datetime
|
||||
action: str
|
||||
price: float
|
||||
quantity: float
|
||||
cash_balance_after: float
|
||||
asset_quantity_after: float
|
||||
total_value_after: float
|
||||
price: float
|
||||
ledger_entry_id: int = 0
|
||||
order_id: str | None = None
|
||||
execution_id: str | None = None
|
||||
side: str | None = None
|
||||
fee_amount: float = 0.0
|
||||
tax_amount: float = 0.0
|
||||
executed_at: datetime | None = None
|
||||
recorded_at: datetime | None = None
|
||||
cash_balance_after: float = 0.0
|
||||
asset_quantity_after: float = 0.0
|
||||
total_value_after: float = 0.0
|
||||
profit_buffer_after: float = 0.0
|
||||
strategy_set_id: str = "legacy"
|
||||
note: str = ""
|
||||
action: str | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.side is None and self.action:
|
||||
self.side = str(self.action).lower()
|
||||
if self.action is None and self.side:
|
||||
self.action = str(self.side).upper()
|
||||
if self.executed_at is None:
|
||||
self.executed_at = self.recorded_at
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -170,17 +183,3 @@ class BrokerCredential:
|
||||
credential_version: int = 1
|
||||
last_rotated_at: datetime | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class LedgerTrade:
|
||||
ledger_entry_id: int
|
||||
portfolio_id: str
|
||||
order_id: str
|
||||
execution_id: str
|
||||
side: str
|
||||
quantity: float
|
||||
price: float
|
||||
fee_amount: float
|
||||
tax_amount: float
|
||||
executed_at: datetime
|
||||
recorded_at: datetime | None = None
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from datetime import datetime
|
||||
from typing import Protocol
|
||||
|
||||
from app.domain.models import Asset, AuthToken, BrokerCredential, Daemon, LedgerEntry, LedgerTrade, Portfolio, PortfolioComment, PortfolioTag, StrategySet, Tag, TokenPrincipal, User
|
||||
from app.domain.models import Asset, AuthToken, BrokerCredential, Daemon, LedgerEntry, Portfolio, PortfolioComment, PortfolioTag, StrategySet, Tag, TokenPrincipal, User
|
||||
|
||||
|
||||
class PortfolioRepository(Protocol):
|
||||
@@ -262,7 +262,7 @@ class PortfolioRepository(Protocol):
|
||||
executed_at: datetime,
|
||||
fee_amount: float = 0.0,
|
||||
tax_amount: float = 0.0,
|
||||
) -> tuple[LedgerTrade, bool]: ...
|
||||
) -> tuple[LedgerEntry, bool]: ...
|
||||
|
||||
def reset(self, portfolio_id: str) -> Portfolio: ...
|
||||
|
||||
|
||||
+216
-71
@@ -261,28 +261,79 @@ class PostgresPortfolioRepository:
|
||||
cur.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS portfolio_ledger (
|
||||
ledger_entry_id BIGINT GENERATED BY DEFAULT AS IDENTITY,
|
||||
portfolio_id TEXT NOT NULL,
|
||||
recorded_at TIMESTAMPTZ NOT NULL,
|
||||
action TEXT NOT NULL,
|
||||
price DOUBLE PRECISION NOT NULL,
|
||||
quantity DOUBLE PRECISION NOT NULL,
|
||||
fee_amount DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
tax_amount DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
cash_balance_after DOUBLE PRECISION NOT NULL,
|
||||
asset_quantity_after DOUBLE PRECISION NOT NULL,
|
||||
total_value_after DOUBLE PRECISION NOT NULL,
|
||||
profit_buffer_after DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
recorded_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
order_id TEXT NULL,
|
||||
execution_id TEXT NULL,
|
||||
side TEXT NOT NULL,
|
||||
price NUMERIC(20,8) NOT NULL,
|
||||
quantity NUMERIC(20,8) NOT NULL,
|
||||
fee_amount NUMERIC(20,8) NOT NULL DEFAULT 0,
|
||||
tax_amount NUMERIC(20,8) NOT NULL DEFAULT 0,
|
||||
cash_balance_after NUMERIC(20,8) NOT NULL,
|
||||
asset_quantity_after NUMERIC(20,8) NOT NULL,
|
||||
total_value_after NUMERIC(20,8) NOT NULL,
|
||||
profit_buffer_after NUMERIC(20,8) NOT NULL DEFAULT 0,
|
||||
strategy_set_id TEXT NOT NULL DEFAULT 'legacy',
|
||||
note TEXT NOT NULL DEFAULT ''
|
||||
note TEXT NOT NULL DEFAULT '',
|
||||
executed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
)
|
||||
"""
|
||||
)
|
||||
cur.execute("ALTER TABLE portfolio_ledger ADD COLUMN IF NOT EXISTS ledger_entry_id BIGINT GENERATED BY DEFAULT AS IDENTITY")
|
||||
cur.execute("ALTER TABLE portfolio_ledger ADD COLUMN IF NOT EXISTS order_id TEXT")
|
||||
cur.execute("ALTER TABLE portfolio_ledger ADD COLUMN IF NOT EXISTS execution_id TEXT")
|
||||
cur.execute("ALTER TABLE portfolio_ledger ADD COLUMN IF NOT EXISTS side TEXT")
|
||||
cur.execute("ALTER TABLE portfolio_ledger ADD COLUMN IF NOT EXISTS executed_at TIMESTAMPTZ")
|
||||
cur.execute(
|
||||
"""
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'portfolio_ledger'
|
||||
AND column_name = 'action'
|
||||
) THEN
|
||||
UPDATE portfolio_ledger SET side = lower(action) WHERE side IS NULL;
|
||||
END IF;
|
||||
END $$;
|
||||
"""
|
||||
)
|
||||
cur.execute("UPDATE portfolio_ledger SET executed_at = recorded_at WHERE executed_at IS NULL")
|
||||
cur.execute("UPDATE portfolio_ledger SET side = 'manual' WHERE side IS NULL")
|
||||
cur.execute("ALTER TABLE portfolio_ledger ALTER COLUMN side SET NOT NULL")
|
||||
cur.execute("ALTER TABLE portfolio_ledger ALTER COLUMN executed_at SET NOT NULL")
|
||||
for column in (
|
||||
"price",
|
||||
"quantity",
|
||||
"fee_amount",
|
||||
"tax_amount",
|
||||
"cash_balance_after",
|
||||
"asset_quantity_after",
|
||||
"total_value_after",
|
||||
"profit_buffer_after",
|
||||
):
|
||||
cur.execute(
|
||||
f"ALTER TABLE portfolio_ledger ALTER COLUMN {column} TYPE NUMERIC(20,8) USING {column}::numeric(20,8)"
|
||||
)
|
||||
cur.execute("ALTER TABLE portfolio_ledger DROP COLUMN IF EXISTS action")
|
||||
cur.execute("DROP TABLE IF EXISTS ledger_trades")
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
CREATE INDEX IF NOT EXISTS idx_portfolio_ledger_portfolio_time
|
||||
ON portfolio_ledger (portfolio_id, recorded_at DESC)
|
||||
"""
|
||||
)
|
||||
cur.execute(
|
||||
"""
|
||||
CREATE INDEX IF NOT EXISTS idx_portfolio_ledger_portfolio_executed
|
||||
ON portfolio_ledger (portfolio_id, executed_at DESC)
|
||||
"""
|
||||
)
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
CREATE INDEX IF NOT EXISTS idx_portfolios_broker_status_updated
|
||||
@@ -506,15 +557,19 @@ class PostgresPortfolioRepository:
|
||||
return portfolios
|
||||
|
||||
@staticmethod
|
||||
def _row_to_ledger_entry(row: Any) -> LedgerEntry:
|
||||
def _row_to_ledger_entry_legacy(row: Any) -> LedgerEntry:
|
||||
return LedgerEntry(
|
||||
ledger_entry_id=0,
|
||||
portfolio_id=row[0],
|
||||
recorded_at=row[1],
|
||||
action=row[2],
|
||||
price=float(row[3]),
|
||||
order_id=None,
|
||||
execution_id=None,
|
||||
side=row[2],
|
||||
quantity=float(row[4]),
|
||||
price=float(row[3]),
|
||||
fee_amount=float(row[5]),
|
||||
tax_amount=float(row[6]),
|
||||
executed_at=row[1],
|
||||
recorded_at=row[1],
|
||||
cash_balance_after=float(row[7]),
|
||||
asset_quantity_after=float(row[8]),
|
||||
total_value_after=float(row[9]),
|
||||
@@ -1223,15 +1278,15 @@ class PostgresPortfolioRepository:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO portfolio_ledger (
|
||||
portfolio_id, recorded_at, action, price, quantity, fee_amount,
|
||||
portfolio_id, recorded_at, side, price, quantity, fee_amount,
|
||||
tax_amount, cash_balance_after, asset_quantity_after,
|
||||
total_value_after, profit_buffer_after, strategy_set_id, note
|
||||
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
total_value_after, profit_buffer_after, strategy_set_id, note, executed_at
|
||||
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
""",
|
||||
(
|
||||
portfolio.portfolio_id,
|
||||
trade_time,
|
||||
action,
|
||||
str(action).lower(),
|
||||
float(trade_price),
|
||||
float(quantity),
|
||||
float(fee_amount),
|
||||
@@ -1242,6 +1297,7 @@ class PostgresPortfolioRepository:
|
||||
float(portfolio.profit_buffer),
|
||||
strategy_set_id,
|
||||
note,
|
||||
trade_time,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1251,12 +1307,16 @@ class PostgresPortfolioRepository:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT
|
||||
portfolio_id, recorded_at, action, price, quantity, fee_amount,
|
||||
tax_amount, cash_balance_after, asset_quantity_after,
|
||||
total_value_after, profit_buffer_after, strategy_set_id, note
|
||||
ledger_entry_id, portfolio_id, order_id, execution_id, side,
|
||||
quantity::float8, price::float8,
|
||||
fee_amount::float8, tax_amount::float8,
|
||||
executed_at, recorded_at,
|
||||
cash_balance_after::float8, asset_quantity_after::float8,
|
||||
total_value_after::float8, profit_buffer_after::float8,
|
||||
strategy_set_id, note
|
||||
FROM portfolio_ledger
|
||||
WHERE portfolio_id = %s
|
||||
ORDER BY recorded_at DESC
|
||||
ORDER BY recorded_at DESC, ledger_entry_id DESC
|
||||
LIMIT %s
|
||||
""",
|
||||
(portfolio_id, int(limit)),
|
||||
@@ -1276,49 +1336,128 @@ class PostgresPortfolioRepository:
|
||||
executed_at,
|
||||
fee_amount: float = 0.0,
|
||||
tax_amount: float = 0.0,
|
||||
) -> tuple:
|
||||
) -> tuple[LedgerEntry, bool]:
|
||||
self.ensure_schema()
|
||||
with self._connect().cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO ledger_trades
|
||||
(portfolio_id, order_id, execution_id, side, quantity, price,
|
||||
fee_amount, tax_amount, executed_at)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
ON CONFLICT ON CONSTRAINT uq_ledger_trades_idempotency DO NOTHING
|
||||
RETURNING
|
||||
ledger_entry_id, portfolio_id, order_id, execution_id, side,
|
||||
quantity::float8, price::float8,
|
||||
fee_amount::float8, tax_amount::float8,
|
||||
executed_at, recorded_at
|
||||
""",
|
||||
(portfolio_id, order_id, execution_id, side, quantity, price,
|
||||
fee_amount, tax_amount, executed_at),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if row is not None:
|
||||
return self._row_to_ledger_trade(row), True
|
||||
# Conflict: fetch existing record
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT
|
||||
ledger_entry_id, portfolio_id, order_id, execution_id, side,
|
||||
quantity::float8, price::float8,
|
||||
fee_amount::float8, tax_amount::float8,
|
||||
executed_at, recorded_at
|
||||
FROM ledger_trades
|
||||
WHERE portfolio_id = %s AND order_id = %s AND execution_id = %s
|
||||
""",
|
||||
(portfolio_id, order_id, execution_id),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
return self._row_to_ledger_trade(row), False
|
||||
side_normalized = str(side).lower()
|
||||
if side_normalized not in {"buy", "sell"}:
|
||||
raise ValueError("side는 buy 또는 sell 이어야 합니다")
|
||||
|
||||
with self._connect() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT
|
||||
portfolio_id, cash_balance, asset_quantity, asset_price, profit_buffer,
|
||||
active_strategy_id
|
||||
FROM portfolios
|
||||
WHERE portfolio_id = %s
|
||||
FOR UPDATE
|
||||
""",
|
||||
(portfolio_id,),
|
||||
)
|
||||
portfolio_row = cur.fetchone()
|
||||
if portfolio_row is None:
|
||||
raise ValueError("포트폴리오를 찾을 수 없습니다")
|
||||
|
||||
cash_balance = float(portfolio_row[1])
|
||||
asset_quantity = float(portfolio_row[2])
|
||||
price_float = float(price)
|
||||
qty_float = float(quantity)
|
||||
fee_float = float(fee_amount)
|
||||
tax_float = float(tax_amount)
|
||||
|
||||
if side_normalized == "buy":
|
||||
cash_balance_after = cash_balance - (qty_float * price_float + fee_float + tax_float)
|
||||
asset_quantity_after = asset_quantity + qty_float
|
||||
else:
|
||||
cash_balance_after = cash_balance + (qty_float * price_float - fee_float - tax_float)
|
||||
asset_quantity_after = asset_quantity - qty_float
|
||||
|
||||
total_value_after = cash_balance_after + asset_quantity_after * price_float
|
||||
strategy_set_id = portfolio_row[5] or "manual"
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO portfolio_ledger (
|
||||
portfolio_id, recorded_at, ledger_entry_id, order_id, execution_id,
|
||||
side, price, quantity, fee_amount, tax_amount,
|
||||
cash_balance_after, asset_quantity_after, total_value_after,
|
||||
profit_buffer_after, strategy_set_id, note, executed_at
|
||||
) VALUES (
|
||||
%s, NOW(), DEFAULT, %s, %s,
|
||||
%s, %s, %s, %s, %s,
|
||||
%s, %s, %s,
|
||||
%s, %s, %s, %s
|
||||
)
|
||||
ON CONFLICT (portfolio_id, order_id, execution_id) WHERE order_id IS NOT NULL
|
||||
DO NOTHING
|
||||
RETURNING
|
||||
ledger_entry_id, portfolio_id, order_id, execution_id, side,
|
||||
quantity::float8, price::float8,
|
||||
fee_amount::float8, tax_amount::float8,
|
||||
executed_at, recorded_at,
|
||||
cash_balance_after::float8, asset_quantity_after::float8,
|
||||
total_value_after::float8, profit_buffer_after::float8,
|
||||
strategy_set_id, note
|
||||
""",
|
||||
(
|
||||
portfolio_id,
|
||||
order_id,
|
||||
execution_id,
|
||||
side_normalized,
|
||||
price_float,
|
||||
qty_float,
|
||||
fee_float,
|
||||
tax_float,
|
||||
cash_balance_after,
|
||||
asset_quantity_after,
|
||||
total_value_after,
|
||||
float(portfolio_row[4]),
|
||||
strategy_set_id,
|
||||
"",
|
||||
executed_at,
|
||||
),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if row is None:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT
|
||||
ledger_entry_id, portfolio_id, order_id, execution_id, side,
|
||||
quantity::float8, price::float8,
|
||||
fee_amount::float8, tax_amount::float8,
|
||||
executed_at, recorded_at,
|
||||
cash_balance_after::float8, asset_quantity_after::float8,
|
||||
total_value_after::float8, profit_buffer_after::float8,
|
||||
strategy_set_id, note
|
||||
FROM portfolio_ledger
|
||||
WHERE portfolio_id = %s AND order_id = %s AND execution_id = %s
|
||||
""",
|
||||
(portfolio_id, order_id, execution_id),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
return self._row_to_ledger_entry(row), False
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE portfolios
|
||||
SET cash_balance = %s,
|
||||
asset_quantity = %s,
|
||||
asset_price = %s,
|
||||
updated_at = NOW(),
|
||||
last_trade_at = %s
|
||||
WHERE portfolio_id = %s
|
||||
""",
|
||||
(cash_balance_after, asset_quantity_after, price_float, executed_at, portfolio_id),
|
||||
)
|
||||
return self._row_to_ledger_entry(row), True
|
||||
|
||||
@staticmethod
|
||||
def _row_to_ledger_trade(row) -> "LedgerTrade":
|
||||
from app.domain.models import LedgerTrade
|
||||
return LedgerTrade(
|
||||
ledger_entry_id=row[0],
|
||||
def _row_to_ledger_entry(row: Any) -> LedgerEntry:
|
||||
if row is None:
|
||||
raise ValueError("원장 항목을 찾을 수 없습니다")
|
||||
return LedgerEntry(
|
||||
ledger_entry_id=int(row[0]),
|
||||
portfolio_id=row[1],
|
||||
order_id=row[2],
|
||||
execution_id=row[3],
|
||||
@@ -1329,6 +1468,12 @@ class PostgresPortfolioRepository:
|
||||
tax_amount=float(row[8]),
|
||||
executed_at=row[9],
|
||||
recorded_at=row[10],
|
||||
cash_balance_after=float(row[11]),
|
||||
asset_quantity_after=float(row[12]),
|
||||
total_value_after=float(row[13]),
|
||||
profit_buffer_after=float(row[14]),
|
||||
strategy_set_id=row[15] or "legacy",
|
||||
note=row[16] or "",
|
||||
)
|
||||
|
||||
def reset(self, portfolio_id: str) -> Portfolio:
|
||||
@@ -1386,14 +1531,14 @@ class PostgresPortfolioRepository:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO portfolio_ledger (
|
||||
portfolio_id, recorded_at, action, price, quantity, fee_amount,
|
||||
portfolio_id, recorded_at, side, price, quantity, fee_amount,
|
||||
tax_amount, cash_balance_after, asset_quantity_after,
|
||||
total_value_after, profit_buffer_after, strategy_set_id, note
|
||||
) VALUES (%s, NOW(), %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
total_value_after, profit_buffer_after, strategy_set_id, note, executed_at
|
||||
) VALUES (%s, NOW(), %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW())
|
||||
""",
|
||||
(
|
||||
portfolio_id,
|
||||
normalized,
|
||||
normalized.lower(),
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
@@ -1450,14 +1595,14 @@ class PostgresPortfolioRepository:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO portfolio_ledger (
|
||||
portfolio_id, recorded_at, action, price, quantity, fee_amount,
|
||||
portfolio_id, recorded_at, side, price, quantity, fee_amount,
|
||||
tax_amount, cash_balance_after, asset_quantity_after,
|
||||
total_value_after, profit_buffer_after, strategy_set_id, note
|
||||
) VALUES (%s, NOW(), %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
total_value_after, profit_buffer_after, strategy_set_id, note, executed_at
|
||||
) VALUES (%s, NOW(), %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW())
|
||||
""",
|
||||
(
|
||||
portfolio_id,
|
||||
ledger_action,
|
||||
ledger_action.lower(),
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
|
||||
+4
-2
@@ -122,8 +122,8 @@ class LedgerTradeRequest(BaseModel):
|
||||
class LedgerTradeResponse(BaseModel):
|
||||
ledger_entry_id: int
|
||||
portfolio_id: str
|
||||
order_id: str
|
||||
execution_id: str
|
||||
order_id: str | None
|
||||
execution_id: str | None
|
||||
side: str
|
||||
quantity: float
|
||||
price: float
|
||||
@@ -131,6 +131,8 @@ class LedgerTradeResponse(BaseModel):
|
||||
tax_amount: float
|
||||
executed_at: datetime
|
||||
recorded_at: datetime | None = None
|
||||
cash_balance_after: float = 0.0
|
||||
asset_quantity_after: float = 0.0
|
||||
status: Literal["new", "duplicate"] = "new"
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user