Reorganize UI for external trading workflow with manual trade logging

- Restructure tabs to analysis-focused workflow:
  * Analysis Hub: AI analysis, risk management, manual trade logger
  * Daily Prep: Market summary, alerts, checklist, news, trading plan
  * Journal & Review: Trading journal, habit tracker, advanced analytics
  * Live Charts: Technical analysis with streaming charts

- Add ManualTradeLogger component for logging trades from MT5/TradingView/cTrader
- Remove execution-focused components (TradeControls, PortfolioTracker)
- Update XAU/USD price to realistic ,084.99
- Add indicator preferences and AI plan service
- Add comprehensive documentation on decision coverage and implementation
This commit is contained in:
Krikorios
2025-11-16 07:50:00 +02:00
parent 73a26ea9b7
commit b5e2b02cb8
20 changed files with 4347 additions and 29 deletions
+78 -2
View File
@@ -1,7 +1,18 @@
from fastapi import APIRouter, HTTPException
from fastapi import APIRouter, HTTPException, Depends
from sqlalchemy.orm import Session
from typing import List, Optional
from app.services.openrouter import openrouter_service
from app.schemas.schemas import AIAnalysisRequest, AIAnalysisResponse
from app.schemas.schemas import (
AIAnalysisRequest,
AIAnalysisResponse,
AIPlanGenerationRequest,
AIPlanGenerationResponse,
AIPlanFeedback
)
from app.services.decisions import log_decision
from app.services.ai_plan_service import ai_plan_service
from app.db.database import get_db
router = APIRouter(prefix="/ai", tags=["AI Analysis"])
@@ -41,3 +52,68 @@ async def analyze_scenario(request: AIAnalysisRequest):
raise HTTPException(
status_code=500, detail=f"AI analysis failed: {str(e)}"
)
@router.post("/generate-plan", response_model=AIPlanGenerationResponse)
async def generate_trading_plan(
request: AIPlanGenerationRequest,
user_id: Optional[str] = None,
db: Session = Depends(get_db)
):
"""
Generate a comprehensive daily trading plan using AI
Uses user's indicator preferences and market data to create:
- Market bias (BULLISH/BEARISH/NEUTRAL)
- Entry zones and targets
- Support and resistance levels
- Risk management parameters
- Trading strategy notes
"""
try:
plan = await ai_plan_service.generate_plan(db, request, user_id)
return plan
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"AI plan generation failed: {str(e)}"
)
@router.get("/plans/history", response_model=List[AIPlanGenerationResponse])
async def get_plan_history(
user_id: Optional[str] = None,
limit: int = 10,
db: Session = Depends(get_db)
):
"""Get historical AI-generated trading plans"""
try:
plans = await ai_plan_service.get_plan_history(db, user_id, limit)
return plans
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Failed to fetch plan history: {str(e)}"
)
@router.post("/plans/feedback")
async def submit_plan_feedback(
feedback: AIPlanFeedback,
db: Session = Depends(get_db)
):
"""Submit feedback on an AI-generated plan"""
try:
plan = await ai_plan_service.submit_feedback(
db,
feedback.plan_id,
feedback.accepted,
feedback.modified,
feedback.feedback
)
return {"success": True, "message": "Feedback submitted successfully"}
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Failed to submit feedback: {str(e)}"
)
+150 -3
View File
@@ -1,9 +1,18 @@
from __future__ import annotations
from fastapi import APIRouter
from typing import Any, Dict
from fastapi import APIRouter, HTTPException, Depends, status
from typing import Any, Dict, List
from sqlalchemy.orm import Session
from app.services.settings import get_models, update_models, get_exchanges, update_exchanges
from app.db.database import get_db
from app.models.models import UserIndicatorPreferences
from app.schemas.schemas import (
IndicatorPreferenceCreate,
IndicatorPreferenceUpdate,
IndicatorPreferenceResponse,
IndicatorPreferencesListResponse
)
router = APIRouter(prefix="/settings", tags=["Settings"])
@@ -25,4 +34,142 @@ async def exchanges_get() -> Dict[str, Any]:
@router.put("/exchanges")
async def exchanges_put(patch: Dict[str, Any]) -> Dict[str, Any]:
return update_exchanges(patch)
return update_exchanges(patch)
# ============================================================================
# INDICATOR PREFERENCES ENDPOINTS
# ============================================================================
@router.get("/indicators/preferences", response_model=IndicatorPreferencesListResponse)
async def get_indicator_preferences(
user_id: str = None,
enabled_only: bool = False,
db: Session = Depends(get_db)
):
"""Get user's indicator preferences"""
query = db.query(UserIndicatorPreferences)
if user_id:
query = query.filter(UserIndicatorPreferences.user_id == user_id)
if enabled_only:
query = query.filter(UserIndicatorPreferences.enabled == True)
preferences = query.order_by(UserIndicatorPreferences.priority.desc()).all()
return {
"preferences": preferences,
"total": len(preferences)
}
@router.post("/indicators/preferences", response_model=IndicatorPreferenceResponse, status_code=status.HTTP_201_CREATED)
async def create_indicator_preference(
preference: IndicatorPreferenceCreate,
user_id: str = None,
db: Session = Depends(get_db)
):
"""Create a new indicator preference"""
# Check if indicator already exists for this user
existing = db.query(UserIndicatorPreferences).filter(
UserIndicatorPreferences.user_id == user_id,
UserIndicatorPreferences.indicator_name == preference.indicator_name
).first()
if existing:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Preference for indicator '{preference.indicator_name}' already exists"
)
db_preference = UserIndicatorPreferences(
user_id=user_id,
**preference.dict()
)
db.add(db_preference)
db.commit()
db.refresh(db_preference)
return db_preference
@router.put("/indicators/preferences/{preference_id}", response_model=IndicatorPreferenceResponse)
async def update_indicator_preference(
preference_id: int,
preference_update: IndicatorPreferenceUpdate,
db: Session = Depends(get_db)
):
"""Update an indicator preference"""
db_preference = db.query(UserIndicatorPreferences).filter(
UserIndicatorPreferences.id == preference_id
).first()
if not db_preference:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Indicator preference not found"
)
update_data = preference_update.dict(exclude_unset=True)
for key, value in update_data.items():
setattr(db_preference, key, value)
db.commit()
db.refresh(db_preference)
return db_preference
@router.delete("/indicators/preferences/{preference_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_indicator_preference(
preference_id: int,
db: Session = Depends(get_db)
):
"""Delete an indicator preference"""
db_preference = db.query(UserIndicatorPreferences).filter(
UserIndicatorPreferences.id == preference_id
).first()
if not db_preference:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Indicator preference not found"
)
db.delete(db_preference)
db.commit()
@router.post("/indicators/preferences/bulk", response_model=IndicatorPreferencesListResponse)
async def create_bulk_indicator_preferences(
preferences: List[IndicatorPreferenceCreate],
user_id: str = None,
db: Session = Depends(get_db)
):
"""Create multiple indicator preferences at once"""
created_preferences = []
for pref in preferences:
# Skip if already exists
existing = db.query(UserIndicatorPreferences).filter(
UserIndicatorPreferences.user_id == user_id,
UserIndicatorPreferences.indicator_name == pref.indicator_name
).first()
if not existing:
db_preference = UserIndicatorPreferences(
user_id=user_id,
**pref.dict()
)
db.add(db_preference)
created_preferences.append(db_preference)
db.commit()
# Refresh all created preferences
for pref in created_preferences:
db.refresh(pref)
return {
"preferences": created_preferences,
"total": len(created_preferences)
}
+52
View File
@@ -169,3 +169,55 @@ class HabitTracker(Base):
total_completions = Column(Integer, default=0)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
class UserIndicatorPreferences(Base):
"""User's preferred technical indicators for analysis and AI plan generation"""
__tablename__ = "user_indicator_preferences"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(String, nullable=True)
indicator_name = Column(String) # SMA, EMA, RSI, MACD, BB, ATR, Stochastic, Fibonacci, VWAP, Pivot
enabled = Column(Boolean, default=True)
parameters = Column(JSON, nullable=True) # Indicator-specific parameters (e.g., period, length)
priority = Column(Integer, default=0) # Higher priority = more important in AI analysis
notes = Column(Text, nullable=True) # User notes about why they prefer this indicator
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
class AIPlanGeneration(Base):
"""AI-generated daily trading plans"""
__tablename__ = "ai_plan_generations"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(String, nullable=True)
plan_date = Column(Date, default=func.current_date())
# AI-generated plan details
market_bias = Column(String) # BULLISH, BEARISH, NEUTRAL
confidence = Column(Float) # 0-100
daily_target = Column(Float, nullable=True)
max_loss = Column(Float, nullable=True)
entry_zone_min = Column(Float, nullable=True)
entry_zone_max = Column(Float, nullable=True)
target_price = Column(Float, nullable=True)
stop_loss = Column(Float, nullable=True)
support_levels = Column(JSON, default=[]) # List of support prices
resistance_levels = Column(JSON, default=[]) # List of resistance prices
max_trades = Column(Integer, default=3)
trading_notes = Column(Text, nullable=True) # AI-generated strategy notes
# AI analysis metadata
indicators_used = Column(JSON, default=[]) # List of indicators used in analysis
reasoning = Column(Text, nullable=True) # AI's reasoning for the plan
market_conditions = Column(JSON, nullable=True) # Market data used in analysis
ai_model = Column(String, nullable=True) # Model used for generation
# User interaction
accepted = Column(Boolean, default=False) # User accepted this plan
modified = Column(Boolean, default=False) # User modified after generation
feedback = Column(Text, nullable=True) # User feedback on plan accuracy
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
+101
View File
@@ -384,3 +384,104 @@ class HabitTrackerResponse(BaseModel):
class HabitCompletionRequest(BaseModel):
habit_id: int
completion_date: Optional[str] = None # ISO date string, defaults to today
# ============================================================================
# INDICATOR PREFERENCES SCHEMAS
# ============================================================================
class IndicatorParameters(BaseModel):
"""Common indicator parameters"""
period: Optional[int] = None
length: Optional[int] = None
multiplier: Optional[float] = None
# Add more as needed
class IndicatorPreferenceCreate(BaseModel):
indicator_name: str = Field(..., description="Name of the indicator (SMA, EMA, RSI, etc.)")
enabled: bool = True
parameters: Optional[dict] = None
priority: int = Field(default=0, description="Higher priority = more important in AI analysis")
notes: Optional[str] = None
class IndicatorPreferenceUpdate(BaseModel):
enabled: Optional[bool] = None
parameters: Optional[dict] = None
priority: Optional[int] = None
notes: Optional[str] = None
class IndicatorPreferenceResponse(BaseModel):
id: int
user_id: Optional[str]
indicator_name: str
enabled: bool
parameters: Optional[dict]
priority: int
notes: Optional[str]
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
class IndicatorPreferencesListResponse(BaseModel):
preferences: List[IndicatorPreferenceResponse]
total: int
# ============================================================================
# AI PLAN GENERATION SCHEMAS
# ============================================================================
class MarketBias(str, Enum):
BULLISH = "BULLISH"
BEARISH = "BEARISH"
NEUTRAL = "NEUTRAL"
class AIPlanGenerationRequest(BaseModel):
"""Request to generate an AI trading plan"""
current_price: float = Field(..., description="Current market price")
user_capital: Optional[float] = Field(None, description="User's available capital")
risk_tolerance: Optional[str] = Field("moderate", description="conservative, moderate, aggressive")
use_indicator_preferences: bool = Field(True, description="Use user's saved indicator preferences")
price_data: Optional[List[PriceData]] = Field(None, description="Recent price data for analysis")
indicators_data: Optional[dict] = Field(None, description="Current indicator values")
class AIPlanGenerationResponse(BaseModel):
"""AI-generated trading plan"""
id: int
plan_date: str # ISO date
market_bias: MarketBias
confidence: float # 0-100
daily_target: Optional[float]
max_loss: Optional[float]
entry_zone_min: Optional[float]
entry_zone_max: Optional[float]
target_price: Optional[float]
stop_loss: Optional[float]
support_levels: List[float]
resistance_levels: List[float]
max_trades: int
trading_notes: Optional[str]
indicators_used: List[str]
reasoning: Optional[str]
market_conditions: Optional[dict]
ai_model: Optional[str]
created_at: datetime
class Config:
from_attributes = True
class AIPlanFeedback(BaseModel):
"""User feedback on AI plan accuracy"""
plan_id: int
accepted: bool
modified: bool = False
feedback: Optional[str] = None
+270
View File
@@ -0,0 +1,270 @@
"""
AI Plan Generation Service
Generates daily trading plans using AI based on user's indicator preferences
"""
from typing import List, Optional, Dict
from datetime import date
from sqlalchemy.orm import Session
import json
from app.models.models import UserIndicatorPreferences, AIPlanGeneration
from app.schemas.schemas import (
AIPlanGenerationRequest,
AIPlanGenerationResponse,
MarketBias,
PriceData
)
from app.services.openrouter import openrouter_service
class AIPlanService:
"""Service for AI-powered trading plan generation"""
def _get_user_indicator_preferences(self, db: Session, user_id: Optional[str] = None) -> List[UserIndicatorPreferences]:
"""Fetch user's enabled indicator preferences"""
query = db.query(UserIndicatorPreferences).filter(
UserIndicatorPreferences.enabled == True
)
if user_id:
query = query.filter(UserIndicatorPreferences.user_id == user_id)
return query.order_by(UserIndicatorPreferences.priority.desc()).all()
def _build_ai_prompt(
self,
request: AIPlanGenerationRequest,
indicator_preferences: List[UserIndicatorPreferences]
) -> str:
"""Build comprehensive prompt for AI plan generation"""
indicator_names = [pref.indicator_name for pref in indicator_preferences] if indicator_preferences else []
prompt = f"""You are an expert gold (XAU/USD) trading analyst. Generate a detailed daily trading plan based on the following information:
CURRENT MARKET DATA:
- Current Price: ${request.current_price:.2f}
- User's Risk Tolerance: {request.risk_tolerance}
- Available Capital: ${request.user_capital if request.user_capital else 'Not specified'}
USER'S PREFERRED TECHNICAL INDICATORS:
{', '.join(indicator_names) if indicator_names else 'No specific preferences - use standard analysis'}
INDICATOR DETAILS:
"""
for pref in indicator_preferences:
prompt += f"- {pref.indicator_name} (Priority: {pref.priority})"
if pref.parameters:
prompt += f" - Parameters: {json.dumps(pref.parameters)}"
if pref.notes:
prompt += f" - Notes: {pref.notes}"
prompt += "\n"
if request.price_data and len(request.price_data) > 0:
recent_prices = request.price_data[-10:] # Last 10 data points
prompt += f"\nRECENT PRICE ACTION (last {len(recent_prices)} periods):\n"
for i, pd in enumerate(recent_prices, 1):
prompt += f" {i}. Open: ${pd.open:.2f}, High: ${pd.high:.2f}, Low: ${pd.low:.2f}, Close: ${pd.close:.2f}\n"
if request.indicators_data:
prompt += f"\nCURRENT INDICATOR VALUES:\n"
for indicator, value in request.indicators_data.items():
prompt += f"- {indicator}: {value}\n"
prompt += """
Please generate a comprehensive daily trading plan with the following structure:
1. MARKET BIAS: Determine if the market is BULLISH, BEARISH, or NEUTRAL
2. CONFIDENCE: Your confidence level in this analysis (0-100)
3. DAILY TARGET: Suggested profit target in dollars (be realistic based on user's capital and risk tolerance)
4. MAX LOSS: Maximum acceptable loss for the day (align with risk tolerance)
5. ENTRY ZONE: Recommended price range for entering positions (min and max)
6. TARGET PRICE: Primary profit-taking level
7. STOP LOSS: Stop-loss level to protect capital
8. SUPPORT LEVELS: 3-5 key support levels below current price
9. RESISTANCE LEVELS: 3-5 key resistance levels above current price
10. MAX TRADES: Recommended maximum number of trades for the day
11. TRADING NOTES: Detailed strategy notes including:
- Why this bias?
- What indicators support this view?
- What to watch for during the day?
- Risk management considerations
- Market conditions and factors
12. REASONING: Detailed explanation of your analysis and why you recommend this plan
Format your response as a valid JSON object with these exact keys:
{
"market_bias": "BULLISH" | "BEARISH" | "NEUTRAL",
"confidence": 75.0,
"daily_target": 500.0,
"max_loss": 250.0,
"entry_zone_min": 2010.0,
"entry_zone_max": 2015.0,
"target_price": 2040.0,
"stop_loss": 2005.0,
"support_levels": [2000.0, 1990.0, 1980.0],
"resistance_levels": [2020.0, 2030.0, 2040.0],
"max_trades": 3,
"trading_notes": "Detailed strategy notes here...",
"reasoning": "Full analysis and reasoning here..."
}
Be specific, actionable, and realistic. Consider the user's risk tolerance and preferred indicators heavily in your analysis.
"""
return prompt
async def generate_plan(
self,
db: Session,
request: AIPlanGenerationRequest,
user_id: Optional[str] = None
) -> AIPlanGenerationResponse:
"""Generate an AI-powered trading plan"""
# Get user's indicator preferences if requested
indicator_preferences = []
if request.use_indicator_preferences:
indicator_preferences = self._get_user_indicator_preferences(db, user_id)
# Build AI prompt
prompt = self._build_ai_prompt(request, indicator_preferences)
# Call AI service
try:
# Use OpenRouter service to get AI response
ai_response = await openrouter_service.generate_trading_plan(prompt)
# Parse AI response (assuming it returns JSON)
if isinstance(ai_response, str):
plan_data = json.loads(ai_response)
else:
plan_data = ai_response
# Create database record
db_plan = AIPlanGeneration(
user_id=user_id,
plan_date=date.today(),
market_bias=plan_data.get("market_bias", "NEUTRAL"),
confidence=plan_data.get("confidence", 50.0),
daily_target=plan_data.get("daily_target"),
max_loss=plan_data.get("max_loss"),
entry_zone_min=plan_data.get("entry_zone_min"),
entry_zone_max=plan_data.get("entry_zone_max"),
target_price=plan_data.get("target_price"),
stop_loss=plan_data.get("stop_loss"),
support_levels=plan_data.get("support_levels", []),
resistance_levels=plan_data.get("resistance_levels", []),
max_trades=plan_data.get("max_trades", 3),
trading_notes=plan_data.get("trading_notes"),
reasoning=plan_data.get("reasoning"),
indicators_used=[pref.indicator_name for pref in indicator_preferences],
market_conditions={
"current_price": request.current_price,
"risk_tolerance": request.risk_tolerance,
},
ai_model=openrouter_service.model,
accepted=False,
modified=False
)
db.add(db_plan)
db.commit()
db.refresh(db_plan)
# Return response
return AIPlanGenerationResponse(
id=db_plan.id,
plan_date=str(db_plan.plan_date),
market_bias=MarketBias(db_plan.market_bias),
confidence=db_plan.confidence,
daily_target=db_plan.daily_target,
max_loss=db_plan.max_loss,
entry_zone_min=db_plan.entry_zone_min,
entry_zone_max=db_plan.entry_zone_max,
target_price=db_plan.target_price,
stop_loss=db_plan.stop_loss,
support_levels=db_plan.support_levels,
resistance_levels=db_plan.resistance_levels,
max_trades=db_plan.max_trades,
trading_notes=db_plan.trading_notes,
indicators_used=db_plan.indicators_used,
reasoning=db_plan.reasoning,
market_conditions=db_plan.market_conditions,
ai_model=db_plan.ai_model,
created_at=db_plan.created_at
)
except json.JSONDecodeError as e:
raise Exception(f"Failed to parse AI response: {str(e)}")
except Exception as e:
raise Exception(f"AI plan generation failed: {str(e)}")
async def get_plan_history(
self,
db: Session,
user_id: Optional[str] = None,
limit: int = 10
) -> List[AIPlanGenerationResponse]:
"""Get historical AI-generated plans"""
query = db.query(AIPlanGeneration)
if user_id:
query = query.filter(AIPlanGeneration.user_id == user_id)
plans = query.order_by(AIPlanGeneration.created_at.desc()).limit(limit).all()
return [
AIPlanGenerationResponse(
id=plan.id,
plan_date=str(plan.plan_date),
market_bias=MarketBias(plan.market_bias),
confidence=plan.confidence,
daily_target=plan.daily_target,
max_loss=plan.max_loss,
entry_zone_min=plan.entry_zone_min,
entry_zone_max=plan.entry_zone_max,
target_price=plan.target_price,
stop_loss=plan.stop_loss,
support_levels=plan.support_levels,
resistance_levels=plan.resistance_levels,
max_trades=plan.max_trades,
trading_notes=plan.trading_notes,
indicators_used=plan.indicators_used,
reasoning=plan.reasoning,
market_conditions=plan.market_conditions,
ai_model=plan.ai_model,
created_at=plan.created_at
)
for plan in plans
]
async def submit_feedback(
self,
db: Session,
plan_id: int,
accepted: bool,
modified: bool = False,
feedback: Optional[str] = None
):
"""Submit user feedback on an AI-generated plan"""
plan = db.query(AIPlanGeneration).filter(AIPlanGeneration.id == plan_id).first()
if not plan:
raise Exception("Plan not found")
plan.accepted = accepted
plan.modified = modified
plan.feedback = feedback
db.commit()
db.refresh(plan)
return plan
# Global instance
ai_plan_service = AIPlanService()
+60
View File
@@ -135,5 +135,65 @@ Respond in JSON format:
risk_level=RiskLevel(analysis_data.get("risk_level", "MEDIUM")),
)
async def generate_trading_plan(self, prompt: str) -> dict:
"""
Generate a comprehensive trading plan using AI
Args:
prompt: Detailed prompt with market data and user preferences
Returns:
Dictionary with trading plan data
"""
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json",
"HTTP-Referer": settings.OPENROUTER_SITE_URL,
"X-Title": settings.OPENROUTER_SITE_NAME,
}
payload = {
"model": self.model,
"messages": [
{
"role": "system",
"content": "You are an expert gold (XAU/USD) trading analyst. Always respond with valid JSON only, no additional text or explanations.",
},
{"role": "user", "content": prompt},
],
"temperature": 0.7,
"max_tokens": 2000,
}
async with httpx.AsyncClient(timeout=90.0) as client:
response = await client.post(
f"{self.base_url}/chat/completions",
headers=headers,
json=payload,
)
response.raise_for_status()
data = response.json()
# Extract AI response
ai_content = data["choices"][0]["message"]["content"]
# Parse JSON response
try:
# Try to extract JSON from markdown code blocks if present
if "```json" in ai_content:
json_start = ai_content.find("```json") + 7
json_end = ai_content.find("```", json_start)
ai_content = ai_content[json_start:json_end].strip()
elif "```" in ai_content:
json_start = ai_content.find("```") + 3
json_end = ai_content.find("```", json_start)
ai_content = ai_content[json_start:json_end].strip()
plan_data = json.loads(ai_content)
return plan_data
except json.JSONDecodeError as e:
raise Exception(f"Failed to parse AI trading plan response: {str(e)}")
openrouter_service = OpenRouterService()