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
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:
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user