import MetaTrader5 as mt5 import json import os import time import math import logging import pandas as pd import numpy as np # These are substituted by the generator script's .format() MT5_LOGIN = 52212233 MT5_PASSWORD = "$bQD8vMwOlbosu" MT5_SERVER = "ICMarketsSC-Demo" MT5_PATH = "C:\\MT5_Demo_ICMarkets_52212233\\terminal64.exe" # Path for MT5 terminal RISK_PERCENT = 2.0 # Default risk percentage BROKER_NAME = "ICMarkets" REAL_MONEY = "Demo" # "Live" or "Demo" ACCOUNT_TYPE = "M1" # e.g., "M1" MONITOR_INTERVAL = 10.0 # Interval for master to monitor positions IS_MASTER = True # Boolean: True if this is the master account # --- Centralized Directory Configuration --- STREAMLINE_DIR = "C:\\trading-bot\\streamline" LOGS_DIR = os.path.join(STREAMLINE_DIR, "logs") # For log files # Create directories if they don't exist os.makedirs(STREAMLINE_DIR, exist_ok=True) os.makedirs(LOGS_DIR, exist_ok=True) # --- Constants derived from ACCOUNT_TYPE or hardcoded paths --- # ORDER_FILE is now dynamic based on ACCOUNT_TYPE but uses STREAMLINE_DIR ORDER_FILE = os.path.join(STREAMLINE_DIR, "order_fast.json") if ACCOUNT_TYPE == "M1" else os.path.join(STREAMLINE_DIR, "temp_order.json") # These prefixes remain the same, but their usage with paths will change FEEDBACK_PREFIX = "trade_feedback_fast" if ACCOUNT_TYPE == "M1" else "trade_feedback" DONE_PREFIX = "done_fast" if ACCOUNT_TYPE == "M1" else "done" HEARTBEAT_FILE = os.path.join(STREAMLINE_DIR, "master_heartbeat.txt") # Corrected path SYMBOL_MAP = { "DE30": ["DE40", "DAX"], "USTEC": ["USTEC", "NSDQ"], "HK50": ["HK50", "HKG"], "JP225": ["JP225", "NIKKEI"], "US500": ["US500", "SPX"], "GBPUSD": ["GBPUSD", "GBPUSD"], "USDJPY": ["USDJPY", "USDJPY"], "AUDUSD": ["AUDUSD", "AUDUSD"], "USDCAD": ["USDCAD", "USDCAD"], "EURUSD": ["EURUSD", "EURUSD"], "ETHUSD": ["ETHUSD", "ETHUSD"], "XAUUSD": ["XAUUSD", "GOLD"], "XTIUSD": ["XTIUSD", "WTI"], "BTCUSD": ["BTCUSD", "BTCUSD"], "TSLA": ["TSLA.NAS", "TSLA"], "NVDA": ["NVDA.NAS", "NVDA"], "AAPL": ["AAPL.NAS", "AAPL"], "MSFT": ["MSFT.NAS", "MSFT"], "MSTR": ["MSTR.NAS", "MSTR"], "BABA": ["BABA.NYSE", "BABA"], "SAP": ["SAP.ETR", "SAP"], "AMZN": ["AMZN.NAS", "AMZN"], "PLTR": ["PLTR.NAS", "PLTR"], "AMD": ["AMD.NAS", "AMD"], "PYPL": ["PYPL.NAS", "PYPL"], "ORCL": ["ORCL.NYSE", "ORCL"], "JPM": ["JPM.NYSE", "JPM"] } # Configure logging for this specific executor instance log_filename = os.path.join(LOGS_DIR, f'mt5_executor_{BROKER_NAME}_{MT5_LOGIN}_{ACCOUNT_TYPE.lower()}.log') # Corrected path logging.basicConfig( filename=log_filename, level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s', force=True ) # --- MT5 Initialization --- if not mt5.initialize(path=MT5_PATH, login=MT5_LOGIN, password=MT5_PASSWORD, server=MT5_SERVER): print(f"MT5 init failed for account {MT5_LOGIN}: {mt5.last_error()}") logging.error(f"MT5 initialization failed for account {MT5_LOGIN}: {mt5.last_error()}") quit() else: print(f"MT5 initialized for {BROKER_NAME} Account {MT5_LOGIN} ({ACCOUNT_TYPE})") logging.info(f"MT5 initialized for {BROKER_NAME} Account {MT5_LOGIN} ({ACCOUNT_TYPE})") # --- Timeframe Mapping --- TIMEFRAME_MAP = { 'M1': mt5.TIMEFRAME_M1, 'M5': mt5.TIMEFRAME_M5, 'M15': mt5.TIMEFRAME_M15, 'M30': mt5.TIMEFRAME_M30, 'H1': mt5.TIMEFRAME_H1, 'H4': mt5.TIMEFRAME_H4, 'D1': mt5.TIMEFRAME_D1 } TRADE_INFO_FILE = os.path.join(STREAMLINE_DIR, f"trade_info_{MT5_LOGIN}.json") # Corrected path # --- Position Management Stages --- STAGE_INITIAL = 0 STAGE_BE_AT_0_4R = 1 STAGE_CLOSE20_AT_0_6R = 2 STAGE_CLOSE20_SL0_2R_AT_1R = 3 STAGE_CLOSE40_SL1R_AT_1_5R = 4 # After 40% closed, SL moved to 1R for remaining 20% STAGE_FULLY_CLOSED_AT_1_5R = 5 # Final 20% closed def save_trade_info(positions_to_save): try: with open(TRADE_INFO_FILE, "w") as f: json.dump(positions_to_save, f, indent=4) logging.info(f"Saved trade info: {len(positions_to_save)} positions to {TRADE_INFO_FILE}") except Exception as e: logging.error(f"Error saving trade info to {TRADE_INFO_FILE}: {str(e)}") def load_trade_info(): try: with open(TRADE_INFO_FILE, "r") as f: return json.load(f) except FileNotFoundError: logging.info(f"Trade info file not found: {TRADE_INFO_FILE}") return [] except json.JSONDecodeError as e: logging.error(f"Error decoding JSON from trade info file {TRADE_INFO_FILE}: {str(e)}") return [] except Exception as e: logging.error(f"Error loading trade info from {TRADE_INFO_FILE}: {str(e)}") return [] def clear_trade_info(symbol_to_clear): positions = load_trade_info() positions = [p for p in positions if p.get('symbol') != symbol_to_clear] save_trade_info(positions) logging.info(f"Cleared trade info for symbol: {symbol_to_clear}") def get_conversion_rate(base_currency, account_curr): if base_currency == account_curr: return 1.0 pairs_to_try = [base_currency + account_curr, account_curr + base_currency] max_attempts = 3 for attempt in range(max_attempts): for i, pair_symbol in enumerate(pairs_to_try): if mt5.symbol_select(pair_symbol, True): tick = mt5.symbol_info_tick(pair_symbol) if tick: if i == 0 and tick.ask > 0: rate = tick.ask logging.info(f"Conversion via {pair_symbol} (ask): base={base_currency}, account={account_curr}, rate={rate}") return rate elif i == 1 and tick.bid > 0: rate = tick.bid # This is rate for ACCOUNT_CURR / BASE_CURRENCY logging.info(f"Conversion via {pair_symbol} (bid): base={base_currency}, account={account_curr}, rate={1/rate} (inverted from {rate})") return 1.0 / rate if attempt < max_attempts -1: time.sleep(0.5) manual_rates = { ('JPY', 'EUR'): 0.00616, ('HKD', 'EUR'): 0.118, ('USD', 'EUR'): 0.926, ('EUR', 'USD'): 1.08 } key = (base_currency, account_curr) reverse_key = (account_curr, base_currency) if key in manual_rates: rate = manual_rates[key] logging.warning(f"Using manual rate for {base_currency} to {account_curr}: {rate}") return rate elif reverse_key in manual_rates: rate = 1.0 / manual_rates[reverse_key] logging.warning(f"Using manual reverse rate for {base_currency} to {account_curr}: {rate}") return rate logging.warning(f"No conversion rate found for {base_currency} to {account_curr}, defaulting to 1.0") return 1.0 def get_pip_size(symbol_upper, spec_info): point_val = spec_info.point if len(symbol_upper) == 6 and symbol_upper[3:] in ['USD', 'JPY', 'CAD', 'AUD', 'EUR', 'GBP', 'CHF', 'NZD']: return point_val * 10 if symbol_upper == 'XAUUSD': return point_val * 10 if symbol_upper == 'XTIUSD': return point_val if symbol_upper in ['DE30', 'DE40', 'USTEC', 'HK50', 'JP225', 'US500', 'NDX', 'SPX', 'DAX', 'NSDQ', 'HKG', 'NIKKEI']: return 10**spec_info.digits * point_val if spec_info.digits > 0 else point_val if '.NAS' in symbol_upper or '.NYSE' in symbol_upper or '.ETR' in symbol_upper or \ symbol_upper in ['TSLA', 'NVDA', 'AAPL', 'MSFT', 'MSTR', 'AMZN', 'PLTR', 'AMD', 'PYPL', 'BABA', 'ORCL', 'JPM', 'SAP']: return 0.01 if symbol_upper == 'ETHUSD': return point_val logging.warning(f"Pip size not defined for symbol {symbol_upper}, defaulting using spec.point ({point_val}). This might be incorrect.") return point_val def snap_to_step(volume_val, vol_step, min_vol, max_vol): if vol_step == 0: return max(min_vol, min(volume_val, max_vol)) if "." in str(vol_step): precision = len(str(vol_step).split('.')[1]) else: precision = 0 volume_val = float(volume_val) if volume_val % vol_step == 0: snapped_volume = round(volume_val, precision) else: snapped_volume = round(volume_val / vol_step) * vol_step snapped_volume = round(snapped_volume, precision) return max(min_vol, min(snapped_volume, max_vol)) def calculate_position_size(calc_symbol, sl_distance_points, risk_perc): acc_info = mt5.account_info() if not acc_info: logging.error("Failed to get account info in calculate_position_size.") return 0.01, "Account info fetch failed", "N/A", 0, risk_perc equity_val = acc_info.equity account_currency_val = acc_info.currency if not equity_val or equity_val <= 0: return 0.01, "Equity fetch failed or zero", account_currency_val, 0, risk_perc spec = mt5.symbol_info(calc_symbol) if not spec: return 0.01, f"Symbol spec failed for {calc_symbol}", account_currency_val, 0, risk_perc if sl_distance_points <= 0: logging.error(f"Invalid SL distance ({sl_distance_points}) for {calc_symbol}.") return 0.01, "Invalid SL distance (<=0)", account_currency_val, 0, risk_perc pip_size_val = get_pip_size(calc_symbol, spec) if pip_size_val is None or pip_size_val <= 0: return 0.01, f"Unknown or invalid pip size ({pip_size_val}) for {calc_symbol}", account_currency_val, 0, risk_perc contract_size_val = spec.trade_contract_size pip_value_one_lot_profit_curr = contract_size_val * pip_size_val conversion_rate_to_account = get_conversion_rate(spec.currency_profit, account_currency_val) pip_value_one_lot_account_curr = pip_value_one_lot_profit_curr * conversion_rate_to_account if pip_value_one_lot_account_curr <= 0: logging.error(f"Calculated pip value ({pip_value_one_lot_account_curr}) is invalid for {calc_symbol}.") return 0.01, "Invalid pip value calculation", account_currency_val, 0, risk_perc sl_pips_count = sl_distance_points / pip_size_val if sl_pips_count <= 0: return 0.01, f"Invalid SL pips ({sl_pips_count}) for {calc_symbol}", account_currency_val, 0, risk_perc risk_amount_account_curr = equity_val * (risk_perc / 100.0) calculated_volume = risk_amount_account_curr / (sl_pips_count * pip_value_one_lot_account_curr) calculated_volume_snapped = snap_to_step(calculated_volume, spec.volume_step, spec.volume_min, spec.volume_max) size_info_str = (f"Equity: {equity_val:.2f} {account_currency_val}, SL dist pts: {sl_distance_points}, " f"PipSize: {pip_size_val}, SL pips: {sl_pips_count:.2f}, " f"PipValue/Lot: {pip_value_one_lot_account_curr:.2f} {account_currency_val}, " f"RiskAmt: {risk_amount_account_curr:.2f} {account_currency_val}, " f"CalcVol (raw): {calculated_volume:.4f}, CalcVol (snapped): {calculated_volume_snapped}") print(size_info_str) logging.info(f"Calculate Position Size for {calc_symbol} - {size_info_str}") return calculated_volume_snapped, size_info_str, account_currency_val, round(risk_amount_account_curr, 2), risk_perc def calculate_sltp(calc_sl_tp_symbol, trade_direction, sl_tp_mode): timeframe_to_use = TIMEFRAME_MAP.get(ACCOUNT_TYPE, mt5.TIMEFRAME_M1) # Account_type is M1, H1 etc for timeframe key. rates = mt5.copy_rates_from_pos(calc_sl_tp_symbol, timeframe_to_use, 0, 200) if rates is None or len(rates) < 20: logging.error(f"Failed to fetch sufficient rates ({len(rates) if rates is not None else 0}/20) for {calc_sl_tp_symbol} on timeframe {ACCOUNT_TYPE}.") # This ACCOUNT_TYPE is M1, not the timeframe itself. return None, None, None, None df = pd.DataFrame(rates) df['time'] = pd.to_datetime(df['time'], unit='s') atr_period = 14 df['h-l'] = df['high'] - df['low'] df['h-pc'] = abs(df['high'] - df['close'].shift(1)) df['l-pc'] = abs(df['low'] - df['close'].shift(1)) df['tr'] = df[['h-l', 'h-pc', 'l-pc']].max(axis=1) df['atr'] = df['tr'].rolling(window=atr_period).mean() current_atr_value = df['atr'].iloc[-1] if pd.isna(current_atr_value) or current_atr_value <= 0: logging.error(f"ATR calculation failed or resulted in non-positive value ({current_atr_value}) for {calc_sl_tp_symbol}.") return None, None, None, None if sl_tp_mode == 'atr10': sl_atr_multiplier = 10.0 tp_atr_multiplier = 15.0 else: logging.error(f"Unsupported sl_tp_mode '{sl_tp_mode}' in calculate_sltp. Defaulting to atr10.") sl_atr_multiplier = 10.0 tp_atr_multiplier = 15.0 sl_distance_from_entry_price = current_atr_value * sl_atr_multiplier tp_distance_from_entry_price = current_atr_value * tp_atr_multiplier tick_info = mt5.symbol_info_tick(calc_sl_tp_symbol) if not tick_info: logging.error(f"Failed to get tick info for {calc_sl_tp_symbol} in SL/TP calculation.") return None, None, None, None entry_price_val = tick_info.ask if trade_direction == 'BUY' else tick_info.bid spec = mt5.symbol_info(calc_sl_tp_symbol) # Define spec here if not spec: logging.error(f"Failed to get symbol spec for {calc_sl_tp_symbol} for SL/TP rounding.") return None, None, None, None if trade_direction == 'BUY': stop_loss_price = entry_price_val - sl_distance_from_entry_price take_profit_price = entry_price_val + tp_distance_from_entry_price else: stop_loss_price = entry_price_val + sl_distance_from_entry_price take_profit_price = entry_price_val - tp_distance_from_entry_price stop_loss_price = round(stop_loss_price, spec.digits) take_profit_price = round(take_profit_price, spec.digits) entry_price_val = round(entry_price_val, spec.digits) logging.info(f"SL/TP Calc for {calc_sl_tp_symbol} ({trade_direction}, {sl_tp_mode}): Entry={entry_price_val:.{spec.digits}f}, SL={stop_loss_price:.{spec.digits}f}, TP={take_profit_price:.{spec.digits}f}, ATR(14)={current_atr_value:.{spec.digits}f}, SL_Dist={sl_distance_from_entry_price:.{spec.digits}f}") return entry_price_val, stop_loss_price, take_profit_price, current_atr_value def monitor_positions(symbols_to_watch): if not IS_MASTER: return tracked_positions = load_trade_info() if not tracked_positions: logging.debug(f"Master {MT5_LOGIN}: No tracked positions to monitor.") return active_pos_indices_to_remove = [] for idx, tracked_pos_item in enumerate(tracked_positions): pos_symbol = tracked_pos_item.get('symbol') pos_ticket_from_file = tracked_pos_item.get('ticket') order_timestamp_from_file = tracked_pos_item.get('timestamp') if not pos_symbol or not pos_ticket_from_file: logging.warning(f"Skipping item with no sym/ticket: {tracked_pos_item}") continue pos_direction = tracked_pos_item.get('direction') try: pos_entry_price = float(tracked_pos_item['entry']) initial_sl_distance_R = float(tracked_pos_item['initial_sl_distance_points']) original_pos_volume = float(tracked_pos_item['original_volume']) current_management_stage = int(tracked_pos_item.get('current_management_stage', STAGE_INITIAL)) closed_partially_volume_total = float(tracked_pos_item.get('closed_partially_volume_total', 0.0)) except (TypeError, ValueError, KeyError) as e_data: logging.error(f"Critical data error for pos {pos_ticket_from_file} ({pos_symbol}): {e_data}. Data: {tracked_pos_item}. Skipping.") continue if initial_sl_distance_R <= 0: logging.warning(f"Initial SL distance (R) is zero or negative for position {pos_ticket_from_file} ({pos_symbol}): {initial_sl_distance_R}. SL management impaired.") continue mt5_position_obj = None found_in_mt5 = False max_retries_for_new_trade = 3 retry_delay_base = 0.8 retry_delay_increment = 0.4 recent_trade_threshold = 30 all_current_positions_list_debug = None final_attempt_count_debug = 0 for attempt_debug in range(max_retries_for_new_trade): final_attempt_count_debug = attempt_debug + 1 all_current_positions_list_debug = mt5.positions_get() if all_current_positions_list_debug is None: logging.error(f"Failed MT5 pos get (all) for {pos_symbol} (T:{pos_ticket_from_file}) att {final_attempt_count_debug}. Err:{mt5.last_error()}") if attempt_debug < max_retries_for_new_trade - 1: time.sleep(retry_delay_base + (attempt_debug * retry_delay_increment)); continue else: break for p_obj_loop_inner in all_current_positions_list_debug: if p_obj_loop_inner.symbol == pos_symbol and p_obj_loop_inner.ticket == pos_ticket_from_file: mt5_position_obj = p_obj_loop_inner found_in_mt5 = True; break if found_in_mt5: break if attempt_debug < max_retries_for_new_trade - 1: is_recent_flag = False if order_timestamp_from_file: try: is_recent_flag = (time.time() - float(order_timestamp_from_file)) < recent_trade_threshold except ValueError: logging.warning(f"Invalid TS '{order_timestamp_from_file}' for pos {pos_ticket_from_file}") if is_recent_flag: delay_val_local = retry_delay_base + (attempt_debug * retry_delay_increment) logging.info(f"Pos {pos_ticket_from_file} ({pos_symbol}) not found att {final_attempt_count_debug}. Retrying in {delay_val_local:.2f}s.") time.sleep(delay_val_local) else: break if not mt5_position_obj: reason_nf_local = "API error" if all_current_positions_list_debug is None and final_attempt_count_debug == max_retries_for_new_trade else f"Not found after {final_attempt_count_debug} attempts" logging.info(f"Pos {pos_ticket_from_file} ({pos_symbol}) {reason_nf_local}. Assuming closed.") feedback_ts_local_nf = int(time.time()) # This feedback is internal to Python or for manual review, not directly for Node.js's main feedback loop feedback_fp_local_nf = os.path.join(STREAMLINE_DIR, f"{FEEDBACK_PREFIX}_{MT5_LOGIN}_{pos_ticket_from_file}_{feedback_ts_local_nf}_closed_nf.txt") initial_sl_price_for_feedback = float(tracked_pos_item.get('sl', 0.0)) fb_content_local_nf = (f"Account:{MT5_LOGIN}\nBroker:{BROKER_NAME}\nSymbol:{pos_symbol}\n" f"Position:{pos_ticket_from_file}\nDirection:{pos_direction}\nEntry:{pos_entry_price:.5f}\n" f"ProfitLvlReached:-100.00%\nProfitLvlVal:{initial_sl_price_for_feedback:.5f}\n" f"Reason:Pos Closed ({reason_nf_local})\n---\n") try: with open(feedback_fp_local_nf, "w") as f_write_nf: f_write_nf.write(fb_content_local_nf) logging.info(f"Wrote 'pos not found' feedback to {feedback_fp_local_nf}") except Exception as e_write_nf: logging.error(f"Error writing 'pos not found' feedback: {str(e_write_nf)}") active_pos_indices_to_remove.append(idx) continue current_market_price = mt5_position_obj.price_current profit_points = (current_market_price - pos_entry_price) if pos_direction == 'BUY' else (pos_entry_price - current_market_price) profit_R_multiple = (profit_points / initial_sl_distance_R) if initial_sl_distance_R > 0 else 0.0 spec_details = mt5.symbol_info(pos_symbol) # spec_details for this scope if not spec_details: logging.error(f"Could not get symbol_info for {pos_symbol} during monitoring. Skipping management for {pos_ticket_from_file}.") continue new_sl_for_modification = None action_for_modification = None volume_for_partial_close = None next_management_stage = current_management_stage closed_volume_this_step = 0.0 current_profit_level_for_feedback = tracked_pos_item.get('profit_level', 0.0) logging.debug(f"Pos {pos_ticket_from_file} ({pos_symbol}): Stage={current_management_stage}, Profit={profit_R_multiple:.2f}R, Mkt={current_market_price:.{spec_details.digits}f}, E={pos_entry_price:.{spec_details.digits}f}, R={initial_sl_distance_R:.{spec_details.digits}f}") if current_management_stage == STAGE_INITIAL and profit_R_multiple >= 0.4: new_sl_for_modification = pos_entry_price action_for_modification = "modify" next_management_stage = STAGE_BE_AT_0_4R current_profit_level_for_feedback = 0.4 logging.info(f"Pos {pos_ticket_from_file}: Profit {profit_R_multiple:.2f}R >= 0.4R. Moving SL to BE ({new_sl_for_modification:.{spec_details.digits}f}). Stage: {current_management_stage} -> {next_management_stage}") elif current_management_stage == STAGE_BE_AT_0_4R and profit_R_multiple >= 0.6: volume_to_close_raw = original_pos_volume * 0.20 volume_for_partial_close = snap_to_step(volume_to_close_raw, spec_details.volume_step, spec_details.volume_min, mt5_position_obj.volume) if volume_for_partial_close >= spec_details.volume_min and volume_for_partial_close <= mt5_position_obj.volume : action_for_modification = 'close' next_management_stage = STAGE_CLOSE20_AT_0_6R closed_volume_this_step = volume_for_partial_close current_profit_level_for_feedback = 0.6 logging.info(f"Pos {pos_ticket_from_file}: Profit {profit_R_multiple:.2f}R >= 0.6R. Closing 20% ({volume_for_partial_close}). Stage: {current_management_stage} -> {next_management_stage}") else: logging.warning(f"Pos {pos_ticket_from_file}: Cannot close 20% ({volume_for_partial_close}) at 0.6R.") elif current_management_stage == STAGE_CLOSE20_AT_0_6R and profit_R_multiple >= 1.0: volume_to_close_raw = original_pos_volume * 0.20 volume_for_partial_close = snap_to_step(volume_to_close_raw, spec_details.volume_step, spec_details.volume_min, mt5_position_obj.volume) if volume_for_partial_close >= spec_details.volume_min and volume_for_partial_close <= mt5_position_obj.volume: action_for_modification = 'close' sl_adj_val = initial_sl_distance_R * 0.2 new_sl_for_modification = (pos_entry_price + sl_adj_val) if pos_direction == 'BUY' else (pos_entry_price - sl_adj_val) next_management_stage = STAGE_CLOSE20_SL0_2R_AT_1R closed_volume_this_step = volume_for_partial_close current_profit_level_for_feedback = 1.0 logging.info(f"Pos {pos_ticket_from_file}: Profit {profit_R_multiple:.2f}R >= 1.0R. Closing 20% ({volume_for_partial_close}). New SL to +0.2R ({new_sl_for_modification:.{spec_details.digits}f}). Stage: {current_management_stage} -> {next_management_stage}") else: logging.warning(f"Pos {pos_ticket_from_file}: Cannot close 20% ({volume_for_partial_close}) at 1.0R.") elif current_management_stage == STAGE_CLOSE20_SL0_2R_AT_1R and profit_R_multiple >= 1.5: volume_to_close_raw = original_pos_volume * 0.40 volume_for_partial_close = snap_to_step(volume_to_close_raw, spec_details.volume_step, spec_details.volume_min, mt5_position_obj.volume) if volume_for_partial_close >= spec_details.volume_min and volume_for_partial_close <= mt5_position_obj.volume: action_for_modification = 'close' sl_adj_val = initial_sl_distance_R * 1.0 new_sl_for_modification = (pos_entry_price + sl_adj_val) if pos_direction == 'BUY' else (pos_entry_price - sl_adj_val) next_management_stage = STAGE_CLOSE40_SL1R_AT_1_5R closed_volume_this_step = volume_for_partial_close current_profit_level_for_feedback = 1.5 logging.info(f"Pos {pos_ticket_from_file}: Profit {profit_R_multiple:.2f}R >= 1.5R. Closing 40% ({volume_for_partial_close}). New SL to +1R ({new_sl_for_modification:.{spec_details.digits}f}). Stage: {current_management_stage} -> {next_management_stage}") else: logging.warning(f"Pos {pos_ticket_from_file}: Cannot close 40% ({volume_for_partial_close}) at 1.5R. Attempting to close all remaining.") if mt5_position_obj.volume > 0: action_for_modification = 'close' volume_for_partial_close = mt5_position_obj.volume next_management_stage = STAGE_FULLY_CLOSED_AT_1_5R closed_volume_this_step = volume_for_partial_close current_profit_level_for_feedback = 1.5 logging.info(f"Pos {pos_ticket_from_file}: Profit {profit_R_multiple:.2f}R >= 1.5R. Closing all remaining ({volume_for_partial_close}). Stage: {current_management_stage} -> {next_management_stage}") elif current_management_stage == STAGE_CLOSE40_SL1R_AT_1_5R: if mt5_position_obj.volume > 0 : volume_for_partial_close = snap_to_step(mt5_position_obj.volume, spec_details.volume_step, spec_details.volume_min, mt5_position_obj.volume) if volume_for_partial_close >= spec_details.volume_min or volume_for_partial_close == mt5_position_obj.volume: action_for_modification = 'close' next_management_stage = STAGE_FULLY_CLOSED_AT_1_5R closed_volume_this_step = volume_for_partial_close current_profit_level_for_feedback = 1.5 logging.info(f"Pos {pos_ticket_from_file}: At 1.5R (final step). Closing remaining ({volume_for_partial_close}). Stage: {current_management_stage} -> {next_management_stage}") else: logging.warning(f"Pos {pos_ticket_from_file}: Stage {current_management_stage}, remaining vol {mt5_position_obj.volume} too small to close ({volume_for_partial_close}).") else: logging.info(f"Pos {pos_ticket_from_file}: Stage {current_management_stage}, but no volume left. Marking as fully closed.") next_management_stage = STAGE_FULLY_CLOSED_AT_1_5R tracked_pos_item['current_management_stage'] = next_management_stage tracked_pos_item['profit_level'] = 1.5 tracked_pos_item['closed_partially_volume_total'] = original_pos_volume if action_for_modification: mod_timestamp_local = int(time.time()) current_tp_on_pos = mt5_position_obj.tp new_tp_to_set_for_mod = current_tp_on_pos if new_sl_for_modification is not None: new_sl_for_modification = round(new_sl_for_modification, spec_details.digits) new_sl_percent_feedback_val = None if new_sl_for_modification is not None: if abs(new_sl_for_modification - pos_entry_price) < 1e-9: new_sl_percent_feedback_val = 0.0 else: sl_profit_points = (new_sl_for_modification - pos_entry_price) if pos_direction == 'BUY' else (pos_entry_price - new_sl_for_modification) new_sl_percent_feedback_val = (sl_profit_points / initial_sl_distance_R) * 100.0 if initial_sl_distance_R > 0 else None modification_data_obj_final = { "symbol": pos_symbol, "action": action_for_modification, "ticket": pos_ticket_from_file, "new_sl": new_sl_for_modification, "new_tp": new_tp_to_set_for_mod, "profit_level_R_multiple": current_profit_level_for_feedback, "profit_level_value_price": current_market_price, "new_sl_R_percent_profit": new_sl_percent_feedback_val, "volume": volume_for_partial_close if action_for_modification == 'close' else None, "timestamp": mod_timestamp_local, "entry_price_for_feedback": pos_entry_price, "next_management_stage": next_management_stage, "closed_volume_this_step": closed_volume_this_step, "original_pos_volume_for_update": original_pos_volume } mod_filename_final = os.path.join(STREAMLINE_DIR, f"modify_order_fast_{mod_timestamp_local}.json") # Corrected path try: with open(mod_filename_final, "w") as f_mod_final_write: json.dump(modification_data_obj_final, f_mod_final_write, indent=4) logging.info(f"Wrote mod command for {pos_ticket_from_file}: {action_for_modification}, details in {mod_filename_final}") tracked_pos_item['current_management_stage'] = next_management_stage tracked_pos_item['profit_level'] = current_profit_level_for_feedback if new_sl_for_modification is not None: tracked_pos_item['current_sl'] = new_sl_for_modification if closed_volume_this_step > 0: precision_vol = spec_details.digits + 2 # Example, ensure high enough for summing tracked_pos_item['closed_partially_volume_total'] = round(closed_partially_volume_total + closed_volume_this_step, precision_vol) except Exception as e_mod_final_write: logging.error(f"Error writing modification file {mod_filename_final}: {str(e_mod_final_write)}") if active_pos_indices_to_remove: for i_remove_final in sorted(active_pos_indices_to_remove, reverse=True): del tracked_positions[i_remove_final] save_trade_info(tracked_positions) def execute_order(order_details, processed_order_timestamps): order_req_timestamp = order_details.get('timestamp') if not order_req_timestamp or order_req_timestamp in processed_order_timestamps: logging.warning(f"Skipping order - Timestamp {order_req_timestamp} already processed or missing.") return "Order already processed or no timestamp - skipping" # Corrected feedback and done file paths feedback_filepath = os.path.join(STREAMLINE_DIR, f"{FEEDBACK_PREFIX}_{MT5_LOGIN}_{order_req_timestamp}.txt") done_filepath = os.path.join(STREAMLINE_DIR, f"{DONE_PREFIX}_{MT5_LOGIN}_{order_req_timestamp}.txt") order_req_action = order_details.get('action') def finalize_execution(feedback_msg, log_level=logging.INFO): logging.log(log_level, feedback_msg) try: with open(feedback_filepath, "a") as f: f.write(feedback_msg + "\n---\n") with open(done_filepath, "w") as f: f.write(f"done_{MT5_LOGIN}_{order_req_timestamp}") # Content for done file processed_order_timestamps.add(order_req_timestamp) except Exception as e: logging.error(f"Error writing feedback/done file for order {order_req_timestamp}: {str(e)}") return feedback_msg tv_symbol = order_details.get('symbol') if not tv_symbol: return finalize_execution(f"Account: {MT5_LOGIN}\nBroker: {BROKER_NAME}\nAction: {order_req_action}\nResult: Failed\nReason: Symbol missing", logging.ERROR) if tv_symbol not in SYMBOL_MAP: return finalize_execution(f"Account: {MT5_LOGIN}\nBroker: {BROKER_NAME}\nSymbol: {tv_symbol}\nAction: {order_req_action}\nResult: Failed\nReason: Symbol {tv_symbol} not in SYMBOL_MAP", logging.WARNING) broker_symbol_candidates = SYMBOL_MAP[tv_symbol] mt5_active_symbol = None for sym_candidate in broker_symbol_candidates: if mt5.symbol_select(sym_candidate, True): mt5_active_symbol = sym_candidate; break if not mt5_active_symbol: return finalize_execution(f"Account: {MT5_LOGIN}\nBroker: {BROKER_NAME}\nSymbol: {tv_symbol}\nAction: {order_req_action}\nResult: Failed\nReason: No MT5 symbol for {tv_symbol} (tried {broker_symbol_candidates})", logging.ERROR) if order_req_action == 'check': open_positions_mt5 = mt5.positions_get(symbol=mt5_active_symbol) if not open_positions_mt5: return finalize_execution(f"Account: {MT5_LOGIN}\nBroker: {BROKER_NAME}\nSymbol: {mt5_active_symbol}\nAction: Check\nResult: Success\nReason: No open positions for {mt5_active_symbol}") feedback_parts = [] for pos_obj in open_positions_mt5: direction_str = "BUY" if pos_obj.type == mt5.ORDER_TYPE_BUY else "SELL" spec_check = mt5.symbol_info(mt5_active_symbol) # Define spec_check here digits_check = spec_check.digits if spec_check else 5 feedback_parts.append(f"Account: {MT5_LOGIN}\nBroker: {BROKER_NAME}\nSymbol: {mt5_active_symbol}\nAction: Check\nDir: {direction_str}\nPos: {pos_obj.ticket}\nVol: {pos_obj.volume}\nEntry: {pos_obj.price_open:.{digits_check}f}\nSL: {pos_obj.sl:.{digits_check}f}\nTP: {pos_obj.tp:.{digits_check}f}\nResult: Success") return finalize_execution("\n---\n".join(feedback_parts)) if order_req_action == 'close': open_positions_mt5 = mt5.positions_get(symbol=mt5_active_symbol) if not open_positions_mt5: clear_trade_info(mt5_active_symbol) return finalize_execution(f"Account: {MT5_LOGIN}\nBroker: {BROKER_NAME}\nSymbol: {mt5_active_symbol}\nAction: Close\nResult: Success\nReason: No open pos for {mt5_active_symbol}") feedback_parts = [] all_closed_successfully = True for pos_obj in open_positions_mt5: current_tick_info_close = mt5.symbol_info_tick(mt5_active_symbol) if not current_tick_info_close: feedback_parts.append(f"Account: {MT5_LOGIN}\nBroker: {BROKER_NAME}\nSymbol: {mt5_active_symbol}\nAction: Close\nPos: {pos_obj.ticket}\nResult: Failed\nReason: No tick info") all_closed_successfully = False; continue price_for_close_op = current_tick_info_close.bid if pos_obj.type == mt5.ORDER_TYPE_BUY else current_tick_info_close.ask close_request_details = {"action": mt5.TRADE_ACTION_DEAL, "symbol": mt5_active_symbol, "volume": pos_obj.volume, "type": mt5.ORDER_TYPE_SELL if pos_obj.type == mt5.ORDER_TYPE_BUY else mt5.ORDER_TYPE_BUY, "position": pos_obj.ticket, "price": price_for_close_op, "deviation": 20, "magic": pos_obj.magic, "comment": "Manual close all by alert", "type_time": mt5.ORDER_TIME_GTC, "type_filling": mt5.ORDER_FILLING_IOC } result_close_op = mt5.order_send(close_request_details) if result_close_op and result_close_op.retcode == mt5.TRADE_RETCODE_DONE: feedback_parts.append(f"Account: {MT5_LOGIN}\nBroker: {BROKER_NAME}\nSymbol: {mt5_active_symbol}\nAction: Close\nPos: {pos_obj.ticket}\nResult: Success") else: all_closed_successfully = False; err_comment_close = result_close_op.comment if result_close_op else "N/A"; err_retcode_close = result_close_op.retcode if result_close_op else "N/A" feedback_parts.append(f"Account: {MT5_LOGIN}\nBroker: {BROKER_NAME}\nSymbol: {mt5_active_symbol}\nAction: Close\nPos: {pos_obj.ticket}\nResult: Failed\nReason: {err_comment_close} ({err_retcode_close})") if all_closed_successfully: clear_trade_info(mt5_active_symbol) return finalize_execution("\n---\n".join(feedback_parts), logging.INFO if all_closed_successfully else logging.ERROR) if order_req_action == 'entry': trade_direction_val = order_details.get('direction') sl_tp_mode_val = order_details.get('sl_type', 'atr10') risk_percent_val = float(order_details.get('risk', RISK_PERCENT)) if not trade_direction_val or trade_direction_val not in ['BUY', 'SELL']: return finalize_execution(f"Account: {MT5_LOGIN}\nBroker: {BROKER_NAME}\nSymbol: {mt5_active_symbol}\nAction: Entry\nResult: Failed\nReason: Invalid direction '{trade_direction_val}'", logging.ERROR) entry_p, sl_p, tp_p, current_atr_for_info = calculate_sltp(mt5_active_symbol, trade_direction_val, sl_tp_mode_val) if entry_p is None or sl_p is None or tp_p is None: return finalize_execution(f"Account: {MT5_LOGIN}\nBroker: {BROKER_NAME}\nSymbol: {mt5_active_symbol}\nDir: {trade_direction_val}\nTradeExec: No\nReason: Failed SL/TP calc (mode: {sl_tp_mode_val})", logging.ERROR) spec_entry_order = mt5.symbol_info(mt5_active_symbol) # Define spec_entry_order here min_stop_level_points = 0.0; digits_entry_order = 5 if spec_entry_order: digits_entry_order = spec_entry_order.digits if hasattr(spec_entry_order, 'trade_stops_level') and hasattr(spec_entry_order, 'point'): min_stop_level_points = spec_entry_order.trade_stops_level * spec_entry_order.point sl_distance_from_entry_points = abs(entry_p - sl_p) if min_stop_level_points > 0 and sl_distance_from_entry_points < min_stop_level_points: sl_p = round((entry_p - min_stop_level_points) if trade_direction_val == 'BUY' else (entry_p + min_stop_level_points), digits_entry_order) sl_distance_from_entry_points = abs(entry_p - sl_p) # Recalculate trade_volume, size_calc_info, acc_curr, risk_amt_val, _ = calculate_position_size(mt5_active_symbol, sl_distance_from_entry_points, risk_percent_val) if trade_volume <= 0 or any(s_err in size_calc_info.lower() for s_err in ["failed","unknown","invalid"]): return finalize_execution(f"Account: {MT5_LOGIN}\nBroker: {BROKER_NAME}\nSymbol: {mt5_active_symbol}\nDir: {trade_direction_val}\nTradeExec: No\nReason: PosSizeFail - {size_calc_info}", logging.ERROR) entry_request = {"action": mt5.TRADE_ACTION_DEAL, "symbol": mt5_active_symbol, "volume": trade_volume, "type": mt5.ORDER_TYPE_BUY if trade_direction_val == 'BUY' else mt5.ORDER_TYPE_SELL, "price": entry_p, "sl": sl_p, "tp": tp_p, "deviation": 20, "magic": 123456, "comment": f"Telegram order {ACCOUNT_TYPE} ATR10", "type_time": mt5.ORDER_TIME_GTC, "type_filling": mt5.ORDER_FILLING_IOC } logging.info(f"Sending order request for {mt5_active_symbol}: {entry_request}") result_entry = mt5.order_send(entry_request) if result_entry and result_entry.retcode == mt5.TRADE_RETCODE_DONE: deal_ticket_entry = result_entry.deal; position_ticket_to_store = deal_ticket_entry deals_history = mt5.history_deals_get(ticket=deal_ticket_entry) if deals_history and len(deals_history) > 0 and deals_history[0].position_id != 0: position_ticket_to_store = deals_history[0].position_id newly_opened_positions = load_trade_info() newly_opened_positions.append({"symbol": mt5_active_symbol, "ticket": position_ticket_to_store, "direction": trade_direction_val, "entry": entry_p, "sl": sl_p, "tp": tp_p, "volume": trade_volume, "original_volume": trade_volume, "profit_level": 0.0, "current_sl": sl_p, "initial_sl_distance_points": sl_distance_from_entry_points, "current_atr_at_entry": current_atr_for_info, "current_management_stage": STAGE_INITIAL, "closed_partially_volume_total": 0.0, "timestamp": order_req_timestamp }) save_trade_info(newly_opened_positions) return finalize_execution(f"Account:{MT5_LOGIN}\nBroker:{BROKER_NAME}\nSymbol:{mt5_active_symbol}\nPos(Tracked):{position_ticket_to_store}\nDeal:{deal_ticket_entry}\nDir:{trade_direction_val}\nE:{entry_p:.{digits_entry_order}f}\nSL:{sl_p:.{digits_entry_order}f}\nTP:{tp_p:.{digits_entry_order}f}\nRisk%:{risk_percent_val:.2f}\nRisk:{acc_curr} {risk_amt_val:.2f}\nLot:{trade_volume}\nTradeExec:Yes") else: err_comment_entry = result_entry.comment if result_entry else "N/A"; err_retcode_entry = result_entry.retcode if result_entry else "N/A" return finalize_execution(f"Account: {MT5_LOGIN}\nBroker: {BROKER_NAME}\nSymbol: {mt5_active_symbol}\nDir: {trade_direction_val}\nTradeExec: No\nReason: {err_comment_entry} ({err_retcode_entry})", logging.ERROR) return finalize_execution(f"Account: {MT5_LOGIN}\nBroker: {BROKER_NAME}\nSymbol: {tv_symbol}\nAction: {order_req_action}\nResult: Failed\nReason: Unknown action.", logging.ERROR) def process_modification(mod_details, processed_mod_timestamps): mod_req_timestamp = mod_details.get('timestamp') if not mod_req_timestamp or mod_req_timestamp in processed_mod_timestamps: return "Modification already processed or no timestamp - skipping" # Corrected feedback and done file paths feedback_filepath = os.path.join(STREAMLINE_DIR, f"{FEEDBACK_PREFIX}_{MT5_LOGIN}_{mod_req_timestamp}.txt") done_filepath = os.path.join(STREAMLINE_DIR, f"{DONE_PREFIX}_{MT5_LOGIN}_{mod_req_timestamp}.txt") # Assuming mods also create done files mod_symbol_val = mod_details.get('symbol'); mod_ticket_val = mod_details.get('ticket'); mod_action_val = mod_details.get('action') def finalize_modification(feedback_msg, log_level=logging.INFO): logging.log(log_level, feedback_msg) try: with open(feedback_filepath, "a") as f: f.write(feedback_msg + "\n---\n") with open(done_filepath, "w") as f: f.write(f"done_mod_{MT5_LOGIN}_{mod_req_timestamp}") # Content for done file processed_mod_timestamps.add(mod_req_timestamp) except Exception as e: logging.error(f"Error writing modification feedback/done file: {str(e)}") return feedback_msg if not mod_symbol_val or not mod_ticket_val or not mod_action_val: return finalize_modification(f"Account: {MT5_LOGIN}\nBroker: {BROKER_NAME}\nResult: Failed\nReason: Missing details: {mod_details}", logging.ERROR) mt5_pos_to_mod = None open_mt5_positions = mt5.positions_get(symbol=mod_symbol_val) if open_mt5_positions: for p_obj in open_mt5_positions: if p_obj.ticket == mod_ticket_val: mt5_pos_to_mod = p_obj; break spec = mt5.symbol_info(mod_symbol_val) # spec for this scope if not spec: return finalize_modification(f"Account: {MT5_LOGIN}\nBroker: {BROKER_NAME}\nSymbol: {mod_symbol_val}\nPos: {mod_ticket_val}\nAction: {mod_action_val}\nResult: Failed\nReason: No symbol spec.", logging.ERROR) if not mt5_pos_to_mod and mod_action_val != 'close': local_tracked_pos_list = [p for p in load_trade_info() if p.get('ticket') == mod_ticket_val and p.get('symbol') == mod_symbol_val] if local_tracked_pos_list: logging.info(f"Modification for {mod_ticket_val} ({mod_symbol_val}): Position not found in MT5, likely already closed.") current_tracked_list = load_trade_info() updated_tracked_list = [p for p in current_tracked_list if not (p.get('ticket') == mod_ticket_val and p.get('symbol') == mod_symbol_val)] if len(current_tracked_list) != len(updated_tracked_list): save_trade_info(updated_tracked_list) return finalize_modification(f"Account: {MT5_LOGIN}\nBroker: {BROKER_NAME}\nSymbol: {mod_symbol_val}\nPosition: {mod_ticket_val}\nAction: {mod_action_val}\nResult: Skipped\nReason: Position not found in MT5 (already closed?)", logging.WARNING) else: return finalize_modification(f"Account: {MT5_LOGIN}\nBroker: {BROKER_NAME}\nSymbol: {mod_symbol_val}\nPosition: {mod_ticket_val}\nAction: {mod_action_val}\nResult: Failed\nReason: Position not found in MT5 and not in local tracking.", logging.ERROR) pos_direction_str = "BUY" if mt5_pos_to_mod and mt5_pos_to_mod.type == mt5.ORDER_TYPE_BUY else "SELL" pos_entry_val = mt5_pos_to_mod.price_open if mt5_pos_to_mod else mod_details.get('entry_price_for_feedback', 0.0) profit_level_R_multiple_feedback = mod_details.get('profit_level_R_multiple', 0.0) profit_level_price_feedback = mod_details.get('profit_level_value_price', 0.0) next_stage_for_trade_info = mod_details.get('next_management_stage') closed_volume_this_step_for_trade_info = mod_details.get('closed_volume_this_step', 0.0) original_pos_volume_for_update = mod_details.get('original_pos_volume_for_update') if mod_action_val == 'modify': if not mt5_pos_to_mod: return finalize_modification(f"Account: {MT5_LOGIN}\nBroker: {BROKER_NAME}\nSymbol: {mod_symbol_val}\nPos: {mod_ticket_val}\nAction: Modify SL/TP\nResult: Failed\nReason: Pos disappeared.", logging.WARNING) new_sl_price = mod_details.get('new_sl'); new_tp_price = mod_details.get('new_tp') if new_sl_price is None and new_tp_price is None: return finalize_modification(f"Account: {MT5_LOGIN}\nBroker: {BROKER_NAME}\nSymbol: {mod_symbol_val}\nPos: {mod_ticket_val}\nAction: Modify\nResult: Failed\nReason: No new SL/TP.", logging.ERROR) final_sl_to_set = round(new_sl_price, spec.digits) if new_sl_price is not None else mt5_pos_to_mod.sl final_tp_to_set = round(new_tp_price, spec.digits) if new_tp_price is not None else mt5_pos_to_mod.tp if final_sl_to_set == 0.0 and new_sl_price is None and mt5_pos_to_mod.sl != 0.0 : final_sl_to_set = mt5_pos_to_mod.sl if final_tp_to_set == 0.0 and new_tp_price is None and mt5_pos_to_mod.tp != 0.0 : final_tp_to_set = mt5_pos_to_mod.tp mod_request = {"action": mt5.TRADE_ACTION_SLTP, "symbol": mod_symbol_val, "position": mod_ticket_val, "sl": final_sl_to_set, "tp": final_tp_to_set} result = mt5.order_send(mod_request) new_sl_R_percent_feedback = mod_details.get('new_sl_R_percent_profit', "N/A") if isinstance(new_sl_R_percent_feedback, float): new_sl_R_percent_feedback = f"{new_sl_R_percent_feedback:.2f}%R" if result and result.retcode == mt5.TRADE_RETCODE_DONE: current_tracked_list = load_trade_info(); updated_item = False for p_item in current_tracked_list: if p_item.get('ticket') == mod_ticket_val and p_item.get('symbol') == mod_symbol_val: if new_sl_price is not None: p_item['current_sl'] = final_sl_to_set if new_tp_price is not None: p_item['tp'] = final_tp_to_set p_item['profit_level'] = profit_level_R_multiple_feedback if next_stage_for_trade_info is not None: p_item['current_management_stage'] = next_stage_for_trade_info updated_item = True; break if updated_item: save_trade_info(current_tracked_list) return finalize_modification( f"Account: {MT5_LOGIN}\nBroker: {BROKER_NAME}\nSymbol: {mod_symbol_val}\nPosition: {mod_ticket_val}\n" f"Direction: {pos_direction_str}\nEntry: {pos_entry_val:.{spec.digits}f}\nProfit Level Reached: {profit_level_R_multiple_feedback:.2f}R (at {profit_level_price_feedback:.{spec.digits}f})\n" f"New SL Level: {new_sl_R_percent_feedback}\nNew SL Value: {final_sl_to_set:.{spec.digits}f}\nNew TP Value: {final_tp_to_set:.{spec.digits}f}\nResult: Success") else: err_comment = result.comment if result else "N/A"; err_retcode = result.retcode if result else "N/A" return finalize_modification(f"Account: {MT5_LOGIN}\nBroker: {BROKER_NAME}\nSymbol: {mod_symbol_val}\nPos: {mod_ticket_val}\nAction: Modify SL/TP\nResult: Failed\nReason: {err_comment} ({err_retcode})", logging.ERROR) if mod_action_val == 'close': close_volume_val_requested = mod_details.get('volume') if close_volume_val_requested is None or close_volume_val_requested <= 0: return finalize_modification(f"Account: {MT5_LOGIN}\nBroker: {BROKER_NAME}\nSymbol: {mod_symbol_val}\nPos: {mod_ticket_val}\nAction: Close\nResult: Failed\nReason: Invalid vol ({close_volume_val_requested}).", logging.ERROR) if not mt5_pos_to_mod: if next_stage_for_trade_info == STAGE_FULLY_CLOSED_AT_1_5R: current_tracked_list = load_trade_info() updated_tracked_list = [p for p in current_tracked_list if not (p.get('ticket') == mod_ticket_val and p.get('symbol') == mod_symbol_val)] if len(current_tracked_list) != len(updated_tracked_list): save_trade_info(updated_tracked_list) return finalize_modification(f"Account: {MT5_LOGIN}\nBroker: {BROKER_NAME}\nSymbol: {mod_symbol_val}\nPos: {mod_ticket_val}\nAction: Close\nResult: Success (Assumed closed)\nReason: Pos not found in MT5 (final close).", logging.INFO) else: return finalize_modification(f"Account: {MT5_LOGIN}\nBroker: {BROKER_NAME}\nSymbol: {mod_symbol_val}\nPos: {mod_ticket_val}\nAction: Close\nResult: Failed\nReason: Pos not found for partial close.", logging.WARNING) close_volume_snapped = snap_to_step(close_volume_val_requested, spec.volume_step, spec.volume_min, mt5_pos_to_mod.volume) close_volume_snapped = min(close_volume_snapped, mt5_pos_to_mod.volume) if close_volume_snapped < spec.volume_min and close_volume_snapped > 0 and close_volume_snapped < mt5_pos_to_mod.volume: if mt5_pos_to_mod.volume == spec.volume_min : close_volume_snapped = mt5_pos_to_mod.volume else: close_volume_snapped = spec.volume_min if close_volume_snapped == 0 and close_volume_val_requested > 0 : return finalize_modification(f"Account: {MT5_LOGIN}\nBroker: {BROKER_NAME}\nSymbol: {mod_symbol_val}\nPos: {mod_ticket_val}\nAction: Close\nResult: Skipped\nReason: Vol ({close_volume_val_requested}) snapped to zero.", logging.WARNING) if close_volume_snapped <= 0 : return finalize_modification(f"Account: {MT5_LOGIN}\nBroker: {BROKER_NAME}\nSymbol: {mod_symbol_val}\nPos: {mod_ticket_val}\nAction: Close\nResult: Failed\nReason: Final vol is zero or neg ({close_volume_snapped}).", logging.ERROR) current_tick_for_close = mt5.symbol_info_tick(mod_symbol_val) if not current_tick_for_close: return finalize_modification(f"Account:{MT5_LOGIN}...No tick info for close.", logging.ERROR) close_price_for_deal = current_tick_for_close.bid if mt5_pos_to_mod.type == mt5.ORDER_TYPE_BUY else current_tick_for_close.ask close_deal_request = {"action": mt5.TRADE_ACTION_DEAL, "symbol": mod_symbol_val, "volume": close_volume_snapped, "type": mt5.ORDER_TYPE_SELL if mt5_pos_to_mod.type == mt5.ORDER_TYPE_BUY else mt5.ORDER_TYPE_BUY, "position": mod_ticket_val, "price": close_price_for_deal, "deviation": 20, "magic": mt5_pos_to_mod.magic, "comment": f"Auto Close {profit_level_R_multiple_feedback:.1f}R lvl", "type_time": mt5.ORDER_TIME_GTC, "type_filling": mt5.ORDER_FILLING_IOC } result = mt5.order_send(close_deal_request) new_sl_after_partial_close = mod_details.get('new_sl'); sl_modification_done_for_remainder = False; sl_mod_feedback_suffix = "" if result and result.retcode == mt5.TRADE_RETCODE_DONE: volume_remaining_after_this_close_attempt = round(mt5_pos_to_mod.volume - close_volume_snapped, int(-math.log10(spec.volume_step)) if spec.volume_step > 0 else 2) if new_sl_after_partial_close is not None and volume_remaining_after_this_close_attempt >= spec.volume_min: sltp_mod_request = {"action": mt5.TRADE_ACTION_SLTP, "symbol": mod_symbol_val, "position": mod_ticket_val, "sl": round(new_sl_after_partial_close, spec.digits), "tp": mt5_pos_to_mod.tp } sltp_result = mt5.order_send(sltp_mod_request) if sltp_result and sltp_result.retcode == mt5.TRADE_RETCODE_DONE: sl_modification_done_for_remainder = True sl_mod_feedback_suffix = f" Remainder SL set to {new_sl_after_partial_close:.{spec.digits}f}." else: sl_mod_err_comment = sltp_result.comment if sltp_result else "N/A"; sl_mod_err_retcode = sltp_result.retcode if sltp_result else "N/A" sl_mod_feedback_suffix = f" Remainder SL set FAILED: {sl_mod_err_comment} ({sl_mod_err_retcode})." current_tracked_list = load_trade_info(); new_tracked_list_after_close = []; closed_item_found_and_updated = False vol_precision = int(-math.log10(spec.volume_step)) if spec.volume_step > 0 else 2 for p_item in current_tracked_list: if p_item.get('ticket') == mod_ticket_val and p_item.get('symbol') == mod_symbol_val: closed_item_found_and_updated = True p_item['volume'] = round(p_item['volume'] - close_volume_snapped, vol_precision) p_item['closed_partially_volume_total'] = round(p_item.get('closed_partially_volume_total', 0.0) + closed_volume_this_step_for_trade_info, vol_precision) p_item['profit_level'] = profit_level_R_multiple_feedback if next_stage_for_trade_info is not None: p_item['current_management_stage'] = next_stage_for_trade_info if new_sl_after_partial_close is not None and sl_modification_done_for_remainder : p_item['current_sl'] = round(new_sl_after_partial_close, spec.digits) is_fully_closed_check = False if original_pos_volume_for_update is not None: is_fully_closed_check = p_item['closed_partially_volume_total'] >= original_pos_volume_for_update - (spec.volume_step / 2) if p_item['volume'] < spec.volume_min or next_stage_for_trade_info == STAGE_FULLY_CLOSED_AT_1_5R or is_fully_closed_check: pass # Don't add else: new_tracked_list_after_close.append(p_item) else: new_tracked_list_after_close.append(p_item) if closed_item_found_and_updated : save_trade_info(new_tracked_list_after_close) return finalize_modification( f"Account: {MT5_LOGIN}\nBroker: {BROKER_NAME}\nSymbol: {mod_symbol_val}\nPosition: {mod_ticket_val}\n" f"Direction: {pos_direction_str}\nEntry: {pos_entry_val:.{spec.digits}f}\nProfit Level Reached: {profit_level_R_multiple_feedback:.2f}R (at {profit_level_price_feedback:.{spec.digits}f})\n" f"Volume Closed: {close_volume_snapped} (req {close_volume_val_requested}).{sl_mod_feedback_suffix}\nResult: Success") else: err_comment = result.comment if result else "N/A"; err_retcode = result.retcode if result else "N/A" return finalize_modification(f"Account:{MT5_LOGIN}...Close Vol {close_volume_snapped} Failed: {err_comment} ({err_retcode})", logging.ERROR) return finalize_modification(f"Account:{MT5_LOGIN}...Unknown mod action: {mod_action_val}.", logging.ERROR) def generate_account_summary(): logging.info(f"Generating account summary for account {MT5_LOGIN}") acc_info_obj = mt5.account_info() if not acc_info_obj: logging.error(f"Failed to fetch account info for {MT5_LOGIN}") return f"Account {MT5_LOGIN} ({BROKER_NAME}): Failed to fetch info" mt5_open_positions = mt5.positions_get() num_of_open_positions = len(mt5_open_positions) if mt5_open_positions else 0 total_floating_pl = 0.0 open_pos_details_str = "" if num_of_open_positions > 0: open_pos_details_str = "Open Positions Details:\n" for pos_item_obj in mt5_open_positions: item_type_str = "Long" if pos_item_obj.type == mt5.ORDER_TYPE_BUY else "Short" total_floating_pl += pos_item_obj.profit # Ensure currency is available for P/L formatting. pos_currency = acc_info_obj.currency if acc_info_obj else "N/A" open_pos_details_str += f" - {item_type_str} {pos_item_obj.symbol}: {pos_item_obj.profit:.2f} {pos_currency}\n" # Construct PL part for Node.js parsing pl_string_for_node = f"PL: {total_floating_pl:.2f} {acc_info_obj.currency}" summary_info = (f"Broker: {BROKER_NAME}\nAccount Number: {MT5_LOGIN}\nBalance: {acc_info_obj.balance:.2f} {acc_info_obj.currency}\nEquity: {acc_info_obj.equity:.2f} {acc_info_obj.currency}\nMargin: {acc_info_obj.margin:.2f} {acc_info_obj.currency}\nFree Margin: {acc_info_obj.margin_free:.2f} {acc_info_obj.currency}\nMargin Level: {acc_info_obj.margin_level:.2f}%\nOpen Positions: {num_of_open_positions}\n" + (open_pos_details_str if open_pos_details_str else "") + f"Total Floating P/L: {total_floating_pl:.2f} {acc_info_obj.currency}\n{pl_string_for_node}") # Added pl_string_for_node logging.info(f"Generated account summary: {summary_info}") return summary_info, acc_info_obj.currency if acc_info_obj else "UNK" def main_executor_loop(): processed_alert_timestamps = set() processed_modification_timestamps = set() last_master_monitor_time = 0 last_heartbeat_check = 0 logging.info(f"Executor started for account {MT5_LOGIN} (Master: {IS_MASTER}). Monitoring order file: {ORDER_FILE}") while True: try: current_time = time.time() if IS_MASTER and (current_time - last_master_monitor_time >= MONITOR_INTERVAL): tracked_syms = list(set(p.get('symbol') for p in load_trade_info() if p.get('symbol'))) if tracked_syms: monitor_positions(tracked_syms) else: logging.debug(f"Master {MT5_LOGIN}: No tracked symbols to monitor.") last_master_monitor_time = current_time try: with open(HEARTBEAT_FILE, "w") as hf: hf.write(str(int(current_time))) logging.debug(f"Master {MT5_LOGIN} updated heartbeat: {current_time}") except Exception as e: logging.error(f"Error writing master heartbeat file {HEARTBEAT_FILE}: {str(e)}") if not IS_MASTER and (current_time - last_heartbeat_check >= 30): if os.path.exists(HEARTBEAT_FILE): try: with open(HEARTBEAT_FILE, "r") as hf: heartbeat_time = float(hf.read().strip()) if current_time - heartbeat_time > max(60, 3 * MONITOR_INTERVAL) : logging.error(f"Master account offline: Heartbeat stale ({current_time - heartbeat_time:.1f}s) in {HEARTBEAT_FILE}") except Exception as e: logging.error(f"Error reading master heartbeat file {HEARTBEAT_FILE}: {str(e)}") else: logging.error(f"Master heartbeat file not found: {HEARTBEAT_FILE}") last_heartbeat_check = current_time if os.path.exists(ORDER_FILE): order_data_content = None try: # Add a small delay and retry for file reading to handle potential write locks for _ in range(3): # Retry up to 3 times try: with open(ORDER_FILE, "r") as f: order_data_content = json.load(f) break # Success except (IOError, json.JSONDecodeError) as e: # Catch IOError for read lock logging.warning(f"Retrying read for {ORDER_FILE} due to: {e}") time.sleep(0.2) # Wait before retry else: # If loop finishes without break logging.error(f"Failed to read/decode {ORDER_FILE} after multiple retries.") raise # Reraise the last error feedback = execute_order(order_data_content, processed_alert_timestamps) try: os.remove(ORDER_FILE) except OSError as e: logging.error(f"Error removing order file {ORDER_FILE}: {str(e)}") except json.JSONDecodeError: logging.error(f"Failed to decode JSON from order file: {ORDER_FILE}.") try: os.remove(ORDER_FILE); logging.warning(f"Removed malformed order file: {ORDER_FILE}") except OSError as e_rem: logging.error(f"Error removing malformed order file {ORDER_FILE}: {str(e_rem)}") except Exception as e: logging.error(f"Unhandled error processing order file {ORDER_FILE}: {str(e)}", exc_info=True) if order_data_content and order_data_content.get('timestamp'): processed_alert_timestamps.add(order_data_content['timestamp']) # Modification file processing uses STREAMLINE_DIR if os.path.exists(STREAMLINE_DIR): modification_filenames = [f for f in os.listdir(STREAMLINE_DIR) if f.startswith("modify_order_fast_") and f.endswith(".json")] for mod_filename_item in modification_filenames: mod_filepath_item = os.path.join(STREAMLINE_DIR, mod_filename_item); mod_data_content = None try: # Similar retry logic for modification files for _ in range(3): try: with open(mod_filepath_item, "r") as f: mod_data_content = json.load(f) break except (IOError, json.JSONDecodeError) as e: logging.warning(f"Retrying read for {mod_filepath_item} due to: {e}") time.sleep(0.2) else: logging.error(f"Failed to read/decode {mod_filepath_item} after multiple retries.") raise feedback = process_modification(mod_data_content, processed_modification_timestamps) try: os.remove(mod_filepath_item) except OSError as e_rem_mod: logging.error(f"Error removing mod file {mod_filepath_item}: {str(e_rem_mod)}") except json.JSONDecodeError: logging.error(f"Failed to decode JSON from mod file: {mod_filepath_item}.") try: os.remove(mod_filepath_item); logging.warning(f"Removed malformed mod file: {mod_filepath_item}") except OSError as e_rem_mod_mal: logging.error(f"Error removing malformed mod file {mod_filepath_item}: {str(e_rem_mod_mal)}") except Exception as e: logging.error(f"Unhandled error processing mod file {mod_filepath_item}: {str(e)}", exc_info=True) if mod_data_content and mod_data_content.get('timestamp'): processed_modification_timestamps.add(mod_data_content['timestamp']) summary_request_filepath = os.path.join(STREAMLINE_DIR, "account_info_request.txt") # Corrected path if os.path.exists(summary_request_filepath): account_summary_text, acc_currency = generate_account_summary() # Get currency # Corrected summary feedback filepath and naming for Node.js summary_feedback_filepath = os.path.join(STREAMLINE_DIR, f"account_info_resp_{MT5_LOGIN}_{acc_currency}.txt") try: with open(summary_feedback_filepath, "w") as f: f.write(account_summary_text) os.remove(summary_request_filepath) except Exception as e: logging.error(f"Error writing account summary or removing request file: {str(e)}") time.sleep(0.5) except KeyboardInterrupt: logging.info(f"Shutdown signal for account {MT5_LOGIN}."); break except Exception as e: logging.error(f"CRITICAL ERROR in main_executor_loop for account {MT5_LOGIN}: {str(e)}", exc_info=True); time.sleep(5) mt5.shutdown() logging.info(f"MT5 instance shutdown for account {MT5_LOGIN}.") if __name__ == '__main__': try: main_executor_loop() except Exception as e: try: account_id_for_log = MT5_LOGIN if 'MT5_LOGIN' in globals() else 'UNKNOWN_ACC_ID_ERROR' logging.critical(f"Executor for account {account_id_for_log} crashed at top level: {str(e)}", exc_info=True) except: print(f"CRITICAL TOP-LEVEL CRASH, logging failed: {str(e)}")