- 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
79 lines
2.7 KiB
Python
79 lines
2.7 KiB
Python
"""Выгрузка таблиц экранов в .xlsx (D13) — без БД и сети.
|
||
|
||
Числа и даты пишутся значениями, не текстом: в Excel по ним можно считать.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import io
|
||
from collections.abc import Sequence
|
||
from dataclasses import asdict, is_dataclass
|
||
from datetime import date, datetime, timedelta, timezone
|
||
from decimal import Decimal
|
||
from typing import Any
|
||
|
||
from openpyxl import Workbook
|
||
from openpyxl.styles import Font
|
||
from openpyxl.utils import get_column_letter
|
||
|
||
MIME = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||
|
||
# Время аудита в БД — с часовым поясом, в Excel пояса нет: пишем московское.
|
||
_MSK = timezone(timedelta(hours=3))
|
||
_DATE_FORMAT = "DD.MM.YYYY"
|
||
_DATETIME_FORMAT = "DD.MM.YYYY HH:MM:SS"
|
||
_MAX_WIDTH = 60
|
||
_SHEET_TITLE_MAX = 31 # ограничение Excel
|
||
|
||
|
||
def table_xlsx(rows: Sequence[Any], columns: dict[str, str], sheet: str) -> bytes:
|
||
"""Список dataclass/dict -> .xlsx с одним листом. columns: поле -> заголовок,
|
||
в том же порядке, что на экране."""
|
||
workbook = Workbook()
|
||
ws = workbook.active
|
||
ws.title = sheet[:_SHEET_TITLE_MAX]
|
||
ws.append(list(columns.values()))
|
||
for cell in ws[1]:
|
||
cell.font = Font(bold=True)
|
||
ws.freeze_panes = "A2"
|
||
|
||
widths = [len(title) for title in columns.values()]
|
||
for row in rows:
|
||
data = asdict(row) if is_dataclass(row) else dict(row)
|
||
values = [_cell_value(data.get(name)) for name in columns]
|
||
ws.append(values)
|
||
for index, value in enumerate(values):
|
||
widths[index] = max(widths[index], _width(value))
|
||
for column, width in enumerate(widths, start=1):
|
||
ws.column_dimensions[get_column_letter(column)].width = min(
|
||
width + 2, _MAX_WIDTH
|
||
)
|
||
for row_cells in ws.iter_rows(min_row=2):
|
||
for cell in row_cells:
|
||
if isinstance(cell.value, datetime):
|
||
cell.number_format = _DATETIME_FORMAT
|
||
elif isinstance(cell.value, date):
|
||
cell.number_format = _DATE_FORMAT
|
||
|
||
buffer = io.BytesIO()
|
||
workbook.save(buffer)
|
||
return buffer.getvalue()
|
||
|
||
|
||
def _cell_value(value: Any) -> Any:
|
||
if isinstance(value, datetime) and value.tzinfo is not None:
|
||
return value.astimezone(_MSK).replace(tzinfo=None)
|
||
return value
|
||
|
||
|
||
def _width(value: Any) -> int:
|
||
if value is None:
|
||
return 0
|
||
if isinstance(value, datetime):
|
||
return len(_DATETIME_FORMAT)
|
||
if isinstance(value, date):
|
||
return len(_DATE_FORMAT)
|
||
if isinstance(value, Decimal):
|
||
return len(format(value.normalize(), "f"))
|
||
return len(str(value))
|