feat(PORTFOLIO-002): add position storage costs
This commit is contained in:
@@ -15,6 +15,7 @@ from app.models.workspace import get_workspace_repository
|
||||
from app.pages.common import dashboard_page, split_page_panes
|
||||
from app.services.alerts import AlertService, build_portfolio_alert_context
|
||||
from app.services.settings_status import save_status_text
|
||||
from app.services.storage_costs import get_default_storage_cost_for_underlying
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -334,6 +335,19 @@ def settings_page(workspace_id: str) -> None:
|
||||
label="Underlying",
|
||||
).classes("w-full")
|
||||
|
||||
def update_storage_cost_default() -> None:
|
||||
"""Update storage cost defaults based on underlying selection."""
|
||||
underlying = str(pos_underlying.value)
|
||||
default_basis, default_period = get_default_storage_cost_for_underlying(underlying)
|
||||
if default_basis is not None:
|
||||
pos_storage_cost_basis.value = float(default_basis)
|
||||
pos_storage_cost_period.value = default_period or "annual"
|
||||
else:
|
||||
pos_storage_cost_basis.value = 0.0
|
||||
pos_storage_cost_period.value = "annual"
|
||||
|
||||
pos_underlying.on_value_change(lambda _: update_storage_cost_default())
|
||||
|
||||
pos_quantity = ui.number(
|
||||
"Quantity",
|
||||
value=100.0,
|
||||
@@ -369,6 +383,23 @@ def settings_page(workspace_id: str) -> None:
|
||||
placeholder="Add notes about this position...",
|
||||
).classes("w-full")
|
||||
|
||||
ui.separator().classes("my-3")
|
||||
ui.label("Storage Costs (optional)").classes("text-sm font-semibold text-slate-700 dark:text-slate-300")
|
||||
ui.label("For physical gold (XAU), defaults to 0.12% annual vault storage.").classes("text-xs text-slate-500 dark:text-slate-400 mb-2")
|
||||
|
||||
pos_storage_cost_basis = ui.number(
|
||||
"Storage cost (% per year or fixed $)",
|
||||
value=0.0,
|
||||
min=0.0,
|
||||
step=0.01,
|
||||
).classes("w-full")
|
||||
|
||||
pos_storage_cost_period = ui.select(
|
||||
{"annual": "Annual", "monthly": "Monthly"},
|
||||
value="annual",
|
||||
label="Cost period",
|
||||
).classes("w-full")
|
||||
|
||||
with ui.row().classes("w-full gap-3 mt-4"):
|
||||
ui.button("Cancel", on_click=lambda: add_position_dialog.close()).props("outline")
|
||||
ui.button("Add Position", on_click=lambda: add_position_from_form()).props("color=primary")
|
||||
@@ -376,15 +407,22 @@ def settings_page(workspace_id: str) -> None:
|
||||
def add_position_from_form() -> None:
|
||||
"""Add a new position from the form."""
|
||||
try:
|
||||
underlying = str(pos_underlying.value)
|
||||
storage_cost_basis_val = float(pos_storage_cost_basis.value)
|
||||
storage_cost_basis = Decimal(str(storage_cost_basis_val)) if storage_cost_basis_val > 0 else None
|
||||
storage_cost_period = str(pos_storage_cost_period.value) if storage_cost_basis else None
|
||||
|
||||
new_position = Position(
|
||||
id=uuid4(),
|
||||
underlying=str(pos_underlying.value),
|
||||
underlying=underlying,
|
||||
quantity=Decimal(str(pos_quantity.value)),
|
||||
unit=str(pos_unit.value),
|
||||
entry_price=Decimal(str(pos_entry_price.value)),
|
||||
entry_date=date.fromisoformat(str(pos_entry_date.value)),
|
||||
entry_basis_mode="weight",
|
||||
notes=str(pos_notes.value or ""),
|
||||
storage_cost_basis=storage_cost_basis,
|
||||
storage_cost_period=storage_cost_period,
|
||||
)
|
||||
workspace_repo.add_position(workspace_id, new_position)
|
||||
add_position_dialog.close()
|
||||
@@ -423,6 +461,19 @@ def settings_page(workspace_id: str) -> None:
|
||||
ui.label(f"Value: ${float(pos.entry_value):,.2f}").classes(
|
||||
"text-xs font-semibold text-emerald-600 dark:text-emerald-400"
|
||||
)
|
||||
# Show storage cost if configured
|
||||
if pos.storage_cost_basis is not None:
|
||||
basis_val = float(pos.storage_cost_basis)
|
||||
period = pos.storage_cost_period or "annual"
|
||||
if basis_val < 1:
|
||||
# Percentage
|
||||
storage_label = f"{basis_val:.2f}% {period} storage"
|
||||
else:
|
||||
# Fixed amount
|
||||
storage_label = f"${basis_val:,.2f} {period} storage"
|
||||
ui.label(f"Storage: {storage_label}").classes(
|
||||
"text-xs text-slate-500 dark:text-slate-400"
|
||||
)
|
||||
with ui.row().classes("gap-1"):
|
||||
ui.button(
|
||||
icon="delete",
|
||||
|
||||
Reference in New Issue
Block a user