Initial commit: Trading Portal - FastAPI + React + PostgreSQL
This commit is contained in:
Executable
+24
@@ -0,0 +1,24 @@
|
||||
from app.models.user import User
|
||||
from app.models.exchange import Exchange
|
||||
from app.models.symbol import Symbol
|
||||
from app.models.candle import Candle
|
||||
from app.models.watchlist import Watchlist
|
||||
from app.models.credential import ExchangeCredential
|
||||
from app.models.refresh_token import RefreshToken
|
||||
from app.models.real_trade import RealTrade
|
||||
from app.models.signal import Signal, HypotheticalTrade
|
||||
from app.models.audit_log import AuditLog
|
||||
|
||||
__all__ = [
|
||||
"User",
|
||||
"Exchange",
|
||||
"Symbol",
|
||||
"Candle",
|
||||
"Watchlist",
|
||||
"ExchangeCredential",
|
||||
"RefreshToken",
|
||||
"Signal",
|
||||
"HypotheticalTrade",
|
||||
"RealTrade",
|
||||
"AuditLog",
|
||||
]
|
||||
Executable
+71
@@ -0,0 +1,71 @@
|
||||
"""AlertCondition ORM model — multi-condition user alerts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import (
|
||||
Boolean,
|
||||
DateTime,
|
||||
ForeignKey,
|
||||
Index,
|
||||
Integer,
|
||||
JSON,
|
||||
String,
|
||||
)
|
||||
from sqlalchemy.dialects.postgresql import UUID as PGUUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class AlertCondition(Base):
|
||||
"""A user-defined multi-condition alert.
|
||||
|
||||
Each alert consists of a name, a list of conditions (stored as JSON),
|
||||
and notification preferences. Alerts are evaluated after each signal
|
||||
generation; all active alerts whose conditions are satisfied trigger
|
||||
a notification.
|
||||
"""
|
||||
|
||||
__tablename__ = "alert_conditions"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
user_id: Mapped[uuid.UUID] = mapped_column(
|
||||
PGUUID(as_uuid=True),
|
||||
ForeignKey("users.id"),
|
||||
nullable=False,
|
||||
comment="The user who owns this alert",
|
||||
)
|
||||
name: Mapped[str] = mapped_column(
|
||||
String(100), nullable=False, comment="Human-readable alert name"
|
||||
)
|
||||
conditions: Mapped[list] = mapped_column(
|
||||
JSON,
|
||||
nullable=False,
|
||||
default=list,
|
||||
comment="Array of condition objects, e.g. [{'indicator': 'rsi', 'operator': '>', 'value': 70, 'timeframe': '1h'}]",
|
||||
)
|
||||
notify_platform: Mapped[str] = mapped_column(
|
||||
String(20),
|
||||
nullable=False,
|
||||
default="telegram",
|
||||
comment="Notification channel: 'telegram', 'discord', or 'both'",
|
||||
)
|
||||
is_active: Mapped[bool] = mapped_column(
|
||||
Boolean, default=True, nullable=False
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, default=datetime.now
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, default=datetime.now, onupdate=datetime.now
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
Index("ix_alert_conditions_user_active", "user_id", "is_active"),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<AlertCondition id={self.id} name={self.name!r} user_id={self.user_id}>"
|
||||
Executable
+48
@@ -0,0 +1,48 @@
|
||||
"""Audit Log model for tracking actions across the system."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import JSON, DateTime, Index, Integer, String
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class AuditLog(Base):
|
||||
__tablename__ = "audit_logs"
|
||||
|
||||
id: Mapped[int] = mapped_column(
|
||||
Integer, primary_key=True, autoincrement=True
|
||||
)
|
||||
user_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||
UUID(as_uuid=True), nullable=True, index=True
|
||||
)
|
||||
action: Mapped[str] = mapped_column(
|
||||
String(50), nullable=False
|
||||
)
|
||||
resource: Mapped[str] = mapped_column(
|
||||
String(100), nullable=False
|
||||
)
|
||||
details: Mapped[dict | None] = mapped_column(
|
||||
JSON, nullable=True
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
server_default=func.now(),
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
Index("ix_audit_logs_action_created_at", "action", "created_at"),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<AuditLog id={self.id} action={self.action!r} "
|
||||
f"resource={self.resource!r}>"
|
||||
)
|
||||
Executable
+71
@@ -0,0 +1,71 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from sqlalchemy import (
|
||||
ForeignKey,
|
||||
Index,
|
||||
Integer,
|
||||
Numeric,
|
||||
PrimaryKeyConstraint,
|
||||
String,
|
||||
desc,
|
||||
)
|
||||
from sqlalchemy.dialects.postgresql import TIMESTAMP
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class Candle(Base):
|
||||
__tablename__ = "candles"
|
||||
|
||||
symbol_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("symbols.id"), nullable=False
|
||||
)
|
||||
timeframe: Mapped[str] = mapped_column(
|
||||
String(10), nullable=False
|
||||
)
|
||||
timestamp: Mapped[datetime] = mapped_column(
|
||||
TIMESTAMP(timezone=True), nullable=False
|
||||
)
|
||||
open: Mapped[Decimal] = mapped_column(
|
||||
Numeric(20, 8), nullable=False
|
||||
)
|
||||
high: Mapped[Decimal] = mapped_column(
|
||||
Numeric(20, 8), nullable=False
|
||||
)
|
||||
low: Mapped[Decimal] = mapped_column(
|
||||
Numeric(20, 8), nullable=False
|
||||
)
|
||||
close: Mapped[Decimal] = mapped_column(
|
||||
Numeric(20, 8), nullable=False
|
||||
)
|
||||
volume: Mapped[Decimal] = mapped_column(
|
||||
Numeric(30, 8), nullable=False
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint(
|
||||
"symbol_id", "timeframe", "timestamp",
|
||||
name="pk_candles",
|
||||
),
|
||||
Index(
|
||||
"ix_candles_symbol_timeframe_ts_desc",
|
||||
"symbol_id",
|
||||
"timeframe",
|
||||
desc("timestamp"),
|
||||
),
|
||||
{
|
||||
"postgresql_partition_by": "RANGE (timestamp)",
|
||||
"info": {"partitioned": True},
|
||||
},
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<Candle symbol_id={self.symbol_id} "
|
||||
f"timeframe={self.timeframe!r} "
|
||||
f"timestamp={self.timestamp}>"
|
||||
)
|
||||
Executable
+73
@@ -0,0 +1,73 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import Boolean, ForeignKey, Integer, String
|
||||
from sqlalchemy.dialects.postgresql import UUID, TIMESTAMP
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class ExchangeCredential(Base):
|
||||
__tablename__ = "exchange_credentials"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True),
|
||||
primary_key=True,
|
||||
default=func.gen_random_uuid(),
|
||||
)
|
||||
user_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True),
|
||||
ForeignKey("users.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
)
|
||||
exchange_id: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey("exchanges.id"),
|
||||
nullable=False,
|
||||
)
|
||||
api_key: Mapped[str] = mapped_column(
|
||||
String(255), nullable=False
|
||||
)
|
||||
api_secret_enc: Mapped[str] = mapped_column(
|
||||
String(512), nullable=False
|
||||
)
|
||||
api_secret_iv: Mapped[str] = mapped_column(
|
||||
String(64), nullable=False
|
||||
)
|
||||
passphrase: Mapped[str | None] = mapped_column(
|
||||
String(255), nullable=True
|
||||
)
|
||||
is_testnet: Mapped[bool] = mapped_column(
|
||||
Boolean, default=False, nullable=False
|
||||
)
|
||||
is_active: Mapped[bool] = mapped_column(
|
||||
Boolean, default=True, nullable=False
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
TIMESTAMP(timezone=True), server_default=func.now(), nullable=False
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
TIMESTAMP(timezone=True),
|
||||
server_default=func.now(),
|
||||
onupdate=func.now(),
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
# Relationships
|
||||
user: Mapped["User"] = relationship(
|
||||
"User", back_populates="credentials"
|
||||
)
|
||||
exchange: Mapped["Exchange"] = relationship(
|
||||
"Exchange", back_populates="credentials"
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<ExchangeCredential id={self.id} "
|
||||
f"user_id={self.user_id} "
|
||||
f"exchange_id={self.exchange_id}>"
|
||||
)
|
||||
Executable
+52
@@ -0,0 +1,52 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import Boolean, Index, Integer, String
|
||||
from sqlalchemy.dialects.postgresql import TIMESTAMP
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class Exchange(Base):
|
||||
__tablename__ = "exchanges"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
name: Mapped[str] = mapped_column(
|
||||
String(50), unique=True, nullable=False
|
||||
)
|
||||
display_name: Mapped[str | None] = mapped_column(
|
||||
String(100), nullable=True
|
||||
)
|
||||
base_url: Mapped[str | None] = mapped_column(
|
||||
String(255), nullable=True
|
||||
)
|
||||
ws_url: Mapped[str | None] = mapped_column(
|
||||
String(255), nullable=True
|
||||
)
|
||||
is_active: Mapped[bool] = mapped_column(
|
||||
Boolean, default=True, nullable=False
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
TIMESTAMP(timezone=True), server_default=func.now(), nullable=False
|
||||
)
|
||||
|
||||
# P2-8: Index for active exchanges
|
||||
__table_args__ = (
|
||||
Index("ix_exchanges_is_active", "is_active"),
|
||||
)
|
||||
|
||||
# Relationships
|
||||
symbols: Mapped[list["Symbol"]] = relationship(
|
||||
"Symbol", back_populates="exchange", cascade="all, delete-orphan"
|
||||
)
|
||||
credentials: Mapped[list["ExchangeCredential"]] = relationship(
|
||||
"ExchangeCredential",
|
||||
back_populates="exchange",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Exchange id={self.id} name={self.name!r}>"
|
||||
Executable
+95
@@ -0,0 +1,95 @@
|
||||
"""RealTrade ORM model for actual exchange trade tracking."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from sqlalchemy import (
|
||||
DateTime,
|
||||
ForeignKey,
|
||||
Index,
|
||||
Integer,
|
||||
Numeric,
|
||||
String,
|
||||
)
|
||||
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class RealTrade(Base):
|
||||
"""A real trade placed on a connected exchange and persisted locally."""
|
||||
|
||||
__tablename__ = "real_trades"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
user_id: Mapped[uuid.UUID] = mapped_column(
|
||||
PG_UUID(as_uuid=True), ForeignKey("users.id"), nullable=False,
|
||||
comment="FK to the user who placed this trade",
|
||||
)
|
||||
exchange: Mapped[str] = mapped_column(String(20), nullable=False)
|
||||
symbol: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||
|
||||
side: Mapped[str] = mapped_column(
|
||||
String(10), nullable=False, comment="buy or sell"
|
||||
)
|
||||
order_type: Mapped[str] = mapped_column(
|
||||
String(10), nullable=False, default="market",
|
||||
comment="market or limit",
|
||||
)
|
||||
|
||||
# Order details
|
||||
amount: Mapped[Decimal] = mapped_column(Numeric(20, 8), nullable=False)
|
||||
price: Mapped[Decimal | None] = mapped_column(
|
||||
Numeric(20, 8), nullable=True,
|
||||
comment="Limit price (null for market orders)",
|
||||
)
|
||||
filled_amount: Mapped[Decimal] = mapped_column(
|
||||
Numeric(20, 8), nullable=False, default=Decimal("0"),
|
||||
)
|
||||
|
||||
status: Mapped[str] = mapped_column(
|
||||
String(20), nullable=False, default="open",
|
||||
comment="open / filled / cancelled / rejected",
|
||||
)
|
||||
|
||||
# P&L (computed when closed)
|
||||
pnl: Mapped[Decimal | None] = mapped_column(
|
||||
Numeric(20, 8), nullable=True,
|
||||
comment="Realised P&L in quote currency",
|
||||
)
|
||||
pnl_percent: Mapped[Decimal | None] = mapped_column(
|
||||
Numeric(10, 4), nullable=True,
|
||||
comment="P&L as percentage",
|
||||
)
|
||||
|
||||
# Exchange reference
|
||||
order_id: Mapped[str | None] = mapped_column(
|
||||
String(100), nullable=True,
|
||||
comment="Exchange-side order ID",
|
||||
)
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False,
|
||||
default=datetime.utcnow,
|
||||
)
|
||||
closed_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True,
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
Index("ix_real_trades_user_id", "user_id"),
|
||||
Index("ix_real_trades_symbol", "symbol"),
|
||||
Index("ix_real_trades_status", "status"),
|
||||
Index("ix_real_trades_created_at", "created_at"),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<RealTrade #{self.id} {self.side.upper()} "
|
||||
f"{self.amount} {self.symbol} on {self.exchange} "
|
||||
f"status={self.status}>"
|
||||
)
|
||||
Executable
+56
@@ -0,0 +1,56 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import Boolean, ForeignKey, Index, String
|
||||
from sqlalchemy.dialects.postgresql import INET, UUID, TIMESTAMP
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class RefreshToken(Base):
|
||||
__tablename__ = "refresh_tokens"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True),
|
||||
primary_key=True,
|
||||
default=func.gen_random_uuid(),
|
||||
)
|
||||
user_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True),
|
||||
ForeignKey("users.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
)
|
||||
token_hash: Mapped[str] = mapped_column(
|
||||
String(64), nullable=False
|
||||
)
|
||||
expires_at: Mapped[datetime] = mapped_column(
|
||||
TIMESTAMP(timezone=True), nullable=False
|
||||
)
|
||||
revoked: Mapped[bool] = mapped_column(
|
||||
Boolean, default=False, nullable=False
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
TIMESTAMP(timezone=True), server_default=func.now(), nullable=False
|
||||
)
|
||||
user_agent: Mapped[str | None] = mapped_column(
|
||||
String(255), nullable=True
|
||||
)
|
||||
ip_address: Mapped[str | None] = mapped_column(
|
||||
INET, nullable=True
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
Index("ix_refresh_tokens_user_id", "user_id"),
|
||||
)
|
||||
|
||||
# Relationships
|
||||
user: Mapped["User"] = relationship(
|
||||
"User", back_populates="refresh_tokens"
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<RefreshToken id={self.id} user_id={self.user_id}>"
|
||||
Executable
+98
@@ -0,0 +1,98 @@
|
||||
"""Signal and HypotheticalTrade ORM models for trading signal detection."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import (
|
||||
DateTime,
|
||||
ForeignKey,
|
||||
Index,
|
||||
Integer,
|
||||
Numeric,
|
||||
String,
|
||||
Text,
|
||||
)
|
||||
from sqlalchemy.dialects.postgresql import UUID as PGUUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class Signal(Base):
|
||||
"""A detected trading signal based on Double Bollinger Bands + RSI analysis."""
|
||||
|
||||
__tablename__ = "signals"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
symbol: Mapped[str] = mapped_column(String(50), nullable=False, index=True)
|
||||
exchange: Mapped[str] = mapped_column(String(20), nullable=False, default="mexc")
|
||||
timeframe: Mapped[str] = mapped_column(String(10), nullable=False)
|
||||
signal_type: Mapped[str] = mapped_column(String(30), nullable=False)
|
||||
strength: Mapped[str] = mapped_column(String(20), nullable=False)
|
||||
price: Mapped[Decimal] = mapped_column(Numeric(20, 8), nullable=False)
|
||||
timestamp: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False
|
||||
)
|
||||
indicators_snapshot: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
status: Mapped[str] = mapped_column(String(20), nullable=False, default="ACTIVE")
|
||||
note: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, default=datetime.now
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
Index("ix_signals_symbol_created", "symbol", "created_at"),
|
||||
)
|
||||
|
||||
|
||||
class HypotheticalTrade(Base):
|
||||
"""A hypothetical (paper) trade automatically entered on signal generation.
|
||||
|
||||
When a STRONG_BUY or BUY signal fires, a LONG trade is opened.
|
||||
When a STRONG_SELL or SELL signal fires, a SHORT trade is opened.
|
||||
Trades are closed when an opposing signal fires or when stopped out.
|
||||
"""
|
||||
|
||||
__tablename__ = "hypothetical_trades"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
user_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||
PGUUID(as_uuid=True), ForeignKey("users.id"), nullable=True,
|
||||
comment="The user who owns this trade",
|
||||
)
|
||||
signal_id: Mapped[int | None] = mapped_column(
|
||||
Integer, ForeignKey("signals.id"), nullable=True,
|
||||
comment="The signal that opened this trade"
|
||||
)
|
||||
symbol: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||
exchange: Mapped[str] = mapped_column(String(20), nullable=False, default="mexc")
|
||||
timeframe: Mapped[str] = mapped_column(String(10), nullable=False)
|
||||
direction: Mapped[str] = mapped_column(String(10), nullable=False)
|
||||
entry_price: Mapped[Decimal] = mapped_column(Numeric(20, 8), nullable=False)
|
||||
entry_time: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False
|
||||
)
|
||||
entry_reason: Mapped[str | None] = mapped_column(String(30), nullable=True)
|
||||
exit_price: Mapped[Decimal | None] = mapped_column(Numeric(20, 8), nullable=True)
|
||||
exit_time: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
)
|
||||
exit_reason: Mapped[str | None] = mapped_column(String(30), nullable=True)
|
||||
quantity: Mapped[Decimal] = mapped_column(Numeric(20, 8), nullable=False)
|
||||
pnl: Mapped[Decimal | None] = mapped_column(Numeric(20, 8), nullable=True)
|
||||
pnl_percent: Mapped[Decimal | None] = mapped_column(Numeric(14, 4), nullable=True)
|
||||
status: Mapped[str] = mapped_column(String(10), nullable=False, default="OPEN")
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, default=datetime.now
|
||||
)
|
||||
closed_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
Index("ix_hyp_trades_symbol", "symbol"),
|
||||
Index("ix_hyp_trades_status", "status"),
|
||||
)
|
||||
Executable
+46
@@ -0,0 +1,46 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from sqlalchemy import Boolean, ForeignKey, Index, Integer, String, UniqueConstraint
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class Symbol(Base):
|
||||
__tablename__ = "symbols"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
exchange_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("exchanges.id"), nullable=False
|
||||
)
|
||||
symbol: Mapped[str] = mapped_column(
|
||||
String(50), nullable=False
|
||||
)
|
||||
base: Mapped[str] = mapped_column(
|
||||
String(20), nullable=False
|
||||
)
|
||||
quote: Mapped[str] = mapped_column(
|
||||
String(20), nullable=False
|
||||
)
|
||||
is_active: Mapped[bool] = mapped_column(
|
||||
Boolean, default=True, nullable=False
|
||||
)
|
||||
is_trading: Mapped[bool] = mapped_column(
|
||||
Boolean, default=False, nullable=False
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint("exchange_id", "symbol", name="uq_symbol_exchange_symbol"),
|
||||
Index("ix_symbols_is_active", "is_active"), # P2-8
|
||||
)
|
||||
|
||||
# Relationships
|
||||
exchange: Mapped["Exchange"] = relationship(
|
||||
"Exchange", back_populates="symbols"
|
||||
)
|
||||
watchlists: Mapped[list["Watchlist"]] = relationship(
|
||||
"Watchlist", back_populates="symbol", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Symbol id={self.id} symbol={self.symbol!r}>"
|
||||
Executable
+72
@@ -0,0 +1,72 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import Boolean, JSON, String, UniqueConstraint
|
||||
from sqlalchemy.dialects.postgresql import UUID, TIMESTAMP
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True),
|
||||
primary_key=True,
|
||||
default=func.gen_random_uuid(),
|
||||
)
|
||||
username: Mapped[str] = mapped_column(
|
||||
String(50), unique=True, nullable=False
|
||||
)
|
||||
email: Mapped[str] = mapped_column(
|
||||
String(255), unique=True, nullable=False
|
||||
)
|
||||
password_hash: Mapped[str] = mapped_column(
|
||||
String(255), nullable=False
|
||||
)
|
||||
display_name: Mapped[str | None] = mapped_column(
|
||||
String(100), nullable=True
|
||||
)
|
||||
is_active: Mapped[bool] = mapped_column(
|
||||
Boolean, default=True, nullable=False
|
||||
)
|
||||
is_admin: Mapped[bool] = mapped_column(
|
||||
Boolean, default=False, nullable=False
|
||||
)
|
||||
role: Mapped[str] = mapped_column(
|
||||
String(20), default="trader", nullable=False
|
||||
)
|
||||
preferences: Mapped[dict | None] = mapped_column(
|
||||
JSON, default={"default_exchange": "mexc", "default_timeframe": "1h"}, nullable=True
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
TIMESTAMP(timezone=True), server_default=func.now(), nullable=False
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
TIMESTAMP(timezone=True),
|
||||
server_default=func.now(),
|
||||
onupdate=func.now(),
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
# Relationships
|
||||
watchlists: Mapped[list["Watchlist"]] = relationship(
|
||||
"Watchlist", back_populates="user", cascade="all, delete-orphan"
|
||||
)
|
||||
credentials: Mapped[list["ExchangeCredential"]] = relationship(
|
||||
"ExchangeCredential",
|
||||
back_populates="user",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
refresh_tokens: Mapped[list["RefreshToken"]] = relationship(
|
||||
"RefreshToken",
|
||||
back_populates="user",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<User id={self.id} username={self.username!r}>"
|
||||
Executable
+61
@@ -0,0 +1,61 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import ForeignKey, Integer, String, UniqueConstraint
|
||||
from sqlalchemy.dialects.postgresql import UUID, TIMESTAMP
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class Watchlist(Base):
|
||||
__tablename__ = "user_watchlists"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True),
|
||||
primary_key=True,
|
||||
default=func.gen_random_uuid(),
|
||||
)
|
||||
user_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True),
|
||||
ForeignKey("users.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
)
|
||||
symbol_id: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey("symbols.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
)
|
||||
label: Mapped[str | None] = mapped_column(
|
||||
String(50), nullable=True
|
||||
)
|
||||
sort_order: Mapped[int] = mapped_column(
|
||||
Integer, default=0, nullable=False
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
TIMESTAMP(timezone=True), server_default=func.now(), nullable=False
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint(
|
||||
"user_id", "symbol_id", name="uq_watchlist_user_symbol"
|
||||
),
|
||||
)
|
||||
|
||||
# Relationships
|
||||
user: Mapped["User"] = relationship(
|
||||
"User", back_populates="watchlists"
|
||||
)
|
||||
symbol: Mapped["Symbol"] = relationship(
|
||||
"Symbol", back_populates="watchlists"
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<Watchlist id={self.id} "
|
||||
f"user_id={self.user_id} "
|
||||
f"symbol_id={self.symbol_id}>"
|
||||
)
|
||||
Reference in New Issue
Block a user