100 lines
4.3 KiB
Python
100 lines
4.3 KiB
Python
from __future__ import annotations
|
||
|
||
from datetime import date, time
|
||
from decimal import Decimal
|
||
|
||
from sqlalchemy import (
|
||
BigInteger,
|
||
Date,
|
||
ForeignKey,
|
||
Index,
|
||
Integer,
|
||
String,
|
||
Text,
|
||
Time,
|
||
UniqueConstraint,
|
||
text,
|
||
)
|
||
from sqlalchemy.orm import Mapped, mapped_column
|
||
|
||
from app.core.config import settings
|
||
from app.db.base import Base
|
||
from app.db.types import Amount, AuditMixin, IdMixin, Price, Quantity, qualified
|
||
|
||
|
||
class Operation(IdMixin, AuditMixin, Base):
|
||
"""Сделка/событие по бумаге в портфеле.
|
||
|
||
BUY/SELL — только из выгрузки QUIK (import_batch_id + source_row — откуда
|
||
строка). CPN, NOM, UWFEE, ADJ — ручной ввод (D21), import_batch_id пуст.
|
||
Лотов нет: каждая покупка — отдельная операция BUY (D20).
|
||
"""
|
||
|
||
__tablename__ = "operation"
|
||
__table_args__ = (
|
||
# Номер сделки QUIK уникален в портфеле; у ручных операций (купон,
|
||
# погашение номинала) его нет — поэтому индекс частичный.
|
||
Index(
|
||
"ux_operation_portfolio_trade_number",
|
||
"portfolio_id",
|
||
"trade_number",
|
||
unique=True,
|
||
postgresql_where=text("trade_number IS NOT NULL"),
|
||
sqlite_where=text("trade_number IS NOT NULL"),
|
||
),
|
||
# Строка файла попадает в операции не больше одного раза (D19).
|
||
UniqueConstraint(
|
||
"import_batch_id", "source_row", name="uq_operation_batch_row"
|
||
),
|
||
Index(
|
||
"ix_operation_portfolio_security_date",
|
||
"portfolio_id",
|
||
"security_id",
|
||
"trade_date",
|
||
),
|
||
{"schema": settings.DATABASE_SCHEMA} if settings.DATABASE_SCHEMA else {},
|
||
)
|
||
|
||
security_id: Mapped[int] = mapped_column(
|
||
BigInteger, ForeignKey(qualified("security.id")), nullable=False
|
||
)
|
||
portfolio_id: Mapped[int] = mapped_column(
|
||
BigInteger, ForeignKey(qualified("portfolio.id")), nullable=False
|
||
)
|
||
operation_type_id: Mapped[int] = mapped_column(
|
||
BigInteger, ForeignKey(qualified("operation_type.id")), nullable=False
|
||
)
|
||
# Валюта расчётов: итоги по разным валютам не смешиваем.
|
||
currency_id: Mapped[int] = mapped_column(
|
||
BigInteger,
|
||
ForeignKey(qualified("currency.id"), name="fk_operation_currency"),
|
||
nullable=False,
|
||
)
|
||
trade_date: Mapped[date] = mapped_column(Date, nullable=False)
|
||
trade_time: Mapped[time | None] = mapped_column(Time)
|
||
settlement_date: Mapped[date | None] = mapped_column(Date)
|
||
account: Mapped[str | None] = mapped_column(String(50))
|
||
quantity: Mapped[Decimal | None] = mapped_column(Quantity)
|
||
price: Mapped[Decimal | None] = mapped_column(Price)
|
||
amount: Mapped[Decimal | None] = mapped_column(Amount)
|
||
# TODO(BIQ-38115): какая сумма «с НКД» — Q17. Для строк QUIK пока пусто,
|
||
# «Расчетная сумма» хранится как есть в settlement_amount.
|
||
amount_with_accrued_interest: Mapped[Decimal | None] = mapped_column(Amount)
|
||
settlement_amount: Mapped[Decimal | None] = mapped_column(Amount)
|
||
comment: Mapped[str | None] = mapped_column(Text)
|
||
settlement_code: Mapped[str | None] = mapped_column(String(20))
|
||
order_number: Mapped[str | None] = mapped_column(String(50))
|
||
trade_number: Mapped[str | None] = mapped_column(String(50))
|
||
counterparty: Mapped[str | None] = mapped_column(String(255))
|
||
commission_ts: Mapped[Decimal | None] = mapped_column(Amount)
|
||
commission_clearing: Mapped[Decimal | None] = mapped_column(Amount)
|
||
commission_exchange: Mapped[Decimal | None] = mapped_column(Amount)
|
||
commission_tc: Mapped[Decimal | None] = mapped_column(Amount)
|
||
commission_broker: Mapped[Decimal | None] = mapped_column(Amount)
|
||
import_batch_id: Mapped[int | None] = mapped_column(
|
||
BigInteger,
|
||
ForeignKey(qualified("import_batch.id"), name="fk_operation_import_batch"),
|
||
)
|
||
# Порядковый номер строки в файле (QuikTrade.ordinal).
|
||
source_row: Mapped[int | None] = mapped_column(Integer)
|