chore(release): prepare v0.3.1
This commit is contained in:
@@ -24,15 +24,19 @@ from app.core.auth import AuthContext, Permission, require_read, require_trade_w
|
||||
from app.core.config import get_settings
|
||||
from app.domain.errors import DaemonInactiveError, DaemonNotFoundError
|
||||
from app.domain.models import CorporateAction, Portfolio, PortfolioComment
|
||||
from app.domain.services import PORTFOLIO_NOT_FOUND_MESSAGE
|
||||
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.schemas.corporate_actions import (
|
||||
CorporateActionRequest,
|
||||
CorporateActionResponse,
|
||||
)
|
||||
from app.schemas.v1 import (
|
||||
CorporateActionLedgerHistoryItem,
|
||||
DividendLedgerHistoryItem,
|
||||
DividendManualLedgerResponse,
|
||||
HoldingsCostBasisResponse,
|
||||
LedgerHistoryItem,
|
||||
CorporateActionLedgerHistoryItem,
|
||||
LedgerHistoryResponse,
|
||||
LedgerTradeRequest,
|
||||
LedgerTradeResponse,
|
||||
@@ -57,7 +61,6 @@ from app.schemas.v1 import (
|
||||
TradeHistoryItem,
|
||||
TradeHistoryResponse,
|
||||
)
|
||||
from app.schemas.corporate_actions import CorporateActionRequest, CorporateActionResponse
|
||||
from app.services.strategy_set_service import (
|
||||
StrategyInactiveError,
|
||||
StrategyNotFoundError,
|
||||
@@ -1313,7 +1316,13 @@ def get_holdings_cost_basis(
|
||||
|
||||
|
||||
@router.post("/{portfolio_id}/corporate-actions", response_model=CorporateActionResponse, status_code=status.HTTP_201_CREATED)
|
||||
def create_corporate_action(portfolio_id: str, payload: CorporateActionRequest, response: Response, auth: AuthContext = Depends(require_trade_write), idempotency_key: str = Header(..., alias="Idempotency-Key")):
|
||||
def create_corporate_action(
|
||||
portfolio_id: str,
|
||||
payload: CorporateActionRequest,
|
||||
response: Response,
|
||||
auth: AuthContext = Depends(require_trade_write), # noqa: B008 - FastAPI dependency declaration
|
||||
idempotency_key: str = Header(..., alias="Idempotency-Key"),
|
||||
):
|
||||
if not idempotency_key or not idempotency_key.strip():
|
||||
raise HTTPException(status_code=422, detail="Idempotency-Key must not be empty")
|
||||
repo = _get_repo()
|
||||
@@ -1328,7 +1337,7 @@ def create_corporate_action(portfolio_id: str, payload: CorporateActionRequest,
|
||||
from app.domain.corporate_actions import canonical_payload_hash
|
||||
digest = canonical_payload_hash(payload)
|
||||
# The repository derives all WAC and immutable snapshot fields under its row lock.
|
||||
action = CorporateAction(str(uuid.uuid4()), portfolio_id, portfolio.asset_id, payload.action_type, payload.effective_date, payload.quantity_before, payload.quantity_after, Decimal("0"), Decimal("0"), Decimal("0"), Decimal(str(portfolio.cash_balance)), Decimal(str(portfolio.profit_buffer)), payload.evidence_ref, idempotency_key, digest, 0, auth.user_id)
|
||||
action = CorporateAction(str(uuid.uuid4()), portfolio_id, portfolio.asset_id, payload.action_type, payload.effective_date, payload.quantity_before, payload.quantity_after, Decimal(0), Decimal(0), Decimal(0), Decimal(str(portfolio.cash_balance)), Decimal(str(portfolio.profit_buffer)), payload.evidence_ref, idempotency_key, digest, 0, auth.user_id)
|
||||
saved = repo.record_corporate_action(portfolio_id, action=action, idempotency_key=idempotency_key, payload_hash=digest)
|
||||
return CorporateActionResponse(**saved.__dict__)
|
||||
except ValueError as exc:
|
||||
|
||||
@@ -288,10 +288,14 @@ def corporate_action_portfolio(
|
||||
force: bool = typer.Option(False, "--force"),
|
||||
) -> None:
|
||||
"""Record an auditable stock consolidation without a trade."""
|
||||
import uuid
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
import uuid
|
||||
from app.domain.corporate_actions import calculate_stock_consolidation, canonical_payload_hash
|
||||
|
||||
from app.domain.corporate_actions import (
|
||||
calculate_stock_consolidation,
|
||||
canonical_payload_hash,
|
||||
)
|
||||
from app.domain.models import CorporateAction
|
||||
if action_type != "stock_consolidation":
|
||||
ctx.obj.console.print("Error: INVALID_CORPORATE_ACTION_TYPE")
|
||||
@@ -312,7 +316,7 @@ def corporate_action_portfolio(
|
||||
raise ValueError("Idempotency-Key must not be empty")
|
||||
payload = {"action_type": action_type, "effective_date": day.isoformat(), "quantity_before": str(quantity_before), "quantity_after": str(quantity_after), "evidence_ref": evidence_ref, "confirm_daemon_managed": False}
|
||||
digest = canonical_payload_hash(payload)
|
||||
calc = calculate_stock_consolidation(Decimal(str(quantity_before)), Decimal(str(quantity_after)), Decimal("0"))
|
||||
calc = calculate_stock_consolidation(Decimal(str(quantity_before)), Decimal(str(quantity_after)), Decimal(0))
|
||||
action = CorporateAction(str(uuid.uuid4()), portfolio_id, portfolio.asset_id, action_type, day, calc.quantity_before, calc.quantity_after, calc.average_price_before, calc.average_price_after, calc.total_cost_basis, Decimal(str(portfolio.cash_balance)), Decimal(str(portfolio.profit_buffer)), evidence_ref, idempotency_key, digest, 0, user)
|
||||
return repository.record_corporate_action(portfolio_id, action=action, idempotency_key=idempotency_key, payload_hash=digest)
|
||||
detail = _with_service_and_repo(ctx, _callback)
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ if env_file.exists():
|
||||
|
||||
class Settings(BaseSettings):
|
||||
app_name: str = "PortfolioHub"
|
||||
app_version: str = "0.3.0"
|
||||
app_version: str = "0.3.1"
|
||||
environment: str = "development"
|
||||
allowed_origins_raw: str = Field(default="http://localhost:3000", validation_alias="PORTFOLIOHUB_ALLOWED_ORIGINS")
|
||||
bootstrap_username: str = Field(default="", validation_alias="PORTFOLIOHUB_BOOTSTRAP_USERNAME")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Additive storage for auditable, idempotent stock consolidations."""
|
||||
from alembic import op
|
||||
from sqlalchemy import text
|
||||
|
||||
revision = "20260913_0031"
|
||||
down_revision = "20260820_0030"
|
||||
branch_labels = None
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"""Pure domain rules for portfolio stock consolidations."""
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from decimal import ROUND_HALF_UP, Decimal
|
||||
|
||||
|
||||
def canonical_payload_hash(payload: object) -> str:
|
||||
if hasattr(payload, "model_dump"):
|
||||
|
||||
@@ -7,7 +7,7 @@ import json
|
||||
import logging
|
||||
import uuid
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, date, datetime
|
||||
from datetime import UTC, datetime
|
||||
from decimal import ROUND_HALF_UP, Decimal, InvalidOperation
|
||||
from typing import Any
|
||||
|
||||
@@ -22,10 +22,10 @@ from app.domain.errors import (
|
||||
StrategyNotFoundError,
|
||||
)
|
||||
from app.domain.models import (
|
||||
CorporateAction,
|
||||
Asset,
|
||||
AuthToken,
|
||||
BrokerCredential,
|
||||
CorporateAction,
|
||||
Daemon,
|
||||
LedgerEntry,
|
||||
Portfolio,
|
||||
@@ -1631,7 +1631,7 @@ class PostgresPortfolioRepository:
|
||||
def record_corporate_action(self, portfolio_id: str, *, action: CorporateAction, idempotency_key: str, payload_hash: str) -> CorporateAction:
|
||||
"""Persist an immutable action and snapshot atomically."""
|
||||
self.ensure_schema()
|
||||
with self._connect() as conn:
|
||||
with self._connect() as conn: # noqa: SIM117 - keep transaction and cursor lifetimes explicit
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT asset_quantity, cash_balance, profit_buffer, asset_id, status FROM portfolios WHERE portfolio_id=%s FOR UPDATE", (portfolio_id,))
|
||||
row = cur.fetchone()
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
from datetime import date, datetime
|
||||
from decimal import Decimal
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
|
||||
|
||||
# Corporate actions (stock_consolidation-only, issue #34)
|
||||
class CorporateActionRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
Reference in New Issue
Block a user