Files
crypto_trading/backend/app/store.py
T
matitos 03dfa369e5
CI/CD / backend (pull_request) Successful in 6m18s
CI/CD / frontend (pull_request) Successful in 4m55s
CI/CD / build (pull_request) Skipped
CI/CD / deploy (pull_request) Skipped
CI/CD / cleanup (pull_request) Skipped
Risk controls, alerts, API token, more exchanges, presets
- RiskManager halts + flattens on max daily loss / drawdown, caps position
  size and trades per day; adds a manual kill switch (POST /api/bot/kill).
- Telegram and webhook notifications on trades, errors and halts.
- Optional API bearer token on /api, injected server-side by the frontend
  nginx so the browser never sees it.
- Kraken and Coinbase market data (public); Coinbase live broker. Live on an
  unsupported exchange now errors instead of silently falling back to paper.
- Strategy presets backed by the StrategyConfig table (CRUD API + UI).
- Idempotent additive-column migration so existing SQLite databases upgrade in
  place (fixes the new strategy_configs.name column).
2026-09-18 19:26:29 +00:00

199 lines
6.5 KiB
Python

from __future__ import annotations
from sqlalchemy import select
from sqlalchemy.orm import sessionmaker
from app.db import SessionLocal
from app.models import (
EquitySnapshot,
Event,
Order,
OrderSide,
Position,
PositionSide,
PositionStatus,
StrategyConfig,
)
class Store:
"""Thin persistence layer around the trading tables."""
def __init__(self, session_factory: sessionmaker = SessionLocal) -> None:
self._session_factory: sessionmaker = session_factory
def create_position(
self,
*,
strategy_key: str,
symbol: str,
side: PositionSide,
qty: float,
entry_price: float,
entry_fee: float,
stop_loss: float | None = None,
take_profit: float | None = None,
) -> Position:
with self._session_factory() as session:
position = Position(
strategy_key=strategy_key,
symbol=symbol,
side=side,
qty=qty,
entry_price=entry_price,
entry_fee=entry_fee,
stop_loss=stop_loss,
take_profit=take_profit,
status=PositionStatus.OPEN,
)
session.add(position)
session.commit()
return position
def close_position(
self, position: Position, *, exit_price: float, exit_fee: float, realized_pnl: float
) -> Position:
from app.models import utcnow
with self._session_factory() as session:
merged = session.merge(position)
merged.status = PositionStatus.CLOSED
merged.exit_price = exit_price
merged.exit_fee = exit_fee
merged.realized_pnl = realized_pnl
merged.closed_at = utcnow()
session.commit()
return merged
def abandon_open_positions(self, strategy_key: str, reason: str = "reset") -> int:
"""Mark lingering open positions closed at entry price (used on restart)."""
from app.models import utcnow
with self._session_factory() as session:
positions = list(
session.scalars(
select(Position).where(
Position.strategy_key == strategy_key,
Position.status == PositionStatus.OPEN,
)
).all()
)
for position in positions:
position.status = PositionStatus.CLOSED
position.exit_price = position.entry_price
position.exit_fee = 0.0
position.realized_pnl = -position.entry_fee
position.closed_at = utcnow()
session.add_all(positions)
session.commit()
return len(positions)
def open_position_for(self, strategy_key: str) -> Position | None:
with self._session_factory() as session:
return session.scalars(
select(Position)
.where(
Position.strategy_key == strategy_key,
Position.status == PositionStatus.OPEN,
)
.order_by(Position.id.desc())
).first()
def record_order(
self,
*,
position_id: int | None,
strategy_key: str,
symbol: str,
side: OrderSide,
qty: float,
price: float,
fee: float,
reason: str,
) -> Order:
with self._session_factory() as session:
order = Order(
position_id=position_id,
strategy_key=strategy_key,
symbol=symbol,
side=side,
qty=qty,
price=price,
fee=fee,
reason=reason,
)
session.add(order)
session.commit()
return order
def record_event(self, level: str, message: str, strategy_key: str | None = None) -> Event:
with self._session_factory() as session:
event = Event(level=level, message=message, strategy_key=strategy_key)
session.add(event)
session.commit()
return event
def record_equity(self, equity: float, cash: float, price: float) -> None:
with self._session_factory() as session:
session.add(EquitySnapshot(equity=equity, cash=cash, price=price))
session.commit()
def list_positions(self, limit: int = 50) -> list[Position]:
with self._session_factory() as session:
return list(
session.scalars(select(Position).order_by(Position.id.desc()).limit(limit)).all()
)
def list_orders(self, limit: int = 50) -> list[Order]:
with self._session_factory() as session:
return list(session.scalars(select(Order).order_by(Order.id.desc()).limit(limit)).all())
def list_events(self, limit: int = 50) -> list[Event]:
with self._session_factory() as session:
return list(session.scalars(select(Event).order_by(Event.id.desc()).limit(limit)).all())
def list_equity(self, limit: int = 500) -> list[EquitySnapshot]:
with self._session_factory() as session:
rows = list(
session.scalars(
select(EquitySnapshot).order_by(EquitySnapshot.id.desc()).limit(limit)
).all()
)
return list(reversed(rows))
def save_strategy_config(
self,
*,
name: str,
strategy_key: str,
symbol: str,
timeframe: str,
params: dict,
) -> StrategyConfig:
with self._session_factory() as session:
config = StrategyConfig(
name=name,
strategy_key=strategy_key,
symbol=symbol,
timeframe=timeframe,
params=params,
)
session.add(config)
session.commit()
return config
def list_strategy_configs(self) -> list[StrategyConfig]:
with self._session_factory() as session:
return list(
session.scalars(select(StrategyConfig).order_by(StrategyConfig.id.desc())).all()
)
def delete_strategy_config(self, config_id: int) -> bool:
with self._session_factory() as session:
config = session.get(StrategyConfig, config_id)
if config is None:
return False
session.delete(config)
session.commit()
return True