169 lines
6.1 KiB
Python
169 lines
6.1 KiB
Python
"""Ручные операции (D21): купон, амортизация, комиссия за андеррайтинг,
|
||
корректировка. Купля и продажа — только загрузкой выгрузки QUIK."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
from datetime import date
|
||
|
||
import streamlit as st
|
||
|
||
from app.db.base import run_db
|
||
from app.db.models import OperationCode
|
||
from app.services import manual_operations
|
||
from app.services.manual_operations import (
|
||
MANUAL_TYPES,
|
||
UWFEE_PORTFOLIO,
|
||
FormOptions,
|
||
ManualOperationError,
|
||
ManualOperationInput,
|
||
parse_decimal,
|
||
)
|
||
from app.ui.common import as_table, call_db, current_user
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
_SECURITY_CURRENCY = "валюта бумаги"
|
||
|
||
_HINTS = {
|
||
OperationCode.CPN: "Сумма купона больше нуля. Количество — по желанию.",
|
||
OperationCode.NOM: "Сумма погашения номинала больше нуля. Количество — по "
|
||
"желанию. Номинал бумаги здесь не меняется.",
|
||
OperationCode.UWFEE: f"Только портфель {UWFEE_PORTFOLIO}. Сумма больше нуля, "
|
||
"без количества. В сводном отчёте добавляется к P/L (Net).",
|
||
OperationCode.ADJ: "Сумма и/или количество со знаком. Комментарий обязателен. "
|
||
"В расчётах пока не участвует — смысл корректировки уточняется.",
|
||
}
|
||
|
||
_COLUMNS = {
|
||
"trade_date": "Дата",
|
||
"operation": "Операция",
|
||
"portfolio": "Портфель",
|
||
"name": "Бумага",
|
||
"isin": "ISIN",
|
||
"quantity": "Кол-во",
|
||
"amount": "Сумма",
|
||
"currency": "Валюта",
|
||
"comment": "Комментарий",
|
||
"created_by": "Кто ввёл",
|
||
}
|
||
|
||
|
||
def render() -> None:
|
||
st.header("Ручные операции")
|
||
st.caption(
|
||
"Здесь — только то, чего нет в выгрузке QUIK. Купля и продажа вводятся "
|
||
"загрузкой файла на странице «Загрузка сделок»."
|
||
)
|
||
options = call_db(
|
||
manual_operations.load_form_options(), "Не удалось загрузить справочники"
|
||
)
|
||
if options is None:
|
||
return
|
||
if options.securities:
|
||
_render_form(options)
|
||
else:
|
||
st.info("Бумаг пока нет — добавьте их на странице «Бумаги».")
|
||
|
||
st.subheader("Последние ручные операции")
|
||
rows = call_db(
|
||
manual_operations.list_manual_operations(),
|
||
"Не удалось загрузить ручные операции",
|
||
)
|
||
if rows:
|
||
st.dataframe(as_table(rows, _COLUMNS), hide_index=True)
|
||
elif rows is not None:
|
||
st.caption("Ручных операций пока нет.")
|
||
|
||
|
||
def _render_form(options: FormOptions) -> None:
|
||
# Тип — вне формы: от него зависят поля формы.
|
||
code = st.selectbox(
|
||
"Тип операции", list(MANUAL_TYPES), format_func=MANUAL_TYPES.__getitem__
|
||
)
|
||
st.caption(_HINTS[code])
|
||
securities = {s.isin: s for s in options.securities}
|
||
portfolios = (
|
||
[UWFEE_PORTFOLIO] if code == OperationCode.UWFEE else list(options.portfolios)
|
||
)
|
||
|
||
with st.form("manual_operation", clear_on_submit=True):
|
||
portfolio = st.selectbox(
|
||
"Портфель",
|
||
portfolios,
|
||
format_func=lambda c: f"{c} — {options.portfolios.get(c, c)}",
|
||
)
|
||
isin = st.selectbox(
|
||
"Бумага",
|
||
list(securities),
|
||
format_func=lambda i: (
|
||
f"{securities[i].name} ({i}, {securities[i].currency})"
|
||
),
|
||
)
|
||
trade_date = st.date_input("Дата", value=date.today(), format="DD.MM.YYYY")
|
||
amount_text = st.text_input("Сумма", placeholder="например 125 000,50")
|
||
quantity_text = (
|
||
""
|
||
if code == OperationCode.UWFEE
|
||
else st.text_input("Количество, шт", placeholder="необязательно")
|
||
)
|
||
currency = st.selectbox("Валюта", [_SECURITY_CURRENCY, *options.currencies])
|
||
comment = st.text_area("Комментарий")
|
||
submitted = st.form_submit_button("Сохранить", type="primary")
|
||
if submitted:
|
||
_save(
|
||
code=code,
|
||
portfolio=portfolio,
|
||
isin=isin,
|
||
trade_date=trade_date,
|
||
amount_text=amount_text,
|
||
quantity_text=quantity_text,
|
||
currency=currency,
|
||
comment=comment,
|
||
)
|
||
|
||
|
||
def _save(
|
||
*,
|
||
code: str,
|
||
portfolio: str,
|
||
isin: str,
|
||
trade_date: date,
|
||
amount_text: str,
|
||
quantity_text: str,
|
||
currency: str,
|
||
comment: str,
|
||
) -> None:
|
||
try:
|
||
operation = ManualOperationInput(
|
||
operation_code=code,
|
||
portfolio_code=portfolio,
|
||
isin=isin,
|
||
trade_date=trade_date,
|
||
amount=parse_decimal(amount_text, "Сумма"),
|
||
quantity=parse_decimal(quantity_text, "Количество"),
|
||
comment=comment,
|
||
currency_code=None if currency == _SECURITY_CURRENCY else currency,
|
||
)
|
||
except ManualOperationError as exc:
|
||
st.error(str(exc))
|
||
return
|
||
errors = manual_operations.validate(operation)
|
||
if errors:
|
||
for error in errors:
|
||
st.error(error)
|
||
return
|
||
try:
|
||
run_db(manual_operations.add_manual_operation(operation, current_user()))
|
||
except ManualOperationError as exc:
|
||
st.error(str(exc))
|
||
return
|
||
except Exception as exc: # noqa: BLE001
|
||
logger.exception("Ошибка сохранения ручной операции %s по %s", code, isin)
|
||
st.error(f"Не удалось сохранить операцию: {exc}")
|
||
return
|
||
st.success(
|
||
f"Сохранено: {MANUAL_TYPES[code].lower()}, {isin}, {portfolio}, "
|
||
f"{trade_date:%d.%m.%Y}."
|
||
)
|