import hashlib import os from datetime import date from sqlalchemy import String, Integer, Float, Date, ForeignKey, Boolean, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column, relationship from app.database import Base class Product(Base): __tablename__ = "products" id: Mapped[int] = mapped_column(primary_key=True) code: Mapped[str] = mapped_column(String(10), unique=True, index=True) name: Mapped[str] = mapped_column(String(20)) exchange: Mapped[str] = mapped_column(String(10), default="CZCE") contracts: Mapped[list["Contract"]] = relationship( back_populates="product", cascade="all, delete-orphan" ) class Contract(Base): __tablename__ = "contracts" __table_args__ = (UniqueConstraint("code"),) id: Mapped[int] = mapped_column(primary_key=True) product_id: Mapped[int] = mapped_column(ForeignKey("products.id"), index=True) code: Mapped[str] = mapped_column(String(10), unique=True, index=True) name: Mapped[str] = mapped_column(String(30)) is_active: Mapped[bool] = mapped_column(Boolean, default=True) product: Mapped["Product"] = relationship(back_populates="contracts") class DailyBar(Base): __tablename__ = "daily_bars" __table_args__ = (UniqueConstraint("contract", "date"),) id: Mapped[int] = mapped_column(primary_key=True) contract: Mapped[str] = mapped_column(String(10), index=True) date: Mapped[date] = mapped_column(Date, index=True) open: Mapped[float] = mapped_column(Float) close: Mapped[float] = mapped_column(Float) high: Mapped[float] = mapped_column(Float) low: Mapped[float] = mapped_column(Float) amp_5d: Mapped[float | None] = mapped_column(Float, nullable=True) @property def diff(self) -> float: """最高-最低价差""" return self.high - self.low class PositionRanking(Base): __tablename__ = "position_rankings" __table_args__ = (UniqueConstraint("contract_code", "institution", "data_type", "date"),) id: Mapped[int] = mapped_column(primary_key=True) contract_code: Mapped[str] = mapped_column(String(10), index=True) institution: Mapped[str] = mapped_column(String(30), index=True) data_type: Mapped[str] = mapped_column(String(10), index=True) date: Mapped[date] = mapped_column(Date, index=True) rank: Mapped[int] = mapped_column(Integer) value: Mapped[int] = mapped_column(Integer) change: Mapped[int] = mapped_column(Integer) class User(Base): __tablename__ = "users" id: Mapped[int] = mapped_column(primary_key=True) username: Mapped[str] = mapped_column(String(50), unique=True) password_hash: Mapped[str] = mapped_column(String(128)) role: Mapped[str] = mapped_column(String(20), default="user") @staticmethod def hash_password(password: str) -> str: salt = os.urandom(16) dk = hashlib.pbkdf2_hmac("sha256", password.encode(), salt, 100000) return salt.hex() + ":" + dk.hex() def check_password(self, password: str) -> bool: try: salt_hex, dk_hex = self.password_hash.split(":") salt = bytes.fromhex(salt_hex) dk = hashlib.pbkdf2_hmac("sha256", password.encode(), salt, 100000) return dk.hex() == dk_hex except (ValueError, AttributeError): return False class Round(Base): __tablename__ = "rounds" id: Mapped[int] = mapped_column(primary_key=True) contract_code: Mapped[str] = mapped_column(String(10), index=True) daily_hands: Mapped[int] = mapped_column(Integer, default=1) max_locks: Mapped[int] = mapped_column(Integer, default=3) status: Mapped[str] = mapped_column(String(20), default="active") started_at: Mapped[date] = mapped_column(Date) positions: Mapped[list["Position"]] = relationship( back_populates="round", cascade="all, delete-orphan" ) @property def total_locks(self) -> int: return sum(p.locked_count for p in self.positions) class Position(Base): __tablename__ = "positions" id: Mapped[int] = mapped_column(primary_key=True) round_id: Mapped[int] = mapped_column(ForeignKey("rounds.id"), index=True) open_price: Mapped[int] = mapped_column(Integer) amp_threshold: Mapped[int] = mapped_column(Integer) lock_price: Mapped[int] = mapped_column(Integer) hands: Mapped[int] = mapped_column(Integer, default=1) locked_count: Mapped[int] = mapped_column(Integer, default=0) opened_at: Mapped[date] = mapped_column(Date) round: Mapped["Round"] = relationship(back_populates="positions") class Trade(Base): __tablename__ = "trades" id: Mapped[int] = mapped_column(primary_key=True) product_code: Mapped[str] = mapped_column(String(10)) contract_code: Mapped[str] = mapped_column(String(10), index=True) direction: Mapped[str] = mapped_column(String(4)) open_date: Mapped[date] = mapped_column(Date) open_price: Mapped[float] = mapped_column(Float) open_fee: Mapped[float | None] = mapped_column(Float, nullable=True, default=0) close_date: Mapped[date | None] = mapped_column(Date, nullable=True) close_price: Mapped[float | None] = mapped_column(Float, nullable=True) close_fee: Mapped[float | None] = mapped_column(Float, nullable=True, default=0) status: Mapped[str] = mapped_column(String(10), default="open") @property def pnl(self) -> float | None: if self.close_price is None: return None mul = 20 # glass futures point value if self.direction == "long": result = (self.close_price - self.open_price) * mul - (self.open_fee or 0) - (self.close_fee or 0) else: result = (self.open_price - self.close_price) * mul - (self.open_fee or 0) - (self.close_fee or 0) return round(result, 2) class OptionTrade(Base): __tablename__ = "option_trades" id: Mapped[int] = mapped_column(primary_key=True) product_code: Mapped[str] = mapped_column(String(10)) contract_code: Mapped[str] = mapped_column(String(10), index=True) option_type: Mapped[str] = mapped_column(String(4)) direction: Mapped[str] = mapped_column(String(4)) strike_price: Mapped[float] = mapped_column(Float) open_date: Mapped[date] = mapped_column(Date) open_price: Mapped[float] = mapped_column(Float) open_fee: Mapped[float | None] = mapped_column(Float, nullable=True, default=0) close_date: Mapped[date | None] = mapped_column(Date, nullable=True) close_price: Mapped[float | None] = mapped_column(Float, nullable=True) close_fee: Mapped[float | None] = mapped_column(Float, nullable=True, default=0) status: Mapped[str] = mapped_column(String(10), default="open") @property def pnl(self) -> float | None: if self.close_price is None: return None mul = 20 # glass futures point value if self.direction == "sell": result = (self.open_price - self.close_price) * mul - (self.open_fee or 0) - (self.close_fee or 0) else: result = (self.close_price - self.open_price) * mul - (self.open_fee or 0) - (self.close_fee or 0) return round(result, 2)