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:
Krikorios
2025-11-27 10:23:58 +02:00
parent b5e2b02cb8
commit 48e60d015f
2019 changed files with 39793 additions and 257 deletions
+79
View File
@@ -0,0 +1,79 @@
from __future__ import annotations
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel, Field, validator
from app.services.broker_bridge import BrokerError, broker_bridge_service
router = APIRouter(prefix="/brokers", tags=["Brokers"])
class ConnectRequest(BaseModel):
provider_id: str = Field(..., description="Broker provider identifier")
api_key: str = Field(..., description="API key or session token")
account_id: str = Field(..., description="Broker account identifier/login")
demo: bool = Field(True, description="If true, stays in practice/demo mode when supported")
@validator("provider_id")
def _trim(cls, value: str) -> str:
value = value.strip()
if not value:
raise ValueError("provider_id is required")
return value
class OrderRequest(BaseModel):
action: str
symbol: str
quantity: float
price: float
type: str | None = None
stopLoss: float | None = None
takeProfit: float | None = None
@router.get("/providers")
async def list_providers():
return broker_bridge_service.list_providers()
@router.get("/session")
async def get_session():
return broker_bridge_service.get_session()
@router.post("/connect")
async def connect(request: ConnectRequest):
try:
return await broker_bridge_service.connect(
request.provider_id,
{
"api_key": request.api_key,
"account_id": request.account_id,
"demo": request.demo,
},
)
except BrokerError as exc: # pragma: no cover - depends on environment
raise HTTPException(status_code=400, detail=str(exc)) from exc
@router.post("/disconnect")
async def disconnect():
await broker_bridge_service.disconnect()
return {"status": "disconnected"}
@router.post("/orders")
async def place_order(request: OrderRequest):
try:
return await broker_bridge_service.place_order(request.dict())
except BrokerError as exc: # pragma: no cover
raise HTTPException(status_code=400, detail=str(exc)) from exc
@router.post("/sync")
async def sync_positions():
try:
return await broker_bridge_service.sync_positions()
except BrokerError as exc: # pragma: no cover
raise HTTPException(status_code=400, detail=str(exc)) from exc
+531
View File
@@ -0,0 +1,531 @@
"""
Trading Journal API
Handles daily/weekly plans, manual trade logging, journal entries, and decision logging
"""
from fastapi import APIRouter, HTTPException, Depends, UploadFile, File
from sqlalchemy.orm import Session
from sqlalchemy import and_, desc
from typing import List, Optional
from datetime import date, datetime, timedelta
from pydantic import BaseModel
import os
import shutil
import uuid
from app.db.database import get_db
from app.models.models import (
TradingPlan,
ManualTrade,
JournalEntry,
DecisionLog,
WeeklyPlan,
TradeAction
)
router = APIRouter(prefix="/api/journal", tags=["Trading Journal"])
# Pydantic Schemas
class TradingPlanCreate(BaseModel):
plan_date: date
plan_type: str = "daily"
market_bias: str
daily_target: Optional[float] = None
max_loss: Optional[float] = None
entry_zone_min: Optional[float] = None
entry_zone_max: Optional[float] = None
target_price: Optional[float] = None
stop_loss: Optional[float] = None
support_levels: List[float] = []
resistance_levels: List[float] = []
trading_notes: Optional[str] = None
max_trades: int = 3
ai_generated: bool = False
ai_confidence: Optional[float] = None
context_metrics: Optional[dict] = None
class TradingPlanUpdate(BaseModel):
market_bias: Optional[str] = None
daily_target: Optional[float] = None
max_loss: Optional[float] = None
entry_zone_min: Optional[float] = None
entry_zone_max: Optional[float] = None
target_price: Optional[float] = None
stop_loss: Optional[float] = None
support_levels: Optional[List[float]] = None
resistance_levels: Optional[List[float]] = None
trading_notes: Optional[str] = None
max_trades: Optional[int] = None
actual_trades: Optional[int] = None
actual_pnl: Optional[float] = None
plan_followed: Optional[bool] = None
class ManualTradeCreate(BaseModel):
plan_id: Optional[int] = None
symbol: str = "XAUUSD"
action: str # BUY or SELL
entry_price: float
exit_price: Optional[float] = None
quantity: float
broker: Optional[str] = None
pnl: Optional[float] = None
pnl_percent: Optional[float] = None
notes: Optional[str] = None
followed_plan: bool = True
entry_time: Optional[datetime] = None
exit_time: Optional[datetime] = None
class ManualTradeUpdate(BaseModel):
exit_price: Optional[float] = None
pnl: Optional[float] = None
pnl_percent: Optional[float] = None
notes: Optional[str] = None
exit_time: Optional[datetime] = None
class JournalEntryCreate(BaseModel):
entry_date: date
mood: Optional[str] = None
energy_level: Optional[int] = None
stress_level: Optional[int] = None
lessons_learned: Optional[str] = None
what_went_well: Optional[str] = None
what_to_improve: Optional[str] = None
tomorrow_focus: Optional[str] = None
mistakes_made: Optional[str] = None
market_conditions: Optional[str] = None
market_notes: Optional[str] = None
class DecisionLogCreate(BaseModel):
ai_recommendation: Optional[str] = None
ai_confidence: Optional[float] = None
ai_reasoning: Optional[str] = None
trader_action: Optional[str] = None
trade_id: Optional[int] = None
outcome: Optional[str] = None
outcome_pnl: Optional[float] = None
notes: Optional[str] = None
class WeeklyPlanCreate(BaseModel):
week_start_date: date
year: int
week_number: int
market_outlook: Optional[str] = None
key_events: List[dict] = []
major_levels: List[float] = []
weekly_target: Optional[float] = None
max_weekly_loss: Optional[float] = None
target_trade_count: Optional[int] = None
primary_strategy: Optional[str] = None
focus_areas: Optional[str] = None
risks_to_watch: Optional[str] = None
# Trading Plans Endpoints
@router.post("/plans", status_code=201)
async def create_trading_plan(
plan: TradingPlanCreate,
db: Session = Depends(get_db)
):
"""Create a new daily/weekly trading plan"""
db_plan = TradingPlan(**plan.dict())
db.add(db_plan)
db.commit()
db.refresh(db_plan)
return db_plan
@router.get("/plans/today")
async def get_today_plan(db: Session = Depends(get_db)):
"""Get today's trading plan"""
today = date.today()
plan = db.query(TradingPlan).filter(
and_(
TradingPlan.plan_date == today,
TradingPlan.plan_type == "daily"
)
).first()
if not plan:
raise HTTPException(status_code=404, detail="No plan found for today")
return plan
@router.get("/plans/date/{plan_date}")
async def get_plan_by_date(
plan_date: date,
db: Session = Depends(get_db)
):
"""Get trading plan for a specific date"""
plan = db.query(TradingPlan).filter(
TradingPlan.plan_date == plan_date
).first()
if not plan:
raise HTTPException(status_code=404, detail=f"No plan found for {plan_date}")
return plan
@router.get("/plans")
async def get_plans(
limit: int = 30,
offset: int = 0,
db: Session = Depends(get_db)
):
"""Get recent trading plans"""
plans = db.query(TradingPlan).order_by(
desc(TradingPlan.plan_date)
).limit(limit).offset(offset).all()
return {"plans": plans, "total": db.query(TradingPlan).count()}
@router.put("/plans/{plan_id}")
async def update_trading_plan(
plan_id: int,
plan_update: TradingPlanUpdate,
db: Session = Depends(get_db)
):
"""Update an existing trading plan"""
db_plan = db.query(TradingPlan).filter(TradingPlan.id == plan_id).first()
if not db_plan:
raise HTTPException(status_code=404, detail="Plan not found")
update_data = plan_update.dict(exclude_unset=True)
for key, value in update_data.items():
setattr(db_plan, key, value)
db.commit()
db.refresh(db_plan)
return db_plan
@router.delete("/plans/{plan_id}")
async def delete_trading_plan(
plan_id: int,
db: Session = Depends(get_db)
):
"""Delete a trading plan"""
db_plan = db.query(TradingPlan).filter(TradingPlan.id == plan_id).first()
if not db_plan:
raise HTTPException(status_code=404, detail="Plan not found")
db.delete(db_plan)
db.commit()
return {"message": "Plan deleted successfully"}
# Manual Trades Endpoints
@router.post("/trades", status_code=201)
async def create_manual_trade(
trade: ManualTradeCreate,
db: Session = Depends(get_db)
):
"""Log a manual trade from broker platform"""
try:
action_enum = TradeAction[trade.action.upper()]
except KeyError:
raise HTTPException(status_code=400, detail=f"Invalid action: {trade.action}")
trade_dict = trade.dict()
trade_dict['action'] = action_enum
db_trade = ManualTrade(**trade_dict)
db.add(db_trade)
# Update plan if linked
if trade.plan_id:
plan = db.query(TradingPlan).filter(TradingPlan.id == trade.plan_id).first()
if plan:
plan.actual_trades += 1
if trade.pnl is not None:
plan.actual_pnl += trade.pnl
db.commit()
db.refresh(db_trade)
return db_trade
@router.get("/trades")
async def get_manual_trades(
limit: int = 50,
offset: int = 0,
plan_id: Optional[int] = None,
db: Session = Depends(get_db)
):
"""Get manual trades, optionally filtered by plan"""
query = db.query(ManualTrade)
if plan_id:
query = query.filter(ManualTrade.plan_id == plan_id)
trades = query.order_by(desc(ManualTrade.created_at)).limit(limit).offset(offset).all()
total = query.count()
return {"trades": trades, "total": total}
@router.get("/trades/{trade_id}")
async def get_manual_trade(
trade_id: int,
db: Session = Depends(get_db)
):
"""Get a specific manual trade"""
trade = db.query(ManualTrade).filter(ManualTrade.id == trade_id).first()
if not trade:
raise HTTPException(status_code=404, detail="Trade not found")
return trade
@router.put("/trades/{trade_id}")
async def update_manual_trade(
trade_id: int,
trade_update: ManualTradeUpdate,
db: Session = Depends(get_db)
):
"""Update a manual trade (e.g., closing a position)"""
db_trade = db.query(ManualTrade).filter(ManualTrade.id == trade_id).first()
if not db_trade:
raise HTTPException(status_code=404, detail="Trade not found")
update_data = trade_update.dict(exclude_unset=True)
# Calculate PnL if exit price provided
if 'exit_price' in update_data and db_trade.exit_price is None:
exit_price = update_data['exit_price']
if db_trade.action == TradeAction.BUY:
pnl = (exit_price - db_trade.entry_price) * db_trade.quantity
else: # SELL
pnl = (db_trade.entry_price - exit_price) * db_trade.quantity
update_data['pnl'] = round(pnl, 2)
update_data['pnl_percent'] = round((pnl / (db_trade.entry_price * db_trade.quantity)) * 100, 2)
# Update plan PnL
if db_trade.plan_id:
plan = db.query(TradingPlan).filter(TradingPlan.id == db_trade.plan_id).first()
if plan:
plan.actual_pnl += pnl
for key, value in update_data.items():
setattr(db_trade, key, value)
db.commit()
db.refresh(db_trade)
return db_trade
@router.post("/trades/{trade_id}/screenshot")
async def upload_trade_screenshot(
trade_id: int,
file: UploadFile = File(...),
db: Session = Depends(get_db)
):
"""Upload a screenshot for a trade"""
db_trade = db.query(ManualTrade).filter(ManualTrade.id == trade_id).first()
if not db_trade:
raise HTTPException(status_code=404, detail="Trade not found")
# Create uploads directory if it doesn't exist
upload_dir = "uploads/trade_screenshots"
os.makedirs(upload_dir, exist_ok=True)
# Generate unique filename
file_extension = os.path.splitext(file.filename)[1]
unique_filename = f"{trade_id}_{uuid.uuid4()}{file_extension}"
file_path = os.path.join(upload_dir, unique_filename)
# Save file
with open(file_path, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
# Update trade record
db_trade.screenshot_url = file_path
db.commit()
return {"filename": unique_filename, "path": file_path}
# Journal Entries Endpoints
@router.post("/entries", status_code=201)
async def create_journal_entry(
entry: JournalEntryCreate,
db: Session = Depends(get_db)
):
"""Create a daily journal entry"""
# Check if entry for this date already exists
existing = db.query(JournalEntry).filter(
JournalEntry.entry_date == entry.entry_date
).first()
if existing:
# Update existing entry
update_data = entry.dict(exclude_unset=True)
for key, value in update_data.items():
setattr(existing, key, value)
db.commit()
db.refresh(existing)
return existing
db_entry = JournalEntry(**entry.dict())
db.add(db_entry)
db.commit()
db.refresh(db_entry)
return db_entry
@router.get("/entries/today")
async def get_today_journal(db: Session = Depends(get_db)):
"""Get today's journal entry"""
today = date.today()
entry = db.query(JournalEntry).filter(
JournalEntry.entry_date == today
).first()
if not entry:
raise HTTPException(status_code=404, detail="No journal entry for today")
return entry
@router.get("/entries")
async def get_journal_entries(
limit: int = 30,
offset: int = 0,
db: Session = Depends(get_db)
):
"""Get recent journal entries"""
entries = db.query(JournalEntry).order_by(
desc(JournalEntry.entry_date)
).limit(limit).offset(offset).all()
return {"entries": entries, "total": db.query(JournalEntry).count()}
# Decision Log Endpoints
@router.post("/decisions", status_code=201)
async def create_decision_log(
decision: DecisionLogCreate,
db: Session = Depends(get_db)
):
"""Log a trading decision"""
db_decision = DecisionLog(**decision.dict())
db.add(db_decision)
db.commit()
db.refresh(db_decision)
return db_decision
@router.get("/decisions")
async def get_decisions(
limit: int = 50,
offset: int = 0,
db: Session = Depends(get_db)
):
"""Get recent decisions"""
decisions = db.query(DecisionLog).order_by(
desc(DecisionLog.decision_time)
).limit(limit).offset(offset).all()
return {"decisions": decisions, "total": db.query(DecisionLog).count()}
@router.get("/decisions/accuracy")
async def get_ai_accuracy(
days: int = 30,
db: Session = Depends(get_db)
):
"""Calculate AI recommendation accuracy"""
cutoff_date = datetime.now() - timedelta(days=days)
decisions = db.query(DecisionLog).filter(
and_(
DecisionLog.decision_time >= cutoff_date,
DecisionLog.trader_action == "FOLLOWED",
DecisionLog.outcome.isnot(None)
)
).all()
if not decisions:
return {
"total_decisions": 0,
"accuracy": 0.0,
"win_rate": 0.0,
"avg_pnl": 0.0
}
wins = sum(1 for d in decisions if d.outcome == "WIN")
total_pnl = sum(d.outcome_pnl for d in decisions if d.outcome_pnl is not None)
return {
"total_decisions": len(decisions),
"wins": wins,
"losses": len(decisions) - wins,
"win_rate": round((wins / len(decisions)) * 100, 2),
"avg_pnl": round(total_pnl / len(decisions), 2) if decisions else 0,
"total_pnl": round(total_pnl, 2)
}
# Weekly Plans Endpoints
@router.post("/weekly-plans", status_code=201)
async def create_weekly_plan(
plan: WeeklyPlanCreate,
db: Session = Depends(get_db)
):
"""Create a weekly trading plan"""
db_plan = WeeklyPlan(**plan.dict())
db.add(db_plan)
db.commit()
db.refresh(db_plan)
return db_plan
@router.get("/weekly-plans/current")
async def get_current_week_plan(db: Session = Depends(get_db)):
"""Get this week's plan"""
today = date.today()
# Get Monday of current week
monday = today - timedelta(days=today.weekday())
plan = db.query(WeeklyPlan).filter(
WeeklyPlan.week_start_date == monday
).first()
if not plan:
raise HTTPException(status_code=404, detail="No plan found for current week")
return plan
@router.get("/weekly-plans")
async def get_weekly_plans(
limit: int = 12,
db: Session = Depends(get_db)
):
"""Get recent weekly plans"""
plans = db.query(WeeklyPlan).order_by(
desc(WeeklyPlan.week_start_date)
).limit(limit).all()
return {"plans": plans, "total": db.query(WeeklyPlan).count()}
+413
View File
@@ -0,0 +1,413 @@
"""
Live Performance Dashboard API - Real-time plan monitoring and alerts
"""
from fastapi import APIRouter, HTTPException, Depends, Query
from sqlalchemy.orm import Session
from typing import Any, Dict, List, Optional, Literal
from datetime import datetime, date, timezone
from pydantic import BaseModel, Field
from app.db.database import get_db
from app.models.models import DailyChecklist, UserProfile
from app.services.simulation_state import load_simulation_state
router = APIRouter(prefix="/api/live-dashboard", tags=["Live Dashboard"])
class DailyPlanStatus(BaseModel):
"""Current status of today's trading plan"""
date: str
target: float
actual_pnl: float
progress_percent: float
max_loss: float
current_drawdown: float
max_trades: int
actual_trades: int
trades_remaining: int
status: Literal["on-track", "near-limit", "limit-reached", "target-met"]
alerts: List[str]
class PerformanceWidget(BaseModel):
"""Sticky dashboard widget data"""
daily_plan: DailyPlanStatus
position_summary: Dict
risk_metrics: Dict
alerts: List[Dict]
recommendations: List[str]
class AlertConfig(BaseModel):
"""Alert configuration"""
alert_type: str # trade_limit, loss_limit, target_achieved, break_recommended
enabled: bool
threshold: Optional[float] = None
message: str
# In-memory simulation state (shared with trading.py)
def _get_today_plan_from_storage() -> Optional[Dict]:
"""Get today's trading plan blueprint (defaults until persistence is added)."""
# In production, this would query the database
# For now, we'll use a default plan structure
return {
"date": date.today().isoformat(),
"daily_target": 500.0,
"max_loss": 250.0,
"max_trades": 3,
"bias": "NEUTRAL",
}
def _calculate_daily_pnl(trades: List[Dict[str, Any]], target_date: date | None = None) -> float:
"""Calculate P&L for trades executed on the target date"""
target_date = target_date or date.today()
daily_pnl = 0.0
for trade in trades:
trade_ts = trade.get("timestamp", 0)
trade_date = datetime.fromtimestamp(trade_ts, tz=timezone.utc).date()
if trade_date == target_date:
pnl = trade.get("pnl", 0.0)
if pnl:
daily_pnl += pnl
return daily_pnl
def _count_today_trades(trades: List[Dict[str, Any]], target_date: date | None = None) -> int:
"""Count trades executed on the target date"""
target_date = target_date or date.today()
count = 0
for trade in trades:
trade_ts = trade.get("timestamp", 0)
trade_date = datetime.fromtimestamp(trade_ts, tz=timezone.utc).date()
if trade_date == target_date:
count += 1
return count
def _generate_alerts(plan: Dict, actual_pnl: float, trades_count: int) -> List[str]:
"""Generate smart alerts based on plan vs actual"""
alerts = []
target = plan.get("daily_target", 500.0)
max_loss = plan.get("max_loss", 250.0)
max_trades = plan.get("max_trades", 3)
# Trade limit alerts
trades_remaining = max_trades - trades_count
if trades_remaining == 1:
alerts.append(f"⚠️ Only 1 trade remaining before daily limit")
elif trades_remaining <= 0:
alerts.append(f"🛑 Daily trade limit reached ({max_trades} trades)")
# Loss alerts
if actual_pnl < 0:
loss_percent = (abs(actual_pnl) / max_loss) * 100
if loss_percent >= 100:
alerts.append(f"🚨 Max loss limit reached (${abs(actual_pnl):.2f})")
elif loss_percent >= 80:
alerts.append(f"⚠️ Near max loss limit ({loss_percent:.0f}% of ${max_loss})")
elif loss_percent >= 50:
alerts.append(f"⚡ Drawdown at {loss_percent:.0f}% of max loss")
# Target achievement alerts
if actual_pnl > 0:
progress_percent = (actual_pnl / target) * 100
if progress_percent >= 100:
alerts.append(f"🎉 Daily target achieved! (+${actual_pnl:.2f})")
elif progress_percent >= 80:
alerts.append(f"🎯 ${target - actual_pnl:.2f} away from daily target")
# Trading duration alerts (if 2+ hours and significant losses)
if trades_count >= 2 and actual_pnl < -100:
alerts.append(f"💡 Consider taking a break. ${abs(actual_pnl):.2f} in losses after {trades_count} trades")
return alerts
def _determine_status(
actual_pnl: float,
target: float,
max_loss: float,
trades_count: int,
max_trades: int
) -> Literal["on-track", "near-limit", "limit-reached", "target-met"]:
"""Determine overall plan status"""
# Target met
if actual_pnl >= target:
return "target-met"
# Limits reached
if trades_count >= max_trades:
return "limit-reached"
if actual_pnl <= -max_loss:
return "limit-reached"
# Near limits
loss_percent = (abs(actual_pnl) / max_loss) * 100 if actual_pnl < 0 else 0
trades_percent = (trades_count / max_trades) * 100
if loss_percent >= 80 or trades_percent >= 80:
return "near-limit"
# On track
return "on-track"
@router.get("/status", response_model=DailyPlanStatus)
async def get_dashboard_status(db: Session = Depends(get_db)) -> DailyPlanStatus:
"""
Get current status of today's trading plan with real-time metrics
"""
try:
plan = _get_today_plan_from_storage()
if not plan:
raise HTTPException(status_code=404, detail="No trading plan found for today")
state = load_simulation_state(db)
trades = state.get("trades", [])
actual_pnl = _calculate_daily_pnl(trades)
trades_count = _count_today_trades(trades)
target = plan.get("daily_target", 500.0)
max_loss = plan.get("max_loss", 250.0)
max_trades = plan.get("max_trades", 3)
progress_percent = (actual_pnl / target) * 100 if target > 0 else 0
current_drawdown = abs(actual_pnl) if actual_pnl < 0 else 0
trades_remaining = max(0, max_trades - trades_count)
alerts = _generate_alerts(plan, actual_pnl, trades_count)
status = _determine_status(actual_pnl, target, max_loss, trades_count, max_trades)
return DailyPlanStatus(
date=plan["date"],
target=target,
actual_pnl=actual_pnl,
progress_percent=round(progress_percent, 1),
max_loss=max_loss,
current_drawdown=current_drawdown,
max_trades=max_trades,
actual_trades=trades_count,
trades_remaining=trades_remaining,
status=status,
alerts=alerts,
)
except HTTPException:
raise
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Failed to get dashboard status: {str(e)}"
)
@router.get("/widget", response_model=PerformanceWidget)
async def get_performance_widget(db: Session = Depends(get_db)) -> PerformanceWidget:
"""
Get complete performance widget data for sticky dashboard
"""
try:
# Get daily plan status
daily_plan = await get_dashboard_status(db=db)
state = load_simulation_state(db)
position = state.get("position")
cash = float(state.get("cash", 100000.0))
position_value = 0.0
if position:
position_value = float(position.get("quantity", 0.0)) * float(position.get("avg_price", 0.0))
total_equity = cash + position_value
position_summary = {
"has_position": position is not None,
"quantity": float(position.get("quantity", 0.0)) if position else 0,
"avg_price": float(position.get("avg_price", 0.0)) if position else 0,
"cash": cash,
"total_equity": total_equity,
}
# Calculate risk metrics
initial_capital = float(state.get("initial_capital", 100000.0))
safe_equity = total_equity if total_equity != 0 else 1
total_return = ((total_equity - initial_capital) / initial_capital) * 100 if initial_capital else 0
risk_metrics = {
"total_equity": total_equity,
"total_return_percent": round(total_return, 2),
"position_size_percent": round((position_value / safe_equity * 100), 2) if position else 0,
"cash_percent": round((cash / safe_equity * 100), 2),
}
# Generate smart recommendations
recommendations = []
if daily_plan.status == "target-met":
recommendations.append("🎉 Consider closing for the day - target achieved!")
elif daily_plan.status == "limit-reached":
recommendations.append("🛑 Trading halt recommended - daily limits reached")
elif daily_plan.status == "near-limit":
if daily_plan.trades_remaining == 1:
recommendations.append("⚠️ Last trade available - make it count")
if daily_plan.current_drawdown > daily_plan.max_loss * 0.8:
recommendations.append("🔻 Consider defensive position sizing")
else:
if daily_plan.actual_pnl > daily_plan.target * 0.7:
recommendations.append("🎯 Near target - consider taking profits")
# Alert objects with metadata
alert_objects = [
{
"type": "info",
"message": alert,
"timestamp": datetime.now(timezone.utc).isoformat(),
}
for alert in daily_plan.alerts
]
return PerformanceWidget(
daily_plan=daily_plan,
position_summary=position_summary,
risk_metrics=risk_metrics,
alerts=alert_objects,
recommendations=recommendations,
)
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Failed to get performance widget: {str(e)}"
)
@router.post("/check-limits")
async def check_trading_limits(db: Session = Depends(get_db)) -> Dict:
"""
Check if trading should be halted based on plan limits
Returns: {can_trade: bool, reason: str}
"""
try:
plan = _get_today_plan_from_storage()
if not plan:
return {"can_trade": True, "reason": "No plan configured"}
state = load_simulation_state(db)
trades = state.get("trades", [])
actual_pnl = _calculate_daily_pnl(trades)
trades_count = _count_today_trades(trades)
max_loss = plan.get("max_loss", 250.0)
max_trades = plan.get("max_trades", 3)
target = plan.get("daily_target", 500.0)
if actual_pnl <= -max_loss:
return {
"can_trade": False,
"reason": f"Max loss limit reached (${abs(actual_pnl):.2f})",
"limit_type": "loss",
}
if trades_count >= max_trades:
return {
"can_trade": False,
"reason": f"Max trades limit reached ({trades_count}/{max_trades})",
"limit_type": "trades",
}
if actual_pnl >= target:
return {
"can_trade": True,
"reason": f"Target achieved (+${actual_pnl:.2f}) - consider closing for the day",
"warning": True,
}
return {
"can_trade": True,
"reason": "Within limits",
"remaining_trades": max_trades - trades_count,
"remaining_loss_buffer": max_loss + actual_pnl,
}
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Failed to check trading limits: {str(e)}"
)
@router.get("/session-summary")
async def get_session_summary(db: Session = Depends(get_db)) -> Dict:
"""
Get end-of-day session summary with AI coaching suggestions
"""
try:
plan = _get_today_plan_from_storage()
state = load_simulation_state(db)
trades = state.get("trades", [])
actual_pnl = _calculate_daily_pnl(trades)
trades_count = _count_today_trades(trades)
if not plan:
raise HTTPException(status_code=404, detail="No trading plan found")
target = plan.get("daily_target", 500.0)
max_loss = plan.get("max_loss", 250.0)
target_achieved = actual_pnl >= target
within_limits = actual_pnl > -max_loss and trades_count <= plan.get("max_trades", 3)
today = date.today()
today_trades = [
t for t in trades
if datetime.fromtimestamp(t.get("timestamp", 0), tz=timezone.utc).date() == today
]
winning_trades = sum(1 for t in today_trades if t.get("pnl", 0) > 0)
win_rate = (winning_trades / len(today_trades) * 100) if today_trades else 0
coaching = []
if target_achieved:
coaching.append("✅ Excellent discipline - you met your daily target!")
else:
deficit = target - actual_pnl
coaching.append(f"📊 ${deficit:.2f} short of target. Review your entry setups.")
if win_rate >= 60:
coaching.append(f"🎯 Strong win rate ({win_rate:.0f}%). Keep following your strategy.")
elif win_rate < 40:
coaching.append(f"⚠️ Low win rate ({win_rate:.0f}%). Review your trade selection criteria.")
if not within_limits:
coaching.append("🔻 Limits exceeded. Focus on risk management tomorrow.")
if trades_count > plan.get("max_trades", 3):
coaching.append("⚠️ Over-trading detected. Stick to your max trades limit.")
return {
"date": plan["date"],
"summary": {
"target": target,
"actual_pnl": actual_pnl,
"target_achieved": target_achieved,
"within_limits": within_limits,
"trades_count": trades_count,
"win_rate": round(win_rate, 1),
},
"coaching": coaching,
"next_session_suggestions": [
"Review today's winning trades for patterns",
"Adjust stop loss strategy if needed",
"Focus on high-probability setups only",
],
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Failed to generate session summary: {str(e)}"
)
+125
View File
@@ -0,0 +1,125 @@
"""
Ollama API endpoints for local AI status and simple tasks.
"""
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from typing import Optional, List
from app.services.ollama_service import ollama_service
from app.config import settings
router = APIRouter(prefix="/api/ollama", tags=["Local AI"])
class OllamaStatus(BaseModel):
available: bool
model: str
embed_model: str
base_url: str
class GenerateRequest(BaseModel):
prompt: str
system: Optional[str] = None
temperature: float = 0.7
max_tokens: int = 500
class GenerateResponse(BaseModel):
response: Optional[str]
model: str
success: bool
class SentimentRequest(BaseModel):
text: str
class SentimentResponse(BaseModel):
sentiment: Optional[str]
confidence: Optional[float]
success: bool
class ClassifyRequest(BaseModel):
text: str
categories: List[str]
class ClassifyResponse(BaseModel):
category: Optional[str]
success: bool
class SummarizeRequest(BaseModel):
text: str
max_sentences: int = 2
class SummarizeResponse(BaseModel):
summary: Optional[str]
success: bool
@router.get("/status", response_model=OllamaStatus)
async def get_ollama_status():
"""Check if Ollama is available and configured."""
available = await ollama_service.is_available()
return OllamaStatus(
available=available,
model=settings.OLLAMA_MODEL,
embed_model=settings.OLLAMA_MODEL_EMBED,
base_url=settings.OLLAMA_BASE_URL
)
@router.post("/generate", response_model=GenerateResponse)
async def generate_text(request: GenerateRequest):
"""Generate text using local Ollama model."""
result = await ollama_service.generate(
prompt=request.prompt,
system=request.system,
temperature=request.temperature,
max_tokens=request.max_tokens
)
return GenerateResponse(
response=result,
model=settings.OLLAMA_MODEL,
success=result is not None
)
@router.post("/sentiment", response_model=SentimentResponse)
async def analyze_sentiment(request: SentimentRequest):
"""Quick sentiment analysis using local model."""
result = await ollama_service.quick_sentiment(request.text)
if result:
return SentimentResponse(
sentiment=result.get("sentiment"),
confidence=result.get("confidence"),
success=True
)
return SentimentResponse(sentiment=None, confidence=None, success=False)
@router.post("/classify", response_model=ClassifyResponse)
async def classify_text(request: ClassifyRequest):
"""Classify text into one of the provided categories."""
result = await ollama_service.quick_classify(request.text, request.categories)
return ClassifyResponse(
category=result,
success=result is not None
)
@router.post("/summarize", response_model=SummarizeResponse)
async def summarize_text(request: SummarizeRequest):
"""Quick text summarization using local model."""
result = await ollama_service.quick_summarize(
text=request.text,
max_sentences=request.max_sentences
)
return SummarizeResponse(
summary=result,
success=result is not None
)
+558
View File
@@ -0,0 +1,558 @@
"""
Position Management Assistant API
Provides intelligent mitigation plans, exit strategies, and risk monitoring for active positions
"""
from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel, Field
from typing import List, Optional, Dict, Literal
from datetime import datetime, timezone, timedelta
import numpy as np
router = APIRouter(prefix="/api/position-assistant", tags=["Position Assistant"])
class ActivePosition(BaseModel):
"""Current active position details"""
symbol: str = Field(default="XAU/USD")
direction: Literal["LONG", "SHORT"]
entry_price: float
quantity: float
stop_loss: float
take_profit: Optional[float] = None
entry_time: str
notes: Optional[str] = None
class MitigationStrategy(BaseModel):
"""Smart mitigation strategy for managing risk"""
strategy_name: str
priority: int # 1 = highest priority
action: str
trigger_price: float
reasoning: str
expected_benefit: str
risk_level: Literal["LOW", "MEDIUM", "HIGH"]
class PriceReversal(BaseModel):
"""Predicted price reversal levels and timing"""
level: float
probability: float # 0-1
timeframe: str # e.g., "2-4 hours", "End of day"
reasoning: str
confluences: List[str]
class PositionHealth(BaseModel):
"""Real-time position health assessment"""
status: Literal["HEALTHY", "AT_RISK", "CRITICAL", "WINNING"]
current_pnl: float
current_pnl_percent: float
distance_to_stop_loss: float
distance_to_stop_loss_percent: float
time_in_trade: str
recommendation: str
urgency: Literal["LOW", "MEDIUM", "HIGH", "URGENT"]
class PositionManagementPlan(BaseModel):
"""Complete position management plan"""
position: ActivePosition
current_price: float
health: PositionHealth
mitigation_strategies: List[MitigationStrategy]
reversal_zones: List[PriceReversal]
exit_plan: Dict
alerts: List[str]
next_actions: List[str]
def _calculate_position_health(
position: ActivePosition,
current_price: float
) -> PositionHealth:
"""Calculate real-time position health"""
# Calculate P&L
if position.direction == "SHORT":
pnl = (position.entry_price - current_price) * position.quantity
pnl_percent = ((position.entry_price - current_price) / position.entry_price) * 100
distance_to_sl = position.stop_loss - current_price
else: # LONG
pnl = (current_price - position.entry_price) * position.quantity
pnl_percent = ((current_price - position.entry_price) / position.entry_price) * 100
distance_to_sl = current_price - position.stop_loss
distance_to_sl_percent = (distance_to_sl / position.entry_price) * 100
# Calculate time in trade
entry_dt = datetime.fromisoformat(position.entry_time.replace('Z', '+00:00'))
now_dt = datetime.now(timezone.utc)
time_diff = now_dt - entry_dt
hours = time_diff.total_seconds() / 3600
if hours < 1:
time_in_trade = f"{int(time_diff.total_seconds() / 60)} minutes"
elif hours < 24:
time_in_trade = f"{hours:.1f} hours"
else:
time_in_trade = f"{hours/24:.1f} days"
# Determine status and urgency
if pnl > 0:
if pnl_percent > 2:
status = "WINNING"
urgency = "LOW"
recommendation = "Consider taking partial profits to secure gains"
else:
status = "HEALTHY"
urgency = "LOW"
recommendation = "Monitor for continuation or reversal signals"
else:
loss_percent_of_sl = abs(pnl_percent) / abs((position.stop_loss - position.entry_price) / position.entry_price * 100)
if loss_percent_of_sl > 0.8:
status = "CRITICAL"
urgency = "URGENT"
recommendation = "CLOSE POSITION NOW or implement emergency mitigation"
elif loss_percent_of_sl > 0.5:
status = "AT_RISK"
urgency = "HIGH"
recommendation = "Consider scaling out or tightening stop loss"
else:
status = "AT_RISK"
urgency = "MEDIUM"
recommendation = "Watch for reversal signals, keep stop loss in place"
return PositionHealth(
status=status,
current_pnl=round(pnl, 2),
current_pnl_percent=round(pnl_percent, 2),
distance_to_stop_loss=round(distance_to_sl, 2),
distance_to_stop_loss_percent=round(distance_to_sl_percent, 2),
time_in_trade=time_in_trade,
recommendation=recommendation,
urgency=urgency
)
def _generate_mitigation_strategies(
position: ActivePosition,
current_price: float,
health: PositionHealth
) -> List[MitigationStrategy]:
"""Generate smart mitigation strategies"""
strategies = []
if position.direction == "SHORT":
# SHORT position mitigation strategies
# Strategy 1: Partial close at break-even
strategies.append(MitigationStrategy(
strategy_name="Break-Even Exit (Partial)",
priority=1,
action=f"Close 50% of position at ${position.entry_price:.2f}",
trigger_price=position.entry_price,
reasoning="Lock in zero loss on half the position if price retraces to entry",
expected_benefit="Reduces risk by 50% while keeping upside exposure",
risk_level="LOW"
))
# Strategy 2: Scale out in profit
if current_price < position.entry_price:
target_1 = position.entry_price - (position.entry_price - current_price) * 1.5
strategies.append(MitigationStrategy(
strategy_name="Scale Out (First Target)",
priority=2,
action=f"Close 30% of position at ${target_1:.2f}",
trigger_price=target_1,
reasoning="Take partial profits at 1.5x current movement",
expected_benefit="Secure profits while maintaining exposure",
risk_level="LOW"
))
# Strategy 3: Move stop to break-even
if health.current_pnl > 0:
strategies.append(MitigationStrategy(
strategy_name="Move Stop to Break-Even",
priority=3,
action=f"Move stop loss from ${position.stop_loss:.2f} to ${position.entry_price:.2f}",
trigger_price=current_price,
reasoning="Eliminate downside risk once in profit",
expected_benefit="Cannot lose money on this trade anymore",
risk_level="LOW"
))
# Strategy 4: Emergency hedge
if health.status == "CRITICAL":
hedge_price = position.entry_price + (position.stop_loss - position.entry_price) * 0.5
strategies.append(MitigationStrategy(
strategy_name="Emergency Hedge (LONG)",
priority=1,
action=f"Open LONG position at ${current_price:.2f} (same size)",
trigger_price=current_price,
reasoning="Neutralize the position to stop bleeding while you reassess",
expected_benefit="Stop further losses immediately",
risk_level="HIGH"
))
# Strategy 5: Widen stop temporarily
if health.status == "AT_RISK" and health.urgency == "HIGH":
new_sl = position.stop_loss + (position.stop_loss - position.entry_price) * 0.3
strategies.append(MitigationStrategy(
strategy_name="Temporary Stop Widening",
priority=4,
action=f"Widen stop loss to ${new_sl:.2f} temporarily",
trigger_price=current_price,
reasoning="Give position room to breathe during volatility spike",
expected_benefit="Avoid premature stop-out if reversal is coming",
risk_level="MEDIUM"
))
else: # LONG position
# LONG position mitigation strategies (mirror of SHORT)
strategies.append(MitigationStrategy(
strategy_name="Break-Even Exit (Partial)",
priority=1,
action=f"Close 50% of position at ${position.entry_price:.2f}",
trigger_price=position.entry_price,
reasoning="Lock in zero loss on half the position if price retraces to entry",
expected_benefit="Reduces risk by 50% while keeping upside exposure",
risk_level="LOW"
))
if current_price > position.entry_price:
target_1 = position.entry_price + (current_price - position.entry_price) * 1.5
strategies.append(MitigationStrategy(
strategy_name="Scale Out (First Target)",
priority=2,
action=f"Close 30% of position at ${target_1:.2f}",
trigger_price=target_1,
reasoning="Take partial profits at 1.5x current movement",
expected_benefit="Secure profits while maintaining exposure",
risk_level="LOW"
))
if health.current_pnl > 0:
strategies.append(MitigationStrategy(
strategy_name="Move Stop to Break-Even",
priority=3,
action=f"Move stop loss from ${position.stop_loss:.2f} to ${position.entry_price:.2f}",
trigger_price=current_price,
reasoning="Eliminate downside risk once in profit",
expected_benefit="Cannot lose money on this trade anymore",
risk_level="LOW"
))
# Sort by priority
strategies.sort(key=lambda x: x.priority)
return strategies
def _predict_reversal_zones(
position: ActivePosition,
current_price: float
) -> List[PriceReversal]:
"""Predict potential reversal zones using technical analysis"""
reversals = []
if position.direction == "SHORT":
# For SHORT: Looking for price to drop (reversal down from current)
# Support level 1: 0.5 Fibonacci from entry to current
fib_50 = position.entry_price - (position.entry_price - current_price) * 0.5
if current_price > position.entry_price: # If against us
fib_50 = current_price - (current_price - position.entry_price) * 0.382
reversals.append(PriceReversal(
level=round(fib_50, 2),
probability=0.65,
timeframe="2-4 hours",
reasoning="38.2% Fibonacci retracement - common reversal zone",
confluences=["Fibonacci level", "Potential exhaustion zone"]
))
# Support level 2: Round number below entry
round_number = (int(position.entry_price / 100) * 100) - 100
if round_number < current_price:
reversals.append(PriceReversal(
level=round(round_number, 2),
probability=0.55,
timeframe="4-8 hours",
reasoning="Major round number psychological support",
confluences=["Round number", "Psychological level"]
))
# Support level 3: Previous day low (simulated)
prev_day_low = position.entry_price - (position.entry_price * 0.015) # 1.5% below entry
reversals.append(PriceReversal(
level=round(prev_day_low, 2),
probability=0.70,
timeframe="End of day",
reasoning="Estimated previous day low - strong support",
confluences=["Previous low", "Session support"]
))
else: # LONG
# For LONG: Looking for price to rise (reversal up from current)
fib_50 = position.entry_price + (current_price - position.entry_price) * 0.5
if current_price < position.entry_price: # If against us
fib_50 = current_price + (position.entry_price - current_price) * 0.382
reversals.append(PriceReversal(
level=round(fib_50, 2),
probability=0.65,
timeframe="2-4 hours",
reasoning="38.2% Fibonacci retracement - common reversal zone",
confluences=["Fibonacci level", "Potential exhaustion zone"]
))
round_number = (int(position.entry_price / 100) * 100) + 100
if round_number > current_price:
reversals.append(PriceReversal(
level=round(round_number, 2),
probability=0.55,
timeframe="4-8 hours",
reasoning="Major round number psychological resistance",
confluences=["Round number", "Psychological level"]
))
prev_day_high = position.entry_price + (position.entry_price * 0.015)
reversals.append(PriceReversal(
level=round(prev_day_high, 2),
probability=0.70,
timeframe="End of day",
reasoning="Estimated previous day high - strong resistance",
confluences=["Previous high", "Session resistance"]
))
# Sort by probability (highest first)
reversals.sort(key=lambda x: x.probability, reverse=True)
return reversals
def _create_exit_plan(
position: ActivePosition,
current_price: float,
health: PositionHealth,
reversals: List[PriceReversal]
) -> Dict:
"""Create comprehensive exit plan"""
plan = {
"immediate_action": None,
"optimal_exits": [],
"emergency_exit": None,
"time_based_exit": None
}
if health.status == "CRITICAL":
plan["immediate_action"] = {
"action": "CLOSE IMMEDIATELY",
"reason": "Position is critically at risk",
"price": current_price
}
plan["emergency_exit"] = {
"action": "Market order close if stop loss hit",
"trigger": position.stop_loss,
"loss_amount": health.current_pnl if health.current_pnl < 0 else 0
}
elif health.status == "WINNING":
# Build scaling out plan
if position.direction == "SHORT":
target_1 = current_price - (position.entry_price - current_price) * 0.5
target_2 = current_price - (position.entry_price - current_price) * 1.0
else:
target_1 = current_price + (current_price - position.entry_price) * 0.5
target_2 = current_price + (current_price - position.entry_price) * 1.0
plan["optimal_exits"] = [
{
"level": 1,
"price": round(target_1, 2),
"quantity_percent": 33,
"reason": "First profit target - secure initial gains"
},
{
"level": 2,
"price": round(target_2, 2),
"quantity_percent": 33,
"reason": "Second profit target - let winners run"
},
{
"level": 3,
"price": "Trailing stop",
"quantity_percent": 34,
"reason": "Trail remaining with break-even stop"
}
]
else: # AT_RISK or HEALTHY
# Exit at reversal zones
plan["optimal_exits"] = [
{
"level": i + 1,
"price": rev.level,
"quantity_percent": 100 if i == 0 else 50,
"reason": f"{rev.reasoning} ({int(rev.probability*100)}% probability)"
}
for i, rev in enumerate(reversals[:2])
]
# Time-based exit (end of day or session)
hours_in_trade = (datetime.now(timezone.utc) - datetime.fromisoformat(position.entry_time.replace('Z', '+00:00'))).total_seconds() / 3600
if hours_in_trade > 4 and health.status != "WINNING":
plan["time_based_exit"] = {
"time": "End of trading session",
"action": "Review and consider closing if no reversal",
"reason": "Avoid holding losing position overnight"
}
return plan
@router.post("/analyze", response_model=PositionManagementPlan)
async def analyze_position(
position: ActivePosition,
current_price: float = Query(..., description="Current market price")
) -> PositionManagementPlan:
"""
Analyze active position and provide comprehensive management plan
Example:
```
POST /api/position-assistant/analyze?current_price=4085
{
"direction": "SHORT",
"entry_price": 4070,
"quantity": 1.0,
"stop_loss": 4109,
"entry_time": "2025-11-24T10:00:00Z"
}
```
"""
try:
# Calculate position health
health = _calculate_position_health(position, current_price)
# Generate mitigation strategies
strategies = _generate_mitigation_strategies(position, current_price, health)
# Predict reversal zones
reversals = _predict_reversal_zones(position, current_price)
# Create exit plan
exit_plan = _create_exit_plan(position, current_price, health, reversals)
# Generate alerts
alerts = []
if health.status == "CRITICAL":
alerts.append("🚨 URGENT: Position at critical risk level")
alerts.append(f"⚠️ Stop loss ${abs(health.distance_to_stop_loss):.2f} away")
elif health.status == "AT_RISK" and health.urgency == "HIGH":
alerts.append(f"⚠️ Position down {abs(health.current_pnl_percent):.1f}%")
alerts.append("💡 Consider mitigation strategies")
elif health.status == "WINNING":
alerts.append(f"✅ Position up {health.current_pnl_percent:.1f}%")
alerts.append("🎯 Consider taking partial profits")
# Generate next actions
next_actions = []
if strategies:
top_strategy = strategies[0]
next_actions.append(f"📋 Primary: {top_strategy.action}")
if reversals:
top_reversal = reversals[0]
next_actions.append(f"🎯 Watch for reversal at ${top_reversal.level:.2f} ({top_reversal.timeframe})")
if exit_plan.get("immediate_action"):
next_actions.insert(0, f"🚨 {exit_plan['immediate_action']['action']}")
return PositionManagementPlan(
position=position,
current_price=current_price,
health=health,
mitigation_strategies=strategies,
reversal_zones=reversals,
exit_plan=exit_plan,
alerts=alerts,
next_actions=next_actions
)
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Failed to analyze position: {str(e)}"
)
@router.get("/quick-status")
async def get_quick_status(
direction: str = Query(..., description="LONG or SHORT"),
entry_price: float = Query(...),
current_price: float = Query(...),
stop_loss: float = Query(...)
) -> Dict:
"""
Quick position status check without full analysis
Example:
```
GET /api/position-assistant/quick-status?direction=SHORT&entry_price=4070&current_price=4085&stop_loss=4109
```
"""
try:
# Quick P&L calculation
if direction.upper() == "SHORT":
pnl = entry_price - current_price
pnl_percent = ((entry_price - current_price) / entry_price) * 100
distance_to_sl = stop_loss - current_price
else:
pnl = current_price - entry_price
pnl_percent = ((current_price - entry_price) / entry_price) * 100
distance_to_sl = current_price - stop_loss
distance_to_sl_percent = (distance_to_sl / entry_price) * 100
# Quick status
if pnl > 0:
status = "✅ In Profit"
color = "green"
else:
loss_ratio = abs(distance_to_sl_percent / ((stop_loss - entry_price) / entry_price * 100))
if loss_ratio > 0.8:
status = "🚨 CRITICAL - Close to stop loss"
color = "red"
elif loss_ratio > 0.5:
status = "⚠️ AT RISK"
color = "orange"
else:
status = "📊 Monitoring"
color = "yellow"
return {
"status": status,
"color": color,
"pnl": round(pnl, 2),
"pnl_percent": round(pnl_percent, 2),
"distance_to_stop_loss": round(abs(distance_to_sl), 2),
"distance_to_stop_loss_percent": round(abs(distance_to_sl_percent), 2)
}
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Failed to get quick status: {str(e)}"
)
+27
View File
@@ -0,0 +1,27 @@
from __future__ import annotations
from fastapi import APIRouter, HTTPException, Query
from app.schemas.schemas import PositionMetrics
from app.services.ai_context_builder import ai_context_builder
from app.services.price_anchor import price_anchor_service
router = APIRouter(prefix="/positions", tags=["Positions"])
@router.get("/metrics", response_model=PositionMetrics)
async def get_position_metrics(
symbol: str = Query("XAUUSD", description="Symbol, e.g., XAUUSD or BTCUSDT"),
timeframe: str = Query("1m", description="Timeframe such as 1m,5m,1h"),
limit: int = Query(400, ge=50, le=2000, description="Number of bars to analyze"),
) -> PositionMetrics:
try:
sym = symbol.upper().replace("/", "")
ctx = ai_context_builder.build_request(sym, timeframe, limit)
metrics = ai_context_builder.build_metrics(sym, timeframe, ctx.price_data)
anchor_price = await price_anchor_service.get_anchor_price(sym)
return price_anchor_service.apply_anchor(metrics, anchor_price)
except ValueError as exc:
raise HTTPException(status_code=404, detail=str(exc))
except Exception as exc:
raise HTTPException(status_code=500, detail=f"Failed to compute position metrics: {exc}")
+495
View File
@@ -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)}",
)
+434
View File
@@ -0,0 +1,434 @@
from fastapi import APIRouter, HTTPException, Depends
from sqlalchemy.orm import Session, selectinload
from typing import Dict, Optional, Any, List
from datetime import datetime, timezone
from app.db.database import get_db
from app.models.models import Simulation, Trade, Position, TradeAction, TradeMetadata
from app.services.risk import validate_order
from pydantic import BaseModel
router = APIRouter(prefix="/trading", tags=["Trading"])
# Pydantic models for request/response
class TradeRequest(BaseModel):
action: str
quantity: float
price: float
symbol: str = "XAU/USD"
notes: Optional[str] = None
stop_loss: Optional[float] = None
take_profit: Optional[float] = None
source: Optional[str] = None
platform: Optional[str] = None
risk_percent: Optional[float] = None
entry_time: Optional[str] = None
class PortfolioState(BaseModel):
cash: float
initial_capital: float
position: Optional[Dict[str, Any]] = None
trades: List[Dict[str, Any]]
equity_history: List[Dict[str, Any]]
total_pnl: float
total_pnl_percent: float
def get_or_create_simulation(db: Session, user_id: str = "default") -> Simulation:
"""Get existing simulation or create a new one"""
simulation = db.query(Simulation).filter(Simulation.user_id == user_id).first()
if not simulation:
simulation = Simulation(
user_id=user_id,
symbol="XAU/USD",
initial_capital=100000.0,
current_capital=100000.0,
total_pnl=0.0,
total_pnl_percent=0.0
)
db.add(simulation)
db.commit()
db.refresh(simulation)
return simulation
def _compute_equity_at_price(simulation: Simulation, price: float, db: Session) -> float:
"""Calculate equity based on current position and price"""
position = db.query(Position).filter(
Position.simulation_id == simulation.id
).first()
qty = position.quantity if position else 0.0
return float(simulation.current_capital + qty * price)
def get_portfolio_state_from_db(simulation: Simulation, db: Session) -> PortfolioState:
"""Convert DB simulation to portfolio state"""
# Get current position
position = db.query(Position).filter(
Position.simulation_id == simulation.id
).first()
position_dict = None
if position:
position_dict = {
"symbol": position.symbol,
"quantity": position.quantity,
"avg_price": position.avg_price,
"current_price": position.current_price,
"unrealized_pnl": position.unrealized_pnl,
"unrealized_pnl_percent": position.unrealized_pnl_percent
}
# Get all trades
trades = db.query(Trade).options(selectinload(Trade.details)).filter(
Trade.simulation_id == simulation.id
).order_by(Trade.timestamp).all()
trades_list = []
for trade in trades:
details = trade.details
trades_list.append({
"id": trade.id,
"action": trade.action.value,
"quantity": trade.quantity,
"price": trade.price,
"total": trade.total,
"pnl": trade.pnl,
"timestamp": int(trade.timestamp.timestamp()) if trade.timestamp else None,
"stop_loss": details.stop_loss if details else None,
"take_profit": details.take_profit if details else None,
"notes": details.notes if details else None,
"source": details.source if details else None,
"platform": details.platform if details else None,
"risk_percent": details.risk_percent if details else None,
"entry_time": details.entry_time.isoformat() if details and details.entry_time else None,
})
# Build equity history from trades
equity_history = []
running_equity = simulation.initial_capital
for trade in trades:
if trade.action == TradeAction.SELL and trade.pnl:
running_equity += trade.pnl
equity_history.append({
"time": int(trade.timestamp.timestamp()) if trade.timestamp else 0,
"equity": running_equity
})
return PortfolioState(
cash=simulation.current_capital,
initial_capital=simulation.initial_capital,
position=position_dict,
trades=trades_list,
equity_history=equity_history,
total_pnl=simulation.total_pnl,
total_pnl_percent=simulation.total_pnl_percent
)
@router.post("/execute")
async def execute_trade(
trade_request: TradeRequest,
db: Session = Depends(get_db),
user_id: str = "default"
):
"""
Execute a trade and persist to database.
- Validates risk rules
- Updates cash/position in DB
- Records trade with timestamp
- Returns updated portfolio state
"""
try:
action = trade_request.action.upper()
quantity = trade_request.quantity
price = trade_request.price
if action not in ["BUY", "SELL"]:
raise HTTPException(status_code=400, detail="Action must be BUY or SELL")
# Get or create simulation
simulation = get_or_create_simulation(db, user_id)
# Build state dict for risk validation
position = db.query(Position).filter(
Position.simulation_id == simulation.id
).first()
state_dict = {
"cash": simulation.current_capital,
"position": {
"quantity": position.quantity,
"avg_price": position.avg_price
} if position else None
}
# Risk validation
try:
validate_order(state_dict, action, quantity, price)
except ValueError as ve:
raise HTTPException(status_code=400, detail=str(ve))
total = quantity * price
pnl = None
if action == "BUY":
if total > simulation.current_capital:
raise HTTPException(status_code=400, detail="Insufficient funds")
simulation.current_capital -= total
if not position:
# Create new position
position = Position(
simulation_id=simulation.id,
symbol=trade_request.symbol,
quantity=quantity,
avg_price=price,
current_price=price,
unrealized_pnl=0.0,
unrealized_pnl_percent=0.0
)
db.add(position)
else:
# Update existing position (average up)
new_qty = position.quantity + quantity
new_avg = (position.avg_price * position.quantity + price * quantity) / new_qty
position.quantity = new_qty
position.avg_price = new_avg
position.current_price = price
elif action == "SELL":
if not position or quantity > position.quantity:
raise HTTPException(status_code=400, detail="Insufficient position")
simulation.current_capital += total
pnl = (price - position.avg_price) * quantity
# Update simulation totals
simulation.total_pnl += pnl
if simulation.initial_capital > 0:
simulation.total_pnl_percent = (simulation.total_pnl / simulation.initial_capital) * 100
position.quantity -= quantity
if position.quantity == 0:
# Close position
db.delete(position)
position = None
else:
position.current_price = price
# Create trade record
trade_timestamp = datetime.now(timezone.utc)
trade = Trade(
simulation_id=simulation.id,
action=TradeAction[action],
quantity=quantity,
price=price,
total=total,
pnl=pnl,
timestamp=trade_timestamp
)
db.add(trade)
db.flush()
entry_time_dt = None
if trade_request.entry_time:
try:
entry_time_dt = datetime.fromisoformat(trade_request.entry_time)
if entry_time_dt.tzinfo is None:
entry_time_dt = entry_time_dt.replace(tzinfo=timezone.utc)
except ValueError:
entry_time_dt = trade_timestamp
metadata_fields = (
trade_request.source,
trade_request.platform,
trade_request.notes,
trade_request.stop_loss,
trade_request.take_profit,
trade_request.risk_percent,
entry_time_dt,
)
if any(field is not None for field in metadata_fields):
trade_metadata = TradeMetadata(
trade_id=trade.id,
source=trade_request.source,
platform=trade_request.platform,
notes=trade_request.notes,
stop_loss=trade_request.stop_loss,
take_profit=trade_request.take_profit,
risk_percent=trade_request.risk_percent,
entry_time=entry_time_dt,
)
db.add(trade_metadata)
# Commit all changes
db.commit()
db.refresh(simulation)
# Return updated portfolio state
portfolio_state = get_portfolio_state_from_db(simulation, db)
return {
"trade": {
"id": trade.id,
"action": action,
"quantity": quantity,
"price": price,
"total": total,
"pnl": pnl,
"timestamp": int(trade.timestamp.timestamp()) if trade.timestamp else None,
"stop_loss": trade_request.stop_loss,
"take_profit": trade_request.take_profit,
"notes": trade_request.notes,
"source": trade_request.source,
"platform": trade_request.platform,
"risk_percent": trade_request.risk_percent,
"entry_time": entry_time_dt.isoformat() if entry_time_dt else None,
},
"portfolio": portfolio_state.dict()
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=f"Trade execution failed: {str(e)}")
@router.get("/portfolio")
async def get_portfolio(
db: Session = Depends(get_db),
user_id: str = "default"
):
"""Get current portfolio state from database"""
try:
simulation = get_or_create_simulation(db, user_id)
portfolio_state = get_portfolio_state_from_db(simulation, db)
return portfolio_state.dict()
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to get portfolio: {str(e)}")
@router.post("/reset")
async def reset_simulation(
db: Session = Depends(get_db),
user_id: str = "default"
):
"""Reset simulation to initial state"""
try:
simulation = db.query(Simulation).filter(Simulation.user_id == user_id).first()
if simulation:
# Delete all trades and positions (cascade will handle this)
db.delete(simulation)
db.commit()
# Create new simulation
new_simulation = Simulation(
user_id=user_id,
symbol="XAU/USD",
initial_capital=100000.0,
current_capital=100000.0,
total_pnl=0.0,
total_pnl_percent=0.0
)
db.add(new_simulation)
db.commit()
db.refresh(new_simulation)
portfolio_state = get_portfolio_state_from_db(new_simulation, db)
return {
"message": "Simulation reset successfully",
"portfolio": portfolio_state.dict()
}
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=f"Reset failed: {str(e)}")
@router.get("/history")
async def get_trade_history(
db: Session = Depends(get_db),
user_id: str = "default",
limit: int = 100
):
"""Get trade history from database"""
try:
simulation = get_or_create_simulation(db, user_id)
trades = db.query(Trade).options(selectinload(Trade.details)).filter(
Trade.simulation_id == simulation.id
).order_by(Trade.timestamp.desc()).limit(limit).all()
return [
{
"id": trade.id,
"action": trade.action.value,
"quantity": trade.quantity,
"price": trade.price,
"total": trade.total,
"pnl": trade.pnl,
"timestamp": int(trade.timestamp.timestamp()) if trade.timestamp else None,
"stop_loss": trade.details.stop_loss if trade.details else None,
"take_profit": trade.details.take_profit if trade.details else None,
"notes": trade.details.notes if trade.details else None,
"source": trade.details.source if trade.details else None,
"platform": trade.details.platform if trade.details else None,
"risk_percent": trade.details.risk_percent if trade.details else None,
"entry_time": trade.details.entry_time.isoformat() if trade.details and trade.details.entry_time else None,
}
for trade in trades
]
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to get history: {str(e)}")
@router.get("/stats")
async def get_trading_stats(
db: Session = Depends(get_db),
user_id: str = "default"
):
"""Get trading statistics"""
try:
simulation = get_or_create_simulation(db, user_id)
trades = db.query(Trade).filter(
Trade.simulation_id == simulation.id
).all()
total_trades = len(trades)
winning_trades = sum(1 for t in trades if t.pnl and t.pnl > 0)
losing_trades = sum(1 for t in trades if t.pnl and t.pnl < 0)
total_profit = sum(t.pnl for t in trades if t.pnl and t.pnl > 0)
total_loss = sum(abs(t.pnl) for t in trades if t.pnl and t.pnl < 0)
win_rate = (winning_trades / total_trades * 100) if total_trades > 0 else 0
profit_factor = (total_profit / total_loss) if total_loss > 0 else 0
return {
"total_trades": total_trades,
"winning_trades": winning_trades,
"losing_trades": losing_trades,
"win_rate": round(win_rate, 2),
"total_pnl": simulation.total_pnl,
"total_pnl_percent": simulation.total_pnl_percent,
"total_profit": total_profit,
"total_loss": total_loss,
"profit_factor": round(profit_factor, 2),
"current_capital": simulation.current_capital,
"initial_capital": simulation.initial_capital
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to get stats: {str(e)}")
+411
View File
@@ -0,0 +1,411 @@
"""
Trading Schools API
Endpoints for accessing trading methodologies, strategies, and plan templates
"""
from fastapi import APIRouter, Query, HTTPException
from typing import Optional, List
from pydantic import BaseModel
from app.services.trading_schools import trading_schools, TradingSchool
from app.services.plan_templates import plan_templates, PlanType, MarketCondition
router = APIRouter(prefix="/api/trading-schools", tags=["Trading Schools"])
# Pydantic Models
class TradingSchoolInfo(BaseModel):
"""Trading school information"""
school: str
name: str
description: str
key_concepts: List[str]
timeframes: List[str]
indicators: List[str]
best_for: List[str]
class GeneratePlanRequest(BaseModel):
"""Request to generate a trading plan"""
methodology: str # ict_smc, wyckoff, multi_confluence, etc.
current_price: float
market_condition: Optional[str] = "trending_up"
session: Optional[str] = "london_ny"
risk_tolerance: Optional[str] = "moderate"
# ============================================================================
# TRADING SCHOOLS ENDPOINTS
# ============================================================================
@router.get("/list")
async def get_all_trading_schools():
"""Get list of all available trading schools and methodologies"""
schools = trading_schools.get_all_schools()
return {
"total_schools": len(schools),
"schools": list(schools.keys()),
"schools_detail": schools,
"description": "Comprehensive collection of trading methodologies"
}
@router.get("/school/{school_name}")
async def get_school_details(school_name: str):
"""Get detailed information about a specific trading school"""
schools = trading_schools.get_all_schools()
if school_name not in schools:
raise HTTPException(
status_code=404,
detail=f"School '{school_name}' not found. Available schools: {list(schools.keys())}"
)
return schools[school_name]
@router.get("/combined-strategies")
async def get_combined_strategies():
"""Get hybrid strategies combining multiple trading schools"""
strategies = trading_schools.get_combined_strategies()
return {
"total_strategies": len(strategies),
"strategies": strategies,
"description": "Hybrid approaches combining multiple methodologies for higher probability setups"
}
@router.get("/indicator-presets")
async def get_indicator_presets(school: Optional[str] = Query(None)):
"""Get recommended indicator configurations for trading schools"""
if school:
preset = trading_schools.get_indicator_presets_for_school(TradingSchool(school))
return {
"school": school,
"preset": preset
}
# Get all presets
all_presets = {}
for s in TradingSchool:
all_presets[s.value] = trading_schools.get_indicator_presets_for_school(s)
return {
"total_schools": len(all_presets),
"presets": all_presets
}
@router.get("/risk-models")
async def get_risk_management_models():
"""Get advanced risk management models and position sizing strategies"""
models = trading_schools.get_risk_models()
return {
"total_models": len(models),
"models": models,
"recommendation": "Use Fixed Fractional (1-2% per trade) for beginners, Kelly Criterion for advanced traders with proven edge"
}
# ============================================================================
# TRADING PLAN TEMPLATES ENDPOINTS
# ============================================================================
@router.get("/plan-types")
async def get_plan_types():
"""Get all available trading plan types"""
types = plan_templates.get_all_plan_types()
return {
"total_types": len(types),
"plan_types": types,
"description": "Pre-built trading plan templates for different methodologies"
}
@router.post("/generate-plan")
async def generate_trading_plan(request: GeneratePlanRequest):
"""
Generate a comprehensive trading plan based on selected methodology
Methodologies:
- ict_smc: ICT / Smart Money Concepts
- wyckoff: Wyckoff Method
- multi_confluence: Multi-Method Confluence (ICT + Fib + S/D + PA)
- session_trading: London/NY Session-Based Trading
"""
try:
# Validate market condition
try:
market_cond = MarketCondition(request.market_condition)
except ValueError:
market_cond = MarketCondition.TRENDING_UP
# Generate plan based on methodology
if request.methodology == "ict_smc":
plan = plan_templates.generate_ict_smc_plan(
current_price=request.current_price,
market_condition=market_cond,
session=request.session or "london_ny"
)
elif request.methodology == "wyckoff":
plan = plan_templates.generate_wyckoff_plan(
current_price=request.current_price,
market_condition=market_cond
)
elif request.methodology == "multi_confluence":
plan = plan_templates.generate_multi_method_confluence_plan(
current_price=request.current_price,
market_condition=market_cond
)
elif request.methodology == "session_trading":
plan = plan_templates.generate_session_based_plan(
current_price=request.current_price,
target_session=request.session or "london_ny_overlap"
)
else:
raise HTTPException(
status_code=400,
detail=f"Unknown methodology: {request.methodology}. Use: ict_smc, wyckoff, multi_confluence, or session_trading"
)
return {
"methodology": request.methodology,
"current_price": request.current_price,
"market_condition": request.market_condition,
"plan": plan,
"generated_at": "now"
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/quick-reference/{school}")
async def get_quick_reference(school: str):
"""Get a quick reference guide for a specific trading school"""
schools = trading_schools.get_all_schools()
if school not in schools:
raise HTTPException(status_code=404, detail=f"School '{school}' not found")
school_data = schools[school]
# Create quick reference
quick_ref = {
"name": school_data["name"],
"school_type": school_data["school"],
"elevator_pitch": school_data["description"],
"key_concepts": school_data["key_concepts"][:5], # Top 5
"timeframes": school_data["timeframes"],
"best_for": school_data["best_for"],
"one_sentence_summary": _get_one_liner(school)
}
if "entry_criteria" in school_data:
quick_ref["how_to_trade"] = school_data["entry_criteria"]
if "risk_management" in school_data:
quick_ref["risk_management"] = school_data["risk_management"]
return quick_ref
@router.get("/comparison")
async def compare_trading_schools(
schools_list: str = Query(..., description="Comma-separated list of schools to compare, e.g., ict_smc,wyckoff,price_action")
):
"""Compare multiple trading schools side by side"""
school_names = [s.strip() for s in schools_list.split(",")]
schools_data = trading_schools.get_all_schools()
comparison = {}
for school_name in school_names:
if school_name not in schools_data:
raise HTTPException(
status_code=404,
detail=f"School '{school_name}' not found"
)
data = schools_data[school_name]
comparison[school_name] = {
"name": data["name"],
"description": data["description"],
"timeframes": data["timeframes"],
"indicators": data["indicators"],
"best_for": data["best_for"],
"complexity": _rate_complexity(school_name)
}
return {
"schools_compared": len(comparison),
"comparison": comparison,
"recommendation": _get_comparison_recommendation(school_names)
}
@router.get("/learning-path")
async def get_learning_path():
"""Get recommended learning path for mastering different trading schools"""
return {
"beginner_path": {
"level": "Beginner (0-6 months)",
"schools": [
{
"order": 1,
"school": "price_action",
"name": "Price Action",
"reason": "Foundation - Learn to read candles and basic S/R",
"time_to_learn": "2-3 months"
},
{
"order": 2,
"school": "fibonacci_trading",
"name": "Fibonacci Trading",
"reason": "Simple tool, high applicability",
"time_to_learn": "1 month"
},
{
"order": 3,
"school": "supply_demand",
"name": "Supply & Demand Zones",
"reason": "Logical, builds on S/R knowledge",
"time_to_learn": "2 months"
}
],
"practice": "Demo trade minimum 3 months before real money"
},
"intermediate_path": {
"level": "Intermediate (6-18 months)",
"schools": [
{
"order": 1,
"school": "ict_smc",
"name": "ICT / Smart Money Concepts",
"reason": "Modern, powerful for gold/forex",
"time_to_learn": "4-6 months"
},
{
"order": 2,
"school": "market_profile",
"name": "Market Profile",
"reason": "Understand volume and value",
"time_to_learn": "3 months"
},
{
"order": 3,
"school": "multi_timeframe",
"name": "Multi-Timeframe Analysis",
"reason": "Combine skills, improve timing",
"time_to_learn": "2 months"
}
],
"practice": "Start combining methods, track statistics"
},
"advanced_path": {
"level": "Advanced (18+ months)",
"schools": [
{
"order": 1,
"school": "wyckoff",
"name": "Wyckoff Method",
"reason": "Deep market understanding, institutional perspective",
"time_to_learn": "6-12 months"
},
{
"order": 2,
"school": "elliott_wave",
"name": "Elliott Wave Theory",
"reason": "Complex but powerful for major moves",
"time_to_learn": "6-12 months"
},
{
"order": 3,
"school": "order_flow",
"name": "Order Flow Trading",
"reason": "Real-time institutional activity",
"time_to_learn": "3-6 months (requires specialized tools)"
}
],
"practice": "Develop personal methodology combining multiple schools"
},
"professional_edge": {
"level": "Professional",
"approach": "Multi-Method Confluence",
"description": "Combine 3-4 methodologies for maximum probability setups",
"schools": ["ict_smc", "fibonacci_trading", "supply_demand", "price_action"],
"goal": "Trade only highest-quality setups with 70%+ win rate",
"frequency": "1-3 trades per week (quality over quantity)"
},
"general_advice": [
"Master ONE school completely before moving to next",
"Journal every trade and study every setup",
"Backtest each methodology on historical data",
"Paper trade new methods for 2-3 months minimum",
"Don't skip fundamentals (Price Action first!)",
"Find 1-2 mentors for each major methodology",
"Join communities: ICT students, Wyckoff traders, etc.",
"Most profitable traders use 2-3 methods maximum (confluence)"
]
}
# ============================================================================
# HELPER FUNCTIONS
# ============================================================================
def _get_one_liner(school: str) -> str:
"""Get one-sentence summary of a trading school"""
summaries = {
"ict_smc": "Trade like institutions: Follow liquidity, FVGs, and order blocks during killzones.",
"wyckoff": "Identify accumulation and distribution phases using volume to trade with smart money.",
"elliott_wave": "Count wave structures and use Fibonacci to predict major market moves.",
"market_profile": "Find value areas and trade price rejection from high/low volume nodes.",
"order_flow": "Read real-time buying/selling pressure to anticipate institutional moves.",
"price_action": "Trade pure price patterns at support/resistance without indicators.",
"supply_demand": "Identify fresh zones of imbalance and trade rejections from these levels.",
"fibonacci_trading": "Use golden ratio levels (0.618, 1.618) for entries and targets.",
"gold_fundamental": "Trade gold based on USD strength, yields, inflation, and geopolitical factors.",
"multi_timeframe": "Align multiple timeframes for high-probability entries with HTF targets.",
"london_ny_session": "Trade gold during high-liquidity sessions (3-5 AM, 8-11 AM EST) for best moves."
}
return summaries.get(school, "A proven trading methodology.")
def _rate_complexity(school: str) -> str:
"""Rate the complexity of learning a trading school"""
ratings = {
"price_action": "Beginner",
"fibonacci_trading": "Beginner",
"supply_demand": "Beginner-Intermediate",
"multi_timeframe": "Intermediate",
"ict_smc": "Intermediate",
"market_profile": "Intermediate-Advanced",
"gold_fundamental": "Intermediate",
"london_ny_session": "Intermediate",
"wyckoff": "Advanced",
"elliott_wave": "Advanced",
"order_flow": "Advanced"
}
return ratings.get(school, "Intermediate")
def _get_comparison_recommendation(schools: List[str]) -> str:
"""Get recommendation based on schools being compared"""
if len(schools) == 1:
return f"Focus on mastering {schools[0]} before adding other methods."
if "ict_smc" in schools and "fibonacci_trading" in schools and "supply_demand" in schools:
return "Excellent combination! These three methods work very well together for confluence trading."
if "wyckoff" in schools and any(s in schools for s in ["market_profile", "order_flow"]):
return "Volume-based methods pair well. Focus on volume analysis across all methods."
if len(schools) > 4:
return "⚠️ Too many methods. Focus on mastering 2-3 maximum to avoid analysis paralysis."
return "Good selection. Look for confluence zones where multiple methods confirm the same setup."