Files
finance-talk/ft-app/app/models.py
T
2026-07-26 13:32:21 +08:00

174 lines
6.8 KiB
Python

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)
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))
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
result = (self.close_price - self.open_price) * mul - (self.open_fee or 0) - (self.close_fee or 0)
return round(result, 2)