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:
+78
-2
@@ -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)}"
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
"""
|
||||
Database migration script to add indicator preferences and AI plan generation tables
|
||||
Run this to add the new tables to your existing database
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
from sqlalchemy import create_engine, text
|
||||
|
||||
# Add parent directory to path to import app modules
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from app.config import settings
|
||||
from app.models.models import Base, UserIndicatorPreferences, AIPlanGeneration
|
||||
|
||||
|
||||
def run_migration():
|
||||
"""Create the new tables in the database"""
|
||||
engine = create_engine(settings.DATABASE_URL)
|
||||
|
||||
print("🔄 Starting database migration...")
|
||||
print(f"📊 Database URL: {settings.DATABASE_URL}")
|
||||
|
||||
try:
|
||||
# Create only the new tables
|
||||
print("\n📝 Creating new tables...")
|
||||
UserIndicatorPreferences.__table__.create(engine, checkfirst=True)
|
||||
print("✅ Created table: user_indicator_preferences")
|
||||
|
||||
AIPlanGeneration.__table__.create(engine, checkfirst=True)
|
||||
print("✅ Created table: ai_plan_generations")
|
||||
|
||||
print("\n✨ Migration completed successfully!")
|
||||
print("\n📋 New tables created:")
|
||||
print(" - user_indicator_preferences: Store user's preferred technical indicators")
|
||||
print(" - ai_plan_generations: Store AI-generated daily trading plans")
|
||||
|
||||
# Test connection
|
||||
with engine.connect() as conn:
|
||||
# Check if tables exist
|
||||
result = conn.execute(text("""
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name IN ('user_indicator_preferences', 'ai_plan_generations')
|
||||
"""))
|
||||
tables = [row[0] for row in result]
|
||||
|
||||
print(f"\n✓ Verified tables in database: {', '.join(tables)}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ Migration failed: {str(e)}")
|
||||
print("\nPlease check:")
|
||||
print(" 1. Database is running")
|
||||
print(" 2. Database credentials are correct in .env")
|
||||
print(" 3. Database user has CREATE TABLE permissions")
|
||||
raise
|
||||
|
||||
|
||||
def rollback_migration():
|
||||
"""Drop the new tables (use with caution!)"""
|
||||
engine = create_engine(settings.DATABASE_URL)
|
||||
|
||||
print("⚠️ ROLLBACK: Dropping new tables...")
|
||||
|
||||
try:
|
||||
UserIndicatorPreferences.__table__.drop(engine, checkfirst=True)
|
||||
print("✅ Dropped table: user_indicator_preferences")
|
||||
|
||||
AIPlanGeneration.__table__.drop(engine, checkfirst=True)
|
||||
print("✅ Dropped table: ai_plan_generations")
|
||||
|
||||
print("\n✨ Rollback completed successfully!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ Rollback failed: {str(e)}")
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description='Migrate database for indicator preferences and AI plans')
|
||||
parser.add_argument('--rollback', action='store_true', help='Rollback migration (drop tables)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.rollback:
|
||||
confirm = input("⚠️ Are you sure you want to rollback? This will DELETE data! (yes/no): ")
|
||||
if confirm.lower() == 'yes':
|
||||
rollback_migration()
|
||||
else:
|
||||
print("Rollback cancelled.")
|
||||
else:
|
||||
run_migration()
|
||||
Reference in New Issue
Block a user