Files
Krikorios 48e60d015f 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
2025-11-27 10:23:58 +02:00

496 lines
16 KiB
Python

"""
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)}",
)