feat: Add Phase 4 advanced metrics and components
- Add advanced metrics dashboard with trade analytics - Add new trading components (EntryTypeAnalysis, MultiDayPositionTracker, NewsEventTracker, etc.) - Add strategy mode selector and trend confirmation - Add risk automation panel and slippage correlation analysis - Add daily trading plan enhancements with modal components - Add custom hooks (useApi, useLocalStorage, useAdvancedTradeMetrics) - Add broker service integration and trading API - Add test setup and vitest configuration - Include parquet data files for live market data - Add comprehensive documentation in docs/ folder
This commit is contained in:
@@ -0,0 +1,495 @@
|
||||
"""
|
||||
Smart Trade Hub API - Unified trade entry system
|
||||
Consolidates Simulator, Manual Logger, and Broker Bridge into one intelligent interface
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Any, Dict, List, Optional, Literal
|
||||
from datetime import datetime, timezone
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.db.database import get_db
|
||||
from app.services.simulation_state import load_simulation_state
|
||||
from app.api.trading_persistent import (
|
||||
TradeRequest as PersistentTradeRequest,
|
||||
execute_trade as persistent_execute_trade,
|
||||
)
|
||||
from app.services.risk import validate_order
|
||||
from app.services.ai_context_builder import ai_context_builder
|
||||
from app.services.price_anchor import price_anchor_service
|
||||
|
||||
router = APIRouter(prefix="/api/smart-trade-hub", tags=["Smart Trade Hub"])
|
||||
|
||||
|
||||
class TradeSource(str):
|
||||
"""Enumeration of trade sources"""
|
||||
SIMULATOR = "simulator"
|
||||
MANUAL = "manual"
|
||||
BROKER = "broker"
|
||||
VOICE = "voice"
|
||||
OCR = "ocr"
|
||||
|
||||
|
||||
class SmartTradeRequest(BaseModel):
|
||||
"""Unified trade entry request with auto-detection"""
|
||||
action: Literal["BUY", "SELL", "CLOSE"]
|
||||
symbol: str = Field(default="XAU/USD", description="Trading symbol")
|
||||
quantity: Optional[float] = Field(None, description="Trade quantity (auto-filled if None)")
|
||||
price: Optional[float] = Field(None, description="Entry price (uses current market if None)")
|
||||
|
||||
# Optional guards (auto-calculated if None)
|
||||
stop_loss: Optional[float] = None
|
||||
take_profit: Optional[float] = None
|
||||
risk_percent: Optional[float] = None
|
||||
|
||||
# Source detection and metadata
|
||||
source: Optional[str] = Field(None, description="Trade source: simulator/manual/broker/voice/ocr")
|
||||
platform: Optional[str] = Field(None, description="Trading platform (e.g., MT5, TradingView)")
|
||||
notes: Optional[str] = Field(None, description="Trade notes or voice transcription")
|
||||
entry_time: Optional[str] = Field(None, description="Custom entry time (ISO format)")
|
||||
|
||||
# OCR/Voice metadata
|
||||
image_data: Optional[str] = Field(None, description="Base64 encoded screenshot for OCR")
|
||||
voice_data: Optional[str] = Field(None, description="Voice memo data")
|
||||
|
||||
# Pre-fill hints
|
||||
use_last_trade_defaults: bool = Field(True, description="Auto-fill from last trade")
|
||||
apply_smart_guards: bool = Field(True, description="Apply AI-suggested guards")
|
||||
|
||||
|
||||
class SmartTradeResponse(BaseModel):
|
||||
"""Response with executed trade and suggestions"""
|
||||
trade_id: int
|
||||
action: str
|
||||
symbol: str
|
||||
quantity: float
|
||||
price: float
|
||||
stop_loss: Optional[float]
|
||||
take_profit: Optional[float]
|
||||
risk_percent: Optional[float]
|
||||
|
||||
# Execution details
|
||||
source: str
|
||||
executed_at: str
|
||||
total_cost: float
|
||||
|
||||
# Smart suggestions applied
|
||||
guards_applied: bool
|
||||
guards_suggested: Optional[Dict] = None
|
||||
prefill_used: bool
|
||||
|
||||
# Position state after trade
|
||||
remaining_cash: float
|
||||
total_equity: float
|
||||
position_size: Optional[float]
|
||||
unrealized_pnl: Optional[float]
|
||||
|
||||
|
||||
class SmartPreFillResponse(BaseModel):
|
||||
"""Pre-fill suggestions for trade entry"""
|
||||
symbol: str
|
||||
suggested_quantity: float
|
||||
current_price: float
|
||||
suggested_guards: Dict
|
||||
last_trade_context: Optional[Dict]
|
||||
market_context: Dict
|
||||
confidence: float
|
||||
|
||||
|
||||
class SmartGuardSuggestion(BaseModel):
|
||||
"""AI-suggested risk guards"""
|
||||
stop_loss_price: float
|
||||
stop_loss_percent: float
|
||||
take_profit_price: float
|
||||
take_profit_percent: float
|
||||
risk_percent: float
|
||||
position_size: float
|
||||
risk_reward_ratio: float
|
||||
reasoning: str
|
||||
confidence: float
|
||||
|
||||
|
||||
def _get_current_market_price(symbol: str) -> float:
|
||||
"""Get current market price from price anchor service"""
|
||||
try:
|
||||
anchor_price = price_anchor_service.get_anchor_price_sync(symbol.upper().replace("/", ""))
|
||||
if anchor_price and anchor_price > 0:
|
||||
return anchor_price
|
||||
except:
|
||||
pass
|
||||
|
||||
# Fallback to a reasonable default for XAU/USD
|
||||
return 2034.0
|
||||
|
||||
|
||||
def _compute_equity(state: Dict[str, Any], price_hint: Optional[float] = None) -> float:
|
||||
"""Compute total equity using cash and current position."""
|
||||
cash = float(state.get("cash", 0.0) or 0.0)
|
||||
position = state.get("position") or {}
|
||||
|
||||
if position:
|
||||
current_price = price_hint or position.get("current_price") or position.get("avg_price") or 0.0
|
||||
quantity = position.get("quantity", 0.0) or 0.0
|
||||
cash += float(quantity) * float(current_price)
|
||||
|
||||
return cash
|
||||
|
||||
|
||||
def _get_last_trade_defaults(state: Dict[str, Any]) -> Optional[Dict]:
|
||||
"""Get defaults from the last trade"""
|
||||
trades = state.get("trades", [])
|
||||
if not trades:
|
||||
return None
|
||||
|
||||
last_trade = trades[-1]
|
||||
return {
|
||||
"quantity": last_trade.get("quantity"),
|
||||
"symbol": last_trade.get("symbol", "XAU/USD"),
|
||||
"platform": last_trade.get("platform"),
|
||||
"stop_loss": last_trade.get("stop_loss"),
|
||||
"take_profit": last_trade.get("take_profit"),
|
||||
"risk_percent": last_trade.get("risk_percent"),
|
||||
}
|
||||
|
||||
|
||||
def _calculate_smart_guards(
|
||||
symbol: str,
|
||||
action: str,
|
||||
price: float,
|
||||
quantity: float,
|
||||
equity: float
|
||||
) -> SmartGuardSuggestion:
|
||||
"""
|
||||
Calculate optimal stop loss and take profit using ATR and risk management principles
|
||||
"""
|
||||
try:
|
||||
# Get market metrics including ATR
|
||||
ctx = ai_context_builder.build_request(
|
||||
symbol.upper().replace("/", ""),
|
||||
"1h", # Use hourly for guard calculation
|
||||
100
|
||||
)
|
||||
metrics = ai_context_builder.build_metrics(
|
||||
symbol.upper().replace("/", ""),
|
||||
"1h",
|
||||
ctx.price_data
|
||||
)
|
||||
|
||||
# Extract ATR value
|
||||
atr = metrics.atr_14 if hasattr(metrics, 'atr_14') else (price * 0.015) # Default to 1.5%
|
||||
|
||||
# Calculate stop loss (1.5x ATR from entry)
|
||||
sl_distance = atr * 1.5
|
||||
sl_percent = (sl_distance / price) * 100
|
||||
|
||||
# Calculate take profit (2x stop loss for 1:2 risk/reward minimum)
|
||||
tp_distance = sl_distance * 2.0
|
||||
tp_percent = (tp_distance / price) * 100
|
||||
|
||||
if action == "BUY":
|
||||
sl_price = price - sl_distance
|
||||
tp_price = price + tp_distance
|
||||
else: # SELL
|
||||
sl_price = price + sl_distance
|
||||
tp_price = price - tp_distance
|
||||
|
||||
# Calculate position risk as % of equity
|
||||
risk_amount = quantity * sl_distance
|
||||
risk_percent = (risk_amount / equity) * 100
|
||||
|
||||
# Ensure risk doesn't exceed 2% of equity (conservative default)
|
||||
if risk_percent > 2.0:
|
||||
# Adjust quantity to maintain 2% risk
|
||||
adjusted_quantity = (equity * 0.02) / sl_distance
|
||||
risk_percent = 2.0
|
||||
else:
|
||||
adjusted_quantity = quantity
|
||||
|
||||
return SmartGuardSuggestion(
|
||||
stop_loss_price=round(sl_price, 2),
|
||||
stop_loss_percent=round(sl_percent, 2),
|
||||
take_profit_price=round(tp_price, 2),
|
||||
take_profit_percent=round(tp_percent, 2),
|
||||
risk_percent=round(risk_percent, 2),
|
||||
position_size=round(adjusted_quantity, 2),
|
||||
risk_reward_ratio=2.0,
|
||||
reasoning=f"ATR-based guards: {atr:.2f} | 1.5x ATR stop | 1:2 R:R ratio | Max 2% risk",
|
||||
confidence=0.85
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
# Fallback to simple percentage-based guards
|
||||
sl_percent = 2.0
|
||||
tp_percent = 4.0
|
||||
|
||||
if action == "BUY":
|
||||
sl_price = price * (1 - sl_percent / 100)
|
||||
tp_price = price * (1 + tp_percent / 100)
|
||||
else:
|
||||
sl_price = price * (1 + sl_percent / 100)
|
||||
tp_price = price * (1 - tp_percent / 100)
|
||||
|
||||
risk_amount = quantity * price * (sl_percent / 100)
|
||||
risk_percent = (risk_amount / equity) * 100
|
||||
|
||||
return SmartGuardSuggestion(
|
||||
stop_loss_price=round(sl_price, 2),
|
||||
stop_loss_percent=round(sl_percent, 2),
|
||||
take_profit_price=round(tp_price, 2),
|
||||
take_profit_percent=round(tp_percent, 2),
|
||||
risk_percent=round(risk_percent, 2),
|
||||
position_size=quantity,
|
||||
risk_reward_ratio=2.0,
|
||||
reasoning="Fallback guards: 2% stop loss | 4% take profit | 1:2 ratio",
|
||||
confidence=0.60
|
||||
)
|
||||
|
||||
|
||||
@router.post("/prefill", response_model=SmartPreFillResponse)
|
||||
async def get_smart_prefill(
|
||||
symbol: str = Query("XAU/USD"),
|
||||
action: Optional[str] = Query(None),
|
||||
db: Session = Depends(get_db),
|
||||
user_id: str = "default",
|
||||
) -> SmartPreFillResponse:
|
||||
"""Get smart pre-fill suggestions based on last trade and current market context."""
|
||||
try:
|
||||
current_price = _get_current_market_price(symbol)
|
||||
state = load_simulation_state(db, user_id)
|
||||
last_trade = _get_last_trade_defaults(state)
|
||||
|
||||
suggested_quantity = 1.0
|
||||
if last_trade and last_trade.get("quantity"):
|
||||
suggested_quantity = last_trade["quantity"]
|
||||
|
||||
equity = _compute_equity(state, price_hint=current_price)
|
||||
trade_action = (action or "BUY").upper()
|
||||
guards = _calculate_smart_guards(
|
||||
symbol,
|
||||
trade_action,
|
||||
current_price,
|
||||
suggested_quantity,
|
||||
equity,
|
||||
)
|
||||
|
||||
market_context = {
|
||||
"current_price": current_price,
|
||||
"equity": equity,
|
||||
"cash": state.get("cash", 0.0),
|
||||
"position": state.get("position"),
|
||||
}
|
||||
|
||||
return SmartPreFillResponse(
|
||||
symbol=symbol,
|
||||
suggested_quantity=suggested_quantity,
|
||||
current_price=current_price,
|
||||
suggested_guards={
|
||||
"stop_loss": guards.stop_loss_price,
|
||||
"take_profit": guards.take_profit_price,
|
||||
"risk_percent": guards.risk_percent,
|
||||
"reasoning": guards.reasoning,
|
||||
"confidence": guards.confidence,
|
||||
},
|
||||
last_trade_context=last_trade,
|
||||
market_context=market_context,
|
||||
confidence=0.80,
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to generate pre-fill suggestions: {str(e)}",
|
||||
)
|
||||
|
||||
|
||||
@router.post("/execute", response_model=SmartTradeResponse)
|
||||
async def execute_smart_trade(
|
||||
request: SmartTradeRequest,
|
||||
db: Session = Depends(get_db),
|
||||
user_id: str = "default",
|
||||
) -> SmartTradeResponse:
|
||||
"""Execute a trade through the unified smart trade hub using the persistent state."""
|
||||
try:
|
||||
source = request.source or TradeSource.MANUAL
|
||||
if request.image_data:
|
||||
source = TradeSource.OCR
|
||||
elif request.voice_data:
|
||||
source = TradeSource.VOICE
|
||||
|
||||
price = request.price or _get_current_market_price(request.symbol)
|
||||
state = load_simulation_state(db, user_id)
|
||||
|
||||
last_trade = _get_last_trade_defaults(state) if request.use_last_trade_defaults else None
|
||||
quantity = request.quantity
|
||||
if quantity is None:
|
||||
if last_trade and last_trade.get("quantity"):
|
||||
quantity = last_trade["quantity"]
|
||||
else:
|
||||
quantity = 1.0
|
||||
|
||||
equity = _compute_equity(state, price_hint=price)
|
||||
|
||||
guards_applied = False
|
||||
guards_suggested: Optional[Dict[str, Any]] = None
|
||||
guards: Optional[SmartGuardSuggestion] = None
|
||||
|
||||
if request.apply_smart_guards:
|
||||
guards = _calculate_smart_guards(
|
||||
request.symbol,
|
||||
request.action,
|
||||
price,
|
||||
quantity,
|
||||
equity,
|
||||
)
|
||||
|
||||
if request.stop_loss is None:
|
||||
request.stop_loss = guards.stop_loss_price
|
||||
guards_applied = True
|
||||
|
||||
if request.take_profit is None:
|
||||
request.take_profit = guards.take_profit_price
|
||||
guards_applied = True
|
||||
|
||||
if request.risk_percent is None:
|
||||
request.risk_percent = guards.risk_percent
|
||||
guards_applied = True
|
||||
|
||||
if guards.position_size != quantity:
|
||||
quantity = guards.position_size
|
||||
guards_applied = True
|
||||
|
||||
guards_suggested = guards.model_dump()
|
||||
|
||||
if request.action == "CLOSE":
|
||||
position = state.get("position")
|
||||
if not position:
|
||||
raise HTTPException(status_code=400, detail="No position to close")
|
||||
|
||||
request.action = "SELL"
|
||||
quantity = position.get("quantity", 0.0) or 0.0
|
||||
|
||||
try:
|
||||
validate_order(state, request.action, quantity, price)
|
||||
except ValueError as ve:
|
||||
raise HTTPException(status_code=400, detail=str(ve))
|
||||
|
||||
persistent_request = PersistentTradeRequest(
|
||||
action=request.action,
|
||||
quantity=quantity,
|
||||
price=price,
|
||||
symbol=request.symbol,
|
||||
notes=request.notes,
|
||||
stop_loss=request.stop_loss,
|
||||
take_profit=request.take_profit,
|
||||
source=source,
|
||||
platform=request.platform,
|
||||
risk_percent=request.risk_percent,
|
||||
entry_time=request.entry_time,
|
||||
)
|
||||
|
||||
result = await persistent_execute_trade(persistent_request, db=db, user_id=user_id)
|
||||
trade_info = result["trade"]
|
||||
portfolio = result["portfolio"]
|
||||
|
||||
total_cost = trade_info.get("total", quantity * price)
|
||||
executed_ts = trade_info.get("timestamp")
|
||||
executed_at = (
|
||||
datetime.fromtimestamp(executed_ts, tz=timezone.utc).isoformat()
|
||||
if executed_ts
|
||||
else datetime.now(timezone.utc).isoformat()
|
||||
)
|
||||
|
||||
position_after = portfolio.get("position") or {}
|
||||
position_size = position_after.get("quantity")
|
||||
unrealized_pnl = position_after.get("unrealized_pnl")
|
||||
total_equity = _compute_equity(portfolio, price_hint=price)
|
||||
|
||||
return SmartTradeResponse(
|
||||
trade_id=trade_info["id"],
|
||||
action=trade_info["action"],
|
||||
symbol=request.symbol,
|
||||
quantity=trade_info["quantity"],
|
||||
price=trade_info["price"],
|
||||
stop_loss=trade_info.get("stop_loss"),
|
||||
take_profit=trade_info.get("take_profit"),
|
||||
risk_percent=trade_info.get("risk_percent"),
|
||||
source=source,
|
||||
executed_at=executed_at,
|
||||
total_cost=total_cost,
|
||||
guards_applied=guards_applied,
|
||||
guards_suggested=guards_suggested,
|
||||
prefill_used=request.use_last_trade_defaults,
|
||||
remaining_cash=portfolio.get("cash", 0.0),
|
||||
total_equity=total_equity,
|
||||
position_size=position_size,
|
||||
unrealized_pnl=unrealized_pnl,
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to execute smart trade: {str(e)}",
|
||||
)
|
||||
|
||||
|
||||
@router.get("/suggestions", response_model=SmartGuardSuggestion)
|
||||
async def get_guard_suggestions(
|
||||
symbol: str = Query("XAU/USD"),
|
||||
action: str = Query("BUY"),
|
||||
quantity: float = Query(1.0),
|
||||
price: Optional[float] = Query(None),
|
||||
db: Session = Depends(get_db),
|
||||
user_id: str = "default",
|
||||
) -> SmartGuardSuggestion:
|
||||
"""Get AI-suggested stop loss and take profit guards using persistent state."""
|
||||
try:
|
||||
if price is None:
|
||||
price = _get_current_market_price(symbol)
|
||||
|
||||
state = load_simulation_state(db, user_id)
|
||||
equity = _compute_equity(state, price_hint=price)
|
||||
|
||||
return _calculate_smart_guards(symbol, action, price, quantity, equity)
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to calculate guard suggestions: {str(e)}",
|
||||
)
|
||||
|
||||
|
||||
@router.get("/history")
|
||||
async def get_trade_history(
|
||||
limit: int = Query(50, ge=1, le=500),
|
||||
source: Optional[str] = Query(None),
|
||||
db: Session = Depends(get_db),
|
||||
user_id: str = "default",
|
||||
) -> Dict:
|
||||
"""Get trade history with optional source filtering from persisted trades."""
|
||||
try:
|
||||
state = load_simulation_state(db, user_id)
|
||||
trades = state.get("trades", [])
|
||||
|
||||
if source:
|
||||
trades = [t for t in trades if t.get("source") == source]
|
||||
|
||||
trades = trades[-limit:]
|
||||
|
||||
sources = {
|
||||
(t.get("source") or "unknown")
|
||||
for t in state.get("trades", [])
|
||||
}
|
||||
|
||||
return {
|
||||
"trades": trades,
|
||||
"total": len(trades),
|
||||
"sources": sorted(sources),
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to retrieve trade history: {str(e)}",
|
||||
)
|
||||
Reference in New Issue
Block a user