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