# GATE_DAYTRADER_v1.00.py
#
# --- v1.00: Price Action & Market Structure Engine ---
# 1.  CORE STRATEGY REBUILT: Aligned with a day trading plan based on pure price
#     action, market structure (HH/HL/LL/LH), and Supply/Demand (S/D) zones.
#
# 2.  MARKET STRUCTURE ENGINE:
#     - Implemented a new suite of sandboxed functions (`get_market_structure_analysis`,
#       `find_swing_pivots`, `analyze_trend_and_reversals`, `find_supply_demand_zones`)
#       to provide the Analyst AI with real-time structural context.
#     - This engine tracks recent swing highs and lows, identifies the current trend
#       (Uptrend, Downtrend, Ranging), and detects key reversal signals like a
#       Change of Character (CHoCH).
#     - It automatically calculates and provides the most recent, relevant Supply
#       and Demand zones based on strong market impulse moves.
#
# 3.  SIMPLIFIED AI HIERARCHY:
#     - Removed the complex Junior/Senior AI split. The system now uses a single,
#       specialized Analyst AI focused exclusively on executing the price action plan.
#     - The Analyst AI's prompt (`get_analyst_prompt`) is now built around a
#       "Final Checklist" that forces it to validate every trade against the
#       core principles of the plan (Session, Structure, Zone, Confirmation, R:R).
#
# 4.  REMOVED UNNECESSARY COMPONENTS:
#     - Markov Predictor: Removed as the new strategy is context-driven, not probabilistic.
#     - Playbook Engine: Removed in favor of the single, clear trading plan.
#     - Old Detector Engine: All indicator-based patterns (V-shapes, ADX grinds, etc.)
#       were removed. The new engine focuses solely on market structure.
#     - Complex State Machine: Simplified to focus on the primary trading session.
#     - Daily Maintenance, Heartbeat Phases, Daily Acts: Removed to streamline the script.
#
# 5.  INTELLIGENT RISK MANAGEMENT:
#     - Stop Loss (SL) and Take Profit (TP) are now calculated dynamically based on
#       the identified market structure.
#     - For BUY trades, SL is placed below the identified Demand Zone, and TP targets
#       the last significant high.
#     - For SELL trades, SL is placed above the Supply Zone, and TP targets the
#       last significant low, ensuring a logical R:R ratio for every trade.
#
import MetaTrader5 as mt5
import os
import time
import json
import logging
from logging.handlers import TimedRotatingFileHandler
from datetime import datetime, timezone, time as dt_time
import requests
from dotenv import load_dotenv
import signal
import sys
import pytz
import numpy as np
import pandas as pd
from typing import Dict, List, Tuple, Optional
from scipy.signal import argrelextrema

# --- Script Version ---
SCRIPT_VERSION = "DAYTRADER_v1.00"

# --- Timezone and Session Configuration ---
UTC_TZ = pytz.utc
LONDON_TZ = pytz.timezone('Europe/London')

# The plan specifies a focus on the London/NY overlap for high volume.
# London Open: 08:00 BST = 07:00 UTC
# New York Open: 14:30 BST = 13:30 UTC
# Focus Session: 13:00-17:00 London Time -> 12:00-16:00 UTC
FOCUS_SESSION_START_UTC = dt_time(12, 0)
FOCUS_SESSION_END_UTC = dt_time(16, 0)

# --- Trading Configuration ---
class TradingState:
    def __init__(self, name, min_confidence=0.75, risk_percent=1.0):
        self.name = name
        self.min_confidence = min_confidence
        self.risk_percent = risk_percent

FOCUS_SESSION_STATE = TradingState("FOCUS_SESSION", 0.75, 1.0)
OUTSIDE_SESSION_STATE = TradingState("OUTSIDE_SESSION", 0.95, 0.0) # Risk set to 0 to prevent trading

# --- Constants & Configs ---
MINIMUM_SL_POINTS = 5.0      # Minimum stop loss in points to avoid overly tight stops.
MINIMUM_RR_RATIO = 2.0       # Minimum required Risk-to-Reward ratio (e.g., 2.0 means 1:2).
PIVOT_LOOKBACK = 5           # How many bars on each side to look for a swing high/low.
ANALYSIS_BAR_COUNT = 250     # Number of M5 bars to fetch for analysis.

# --- Global State & Logging ---
g_last_known_trade_id = None
TRADE_LOG_FILE = "trade_log.jsonl"
ACTIVE_TRADE_CONTEXT_FILE = "active_trade_context.json"

# --- Logging Function ---
def setup_logging(log_prefix):
    log_file = f"{log_prefix}_GATE.log"
    logger = logging.getLogger()
    logger.setLevel(logging.INFO)
    if logger.hasHandlers(): logger.handlers.clear()
    formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
    console_handler = logging.StreamHandler(sys.stdout)
    console_handler.setFormatter(formatter)
    logger.addHandler(console_handler)
    file_handler = TimedRotatingFileHandler(log_file, when="midnight", interval=1, backupCount=14, encoding='utf-8')
    file_handler.setFormatter(formatter)
    logger.addHandler(file_handler)

# --- Risk Management ---
def calculate_risk_adjusted_lot_size(symbol, sl_points, account_equity, risk_percent=1.0):
    if sl_points <= 0 or account_equity <= 0 or risk_percent == 0:
        logging.error("Cannot calculate lot size with zero or negative SL, equity, or risk.")
        return 0.0
        
    symbol_info = mt5.symbol_info(symbol)
    if not symbol_info:
        logging.error(f"Could not get symbol info for {symbol}")
        return 0.0

    user_max_lots = float(os.getenv("GATE_MAX_LOTS", 1.0))
    value_per_point_per_lot = symbol_info.trade_tick_value / symbol_info.trade_tick_size
    dollar_risk = account_equity * (risk_percent / 100.0)
    sl_dollar_value_per_lot = sl_points * value_per_point_per_lot
    
    if sl_dollar_value_per_lot <= 1e-9: # Avoid division by zero
        logging.error("Calculated SL dollar value is zero. Cannot divide.")
        return 0.0
    
    lot_size = dollar_risk / sl_dollar_value_per_lot
    
    min_volume = symbol_info.volume_min
    effective_max_volume = min(symbol_info.volume_max, user_max_lots)
    volume_step = symbol_info.volume_step
    
    lot_size = max(min_volume, min(lot_size, effective_max_volume))
    lot_size = round(lot_size / volume_step) * volume_step
    
    logging.info(f"Risk-Adjusted Lot Size: Risking {risk_percent}% of ${account_equity:,.2f} = ${dollar_risk:,.2f}. "
                 f"SL of {sl_points:.2f} points requires lot size of {lot_size:.2f} (Ceiling: {effective_max_volume} lots).")
                 
    return round(lot_size, 2)

# --- ======================================================= ---
# ---              MARKET STRUCTURE ENGINE (NEW)              ---
# --- ======================================================= ---

def find_swing_pivots(rates_df: pd.DataFrame, lookback: int) -> pd.DataFrame:
    """Identifies swing highs and lows from OHLC data in a sandboxed calculation."""
    high_indices = argrelextrema(rates_df['high'].values, np.greater_equal, order=lookback)[0]
    low_indices = argrelextrema(rates_df['low'].values, np.less_equal, order=lookback)[0]
    
    pivots = []
    for i in high_indices:
        pivots.append({'index': i, 'type': 'high', 'price': rates_df['high'].iloc[i]})
    for i in low_indices:
        pivots.append({'index': i, 'type': 'low', 'price': rates_df['low'].iloc[i]})
        
    if not pivots:
        return pd.DataFrame()
        
    pivots_df = pd.DataFrame(pivots).sort_values(by='index').reset_index(drop=True)
    
    # Filter out consecutive pivots of the same type to get a clean alternating sequence
    return pivots_df.loc[pivots_df['type'] != pivots_df['type'].shift()]

def analyze_trend_and_reversals(pivots_df: pd.DataFrame) -> Dict:
    """
    Analyzes a series of pivots to determine market structure (Trend, HH/HL, CHoCH).
    This is a sandboxed calculation.
    """
    analysis = {
        "trend": "RANGING",
        "last_high": None, "prev_high": None,
        "last_low": None, "prev_low": None,
        "change_of_character": None # Will be 'BULLISH' or 'BEARISH'
    }

    highs = pivots_df[pivots_df['type'] == 'high']
    lows = pivots_df[pivots_df['type'] == 'low']

    if len(highs) >= 2:
        analysis["last_high"] = highs.iloc[-1]
        analysis["prev_high"] = highs.iloc[-2]
    if len(lows) >= 2:
        analysis["last_low"] = lows.iloc[-1]
        analysis["prev_low"] = lows.iloc[-2]

    lh, ph = analysis.get("last_high"), analysis.get("prev_high")
    ll, pl = analysis.get("last_low"), analysis.get("prev_low")

    if not all([lh, ph, ll, pl]):
        return analysis # Not enough pivots for a full analysis

    # Trend Identification
    is_hh = lh['price'] > ph['price']
    is_hl = ll['price'] > pl['price']
    is_ll = ll['price'] < pl['price']
    is_lh = lh['price'] < ph['price']

    if is_hh and is_hl:
        analysis['trend'] = "UPTREND"
    elif is_ll and is_lh:
        analysis['trend'] = "DOWNTREND"

    # Change of Character (CHoCH) Detection
    # Bullish CHoCH: Market was in a downtrend, but then broke the last Lower High.
    if analysis['trend'] != "DOWNTREND" and not (is_ll and is_lh):
        if ph and pl and lh and ll:
            if ph['price'] < pivots_df.iloc[ph['index']-2]['price'] and pl['price'] < pivots_df.iloc[pl['index']-2]['price']:
                 if lh['price'] > ph['price']:
                      analysis['change_of_character'] = "BULLISH"

    # Bearish CHoCH: Market was in an uptrend, but then broke the last Higher Low.
    if analysis['trend'] != "UPTREND" and not (is_hh and is_hl):
        if ph and pl and lh and ll:
            if ph['price'] > pivots_df.iloc[ph['index']-2]['price'] and pl['price'] > pivots_df.iloc[pl['index']-2]['price']:
                 if ll['price'] < pl['price']:
                      analysis['change_of_character'] = "BEARISH"
                      
    return analysis

def find_supply_demand_zones(rates_df: pd.DataFrame, structure_analysis: Dict) -> Dict:
    """
    Finds the most recent Supply and Demand zones based on the last impulse move.
    This is a sandboxed calculation.
    """
    zones = {"demand": None, "supply": None}
    
    # Find Demand Zone (origin of the move that created the last high)
    last_high = structure_analysis.get("last_high")
    if last_high is not None:
        try:
            # The impulse move starts from the low *before* the last high
            impulse_start_pivot = structure_analysis.get("prev_low") if structure_analysis.get("trend") == "UPTREND" else structure_analysis.get("last_low")
            if impulse_start_pivot is not None and impulse_start_pivot.name < last_high.name:
                search_df = rates_df.iloc[impulse_start_pivot['index']:last_high['index']]
                # The demand zone is the last bearish candle before the strong up-move
                bearish_candles = search_df[search_df['close'] < search_df['open']]
                if not bearish_candles.empty:
                    origin_candle = bearish_candles.iloc[-1]
                    zones['demand'] = {'top': origin_candle['high'], 'bottom': origin_candle['low']}
        except Exception as e:
            logging.warning(f"Could not calculate demand zone: {e}")

    # Find Supply Zone (origin of the move that created the last low)
    last_low = structure_analysis.get("last_low")
    if last_low is not None:
        try:
            # The impulse move starts from the high *before* the last low
            impulse_start_pivot = structure_analysis.get("prev_high") if structure_analysis.get("trend") == "DOWNTREND" else structure_analysis.get("last_high")
            if impulse_start_pivot is not None and impulse_start_pivot.name < last_low.name:
                search_df = rates_df.iloc[impulse_start_pivot['index']:last_low['index']]
                # The supply zone is the last bullish candle before the strong down-move
                bullish_candles = search_df[search_df['close'] > search_df['open']]
                if not bullish_candles.empty:
                    origin_candle = bullish_candles.iloc[-1]
                    zones['supply'] = {'top': origin_candle['high'], 'bottom': origin_candle['low']}
        except Exception as e:
            logging.warning(f"Could not calculate supply zone: {e}")
            
    return zones

def get_market_structure_analysis(symbol: str) -> Optional[Dict]:
    """Main function to get the complete market structure analysis on the M5 chart."""
    rates = mt5.copy_rates_from_pos(symbol, mt5.TIMEFRAME_M5, 0, ANALYSIS_BAR_COUNT)
    if rates is None or len(rates) < 50:
        logging.warning("Insufficient M5 data for structure analysis.")
        return None

    rates_df = pd.DataFrame(rates)
    rates_df['index'] = rates_df.index
    
    pivots_df = find_swing_pivots(rates_df, lookback=PIVOT_LOOKBACK)
    if pivots_df.empty or len(pivots_df) < 4:
        logging.info("Not enough clear pivots found, market likely ranging or in strong impulse.")
        return {"status": "RANGING", "trend": "RANGING"}
        
    structure = analyze_trend_and_reversals(pivots_df)
    zones = find_supply_demand_zones(rates_df, structure)

    current_price = rates_df.iloc[-1]['close']
    
    # Format for easy consumption by the AI
    final_context = {
        "status": "ANALYSIS_COMPLETE",
        "current_price": round(current_price, 2),
        "trend": structure.get('trend'),
        "change_of_character": structure.get('change_of_character'),
        "last_swing_high": round(structure['last_high']['price'], 2) if structure.get('last_high') is not None else None,
        "last_swing_low": round(structure['last_low']['price'], 2) if structure.get('last_low') is not None else None,
        "active_demand_zone": zones.get('demand'),
        "active_supply_zone": zones.get('supply')
    }
    return final_context
    
# --- End of Market Structure Engine ---


# --- State Management & Context ---
def get_session_and_state():
    """Determines if the current time is within the focus trading session."""
    now_utc_time = datetime.now(timezone.utc).time()
    if FOCUS_SESSION_START_UTC <= now_utc_time < FOCUS_SESSION_END_UTC:
        return "FOCUS_SESSION", "London/NY Overlap", FOCUS_SESSION_STATE
    return "OUTSIDE_SESSION", "Outside of core trading hours", OUTSIDE_SESSION_STATE

def save_trade_context(trade_id, context_data):
    """Saves context for an active trade."""
    try:
        with open(ACTIVE_TRADE_CONTEXT_FILE, 'w') as f:
            json.dump({str(trade_id): context_data}, f)
    except IOError as e:
        logging.error(f"Could not save trade context for {trade_id}: {e}")

def load_trade_context(trade_id):
    """Loads context for an active trade."""
    if not os.path.exists(ACTIVE_TRADE_CONTEXT_FILE): return None
    try:
        with open(ACTIVE_TRADE_CONTEXT_FILE, 'r') as f:
            all_contexts = json.load(f)
            return all_contexts.get(str(trade_id))
    except (IOError, json.JSONDecodeError):
        return None

def clear_trade_context():
    """Clears the active trade context file."""
    if os.path.exists(ACTIVE_TRADE_CONTEXT_FILE):
        try:
            os.remove(ACTIVE_TRADE_CONTEXT_FILE)
            logging.info("Active trade context file cleared.")
        except OSError as e:
            logging.error(f"Error clearing trade context file: {e}")

# --- AI Interaction Layer ---
class PromptManager:
    def get_analyst_prompt(self, market_structure_context: Dict, session_name: str) -> str:
        
        context_str = json.dumps(market_structure_context, indent=2)

        return (
            f"You are a Day Trading Analyst AI specializing in pure Price Action. Your sole strategy is based on Market Structure (Highs/Lows) and Supply/Demand zones.\n\n"
            f"--- CORE STRATEGY RULES ---\n"
            f"1.  **Uptrend (Bullish):** Defined by Higher Highs (HH) and Higher Lows (HL). The primary plan is to BUY pullbacks to Demand Zones.\n"
            f"2.  **Downtrend (Bearish):** Defined by Lower Lows (LL) and Lower Highs (LH). The primary plan is to SELL rallies to Supply Zones.\n"
axf"3.  **Bullish Reversal (CHoCH):** A downtrend is in place, but price breaks ABOVE the last Lower High. The plan is to BUY the first pullback to the NEW Demand Zone created by this break.\n"
            f"4.  **Bearish Reversal (CHoCH):** An uptrend is in place, but price breaks BELOW the last Higher Low. The plan is to SELL the first rally to the NEW Supply Zone created by this break.\n\n"
            f"--- CURRENT MARKET ANALYSIS ---\n"
            f"Session: {session_name}\n"
            f"Analysis UTC Timestamp: {datetime.now(timezone.utc).isoformat()}\n"
            f"{context_str}\n\n"
            f"--- YOUR TASK: THE FINAL CHECKLIST ---\n"
            f"Based ONLY on the rules and market data provided, decide whether to BUY, SELL, or HOLD. You must logically justify your decision by answering these questions in your rationale:\n"
            f"1.  **Session Check:** Is the market in the FOCUS_SESSION (high volume)?\n"
            f"2.  **Structure Check:** What is the current market structure (Uptrend, Downtrend, or potential Reversal/CHoCH)?\n"
            f"3.  **Zone Check:** Has price pulled back to a valid Supply or Demand zone that aligns with the structure?\n"
            f"4.  **Confirmation Check:** Is the current price action showing signs of rejecting the zone (e.g., price moving away from the zone after touching it)? This is your entry confirmation.\n"
            f"5.  **Risk:Reward Check:** Does the setup offer a potential R:R of at least 1:{MINIMUM_RR_RATIO}? (e.g., For a BUY, is the distance to the last high at least {MINIMUM_RR_RATIO}x the distance to the bottom of the demand zone?).\n"
            f"If all checks pass, recommend a trade. If any check fails, you MUST recommend 'HOLD'.\n\n"
            f"--- CRITICAL OUTPUT FORMAT ---\n"
            f"Your *entire response* MUST be a single, valid JSON object.\n"
            f'**Schema:**\n{{"action": "BUY|SELL|HOLD", "confidence": <0.0-1.0 float>, "rationale": "Your concise reasoning based on the 5 checklist points."}}'
        )

class GeminiAnalyzer:
    def __init__(self, api_key):
        self.api_key = api_key
        self.headers = {"Content-Type": "application/json"}
        self.model = "gemini-2.5-flash-lite"
        self.api_url = f"https://generativelanguage.googleapis.com/v1beta/models/{self.model}:generateContent?key={self.api_key}"
    
    def get_decision(self, prompt):
        default_response = {"action": "HOLD", "confidence": 0.0, "rationale": "API Error or failed to parse response."}
        payload = {"contents": [{"parts": [{"text": prompt}]}]}
        text_response = ""
        try:
            response = requests.post(self.api_url, headers=self.headers, json=payload, timeout=90)
            response.raise_for_status()
            text_response = response.json()["candidates"][0]["content"]["parts"][0]["text"]
            
            start_index = text_response.find('{')
            end_index = text_response.rfind('}')
            
            if start_index != -1 and end_index != -1 and end_index > start_index:
                json_str = text_response[start_index:end_index+1]
                return json.loads(json_str)
            else:
                raise json.JSONDecodeError("Could not find valid JSON object in response.", text_response, 0)

        except json.JSONDecodeError as e:
            logging.error(f"Could not parse JSON from Gemini. Error: {e}. Full response:\n{text_response}")
        except requests.exceptions.HTTPError as http_err:
            logging.error(f"Gemini API call failed with HTTP Error: {http_err}", exc_info=True)
        except Exception as e:
            logging.error(f"Gemini API request failed unexpectedly: {e}", exc_info=True)
            
        return default_response

# --- Trade Execution ---
def get_active_trade(symbol, magic_number):
    positions = mt5.positions_get(symbol=symbol, magic=magic_number)
    return positions[0] if positions else None

def open_trade(symbol, direction, lots, magic_number, sl_price, tp_price, context_to_save={}):
    tick = mt5.symbol_info_tick(symbol)
    if not tick:
        logging.error("Failed to get tick for symbol.")
        return None
    price = tick.ask if direction == "BUY" else tick.bid
    digits = mt5.symbol_info(symbol).digits
    comment = f"{SCRIPT_VERSION}"[:31]
    
    request = {
        "action": mt5.TRADE_ACTION_DEAL,
        "symbol": symbol,
        "volume": float(lots),
        "type": mt5.ORDER_TYPE_BUY if direction == "BUY" else mt5.ORDER_TYPE_SELL,
        "price": price,
        "sl": round(sl_price, digits),
        "tp": round(tp_price, digits),
        "magic": magic_number,
        "comment": comment
    }
    
    logging.info(f"Attempting to open {direction} {lots} lots of {symbol} @ {price:.{digits}f}")
    result = mt5.order_send(request)
    
    if result and result.retcode == mt5.TRADE_RETCODE_DONE:
        logging.info(f"SUCCESS: Order {result.order} sent. Finding position ID...")
        time.sleep(1) # Allow time for the position to appear
        positions = mt5.positions_get(symbol=symbol, magic=magic_number)
        if positions:
            new_position = max(positions, key=lambda p: p.ticket)
            logging.info(f"✅ Position opened with ID: #{new_position.ticket}.")
            if context_to_save:
                save_trade_context(new_position.ticket, context_to_save)
            return new_position.ticket
        else:
            logging.error("Order executed but could not find the resulting position.")
    else:
        logging.error(f"FAILED: Order send failed. Retcode: {result.retcode if result else 'N/A'}. Error: {mt5.last_error()}")
        
    return None

# --- Main Analysis & Execution Cycle ---
def run_analysis_cycle(analyzer, prompter, symbol, magic, current_trading_state, session_name):
    global g_last_known_trade_id
    logging.info("--- Running Analysis Cycle ---")

    active_trade = get_active_trade(symbol, magic)
    if active_trade:
        logging.info(f"Holding active trade #{active_trade.ticket}. Skipping new trade analysis.")
        return

    # 1. Get Market Structure Context
    market_structure_context = get_market_structure_analysis(symbol)
    if not market_structure_context or market_structure_context['status'] != 'ANALYSIS_COMPLETE':
        logging.warning("Analysis skipped: Market structure could not be determined.")
        return

    # 2. Get AI Decision
    analyst_prompt = prompter.get_analyst_prompt(market_structure_context, session_name)
    decision = analyzer.get_decision(analyst_prompt)
    logging.info(f"Analyst AI Decision: {decision.get('action')} (Conf: {decision.get('confidence')}). Rationale: {decision.get('rationale')}")

    action = decision.get("action", "HOLD").upper()
    confidence = decision.get('confidence', 0.0)

    # 3. Execute Trade if Conditions Met
    if action in ["BUY", "SELL"] and confidence >= current_trading_state.min_confidence:
        
        # Calculate SL and TP based on the structure provided to the AI
        sl_price, tp_price = 0.0, 0.0
        if action == "BUY":
            demand_zone = market_structure_context.get("active_demand_zone")
            last_high = market_structure_context.get("last_swing_high")
            if not demand_zone or not last_high:
                logging.warning("VETO: Cannot execute BUY. Missing Demand Zone or Last High for risk calculation.")
                return
            sl_price = demand_zone['bottom'] - (MINIMUM_SL_POINTS / 10) # Buffer SL slightly
            tp_price = last_high
        
        elif action == "SELL":
            supply_zone = market_structure_context.get("active_supply_zone")
            last_low = market_structure_context.get("last_swing_low")
            if not supply_zone or not last_low:
                logging.warning("VETO: Cannot execute SELL. Missing Supply Zone or Last Low for risk calculation.")
                return
            sl_price = supply_zone['top'] + (MINIMUM_SL_POINTS / 10) # Buffer SL slightly
            tp_price = last_low
        
        # Final Risk Check
        current_price = market_structure_context['current_price']
        sl_points = abs(current_price - sl_price)
        tp_points = abs(tp_price - current_price)
        
        if sl_points < MINIMUM_SL_POINTS or (tp_points / sl_points) < MINIMUM_RR_RATIO:
            logging.warning(f"VETO: Trade does not meet risk criteria. SL Points: {sl_points:.2f}, R:R: {tp_points/sl_points:.2f}")
            return

        account_info = mt5.account_info()
        if not account_info:
            logging.error("VETO: Could not retrieve account info for lot size calculation.")
            return
            
        lot_size = calculate_risk_adjusted_lot_size(symbol, sl_points, account_info.equity, current_trading_state.risk_percent)

        if lot_size > 0:
            context_to_save = {
                "entry_strategy": f"GATE_{market_structure_context['trend']}",
                "market_context": market_structure_context,
                "ai_rationale": decision.get('rationale')
            }
            trade_id = open_trade(symbol, action, lot_size, magic, sl_price, tp_price, context_to_save)
            if trade_id:
                g_last_known_trade_id = trade_id
        else:
            logging.error("Trade execution halted due to zero lot size calculation.")

def log_closed_trade(trade_id, symbol):
    """Logs the summary of a closed trade."""
    global g_last_known_trade_id
    try:
        history_deals = mt5.history_deals_get(position=trade_id)
        if not history_deals:
            logging.warning(f"Could not find deal history for closed position #{trade_id}.")
            return
            
        entry_deal = next((d for d in history_deals if d.entry == mt5.DEAL_ENTRY_IN), None)
        if not entry_deal:
            logging.warning(f"Could not find entry deal for position #{trade_id}.")
            return
            
        total_profit = sum(d.profit for d in history_deals if d.position_id == trade_id)
        exit_deal = history_deals[-1]

        details = {
            "trade_id": int(trade_id), "symbol": symbol,
            "direction": "BUY" if entry_deal.type == mt5.ORDER_TYPE_BUY else "SELL",
            "profit_loss": round(total_profit, 2),
            "outcome": "WIN" if total_profit > 0 else "LOSS",
            "entry_time": datetime.fromtimestamp(entry_deal.time, tz=UTC_TZ).isoformat(),
            "exit_time": datetime.fromtimestamp(exit_deal.time, tz=UTC_TZ).isoformat(),
            "exit_reason": exit_deal.comment
        }

        with open(TRADE_LOG_FILE, "a") as f:
            f.write(json.dumps(details) + "\n")
        
        logging.info(f"Trade Closed: ID #{details['trade_id']} | Outcome: {details['outcome']} | P/L: ${details['profit_loss']:.2f} | Reason: {details['exit_reason']}")

    except Exception as e:
        logging.error(f"Failed during closed trade logging for #{trade_id}: {e}", exc_info=True)
    finally:
        g_last_known_trade_id = None
        clear_trade_context()


# --- Main Entry Point ---
def main():
    global g_last_known_trade_id
    load_dotenv()
    
    # Environment Variable Validation
    required_vars = ["GATE_SYMBOL", "GEMINI_API_KEY", "MT5_TERMINAL_PATH", "MT5_LOGIN", "MT5_PASSWORD", "MT5_BROKER", "GATE_MAGIC"]
    if any(not os.getenv(var) for var in required_vars):
        logging.basicConfig()
        logging.critical(f"FATAL ERROR: Missing one or more required environment variables: {', '.join(required_vars)}")
        sys.exit(1)

    SYMBOL = os.getenv("GATE_SYMBOL")
    GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
    TERMINAL_PATH = os.getenv("MT5_TERMINAL_PATH")
    LOGIN = int(os.getenv("MT5_LOGIN"))
    PASSWORD = os.getenv("MT5_PASSWORD")
    BROKER = os.getenv("MT5_BROKER")
    MAGIC = int(os.getenv("GATE_MAGIC"))

    setup_logging(SYMBOL)

    if not mt5.initialize(path=TERMINAL_PATH, timeout=60000) or not mt5.login(login=LOGIN, password=PASSWORD, server=BROKER):
        logging.error(f"MT5 Connection Failed: {mt5.last_error()}"); sys.exit(1)

    logging.info(f"--- Starting {SCRIPT_VERSION} for {SYMBOL} ---")
    
    analyzer = GeminiAnalyzer(GEMINI_API_KEY)
    prompter = PromptManager()
    
    active_trade = get_active_trade(SYMBOL, MAGIC)
    g_last_known_trade_id = active_trade.ticket if active_trade else None
    logging.info(f"Initial check: {'Active trade #' + str(g_last_known_trade_id) if g_last_known_trade_id else 'No active trade found'}.")
    
    def shutdown_handler(signum, frame):
        logging.info("Shutdown signal received. Exiting gracefully...");
        mt5.shutdown();
        sys.exit(0)
    signal.signal(signal.SIGINT, shutdown_handler)
    signal.signal(signal.SIGTERM, shutdown_handler)

    last_bar_time = 0
    while True:
        try:
            # Check for closed trades
            if g_last_known_trade_id and not get_active_trade(SYMBOL, MAGIC):
                log_closed_trade(g_last_known_trade_id, SYMBOL)

            new_bar = mt5.copy_rates_from_pos(SYMBOL, mt5.TIMEFRAME_M5, 0, 1)
            if not new_bar:
                time.sleep(10)
                continue

            current_bar_time = new_bar[0]['time']
            if current_bar_time != last_bar_time:
                last_bar_time = current_bar_time
                logging.info(f"--- New M5 Bar [{datetime.fromtimestamp(last_bar_time, tz=UTC_TZ).strftime('%Y-%m-%d %H:%M:%S UTC')}] ---")
                
                session_name, session_reason, trading_state = get_session_and_state()
                logging.info(f"Current Session: {session_name} ({session_reason})")

                if trading_state.risk_percent > 0: # Only run analysis if trading is allowed
                    run_analysis_cycle(analyzer, prompter, SYMBOL, MAGIC, trading_state, session_name)
            
            time.sleep(5)
        except Exception as e:
            logging.error(f"Main loop error: {e}", exc_info=True)
            time.sleep(30)

if __name__ == "__main__":
    main()