feat(portfolios): expose ledger investment aggregates
Add reversal-aware grouped manual-ledger aggregates to portfolio list responses and align the single-portfolio return summary with the same semantics.\n\nRefs #23
This commit is contained in:
@@ -2053,29 +2053,56 @@ class PostgresPortfolioRepository:
|
||||
)
|
||||
return [self._row_to_ledger_entry(row) for row in cur.fetchall()]
|
||||
|
||||
def get_return_summary(self, portfolio_id: str) -> dict[str, float]:
|
||||
def get_return_summaries(self, portfolio_ids: list[str]) -> dict[str, dict[str, float]]:
|
||||
if not portfolio_ids:
|
||||
return {}
|
||||
self.ensure_schema()
|
||||
with self._connect().cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT side, COALESCE(SUM(price), 0)
|
||||
FROM portfolio_ledger
|
||||
WHERE portfolio_id = %s AND source = 'manual' AND side IN ('invest', 'withdraw', 'profit')
|
||||
GROUP BY side
|
||||
SELECT ledger.portfolio_id,
|
||||
COALESCE(SUM(ledger.price) FILTER (WHERE ledger.side = 'invest'), 0),
|
||||
COALESCE(SUM(ledger.price) FILTER (WHERE ledger.side = 'withdraw'), 0),
|
||||
COALESCE(SUM(ledger.price) FILTER (WHERE ledger.side = 'profit'), 0)
|
||||
FROM portfolio_ledger AS ledger
|
||||
WHERE ledger.portfolio_id = ANY(%s)
|
||||
AND ledger.source = 'manual'
|
||||
AND ledger.side IN ('invest', 'withdraw', 'profit')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM portfolio_ledger AS reversal
|
||||
WHERE reversal.portfolio_id = ledger.portfolio_id
|
||||
AND reversal.source = 'manual'
|
||||
AND reversal.side = 'reversal'
|
||||
AND reversal.target_ledger_entry_id = ledger.ledger_entry_id
|
||||
)
|
||||
GROUP BY ledger.portfolio_id
|
||||
""",
|
||||
(portfolio_id,),
|
||||
(portfolio_ids,),
|
||||
)
|
||||
sums = {str(side).upper(): float(total) for side, total in cur.fetchall()}
|
||||
invested = sums.get("INVEST", 0.0)
|
||||
withdrawn = sums.get("WITHDRAW", 0.0)
|
||||
profit = sums.get("PROFIT", 0.0)
|
||||
rows = cur.fetchall()
|
||||
return {
|
||||
"total_invested": invested,
|
||||
"total_withdrawn": withdrawn,
|
||||
"net_contributed_capital": invested - withdrawn,
|
||||
"cumulative_profit_withdrawn": profit,
|
||||
str(portfolio_id): {
|
||||
"total_invested": float(invested),
|
||||
"total_withdrawn": float(withdrawn),
|
||||
"net_contributed_capital": float(invested) - float(withdrawn),
|
||||
"cumulative_profit_withdrawn": float(profit),
|
||||
}
|
||||
for portfolio_id, invested, withdrawn, profit in rows
|
||||
}
|
||||
|
||||
def get_return_summary(self, portfolio_id: str) -> dict[str, float]:
|
||||
summaries = self.get_return_summaries([portfolio_id])
|
||||
return summaries.get(
|
||||
portfolio_id,
|
||||
{
|
||||
"total_invested": 0.0,
|
||||
"total_withdrawn": 0.0,
|
||||
"net_contributed_capital": 0.0,
|
||||
"cumulative_profit_withdrawn": 0.0,
|
||||
},
|
||||
)
|
||||
|
||||
def get_asset_by_id(self, asset_id: str) -> Asset | None:
|
||||
self.ensure_schema()
|
||||
with self._connect().cursor() as cur:
|
||||
|
||||
Reference in New Issue
Block a user