- 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
85 lines
2.4 KiB
Python
85 lines
2.4 KiB
Python
"""Выгрузка таблиц в .xlsx (D13): значения, не текст."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import io
|
||
from dataclasses import dataclass
|
||
from datetime import date, datetime, timezone
|
||
from decimal import Decimal
|
||
|
||
from openpyxl import load_workbook
|
||
|
||
from app.exports.xlsx import table_xlsx
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class _Row:
|
||
name: str
|
||
quantity: Decimal
|
||
on_date: date
|
||
updated_at: datetime
|
||
comment: str | None
|
||
|
||
|
||
_COLUMNS = {
|
||
"name": "Бумага",
|
||
"quantity": "Остаток, шт",
|
||
"on_date": "Дата",
|
||
"updated_at": "Когда",
|
||
}
|
||
|
||
|
||
def _sheet(data: bytes):
|
||
return load_workbook(io.BytesIO(data)).active
|
||
|
||
|
||
def test_header_and_values() -> None:
|
||
rows = [
|
||
_Row(
|
||
"РЭО-02",
|
||
Decimal("1834360.000000"),
|
||
date(2026, 3, 30),
|
||
datetime(2026, 10, 26, 6, 30, tzinfo=timezone.utc),
|
||
"не выгружается",
|
||
),
|
||
_Row(
|
||
"ЕврХол3P06", Decimal("-5"), date(2026, 3, 31), datetime(2026, 1, 1), None
|
||
),
|
||
]
|
||
|
||
ws = _sheet(table_xlsx(rows, _COLUMNS, "Остатки"))
|
||
|
||
assert ws.title == "Остатки"
|
||
assert ws.freeze_panes == "A2"
|
||
assert [c.value for c in ws[1]] == list(_COLUMNS.values())
|
||
assert all(c.font.bold for c in ws[1])
|
||
first = [c.value for c in ws[2]]
|
||
assert first[:3] == ["РЭО-02", 1834360, datetime(2026, 3, 30)]
|
||
assert first[3] == datetime(2026, 10, 26, 9, 30) # московское время
|
||
assert ws["C2"].number_format == "DD.MM.YYYY"
|
||
assert ws["D2"].number_format == "DD.MM.YYYY HH:MM:SS"
|
||
assert ws["B3"].value == -5
|
||
assert ws["D3"].value == datetime(2026, 1, 1) # без пояса — как есть
|
||
assert ws.max_column == 4
|
||
|
||
|
||
def test_fractional_decimal_and_dicts() -> None:
|
||
ws = _sheet(
|
||
table_xlsx(
|
||
[{"rate": Decimal("18.500000"), "extra": "x"}, {"rate": None}],
|
||
{"rate": "Ставка"},
|
||
"Очень длинное название листа для Excel",
|
||
)
|
||
)
|
||
|
||
assert ws.title == "Очень длинное название листа дл"
|
||
assert ws["A2"].value == 18.5
|
||
assert ws["A3"].value is None
|
||
|
||
|
||
def test_empty_table_has_header_only() -> None:
|
||
ws = _sheet(table_xlsx([], {"name": "Бумага"}, "Пусто"))
|
||
|
||
assert ws.max_row == 1
|
||
assert ws["A1"].value == "Бумага"
|