Preserve ledger venue provenance (#39)
* feat: preserve ledger venue provenance * fix: satisfy ledger test lint ratchet * fix: advance image migration inventory
This commit is contained in:
@@ -27,6 +27,7 @@ from app.domain.models import CorporateAction, Portfolio, PortfolioComment
|
||||
from app.domain.pnl import calculate_current_wac, calculate_realized_pnl
|
||||
from app.domain.services import PORTFOLIO_NOT_FOUND_MESSAGE
|
||||
from app.repositories.factory import get_portfolio_repository_from_settings
|
||||
from app.repositories.portfolio_repository import LedgerTradeConflictError
|
||||
from app.schemas.corporate_actions import (
|
||||
CorporateActionRequest,
|
||||
CorporateActionResponse,
|
||||
@@ -604,7 +605,13 @@ def _require_service_token(auth: AuthContext = Depends(require_trade_write)) ->
|
||||
return auth
|
||||
|
||||
|
||||
def _payloads_match(existing, body: LedgerTradeRequest) -> bool:
|
||||
def _economic_payloads_match(existing, body: LedgerTradeRequest) -> bool:
|
||||
"""Defence in depth for non-Postgres repository implementations.
|
||||
|
||||
The Postgres repository performs this comparison under the portfolio and
|
||||
ledger row locks; this check preserves the public contract for test or
|
||||
alternate repository implementations.
|
||||
"""
|
||||
return (
|
||||
existing.side == body.side
|
||||
and abs(existing.quantity - float(body.quantity)) < 1e-9
|
||||
@@ -659,14 +666,21 @@ def record_ledger_trade_v1(
|
||||
executed_at=body.executed_at,
|
||||
fee_amount=float(body.fee_amount),
|
||||
tax_amount=float(body.tax_amount),
|
||||
execution_venue=body.execution_venue,
|
||||
order_venue=body.order_venue,
|
||||
)
|
||||
except LedgerTradeConflictError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="Duplicate key with different payload",
|
||||
) from None
|
||||
except HTTPException:
|
||||
raise
|
||||
finally:
|
||||
_close_repo(repo)
|
||||
|
||||
if not created:
|
||||
if not _payloads_match(trade, body):
|
||||
if not _economic_payloads_match(trade, body):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="Duplicate key with different payload",
|
||||
@@ -687,6 +701,8 @@ def record_ledger_trade_v1(
|
||||
fee_amount=trade.fee_amount,
|
||||
tax_amount=trade.tax_amount,
|
||||
executed_at=trade.executed_at,
|
||||
execution_venue=trade.execution_venue,
|
||||
order_venue=trade.order_venue,
|
||||
recorded_at=trade.recorded_at,
|
||||
cash_balance_after=trade.cash_balance_after,
|
||||
asset_quantity_after=trade.asset_quantity_after,
|
||||
@@ -827,6 +843,7 @@ def _ledger_entry_to_history_item(entry) -> LedgerHistoryItem | DividendLedgerHi
|
||||
total_cost_basis=event["total_cost_basis"], cash_balance=event["cash_balance"],
|
||||
profit_buffer=event["profit_buffer"], evidence_ref=event["evidence_ref"],
|
||||
actor_user_id=event.get("actor_user_id"), recorded_at=entry.recorded_at,
|
||||
execution_venue=entry.execution_venue, order_venue=entry.order_venue,
|
||||
)
|
||||
action = (entry.action or entry.side or "").upper()
|
||||
if action == "DIVIDEND":
|
||||
@@ -853,6 +870,8 @@ def _ledger_entry_to_history_item(entry) -> LedgerHistoryItem | DividendLedgerHi
|
||||
total_value_after=entry.total_value_after,
|
||||
target_ledger_entry_id=entry.target_ledger_entry_id,
|
||||
confirmation_evidence=entry.confirmation_evidence,
|
||||
execution_venue=entry.execution_venue,
|
||||
order_venue=entry.order_venue,
|
||||
)
|
||||
return LedgerHistoryItem(
|
||||
ledger_entry_id=entry.ledger_entry_id,
|
||||
@@ -875,6 +894,8 @@ def _ledger_entry_to_history_item(entry) -> LedgerHistoryItem | DividendLedgerHi
|
||||
total_value_after=entry.total_value_after,
|
||||
target_ledger_entry_id=entry.target_ledger_entry_id,
|
||||
confirmation_evidence=entry.confirmation_evidence,
|
||||
execution_venue=entry.execution_venue,
|
||||
order_venue=entry.order_venue,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
"""Add nullable execution and order venue provenance to portfolio ledger.
|
||||
|
||||
Revision ID: 20260919_0032
|
||||
Revises: 20260913_0031
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision = "20260919_0032"
|
||||
down_revision = "20260913_0031"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute("ALTER TABLE portfolio_ledger ADD COLUMN IF NOT EXISTS execution_venue TEXT")
|
||||
op.execute("ALTER TABLE portfolio_ledger ADD COLUMN IF NOT EXISTS order_venue TEXT")
|
||||
op.execute(
|
||||
"""
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'ck_portfolio_ledger_execution_venue'
|
||||
AND conrelid = 'portfolio_ledger'::regclass
|
||||
) THEN
|
||||
ALTER TABLE portfolio_ledger
|
||||
ADD CONSTRAINT ck_portfolio_ledger_execution_venue
|
||||
CHECK (execution_venue IN ('KRX', 'NXT'));
|
||||
END IF;
|
||||
END $$
|
||||
"""
|
||||
)
|
||||
op.execute(
|
||||
"""
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'ck_portfolio_ledger_order_venue'
|
||||
AND conrelid = 'portfolio_ledger'::regclass
|
||||
) THEN
|
||||
ALTER TABLE portfolio_ledger
|
||||
ADD CONSTRAINT ck_portfolio_ledger_order_venue
|
||||
CHECK (order_venue IN ('KRX', 'NXT', 'SOR'));
|
||||
END IF;
|
||||
END $$
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_constraint("ck_portfolio_ledger_order_venue", "portfolio_ledger", type_="check")
|
||||
op.drop_constraint("ck_portfolio_ledger_execution_venue", "portfolio_ledger", type_="check")
|
||||
op.drop_column("portfolio_ledger", "order_venue")
|
||||
op.drop_column("portfolio_ledger", "execution_venue")
|
||||
@@ -6,7 +6,7 @@ from dataclasses import dataclass, field
|
||||
from datetime import date, datetime
|
||||
from decimal import Decimal
|
||||
from enum import Enum
|
||||
from typing import Any, Protocol, runtime_checkable
|
||||
from typing import Any, Literal, Protocol, runtime_checkable
|
||||
|
||||
|
||||
class MarketCode(str, Enum):
|
||||
@@ -181,6 +181,8 @@ class LedgerEntry:
|
||||
target_ledger_entry_id: int | None = None
|
||||
confirmation_evidence: str | None = None
|
||||
actor_user_id: str | None = None
|
||||
execution_venue: Literal["KRX", "NXT"] | None = None
|
||||
order_venue: Literal["KRX", "NXT", "SOR"] | None = None
|
||||
corporate_action: dict[str, Any] | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
|
||||
@@ -9,6 +9,10 @@ from typing import Protocol
|
||||
from app.domain.models import Asset, AuthToken, BrokerCredential, CorporateAction, Daemon, LedgerEntry, Portfolio, PortfolioComment, PortfolioTag, StrategySet, Tag, TokenPrincipal, User
|
||||
|
||||
|
||||
class LedgerTradeConflictError(ValueError):
|
||||
"""A duplicate broker-ledger key disagrees with its persisted payload."""
|
||||
|
||||
|
||||
class PortfolioRepository(Protocol):
|
||||
def ping(self) -> None: ...
|
||||
|
||||
@@ -324,6 +328,8 @@ class PortfolioRepository(Protocol):
|
||||
executed_at: datetime,
|
||||
fee_amount: float = 0.0,
|
||||
tax_amount: float = 0.0,
|
||||
execution_venue: str | None = None,
|
||||
order_venue: str | None = None,
|
||||
) -> tuple[LedgerEntry, bool]: ...
|
||||
|
||||
def record_manual_ledger(
|
||||
|
||||
+116
-10
@@ -40,6 +40,7 @@ from app.domain.models import (
|
||||
TokenPrincipal,
|
||||
User,
|
||||
)
|
||||
from app.repositories.portfolio_repository import LedgerTradeConflictError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
_LEDGER_NUMERIC_SCALE = 8
|
||||
@@ -382,7 +383,11 @@ class PostgresPortfolioRepository:
|
||||
profit_buffer_after NUMERIC(20,8) NOT NULL DEFAULT 0,
|
||||
strategy_set_id TEXT NOT NULL DEFAULT 'legacy',
|
||||
note TEXT NOT NULL DEFAULT '',
|
||||
executed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
executed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
execution_venue TEXT NULL CONSTRAINT ck_portfolio_ledger_execution_venue
|
||||
CHECK (execution_venue IN ('KRX', 'NXT')),
|
||||
order_venue TEXT NULL CONSTRAINT ck_portfolio_ledger_order_venue
|
||||
CHECK (order_venue IN ('KRX', 'NXT', 'SOR'))
|
||||
)
|
||||
"""
|
||||
)
|
||||
@@ -395,6 +400,33 @@ class PostgresPortfolioRepository:
|
||||
cur.execute("ALTER TABLE portfolio_ledger ADD COLUMN IF NOT EXISTS target_ledger_entry_id BIGINT")
|
||||
cur.execute("ALTER TABLE portfolio_ledger ADD COLUMN IF NOT EXISTS confirmation_evidence TEXT")
|
||||
cur.execute("ALTER TABLE portfolio_ledger ADD COLUMN IF NOT EXISTS actor_user_id TEXT")
|
||||
cur.execute("ALTER TABLE portfolio_ledger ADD COLUMN IF NOT EXISTS execution_venue TEXT")
|
||||
cur.execute("ALTER TABLE portfolio_ledger ADD COLUMN IF NOT EXISTS order_venue TEXT")
|
||||
cur.execute(
|
||||
"""
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'ck_portfolio_ledger_execution_venue'
|
||||
AND conrelid = 'portfolio_ledger'::regclass
|
||||
) THEN
|
||||
ALTER TABLE portfolio_ledger
|
||||
ADD CONSTRAINT ck_portfolio_ledger_execution_venue
|
||||
CHECK (execution_venue IS NULL OR execution_venue IN ('KRX', 'NXT'));
|
||||
END IF;
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'ck_portfolio_ledger_order_venue'
|
||||
AND conrelid = 'portfolio_ledger'::regclass
|
||||
) THEN
|
||||
ALTER TABLE portfolio_ledger
|
||||
ADD CONSTRAINT ck_portfolio_ledger_order_venue
|
||||
CHECK (order_venue IS NULL OR order_venue IN ('KRX', 'NXT', 'SOR'));
|
||||
END IF;
|
||||
END $$;
|
||||
"""
|
||||
)
|
||||
cur.execute(
|
||||
"""
|
||||
DO $$
|
||||
@@ -2018,6 +2050,8 @@ class PostgresPortfolioRepository:
|
||||
executed_at,
|
||||
fee_amount: float = 0.0,
|
||||
tax_amount: float = 0.0,
|
||||
execution_venue: str | None = None,
|
||||
order_venue: str | None = None,
|
||||
) -> tuple[LedgerEntry, bool]:
|
||||
self.ensure_schema()
|
||||
side_normalized = str(side).lower()
|
||||
@@ -2078,16 +2112,70 @@ class PostgresPortfolioRepository:
|
||||
executed_at, recorded_at,
|
||||
cash_balance_after::float8, asset_quantity_after::float8,
|
||||
total_value_after::float8, profit_buffer_after::float8,
|
||||
strategy_set_id, note
|
||||
strategy_set_id, note, source, target_ledger_entry_id,
|
||||
confirmation_evidence, actor_user_id, execution_venue, order_venue
|
||||
FROM portfolio_ledger
|
||||
WHERE portfolio_id = %s AND order_id = %s AND execution_id = %s
|
||||
ORDER BY recorded_at DESC, ledger_entry_id DESC
|
||||
LIMIT 1
|
||||
FOR UPDATE
|
||||
""",
|
||||
(portfolio_id, order_id, execution_id),
|
||||
)
|
||||
existing = cur.fetchone()
|
||||
if existing is not None:
|
||||
economic_payload_matches = (
|
||||
existing[4] == side_normalized
|
||||
and abs(float(existing[5]) - qty_float) < 1e-9
|
||||
and abs(float(existing[6]) - price_float) < 1e-9
|
||||
and abs(float(existing[7]) - fee_float) < 1e-9
|
||||
and abs(float(existing[8]) - tax_float) < 1e-9
|
||||
)
|
||||
if not economic_payload_matches:
|
||||
raise LedgerTradeConflictError("duplicate economic payload conflict")
|
||||
|
||||
existing_execution_venue = existing[21]
|
||||
existing_order_venue = existing[22]
|
||||
if (
|
||||
existing_execution_venue is not None
|
||||
and execution_venue is not None
|
||||
and existing_execution_venue != execution_venue
|
||||
) or (
|
||||
existing_order_venue is not None
|
||||
and order_venue is not None
|
||||
and existing_order_venue != order_venue
|
||||
):
|
||||
raise LedgerTradeConflictError("duplicate venue payload conflict")
|
||||
|
||||
enriched_execution_venue = existing_execution_venue or execution_venue
|
||||
enriched_order_venue = existing_order_venue or order_venue
|
||||
if (
|
||||
enriched_execution_venue != existing_execution_venue
|
||||
or enriched_order_venue != existing_order_venue
|
||||
):
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE portfolio_ledger
|
||||
SET execution_venue = %s, order_venue = %s
|
||||
WHERE ledger_entry_id = %s AND recorded_at = %s
|
||||
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, source, target_ledger_entry_id,
|
||||
confirmation_evidence, actor_user_id, execution_venue, order_venue
|
||||
""",
|
||||
(
|
||||
enriched_execution_venue,
|
||||
enriched_order_venue,
|
||||
existing[0],
|
||||
existing[10],
|
||||
),
|
||||
)
|
||||
existing = cur.fetchone()
|
||||
# Duplicate: do not insert a new row, do not mutate the portfolio.
|
||||
# OQ-4 (b): return the CURRENT authoritative portfolio state so the
|
||||
# caller always syncs to the latest balances, not the first-call snapshot.
|
||||
@@ -2102,12 +2190,13 @@ class PostgresPortfolioRepository:
|
||||
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
|
||||
profit_buffer_after, strategy_set_id, note, executed_at,
|
||||
execution_venue, order_venue
|
||||
) VALUES (
|
||||
%s, NOW(), DEFAULT, %s, %s,
|
||||
%s, %s, %s, %s, %s,
|
||||
%s, %s, %s,
|
||||
%s, %s, %s, %s
|
||||
%s, %s, %s, %s, %s, %s
|
||||
)
|
||||
RETURNING
|
||||
ledger_entry_id, portfolio_id, order_id, execution_id, side,
|
||||
@@ -2116,7 +2205,8 @@ class PostgresPortfolioRepository:
|
||||
executed_at, recorded_at,
|
||||
cash_balance_after::float8, asset_quantity_after::float8,
|
||||
total_value_after::float8, profit_buffer_after::float8,
|
||||
strategy_set_id, note
|
||||
strategy_set_id, note, source, target_ledger_entry_id,
|
||||
confirmation_evidence, actor_user_id, execution_venue, order_venue
|
||||
""",
|
||||
(
|
||||
portfolio_id,
|
||||
@@ -2134,6 +2224,8 @@ class PostgresPortfolioRepository:
|
||||
strategy_set_id,
|
||||
"",
|
||||
executed_at,
|
||||
execution_venue,
|
||||
order_venue,
|
||||
),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
@@ -2182,6 +2274,16 @@ class PostgresPortfolioRepository:
|
||||
target_ledger_entry_id=int(row[18]) if len(row) > 18 and row[18] is not None else None,
|
||||
confirmation_evidence=row[19] if len(row) > 19 else None,
|
||||
actor_user_id=row[20] if len(row) > 20 else None,
|
||||
execution_venue=(
|
||||
row[21]
|
||||
if len(row) > 21 and row[21] in {None, "KRX", "NXT"}
|
||||
else None
|
||||
),
|
||||
order_venue=(
|
||||
row[22]
|
||||
if len(row) > 22 and row[22] in {None, "KRX", "NXT", "SOR"}
|
||||
else None
|
||||
),
|
||||
)
|
||||
|
||||
def reset(self, portfolio_id: str) -> Portfolio:
|
||||
@@ -2492,7 +2594,8 @@ class PostgresPortfolioRepository:
|
||||
SELECT ledger_entry_id, portfolio_id, order_id, execution_id, side, quantity, price,
|
||||
fee_amount, tax_amount, executed_at, recorded_at, cash_balance_after,
|
||||
asset_quantity_after, total_value_after, profit_buffer_after, strategy_set_id,
|
||||
note, source, target_ledger_entry_id, confirmation_evidence, actor_user_id
|
||||
note, source, target_ledger_entry_id, confirmation_evidence, actor_user_id,
|
||||
execution_venue, order_venue
|
||||
FROM portfolio_ledger
|
||||
WHERE portfolio_id = %s AND ledger_entry_id = %s
|
||||
""",
|
||||
@@ -2591,7 +2694,8 @@ class PostgresPortfolioRepository:
|
||||
RETURNING ledger_entry_id, portfolio_id, order_id, execution_id, side, quantity, price,
|
||||
fee_amount, tax_amount, executed_at, recorded_at, cash_balance_after,
|
||||
asset_quantity_after, total_value_after, profit_buffer_after, strategy_set_id,
|
||||
note, source, target_ledger_entry_id, confirmation_evidence, actor_user_id
|
||||
note, source, target_ledger_entry_id, confirmation_evidence, actor_user_id,
|
||||
execution_venue, order_venue
|
||||
""",
|
||||
(
|
||||
portfolio_id,
|
||||
@@ -2623,7 +2727,8 @@ class PostgresPortfolioRepository:
|
||||
SELECT ledger_entry_id, portfolio_id, order_id, execution_id, side, quantity, price,
|
||||
fee_amount, tax_amount, executed_at, recorded_at, cash_balance_after,
|
||||
asset_quantity_after, total_value_after, profit_buffer_after, strategy_set_id,
|
||||
note, source, target_ledger_entry_id, confirmation_evidence, actor_user_id
|
||||
note, source, target_ledger_entry_id, confirmation_evidence, actor_user_id,
|
||||
execution_venue, order_venue
|
||||
FROM portfolio_ledger
|
||||
WHERE portfolio_id = %s AND source = 'manual'
|
||||
ORDER BY recorded_at DESC, ledger_entry_id DESC
|
||||
@@ -2661,6 +2766,7 @@ class PostgresPortfolioRepository:
|
||||
ledger.asset_quantity_after, ledger.total_value_after, ledger.profit_buffer_after,
|
||||
ledger.strategy_set_id, ledger.note, ledger.source, ledger.target_ledger_entry_id,
|
||||
ledger.confirmation_evidence, ledger.actor_user_id,
|
||||
ledger.execution_venue, ledger.order_venue,
|
||||
ca.corporate_action_id, ca.action_type, ca.effective_date,
|
||||
ca.quantity_before, ca.quantity_after, ca.average_price_before,
|
||||
ca.average_price_after, ca.total_cost_basis, ca.cash_balance,
|
||||
@@ -2676,8 +2782,8 @@ class PostgresPortfolioRepository:
|
||||
result = []
|
||||
for row in cur.fetchall():
|
||||
entry = self._row_to_ledger_entry(row)
|
||||
if row[21] is not None:
|
||||
entry.corporate_action = {"corporate_action_id": str(row[21]), "action_type": row[22], "effective_date": row[23], "quantity_before": Decimal(str(row[24])), "quantity_after": Decimal(str(row[25])), "average_price_before": Decimal(str(row[26])), "average_price_after": Decimal(str(row[27])), "total_cost_basis": Decimal(str(row[28])), "cash_balance": Decimal(str(row[29])), "profit_buffer": Decimal(str(row[30])), "evidence_ref": row[31], "actor_user_id": row[32]}
|
||||
if row[23] is not None:
|
||||
entry.corporate_action = {"corporate_action_id": str(row[23]), "action_type": row[24], "effective_date": row[25], "quantity_before": Decimal(str(row[26])), "quantity_after": Decimal(str(row[27])), "average_price_before": Decimal(str(row[28])), "average_price_after": Decimal(str(row[29])), "total_cost_basis": Decimal(str(row[30])), "cash_balance": Decimal(str(row[31])), "profit_buffer": Decimal(str(row[32])), "evidence_ref": row[33], "actor_user_id": row[34]}
|
||||
result.append(entry)
|
||||
return result
|
||||
|
||||
|
||||
@@ -37,6 +37,8 @@ class LedgerEntryResponse(BaseModel):
|
||||
profit_buffer_after: float = 0.0
|
||||
strategy_set_id: str = "manual"
|
||||
note: str = ""
|
||||
execution_venue: Literal["KRX", "NXT"] | None = None
|
||||
order_venue: Literal["KRX", "NXT", "SOR"] | None = None
|
||||
|
||||
|
||||
class PortfolioBase(BaseModel):
|
||||
|
||||
@@ -108,6 +108,8 @@ class UserInfoResponse(BaseModel):
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class LedgerTradeRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
order_id: str = Field(..., min_length=1)
|
||||
execution_id: str = Field(..., min_length=1)
|
||||
side: Literal["buy", "sell"]
|
||||
@@ -116,6 +118,8 @@ class LedgerTradeRequest(BaseModel):
|
||||
fee_amount: Decimal = Field(default=Decimal(0), ge=0)
|
||||
tax_amount: Decimal = Field(default=Decimal(0), ge=0)
|
||||
executed_at: datetime
|
||||
execution_venue: Literal["KRX", "NXT"] | None = None
|
||||
order_venue: Literal["KRX", "NXT", "SOR"] | None = None
|
||||
|
||||
|
||||
class LedgerTradeResponse(BaseModel):
|
||||
@@ -129,6 +133,8 @@ class LedgerTradeResponse(BaseModel):
|
||||
fee_amount: float
|
||||
tax_amount: float
|
||||
executed_at: datetime
|
||||
execution_venue: Literal["KRX", "NXT"] | None = None
|
||||
order_venue: Literal["KRX", "NXT", "SOR"] | None = None
|
||||
recorded_at: datetime | None = None
|
||||
cash_balance_after: float = 0.0
|
||||
asset_quantity_after: float = 0.0
|
||||
@@ -286,6 +292,8 @@ class LedgerHistoryItem(BaseModel):
|
||||
total_value_after: float | None = None
|
||||
target_ledger_entry_id: int | None = None
|
||||
confirmation_evidence: str | None = None
|
||||
execution_venue: Literal["KRX", "NXT"] | None = None
|
||||
order_venue: Literal["KRX", "NXT", "SOR"] | None = None
|
||||
|
||||
|
||||
class DividendLedgerHistoryItem(BaseModel):
|
||||
@@ -311,6 +319,8 @@ class DividendLedgerHistoryItem(BaseModel):
|
||||
total_value_after: float | None = None
|
||||
target_ledger_entry_id: int | None = None
|
||||
confirmation_evidence: str | None = None
|
||||
execution_venue: Literal["KRX", "NXT"] | None = None
|
||||
order_venue: Literal["KRX", "NXT", "SOR"] | None = None
|
||||
|
||||
|
||||
class CorporateActionLedgerHistoryItem(BaseModel):
|
||||
@@ -331,6 +341,8 @@ class CorporateActionLedgerHistoryItem(BaseModel):
|
||||
evidence_ref: str
|
||||
actor_user_id: str | None = None
|
||||
recorded_at: datetime
|
||||
execution_venue: Literal["KRX", "NXT"] | None = None
|
||||
order_venue: Literal["KRX", "NXT", "SOR"] | None = None
|
||||
|
||||
|
||||
class LedgerHistoryResponse(BaseModel):
|
||||
|
||||
Reference in New Issue
Block a user