feat: Add core trading modules for risk management, backtesting, and execution algorithms, alongside a new ML transparency widget and related frontend dependencies.
Some checks are pending
Documentation / build-docs (push) Waiting to run
Tests / test (macos-latest, 3.11) (push) Waiting to run
Tests / test (macos-latest, 3.12) (push) Waiting to run
Tests / test (macos-latest, 3.13) (push) Waiting to run
Tests / test (macos-latest, 3.14) (push) Waiting to run
Tests / test (ubuntu-latest, 3.11) (push) Waiting to run
Tests / test (ubuntu-latest, 3.12) (push) Waiting to run
Tests / test (ubuntu-latest, 3.13) (push) Waiting to run
Tests / test (ubuntu-latest, 3.14) (push) Waiting to run

This commit is contained in:
2025-12-31 21:25:06 -05:00
parent 099432bf3f
commit 7bd6be64a4
743 changed files with 8617 additions and 5042 deletions

View File

@@ -6,8 +6,10 @@ from sqlalchemy import select
import uuid
from ..core.dependencies import get_backtesting_engine, get_strategy_registry
from ..core.schemas import BacktestRequest, BacktestResponse
from ..core.schemas import BacktestRequest, BacktestResponse, WalkForwardRequest, MonteCarloRequest
from src.core.database import Strategy, get_database
from src.backtesting.walk_forward import WalkForwardAnalyzer
from src.backtesting.monte_carlo import MonteCarloSimulator
router = APIRouter()
@@ -76,3 +78,107 @@ async def get_backtest_results(backtest_id: str):
if backtest_id not in _backtests:
raise HTTPException(status_code=404, detail="Backtest not found")
return _backtests[backtest_id]
@router.post("/walk-forward")
async def run_walk_forward(
walk_forward_data: WalkForwardRequest,
backtest_engine=Depends(get_backtesting_engine)
):
"""Run walk-forward analysis for robust parameter optimization."""
try:
db = get_database()
async with db.get_session() as session:
# Get strategy
stmt = select(Strategy).where(Strategy.id == walk_forward_data.strategy_id)
result = await session.execute(stmt)
strategy_db = result.scalar_one_or_none()
if not strategy_db:
raise HTTPException(status_code=404, detail="Strategy not found")
# Get strategy class
registry = get_strategy_registry()
strategy_class = registry.get_strategy_class(strategy_db.class_name)
if not strategy_class:
raise HTTPException(status_code=400, detail=f"Strategy class {strategy_db.class_name} not found")
# Run walk-forward analysis
analyzer = WalkForwardAnalyzer(backtest_engine)
results = await analyzer.run_walk_forward(
strategy_class=strategy_class,
symbol=walk_forward_data.symbol,
exchange=walk_forward_data.exchange,
timeframe=walk_forward_data.timeframe,
start_date=walk_forward_data.start_date,
end_date=walk_forward_data.end_date,
train_period_days=walk_forward_data.train_period_days,
test_period_days=walk_forward_data.test_period_days,
step_days=walk_forward_data.step_days,
initial_capital=walk_forward_data.initial_capital,
parameter_grid=walk_forward_data.parameter_grid,
optimization_metric=walk_forward_data.optimization_metric
)
if "error" in results:
raise HTTPException(status_code=400, detail=results["error"])
return results
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/monte-carlo")
async def run_monte_carlo(
monte_carlo_data: MonteCarloRequest,
backtest_engine=Depends(get_backtesting_engine)
):
"""Run Monte Carlo simulation for risk analysis."""
try:
db = get_database()
async with db.get_session() as session:
# Get strategy
stmt = select(Strategy).where(Strategy.id == monte_carlo_data.strategy_id)
result = await session.execute(stmt)
strategy_db = result.scalar_one_or_none()
if not strategy_db:
raise HTTPException(status_code=404, detail="Strategy not found")
# Get strategy class
registry = get_strategy_registry()
strategy_class = registry.get_strategy_class(strategy_db.class_name)
if not strategy_class:
raise HTTPException(status_code=400, detail=f"Strategy class {strategy_db.class_name} not found")
# Convert parameter_ranges format if provided
param_ranges = None
if monte_carlo_data.parameter_ranges:
param_ranges = {
k: (v[0], v[1]) for k, v in monte_carlo_data.parameter_ranges.items()
if len(v) >= 2
}
# Run Monte Carlo simulation
simulator = MonteCarloSimulator(backtest_engine)
results = await simulator.run_monte_carlo(
strategy_class=strategy_class,
symbol=monte_carlo_data.symbol,
exchange=monte_carlo_data.exchange,
timeframe=monte_carlo_data.timeframe,
start_date=monte_carlo_data.start_date,
end_date=monte_carlo_data.end_date,
initial_capital=monte_carlo_data.initial_capital,
num_simulations=monte_carlo_data.num_simulations,
parameter_ranges=param_ranges,
random_seed=monte_carlo_data.random_seed
)
if "error" in results:
raise HTTPException(status_code=400, detail=results["error"])
return results
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

View File

@@ -8,6 +8,7 @@ import pandas as pd
from src.core.database import MarketData, get_database
from src.data.pricing_service import get_pricing_service
from src.data.indicators import get_indicators
from src.core.config import get_config
router = APIRouter()
@@ -278,3 +279,130 @@ async def get_spread_data(
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/indicators/{symbol:path}")
async def get_indicators_data(
symbol: str,
timeframe: str = "1h",
limit: int = 100,
exchange: str = "coinbase",
indicators: str = Query("", description="Comma-separated list of indicators (e.g., 'sma_20,ema_20,rsi,macd,bollinger_bands')")
):
"""Get OHLCV data with technical indicators for a symbol.
Supported indicators:
- sma_<period>: Simple Moving Average (e.g., sma_20, sma_50)
- ema_<period>: Exponential Moving Average (e.g., ema_20, ema_50)
- rsi: Relative Strength Index
- macd: MACD (returns macd, signal, histogram)
- bollinger_bands: Bollinger Bands (returns upper, middle, lower)
- atr: Average True Range
- obv: On Balance Volume
- adx: Average Directional Index
"""
from sqlalchemy import select
try:
# Fetch OHLCV data first (reuse existing logic)
ohlcv_data = []
try:
db = get_database()
async with db.get_session() as session:
stmt = select(MarketData).filter_by(
symbol=symbol,
timeframe=timeframe,
exchange=exchange
).order_by(MarketData.timestamp.desc()).limit(limit)
result = await session.execute(stmt)
data = result.scalars().all()
if data:
ohlcv_data = [
{
"time": int(d.timestamp.timestamp()),
"open": float(d.open),
"high": float(d.high),
"low": float(d.low),
"close": float(d.close),
"volume": float(d.volume)
}
for d in reversed(data)
]
except Exception:
pass
# If no DB data, fetch from pricing service
if not ohlcv_data:
try:
pricing_service = get_pricing_service()
ohlcv_raw = pricing_service.get_ohlcv(
symbol=symbol,
timeframe=timeframe,
limit=limit
)
if ohlcv_raw:
ohlcv_data = [
{
"time": int(candle[0] / 1000),
"open": float(candle[1]),
"high": float(candle[2]),
"low": float(candle[3]),
"close": float(candle[4]),
"volume": float(candle[5])
}
for candle in ohlcv_raw
]
except Exception:
pass
if not ohlcv_data:
return {"ohlcv": [], "indicators": {}}
# Convert to DataFrame for indicator calculation
df = pd.DataFrame(ohlcv_data)
df.set_index('time', inplace=True)
# Prepare DataFrame for indicators (needs columns: open, high, low, close, volume)
df_ind = pd.DataFrame({
'open': df['open'],
'high': df['high'],
'low': df['low'],
'close': df['close'],
'volume': df['volume']
})
# Parse indicator list
indicator_list = [ind.strip() for ind in indicators.split(',') if ind.strip()] if indicators else []
# Calculate indicators
indicators_calc = get_indicators()
if indicator_list:
df_with_indicators = indicators_calc.calculate_all(df_ind, indicators=indicator_list)
else:
# Default indicators if none specified
df_with_indicators = indicators_calc.calculate_all(df_ind)
# Build response
result_ohlcv = df.reset_index().to_dict('records')
# Extract indicator data
indicator_data = {}
for col in df_with_indicators.columns:
if col not in ['open', 'high', 'low', 'close', 'volume']:
# Convert NaN to None for JSON serialization
values = df_with_indicators[col].replace({pd.NA: None, pd.NaT: None}).tolist()
indicator_data[col] = [
float(v) if v is not None and not pd.isna(v) else None
for v in values
]
return {
"ohlcv": result_ohlcv,
"indicators": indicator_data,
"times": [int(t) for t in df.index.tolist()]
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

View File

@@ -11,6 +11,7 @@ from src.core.database import Order, OrderSide, OrderType, OrderStatus
from src.core.repositories import OrderRepository, PositionRepository
from src.core.logger import get_logger
from src.trading.paper_trading import get_paper_trading
from src.trading.advanced_orders import get_advanced_order_manager
router = APIRouter()
logger = get_logger(__name__)
@@ -21,12 +22,20 @@ async def create_order(
order_data: OrderCreate,
trading_engine=Depends(get_trading_engine)
):
"""Create and execute a trading order."""
"""Create and execute a trading order.
Supports advanced order types:
- Trailing stop: Set trail_percent (e.g., 0.02 for 2%)
- Bracket orders: Set bracket_take_profit and bracket_stop_loss
- OCO orders: Set oco_price for the second order
- Iceberg orders: Set visible_quantity
"""
try:
# Convert string enums to actual enums
side = OrderSide(order_data.side.value)
order_type = OrderType(order_data.order_type.value)
# Execute the base order
order = await trading_engine.execute_order(
exchange_id=order_data.exchange_id,
strategy_id=order_data.strategy_id,
@@ -41,8 +50,71 @@ async def create_order(
if not order:
raise HTTPException(status_code=400, detail="Order execution failed")
# Handle advanced order types
advanced_manager = get_advanced_order_manager()
# Trailing stop
if order_type == OrderType.TRAILING_STOP and order_data.trail_percent:
if not order_data.stop_loss_price:
raise HTTPException(status_code=400, detail="stop_loss_price required for trailing stop")
advanced_manager.create_trailing_stop(
base_order_id=order.id,
initial_stop_price=order_data.stop_loss_price,
trail_percent=order_data.trail_percent
)
# Take profit
if order_data.take_profit_price:
advanced_manager.create_take_profit(
base_order_id=order.id,
target_price=order_data.take_profit_price
)
# Bracket order (entry + take profit + stop loss)
if order_data.bracket_take_profit or order_data.bracket_stop_loss:
if order_data.bracket_take_profit:
advanced_manager.create_take_profit(
base_order_id=order.id,
target_price=order_data.bracket_take_profit
)
if order_data.bracket_stop_loss:
advanced_manager.create_trailing_stop(
base_order_id=order.id,
initial_stop_price=order_data.bracket_stop_loss,
trail_percent=Decimal("0.0") # Fixed stop, not trailing
)
# OCO order
if order_type == OrderType.OCO and order_data.oco_price:
# Create second order
oco_order = await trading_engine.execute_order(
exchange_id=order_data.exchange_id,
strategy_id=order_data.strategy_id,
symbol=order_data.symbol,
side=OrderSide.SELL if side == OrderSide.BUY else OrderSide.BUY,
order_type=OrderType.LIMIT,
quantity=order_data.quantity,
price=order_data.oco_price,
paper_trading=order_data.paper_trading
)
if oco_order:
advanced_manager.create_oco(order.id, oco_order.id)
# Iceberg order
if order_type == OrderType.ICEBERG and order_data.visible_quantity:
advanced_manager.create_iceberg(
total_quantity=order_data.quantity,
visible_quantity=order_data.visible_quantity,
symbol=order_data.symbol,
side=side,
price=order_data.price
)
return OrderResponse.model_validate(order)
except HTTPException:
raise
except Exception as e:
logger.error(f"Error creating order: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))

View File

@@ -46,6 +46,17 @@ class OrderCreate(BaseModel):
price: Optional[Decimal] = None
strategy_id: Optional[int] = None
paper_trading: bool = True
# Advanced order parameters
take_profit_price: Optional[Decimal] = None
stop_loss_price: Optional[Decimal] = None
trail_percent: Optional[Decimal] = None # For trailing stop (e.g., 0.02 for 2%)
# For bracket orders
bracket_take_profit: Optional[Decimal] = None
bracket_stop_loss: Optional[Decimal] = None
# For OCO orders
oco_price: Optional[Decimal] = None # Second order price for OCO
# For iceberg orders
visible_quantity: Optional[Decimal] = None # Visible quantity for iceberg
class OrderResponse(BaseModel):
@@ -168,6 +179,36 @@ class BacktestRequest(BaseModel):
fee_rate: float = 0.001
class WalkForwardRequest(BaseModel):
"""Walk-forward analysis request."""
strategy_id: int
symbol: str
exchange: str
timeframe: str
start_date: datetime
end_date: datetime
train_period_days: int = 90
test_period_days: int = 30
step_days: int = 30
initial_capital: Decimal = Decimal("10000.0")
parameter_grid: Optional[Dict[str, List[Any]]] = None
optimization_metric: str = "sharpe_ratio"
class MonteCarloRequest(BaseModel):
"""Monte Carlo simulation request."""
strategy_id: int
symbol: str
exchange: str
timeframe: str
start_date: datetime
end_date: datetime
initial_capital: Decimal = Decimal("10000.0")
num_simulations: int = 1000
parameter_ranges: Optional[Dict[str, List[float]]] = None # {param_name: [min, max]}
random_seed: Optional[int] = None
class BacktestResponse(BaseModel):
"""Backtest response."""
backtest_id: Optional[int] = None