feat: support manual dividend ledger entries

Co-authored-by: genichin <4282227+genichin@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-09-09 02:04:02 +00:00
committed by GitHub
co-authored by genichin
parent 9d2c2aea5f
commit ccde81b7a0
6 changed files with 281 additions and 12 deletions
+2 -1
View File
@@ -325,10 +325,12 @@ class PortfolioRepository(Protocol):
price: float | None = None,
fee: float = 0.0,
tax: float = 0.0,
executed_at: datetime | None = None,
target_ledger_entry_id: int | None = None,
note: str = "",
actor_user_id: str | None = None,
confirmation_evidence: str | None = None,
confirm_duplicate: bool = False,
) -> LedgerEntry: ...
def list_manual_ledger_entries(self, portfolio_id: str, limit: int = 50) -> list[LedgerEntry]: ...
@@ -362,4 +364,3 @@ class PortfolioRepository(Protocol):
def update_comment(self, portfolio_id: str, comment_id: str, *, content: str) -> PortfolioComment: ...
def delete_comment(self, portfolio_id: str, comment_id: str) -> None: ...
+38 -3
View File
@@ -2017,14 +2017,16 @@ class PostgresPortfolioRepository:
price: float | None = None,
fee: float = 0.0,
tax: float = 0.0,
executed_at: datetime | None = None,
target_ledger_entry_id: int | None = None,
note: str = "",
actor_user_id: str | None = None,
confirmation_evidence: str | None = None,
confirm_duplicate: bool = False,
) -> LedgerEntry:
self.ensure_schema()
normalized = str(action).upper()
if normalized not in {"INVEST", "BUFFER_OUT", "BUFFER_IN", "BUY", "SELL", "WITHDRAW", "PROFIT", "REVERSAL"}:
if normalized not in {"INVEST", "BUFFER_OUT", "BUFFER_IN", "BUY", "SELL", "WITHDRAW", "PROFIT", "DIVIDEND", "REVERSAL"}:
raise ValueError("unsupported manual ledger action")
conn = self._connect()
with conn.transaction():
@@ -2043,6 +2045,9 @@ class PostgresPortfolioRepository:
fee_f = float(fee or 0.0)
tax_f = float(tax or 0.0)
amount_f = float(amount or 0.0)
ledger_executed_at = executed_at or datetime.now(timezone.utc)
if ledger_executed_at.tzinfo is None:
ledger_executed_at = ledger_executed_at.replace(tzinfo=timezone.utc)
reverse_target: LedgerEntry | None = None
if normalized == "INVEST":
@@ -2070,6 +2075,35 @@ class PostgresPortfolioRepository:
raise ValueError("INSUFFICIENT_BUFFER")
buffer -= amount_f
ledger_price = amount_f
elif normalized == "DIVIDEND":
if not confirm_duplicate:
cur.execute(
"""
SELECT existing.ledger_entry_id
FROM portfolio_ledger AS existing
WHERE existing.portfolio_id = %s
AND existing.source = 'manual'
AND existing.side = 'dividend'
AND existing.price = %s::numeric(20,8)
AND existing.executed_at = %s
AND existing.note = %s
AND NOT EXISTS (
SELECT 1
FROM portfolio_ledger AS reversal
WHERE reversal.portfolio_id = existing.portfolio_id
AND reversal.side = 'reversal'
AND reversal.target_ledger_entry_id = existing.ledger_entry_id
)
ORDER BY existing.ledger_entry_id DESC
LIMIT 1
""",
(portfolio_id, amount_f, ledger_executed_at, note),
)
duplicate_row = cur.fetchone()
if duplicate_row is not None:
raise ValueError(f"POTENTIAL_DUPLICATE_DIVIDEND:{int(duplicate_row[0])}")
buffer += amount_f
ledger_price = amount_f
elif normalized == "BUY":
required = ledger_quantity * ledger_price + fee_f + tax_f
if cash < required:
@@ -2128,7 +2162,7 @@ class PostgresPortfolioRepository:
raise ValueError("INSUFFICIENT_BUFFER")
cash += amount_f
buffer -= amount_f
elif target_action == "INVEST":
elif target_action in {"INVEST", "DIVIDEND"}:
if buffer < amount_f:
raise ValueError("INSUFFICIENT_BUFFER")
buffer -= amount_f
@@ -2165,7 +2199,7 @@ class PostgresPortfolioRepository:
tax_amount, cash_balance_after, asset_quantity_after,
total_value_after, profit_buffer_after, strategy_set_id, note, executed_at,
source, target_ledger_entry_id, confirmation_evidence, actor_user_id
) VALUES (%s, NOW(), %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), %s, %s, %s, %s)
) VALUES (%s, NOW(), %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, 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,
@@ -2184,6 +2218,7 @@ class PostgresPortfolioRepository:
float(buffer),
"manual",
note,
ledger_executed_at,
"manual",
target_ledger_entry_id,
confirmation_evidence,