fix corporate action replay and trade filtering

This commit is contained in:
2026-09-13 14:17:32 +09:00
parent cb19c35d23
commit 925c6b4fa4
3 changed files with 20 additions and 2 deletions
+2
View File
@@ -1332,6 +1332,8 @@ def create_corporate_action(portfolio_id: str, payload: CorporateActionRequest,
return CorporateActionResponse(**saved.__dict__) return CorporateActionResponse(**saved.__dict__)
except ValueError as exc: except ValueError as exc:
code = str(exc) code = str(exc)
if code == "portfolio not found":
raise HTTPException(status_code=404, detail="Portfolio not found") from exc
raise HTTPException(status_code=409 if code.startswith(("IDEMPOTENCY", "CORPORATE_ACTION_")) else 400, detail=code) from exc raise HTTPException(status_code=409 if code.startswith(("IDEMPOTENCY", "CORPORATE_ACTION_")) else 400, detail=code) from exc
finally: finally:
_close_repo(repo) _close_repo(repo)
+2 -1
View File
@@ -290,6 +290,7 @@ def corporate_action_portfolio(
"""Record an auditable stock consolidation without a trade.""" """Record an auditable stock consolidation without a trade."""
from datetime import date from datetime import date
from decimal import Decimal 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 from app.domain.models import CorporateAction
if action_type != "stock_consolidation": if action_type != "stock_consolidation":
@@ -312,7 +313,7 @@ def corporate_action_portfolio(
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} 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) 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("", 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) 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) return repository.record_corporate_action(portfolio_id, action=action, idempotency_key=idempotency_key, payload_hash=digest)
detail = _with_service_and_repo(ctx, _callback) detail = _with_service_and_repo(ctx, _callback)
render_output(ctx.obj.console, ctx.obj.output_format, detail) render_output(ctx.obj.console, ctx.obj.output_format, detail)
+16 -1
View File
@@ -1637,6 +1637,21 @@ class PostgresPortfolioRepository:
row = cur.fetchone() row = cur.fetchone()
if row is None: if row is None:
raise ValueError("portfolio not found") raise ValueError("portfolio not found")
cur.execute("SELECT corporate_action_id, portfolio_id, asset_id, action_type, effective_date, actor_user_id, evidence_ref, idempotency_key, payload_hash, quantity_before, quantity_after, average_price_before, average_price_after, total_cost_basis, cash_balance, profit_buffer, ledger_entry_id, recorded_at FROM portfolio_corporate_actions WHERE portfolio_id=%s AND idempotency_key=%s", (portfolio_id, idempotency_key))
prior = cur.fetchone()
if prior:
if prior[8] != payload_hash:
raise ValueError("IDEMPOTENCY_KEY_REUSE")
(action.corporate_action_id, action.portfolio_id, action.asset_id, action.action_type,
action.effective_date, action.actor_user_id, action.evidence_ref,
action.idempotency_key, action.payload_hash, action.quantity_before,
action.quantity_after, action.average_price_before, action.average_price_after,
action.total_cost_basis, action.cash_balance, action.profit_buffer,
ledger_id, action.recorded_at) = prior
action.corporate_action_id = str(action.corporate_action_id)
action.ledger_entry_id = int(ledger_id)
action.idempotent_replay = True
return action
if row[4] != "enabled": if row[4] != "enabled":
raise ValueError("CORPORATE_ACTION_PORTFOLIO_DISABLED") raise ValueError("CORPORATE_ACTION_PORTFOLIO_DISABLED")
if row[3] != action.asset_id: if row[3] != action.asset_id:
@@ -1720,7 +1735,7 @@ class PostgresPortfolioRepository:
boundaries (start of day / start of next day). boundaries (start of day / start of next day).
""" """
self.ensure_schema() self.ensure_schema()
clauses = ["portfolio_id = %s"] clauses = ["portfolio_id = %s", "side IN ('buy','sell')"]
params: list[object] = [portfolio_id] params: list[object] = [portfolio_id]
if date is not None: if date is not None: