DRRK/app/ui/pages/business_calendar.py
Raykov-MS c1c73e96f5 Add funding rates, positions, business calendar and xlsx export
- funding_rate table and "Фондирование" page: rate from date, history, rate on date (D27)
- positions by issue: BUY - SELL per portfolio, "Остатки" page (D29)
- quotes are loaded only for securities with an open position (D10)
- business calendar from xmlcalendar.ru: yearly file upload, report date
  and update job use previous working day (D30)
- xlsx download for positions, quotes and funding rate history
- manual operation type as radio buttons
- QUIK instruments with purchase marks ("МТС 2Р-18_1") match the issue,
  file name kept in operation.instrument (D17)
- migrations d3a6f8b1c254, e7b2c9d4a813, f1c4a7e9b352
- project.md: decisions and questions after customer meetings
2026-10-29 11:20:15 +03:00

115 lines
4.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Производственный календарь (D30): загрузка года из файла xmlcalendar.ru."""
from __future__ import annotations
import logging
from datetime import date
import streamlit as st
from app.db.base import run_db
from app.imports.xmlcalendar import (
SOURCE,
CalendarFormatError,
ParsedCalendar,
parse_calendar_json,
)
from app.services import business_calendar
from app.ui.common import as_table, call_db, current_user
logger = logging.getLogger(__name__)
# Разобранный файл (ParsedCalendar) или текст ошибки — кладётся при загрузке.
_UPLOAD_KEY = "calendar_upload"
# Сброс поля загрузки после сохранения: у виджета меняется ключ.
_NONCE_KEY = "calendar_file_nonce"
_YEAR_COLUMNS = {
"year": "Год",
"non_working": "Нерабочих дней",
"weekday_holidays": "Праздники и переносы в будни",
"working_weekends": "Рабочие выходные",
"updated_by": "Кто загрузил",
"updated_at": "Когда",
}
def render() -> None:
st.header("Производственный календарь")
st.caption(
f"Файл на год с {SOURCE}: https://xmlcalendar.ru/data/ru/ГГГГ/calendar.json. "
"От календаря считается дата отчёта по умолчанию — предыдущий рабочий "
"день. Для года без календаря рабочими считаются будни. Повторная "
"загрузка года заменяет его целиком."
)
years = call_db(business_calendar.list_years(), "Не удалось загрузить календарь")
if years is None:
return
loaded = {row.year for row in years}
if date.today().year not in loaded:
st.warning(
f"Календарь на {date.today().year} не загружен: праздники при выборе "
"даты отчёта не учитываются."
)
widget_key = f"calendar_file_{st.session_state.get(_NONCE_KEY, 0)}"
st.file_uploader(
"Файл календаря (.json)",
type=["json"],
key=widget_key,
on_change=_on_upload,
args=(widget_key,),
)
upload = st.session_state.get(_UPLOAD_KEY)
if isinstance(upload, str):
st.error(upload)
elif isinstance(upload, ParsedCalendar):
_render_preview(upload, loaded)
st.subheader("Загруженные годы")
if years:
st.dataframe(as_table(years, _YEAR_COLUMNS), hide_index=True)
else:
st.caption("Календарь пока не загружен.")
def _on_upload(widget_key: str) -> None:
file = st.session_state.get(widget_key)
if file is None:
st.session_state.pop(_UPLOAD_KEY, None)
return
try:
st.session_state[_UPLOAD_KEY] = parse_calendar_json(file.getvalue())
except CalendarFormatError as exc:
st.session_state[_UPLOAD_KEY] = str(exc)
def _render_preview(parsed: ParsedCalendar, loaded: set[int]) -> None:
st.write(
f"Год {parsed.year}: нерабочих дней {len(parsed.non_working)}, "
f"сокращённых {len(parsed.shortened)}."
)
st.write(f"Праздники и переносы в будни: {_dates(parsed.weekday_holidays)}.")
st.write(f"Рабочие выходные: {_dates(parsed.working_weekends)}.")
if parsed.year in loaded:
st.warning(f"Календарь на {parsed.year} уже загружен — он будет заменён.")
if st.button(f"Сохранить календарь на {parsed.year}", type="primary"):
_save(parsed)
def _dates(days: tuple[date, ...]) -> str:
return ", ".join(f"{d:%d.%m}" for d in days) if days else "нет"
def _save(parsed: ParsedCalendar) -> None:
try:
run_db(business_calendar.save_year(parsed, current_user()))
except Exception as exc: # noqa: BLE001
logger.exception("Ошибка сохранения календаря на %d", parsed.year)
st.error(f"Не удалось сохранить календарь: {exc}")
return
st.session_state.pop(_UPLOAD_KEY, None)
st.session_state[_NONCE_KEY] = st.session_state.get(_NONCE_KEY, 0) + 1
st.toast(f"Календарь на {parsed.year} сохранён.")
st.rerun()