import pandas as pd import numpy as np import os import pickle from io import StringIO import random from collections import deque from datetime import datetime, timedelta import subprocess import time import argparse import threading import math import glob import MetaTrader5 as mt5 import torch import torch.nn as nn import torch.nn.functional as F from torch.distributions import Categorical """ from xlstm import ( xLSTMBlockStack, xLSTMBlockStackConfig, mLSTMBlockConfig, mLSTMLayerConfig, FeedForwardConfig, ) """ from xlstm import ( xLSTMBlockStack, xLSTMBlockStackConfig, sLSTMBlockConfig, sLSTMLayerConfig, ) ACTIONS = ['hold', 'long', 'short'] def find_latest_dukascopy_csv(): folder = os.path.join(os.path.dirname(os.path.abspath(__file__)), "download") pattern = os.path.join(folder, "xauusd-m5-bid-*.csv") # print(pattern) # print(glob.glob(pattern)) # print(os.listdir(folder)) files = glob.glob(pattern) if not files: raise FileNotFoundError(f"No CSV found matching {pattern}") return max(files, key=os.path.getmtime) def load_last_mb_xauusd(file_path=None, mb=8, delimiter=',', col_names=None): if file_path is None: file_path = find_latest_dukascopy_csv() print(f"Loading: {file_path}") file_size = os.path.getsize(file_path) offset = max(file_size - mb * 1024 * 1024, 0) # start position with open(file_path, 'rb') as f: # Seek to approximately 20 MB before EOF f.seek(offset) # Read to the end of file from that offset data = f.read().decode(errors='ignore') # If not at start of file, discard partial first line (incomplete) if offset > 0: data = data.split('\n', 1)[-1] """ df = pd.read_csv(StringIO(data), delimiter=delimiter, header=None, engine='python') #if col_names: print(df.head()) print(df.columns) print(df.shape) df.columns = ["Date", "Open", "High", "Low", "Close", "Volume"] """ df = pd.read_csv( StringIO(data), delimiter=delimiter, header=0, engine="python" ) """ df.rename(columns={ "timestamp": "Date", "open": "Open", "high": "High", "low": "Low", "close": "Close", "volume": "Volume" }, inplace=True) """ df.columns = ["Date", "Open", "High", "Low", "Close", "Volume"] # Convert columns if needed, e.g.: # df["Date"] = pd.to_datetime(df["Date"], format="%Y.%m.%d %H:%M", errors='coerce') df["Date"] = pd.to_datetime( df["Date"], unit="ms", utc=True ) # print(df.columns.tolist()) # print(df.head()) # for col in ["Open", "High", "Low", "Close", "Volume"]: # df[col] = pd.to_numeric(df[col], errors='coerce') # df['Date'] = pd.to_datetime(df['Date']) df.set_index('Date', inplace=True) df = df[['Open', 'High', 'Low', 'Close', 'Volume']].copy() # df = df.resample('15min').agg({ # 'Open': 'first', # 'High': 'max', # 'Low': 'min', # 'Close': 'last' # }).dropna() print(f"Loaded: {file_path}") df = df.dropna() return df def ADX(df, period=14): """ Returns +DI, -DI and ADX using Wilder's smoothing. Columns required: High, Low, Close """ high = df['High'] low = df['Low'] close = df['Close'] # --- directional movement ----------------------------------------- # plus_dm = (high.diff() > low.diff()) * (high.diff()).clip(lower=0) # minus_dm = (low.diff() > high.diff()) * (low.diff().abs()).clip(lower=0)̈́ up = high.diff() dn = -low.diff() plus_dm_array = np.where((up > dn) & (up > 0), up, 0.0) minus_dm_array = np.where((dn > up) & (dn > 0), dn, 0.0) plus_dm = pd.Series(plus_dm_array, index=df.index) # ← wrap minus_dm = pd.Series(minus_dm_array, index=df.index) # ← wrap # --- true range ---------------------------------------------------- tr = pd.concat([ (high - low), (high - close.shift()).abs(), (low - close.shift()).abs() ], axis=1).max(axis=1) # --- Wilder smoothing --------------------------------------------- atr = tr.ewm(alpha=1/period, adjust=False).mean() plus_di = 100 * (plus_dm.ewm(alpha=1/period, adjust=False).mean() / atr) minus_di = 100 * (minus_dm.ewm(alpha=1/period, adjust=False).mean() / atr) dx = 100 * (plus_di - minus_di).abs() / (plus_di + minus_di) adx = dx.ewm(alpha=1/period, adjust=False).mean() adx = round(adx , 2) plus_di = round(plus_di, 2) minus_di = round(minus_di, 2) return adx, plus_di, minus_di def STOCH(df, period=14, smooth_d=3): """ Returns %K and %D stochastic oscillator. Columns required: High, Low, Close """ high = df['High'] low = df['Low'] close = df['Close'] # --- highest high / lowest low ------------------------------------ lowest_low = low.rolling(window=period).min() highest_high = high.rolling(window=period).max() # --- %K ------------------------------------------------------------ k = 100 * ((close - lowest_low) / (highest_high - lowest_low)) # --- %D (smoothed %K) --------------------------------------------- d = k.rolling(window=smooth_d).mean() k = round(k, 2) d = round(d, 2) return k, d def EMA(df, period): return df['Close'].ewm(span=period, adjust=False).mean().round(2) def EQH_EQL(df, threshold=10, lookback=72, min_distance=3): open_ = df["Open"].to_numpy() close = df["Close"].to_numpy() high = df["High"].to_numpy() low = df["Low"].to_numpy() body_high = np.maximum(open_, close) body_low = np.minimum(open_, close) eqh = np.zeros(len(df), dtype=np.bool_) eql = np.zeros(len(df), dtype=np.bool_) n = len(df) for i in range(n): start = max(0, i - lookback) for j in range(i - min_distance, start - 1, -1): # Equal High if ( abs(high[i] - high[j]) <= threshold or abs(high[i] - body_high[j]) <= threshold or abs(body_high[i] - high[j]) <= threshold or abs(body_high[i] - body_high[j]) <= threshold ): eqh[i] = True # Equal Low if ( abs(low[i] - low[j]) <= threshold or abs(low[i] - body_low[j]) <= threshold or abs(body_low[i] - low[j]) <= threshold or abs(body_low[i] - body_low[j]) <= threshold ): eql[i] = True if eqh[i] and eql[i]: break return eqh, eql def EQHEQLRetest(df, threshold=20, lookback=12): open_ = df["Open"].to_numpy() close = df["Close"].to_numpy() high = df["High"].to_numpy() low = df["Low"].to_numpy() body_high = np.maximum(open_, close) body_low = np.minimum(open_, close) eqh = df["eqh"].to_numpy() eql = df["eql"].to_numpy() eqh_retest = np.zeros(len(df), dtype=np.bool_) eql_retest = np.zeros(len(df), dtype=np.bool_) n = len(df) for i in range(n): start = max(0, i - lookback) # Previous EQH for j in range(i - 1, start - 1, -1): if not eqh[j]: continue level = max(high[j], body_high[j]) if ( abs(high[i] - level) <= threshold or abs(body_high[i] - level) <= threshold # and df["bullish"].iloc[i] ): eqh_retest[i] = True break # Previous EQL for j in range(i - 1, start - 1, -1): if not eql[j]: continue level = min(low[j], body_low[j]) if ( ( abs(low[i] - level) <= threshold or abs(body_low[i] - level) <= threshold ) and df["bearish"].iloc[i] ): eql_retest[i] = True break return eqh_retest, eql_retest def Indecision(df, threshold=0.2): body = (df["Close"] - df["Open"]).abs() candle_range = (df["High"] - df["Low"]).replace(0, 1e-9) return (body / candle_range < threshold).astype(int) def RejectionBlocks(df, wick_ratio=2.0): body = (df["Close"] - df["Open"]).abs() upper = df["High"] - df[["Open", "Close"]].max(axis=1) lower = df[["Open", "Close"]].min(axis=1) - df["Low"] bullish_rb = ( (lower > body * wick_ratio) & (lower > upper) ).astype(int) bearish_rb = ( (upper > body * wick_ratio) & (upper > lower) ).astype(int) return bullish_rb, bearish_rb def BullishOB(df, multiplier=1.5): body = (df["Close"] - df["Open"]).abs() next_body = body.shift(-1) bearish = df["Close"] < df["Open"] next_bullish = df["Close"].shift(-1) > df["Open"].shift(-1) return ( bearish & next_bullish & (body > df["range_ma"]) & (next_body >= body * multiplier) ).astype(int) def BearishOB(df, multiplier=1.5): body = (df["Close"] - df["Open"]).abs() next_body = body.shift(-1) bullish = df["Close"] > df["Open"] next_bearish = df["Close"].shift(-1) < df["Open"].shift(-1) return ( bullish & next_bearish & (body > df["range_ma"]) & (next_body >= body * multiplier) ).astype(int) def BullishFVG(df): return (df["Low"].shift(-1) > df["High"].shift(1)).astype(int) def BearishFVG(df): return (df["High"].shift(-1) < df["Low"].shift(1)).astype(int) def BullishIFVG(df): return ( (df["bearish_fvg"].shift(1) == 1) & (df["Close"] > df["Low"].shift(2)) ).astype(int) def BearishIFVG(df): return ( (df["bullish_fvg"].shift(1) == 1) & (df["Close"] < df["High"].shift(2)) ).astype(int) def BullishMB(df, multiplier=1.0): body = (df["Close"] - df["Open"]).abs() bearish = df["Close"] < df["Open"] next_bullish = df["Close"].shift(-1) > df["Open"].shift(-1) displacement = body.shift(-1) >= body * multiplier ob = bearish & next_bullish & displacement ob_high = df["High"].where(ob).ffill() ob_low = df["Low"].where(ob).ffill() return ((df["Low"] <= ob_high) & (df["High"] >= ob_low)).astype(int) def BearishMB(df, multiplier=1.0): body = (df["Close"] - df["Open"]).abs() bullish = df["Close"] > df["Open"] next_bearish = df["Close"].shift(-1) < df["Open"].shift(-1) displacement = body.shift(-1) >= body * multiplier ob = bullish & next_bearish & displacement ob_high = df["High"].where(ob).ffill() ob_low = df["Low"].where(ob).ffill() return ((df["Low"] <= ob_high) & (df["High"] >= ob_low)).astype(int) def Swings(df, left=2, right=2): body_high = np.maximum(df["Open"].to_numpy(), df["Close"].to_numpy()) body_low = np.minimum(df["Open"].to_numpy(), df["Close"].to_numpy()) swing_high = [0] * len(df) swing_low = [0] * len(df) for i in range(left, len(df) - right): # Swing High (body only) if ( body_high[i] > body_high[i-1] and body_high[i] > body_high[i-2] and body_high[i] > body_high[i+1] and body_high[i] > body_high[i+2] ): swing_high[i] = 1 # Swing Low (body only) if ( body_low[i] < body_low[i-1] and body_low[i] < body_low[i-2] and body_low[i] < body_low[i+1] and body_low[i] < body_low[i+2] ): swing_low[i] = 1 return swing_high, swing_low def MSS(df): bullish = pd.Series(0, index=df.index) bearish = pd.Series(0, index=df.index) last_swing_high = None last_swing_low = None for i in range(len(df)): if df["swing_high"].iloc[i]: last_swing_high = df["High"].iloc[i] if df["swing_low"].iloc[i]: last_swing_low = df["Low"].iloc[i] if ( last_swing_high is not None and df["Close"].iloc[i] > last_swing_high ): bullish.iloc[i] = 1 last_swing_high = None if ( last_swing_low is not None and df["Close"].iloc[i] < last_swing_low ): bearish.iloc[i] = 1 last_swing_low = None return bullish, bearish def MSS_Retest(df, lookback=72, threshold=3): high = df["High"].to_numpy() low = df["Low"].to_numpy() bullish_mss = df["bullish_mss"].to_numpy() bearish_mss = df["bearish_mss"].to_numpy() bullish_retest = np.zeros(len(df), dtype=np.int8) bearish_retest = np.zeros(len(df), dtype=np.int8) last_bull_level = None last_bear_level = None last_bull_index = -1 last_bear_index = -1 for i in range(len(df)): # Remember bullish MSS level if bullish_mss[i]: last_bull_level = high[i] last_bull_index = i # Remember bearish MSS level if bearish_mss[i]: last_bear_level = low[i] last_bear_index = i # Bullish retest if ( last_bull_level is not None and i - last_bull_index <= lookback and low[i] <= last_bull_level + threshold and high[i] >= last_bull_level - threshold ): bullish_retest[i] = 1 last_bull_level = None # Bearish retest if ( last_bear_level is not None and i - last_bear_index <= lookback and high[i] >= last_bear_level - threshold and low[i] <= last_bear_level + threshold ): bearish_retest[i] = 1 last_bear_level = None return bullish_retest, bearish_retest def BoS(df): bullish_bos = np.zeros(len(df), dtype=np.int8) bearish_bos = np.zeros(len(df), dtype=np.int8) last_high = None last_low = None trend = None for i in range(len(df)): # New swing high if df["swing_high"].iat[i]: high = df["High"].iat[i] if last_high is not None: if high > last_high: # Higher High if trend == "bull": bullish_bos[i] = 1 trend = "bull" elif high < last_high: # Lower High trend = "bear" last_high = high # New swing low if df["swing_low"].iat[i]: low = df["Low"].iat[i] if last_low is not None: if low < last_low: # Lower Low if trend == "bear": bearish_bos[i] = 1 trend = "bear" elif low > last_low: # Higher Low trend = "bull" last_low = low return bullish_bos, bearish_bos def Trend(df): bullish_trend = np.zeros(len(df), dtype=np.int8) bearish_trend = np.zeros(len(df), dtype=np.int8) bull_count = 0 bear_count = 0 trend = 0 # 0 = none # 1 = bullish # -1 = bearish for i in range(len(df)): if df["bullish_bos"].iat[i]: bull_count += 1 bear_count = 0 if bull_count >= 2: trend = 1 elif df["bearish_bos"].iat[i]: bear_count += 1 bull_count = 0 if bear_count >= 2: trend = -1 if trend == 1: bullish_trend[i] = 1 elif trend == -1: bearish_trend[i] = 1 return bullish_trend, bearish_trend def CHoCH(df, lookback=72): bullish_choch = np.zeros(len(df), dtype=np.int8) bearish_choch = np.zeros(len(df), dtype=np.int8) for i in range(lookback, len(df)): start = max(0, i - lookback) # ---------- Bullish trend -> look for bearish CHoCH ---------- if df["bullish_trend"].iat[i]: last_hl = None for j in range(i - 1, start - 1, -1): if df["swing_low"].iat[j]: last_hl = df["Low"].iat[j] break if last_hl is not None and df["Close"].iat[i] < last_hl: bearish_choch[i] = 1 # ---------- Bearish trend -> look for bullish CHoCH ---------- elif df["bearish_trend"].iat[i]: last_lh = None for j in range(i - 1, start - 1, -1): if df["swing_high"].iat[j]: last_lh = df["High"].iat[j] break if last_lh is not None and df["Close"].iat[i] > last_lh: bullish_choch[i] = 1 return bullish_choch, bearish_choch def BullishBB(df, lookback=72): bb = np.zeros(len(df), dtype=np.int8) high = df["High"].to_numpy() low = df["Low"].to_numpy() bearish_ob = df["bearish_ob"].to_numpy() n = len(df) for i in range(n): start = max(0, i - lookback) for j in range(i - 1, start - 1, -1): if bearish_ob[j] != 1: continue ob_high = high[j] ob_low = low[j] # Highest high and lowest low since the OB formed highest = np.max(high[j+1:i+1]) lowest = np.min(low[j+1:i+1]) if highest >= ob_high and lowest <= ob_low: bb[i] = 1 break return bb def BearishBB(df, lookback=72): bb = np.zeros(len(df), dtype=np.int8) high = df["High"].to_numpy() low = df["Low"].to_numpy() bullish_ob = df["bullish_ob"].to_numpy() n = len(df) for i in range(n): start = max(0, i - lookback) for j in range(i - 1, start - 1, -1): if bullish_ob[j] != 1: continue ob_high = high[j] ob_low = low[j] # Highest high and lowest low since the OB formed highest = np.max(high[j + 1:i + 1]) lowest = np.min(low[j + 1:i + 1]) # Order block has been fully mitigated if highest >= ob_high and lowest <= ob_low: bb[i] = 1 break return bb def ExhaustionCandle(df): open_ = df["Open"].to_numpy() close = df["Close"].to_numpy() body = np.abs(close - open_) exhaustion = np.zeros(len(df), dtype=np.bool_) exhaustion[1:] = body[1:] <= body[:-1] * 0.10 return exhaustion def OBMitigation(df, threshold=30, lookback=72): low = df["Low"].to_numpy() high = df["High"].to_numpy() bullish_ob = df["bullish_ob"].to_numpy() bearish_ob = df["bearish_ob"].to_numpy() bull = np.zeros(len(df), dtype=np.bool_) bear = np.zeros(len(df), dtype=np.bool_) n = len(df) for i in range(n): start = max(0, i - lookback) for j in range(i - 1, start - 1, -1): if not np.isnan(bullish_ob[j]): if abs(low[i] - bullish_ob[j]) <= threshold: bull[i] = True break for j in range(i - 1, start - 1, -1): if not np.isnan(bearish_ob[j]): if abs(high[i] - bearish_ob[j]) <= threshold: bear[i] = True break return bull, bear def TrendLines(df): high = df["High"].to_numpy() low = df["Low"].to_numpy() close = df["Close"].to_numpy() swing_high = df["swing_high"].to_numpy() swing_low = df["swing_low"].to_numpy() above_support = np.zeros(len(df), dtype=np.bool_) below_resistance = np.zeros(len(df), dtype=np.bool_) n = len(df) # ===================================================== # Support # ===================================================== lows = np.where(swing_low)[0] for k in range(1, len(lows)): i1 = lows[k - 1] i2 = lows[k] # Only higher lows if low[i2] <= low[i1]: continue slope = (low[i2] - low[i1]) / (i2 - i1) end = lows[k + 1] if k + 1 < len(lows) else n for j in range(i2 + 1, end): trend_price = low[i2] + slope * (j - i2) if close[j] >= trend_price: above_support[j] = True # ===================================================== # Resistance # ===================================================== highs = np.where(swing_high)[0] for k in range(1, len(highs)): i1 = highs[k - 1] i2 = highs[k] # Only lower highs if high[i2] >= high[i1]: continue slope = (high[i2] - high[i1]) / (i2 - i1) end = highs[k + 1] if k + 1 < len(highs) else n for j in range(i2 + 1, end): trend_price = high[i2] + slope * (j - i2) if close[j] <= trend_price: below_resistance[j] = True return above_support, below_resistance def Fibonacci(df, min_swing=7.5, tolerance=0.05): high = df["High"].to_numpy() low = df["Low"].to_numpy() open_ = df["Open"].to_numpy() close = df["Close"].to_numpy() body_high = np.maximum(open_, close) body_low = np.minimum(open_, close) swing_high = df["swing_high"].to_numpy() swing_low = df["swing_low"].to_numpy() bullish_fib382 = np.zeros(len(df), dtype=np.bool_) bullish_fib500 = np.zeros(len(df), dtype=np.bool_) bullish_fib618 = np.zeros(len(df), dtype=np.bool_) bullish_fib786 = np.zeros(len(df), dtype=np.bool_) bearish_fib382 = np.zeros(len(df), dtype=np.bool_) bearish_fib500 = np.zeros(len(df), dtype=np.bool_) bearish_fib618 = np.zeros(len(df), dtype=np.bool_) bearish_fib786 = np.zeros(len(df), dtype=np.bool_) last_type = None last_index = None n = len(df) for i in range(n): # ========================================================== # Bullish impulse (Swing Low -> Swing High) # ========================================================== if swing_high[i] and last_type == "low": swing = high[i] - low[last_index] if swing >= min_swing: fib382 = high[i] - swing * 0.382 fib500 = high[i] - swing * 0.500 fib618 = high[i] - swing * 0.618 fib786 = high[i] - swing * 0.786 threshold = swing * tolerance j = i while ( j < n and not swing_high[j] and not swing_low[j] ): bullish_fib382[j] = ( body_low[j] <= fib382 + threshold and body_high[j] >= fib382 - threshold ) bullish_fib500[j] = ( body_low[j] <= fib500 + threshold and body_high[j] >= fib500 - threshold ) bullish_fib618[j] = ( body_low[j] <= fib618 + threshold and body_high[j] >= fib618 - threshold ) bullish_fib786[j] = ( body_low[j] <= fib786 + threshold and body_high[j] >= fib786 - threshold ) j += 1 last_type = "high" last_index = i # ========================================================== # Bearish impulse (Swing High -> Swing Low) # ========================================================== elif swing_low[i] and last_type == "high": swing = high[last_index] - low[i] if swing >= min_swing: fib382 = low[i] + swing * 0.382 fib500 = low[i] + swing * 0.500 fib618 = low[i] + swing * 0.618 fib786 = low[i] + swing * 0.786 j = i while ( j < n and not swing_high[j] and not swing_low[j] ): bearish_fib382[j] = ( body_low[j] <= fib382 + threshold and body_high[j] >= fib382 - threshold ) bearish_fib500[j] = ( body_low[j] <= fib500 + threshold and body_high[j] >= fib500 - threshold ) bearish_fib618[j] = ( body_low[j] <= fib618 + threshold and body_high[j] >= fib618 - threshold ) bearish_fib786[j] = ( body_low[j] <= fib786 + threshold and body_high[j] >= fib786 - threshold ) j += 1 last_type = "low" last_index = i elif swing_low[i]: last_type = "low" last_index = i elif swing_high[i]: last_type = "high" last_index = i return ( bullish_fib382, bullish_fib500, bullish_fib618, bullish_fib786, bearish_fib382, bearish_fib500, bearish_fib618, bearish_fib786, ) def FVGRetracement(df, tolerance=0.05): open_ = df["Open"].to_numpy() close = df["Close"].to_numpy() body_high = np.maximum(open_, close) body_low = np.minimum(open_, close) body_mid = (body_high + body_low) / 2 bull_fvg = df["bullish_fvg"].to_numpy() bear_fvg = df["bearish_fvg"].to_numpy() bull_ret = np.zeros(len(df), dtype=np.bool_) bear_ret = np.zeros(len(df), dtype=np.bool_) n = len(df) # ------------------------- # Bullish FVG # ------------------------- for i in range(1, n - 1): if not bull_fvg[i]: continue # Fib: 0 = first candle low, 1 = third candle high swing_low = df["Low"].iat[i - 1] swing_high = df["High"].iat[i + 1] height = swing_high - swing_low if height <= 0: continue fib382 = swing_high - 0.382 * height fib500 = swing_high - 0.500 * height tol = height * tolerance for j in range(i + 2, n): # Stop when bullish continuation resumes if close[j] >= open_[j]: break if ( abs(body_mid[j] - fib382) <= tol or abs(body_mid[j] - fib500) <= tol ): bull_ret[j] = True break # ------------------------- # Bearish FVG # ------------------------- for i in range(1, n - 1): if not bear_fvg[i]: continue # Fib: 0 = first candle high, 1 = third candle low swing_high = df["High"].iat[i - 1] swing_low = df["Low"].iat[i + 1] height = swing_high - swing_low if height <= 0: continue fib382 = swing_low + 0.382 * height fib500 = swing_low + 0.500 * height tol = height * tolerance for j in range(i + 2, n): # Stop when bearish continuation resumes if close[j] <= open_[j]: break if ( abs(body_mid[j] - fib382) <= tol or abs(body_mid[j] - fib500) <= tol ): bear_ret[j] = True break return bull_ret, bear_ret def AsiaHighDistance(df): # Asia session: 01:00-08:59 (your timezone) asia = (df.index.hour >= 1) | (df.index.hour <= 9) trade_day = (df.index - pd.Timedelta(hours=24)).date asia_high = ( df["High"] .where(asia) .groupby(trade_day) .transform("max") .ffill() ) return asia_high - df["Close"] def AsiaLowDistance(df): asia = (df.index.hour >= 1) | (df.index.hour <= 9) trade_day = (df.index - pd.Timedelta(hours=24)).date asia_low = ( df["Low"] .where(asia) .groupby(trade_day) .transform("min") .ffill() ) return df["Close"] - asia_low def PDHDistance(df): day = df.index.date daily_high = ( df["High"] .groupby(day) .transform("max") ) pdh = ( daily_high .groupby(day) .first() .shift(1) .reindex(day) .to_numpy() ) return pdh - df["Close"] def PDLDistance(df): day = df.index.date daily_low = ( df["Low"] .groupby(day) .transform("min") ) pdl = ( daily_low .groupby(day) .first() .shift(1) .reindex(day) .to_numpy() ) return df["Close"] - pdl def PDPOCDistance(df, bins=50): day = df.index.date poc = np.full(len(df), np.nan) unique_days = np.unique(day) prev_poc = np.nan for d in unique_days: # Assign previous day's POC to today's candles mask = day == d poc[mask] = prev_poc # Compute today's POC for use tomorrow today = df.loc[mask] if len(today) > 1: prices = ((today["High"] + today["Low"] + today["Close"]) / 3).values volumes = today["Volume"].values hist, edges = np.histogram( prices, bins=bins, weights=volumes ) idx = np.argmax(hist) prev_poc = (edges[idx] + edges[idx + 1]) / 2 return df["Close"] - poc def VWAP(df): # print(type(df.index)) # print(df.index.dtype) # print(df.index[:5]) # -------------------------------------------------- # Select volume column # -------------------------------------------------- if "Volume" in df.columns: volume = df["Volume"] elif "tick_volume" in df.columns: volume = df["tick_volume"] elif "real_volume" in df.columns: volume = df["real_volume"] else: raise ValueError("No volume column found.") # -------------------------------------------------- # VWAP # -------------------------------------------------- typical_price = ( df["High"] + df["Low"] + df["Close"] ) / 3 # print(df.columns) # df["Date"] = ( # pd.to_datetime(df["Date"], unit="ms", utc=True) # .dt.tz_convert("Europe/Denmark") # .dt.tz_localize(None) # ) # index = df.index.tz_convert("Europe/Copenhagen") # print(f"vwap time: {df.index[-1]}") session = df.index.normalize() # session = (df.index - pd.Timedelta(hours=1)).normalize() cum_tpv = (typical_price * volume).groupby(session).cumsum() cum_volume = volume.groupby(session).cumsum() vwap = round(cum_tpv / cum_volume, 2) # -------------------------------------------------- # Session VWAP Standard Deviation # -------------------------------------------------- # Squared distance from VWAP sq_diff = ((typical_price - vwap) ** 2) * volume # Cumulative weighted variance cum_sq_diff = sq_diff.groupby(session).cumsum() variance = cum_sq_diff / cum_volume stddev = variance.pow(0.5) upper = round(vwap + stddev, 2) lower = round(vwap - stddev, 2) # -------------------------------------------------- # Derived features # -------------------------------------------------- # dist = df["Close"] - vwap above = (df["Close"] > vwap).astype(int) below = (df["Close"] < vwap).astype(int) above_upper = (df["Close"] > upper).astype(int) below_lower = (df["Close"] < lower).astype(int) slope = vwap.diff() return ( vwap, upper, lower, # dist, above, below, above_upper, below_lower, slope ) def GetRange(df): """ Returns normalized candle range as a percentage of price. """ return (df["High"] - df["Low"]) * 10 def RangeMA(df, period=14): # ----------------------------------------- # Select volume column # ----------------------------------------- # if "volume" in df.columns: # volume = df["Volume"] # elif "tick_volume" in df.columns: # volume = df["tick_volume"] # elif "Volume" in df.columns: # volume = df["Volume"] # else: # raise ValueError("No volume column found.") volume = df["range"] # print(type(volume)) # print(volume.shape) # print(df.columns.tolist()) return round(volume.rolling(period).mean(), 2) def BullishBearish(df): """ Returns: bullish (np.ndarray[bool]) bearish (np.ndarray[bool]) """ open = df["Open"].to_numpy() close = df["Close"].to_numpy() bullish = close > open bearish = close < open return bullish, bearish def add_irl_erl(df, swing_window=5): """ Adds: range_high range_low erl_high erl_low irl Assumes df contains: High Low Close """ # df = df.copy() # ---------------------------- # Current dealing range # ---------------------------- df["range_high"] = np.where(df["swing_high"], df["High"], np.nan) df["range_low"] = np.where(df["swing_low"], df["Low"], np.nan) df["range_high"] = df["range_high"].ffill() df["range_low"] = df["range_low"].ffill() # ---------------------------- # External liquidity # ---------------------------- df["erl_high"] = df["High"] > df["range_high"] df["erl_low"] = df["Low"] < df["range_low"] # ---------------------------- # Internal liquidity # ---------------------------- df["irl"] = ( (df["High"] <= df["range_high"]) & (df["Low"] >= df["range_low"]) ) return df def GetKillzone(df, asia=(1, 4), london=(10, 13), newyork=(15, 18)): """ Returns: 0 = None 1 = Asia 2 = London 3 = New York Expects df.index to be a DatetimeIndex. """ hours = df.index.hour session = np.zeros(len(df), dtype=np.int8) session[(hours >= asia[0]) & (hours < asia[1])] = 1 session[(hours >= london[0]) & (hours < london[1])] = 2 session[(hours >= newyork[0]) & (hours < newyork[1])] = 3 return session def BuyScore(df): score = ( # Trend filter (df["EMA7"] > df["EMA21"]).astype(int) + (df["EMA721_DIFF"] > 0).astype(int) + (df["EMA7_Slope"] > 0).astype(int) + (df["EMA2150_DIFF"] > 0).astype(int) + (df["EMA21_Slope"] > 0).astype(int) + (df["EMA50"] > df["EMA200"]).astype(int) + (df["EMA50200_DIFF"] > 0).astype(int) + (df["EMA50_Slope"] > 0).astype(int) + (df["EMA200_Slope"] > 0).astype(int) + (df["+di"] > df["-di"]).astype(int) + (df["adx"] > 20).astype(int) + (df["k"] > df["k_smooth"]).astype(int) + # OB mitigation df["bullish_ob_mitigation"] + # Breaker Block df["bullish_bb"] + # MSS (df["bullish_mss"] & (df["bullish_trend"] == 0) ).astype(int) + df["bullish_choch"] + # MSS Retest df["bullish_mss_retest"] + # FVG + MB/RB confluence ( df["bullish_fvg_retracement"] & (df["bullish_mb"] | df["bullish_rb"]) ).astype(int) + # Liquidity ((df["asia_high_dist"] >= 0) & (df["asia_high_dist"] <= 3)).astype(int) + ((df["asia_low_dist"] >= -3) & (df["asia_low_dist"] <= 0)).astype(int) + ((df["pdh_dist"] >= 0) & (df["pdh_dist"] <= 3)).astype(int) + ((df["pdl_dist"] >= -3) & (df["pdl_dist"] <= 0)).astype(int) + # Equal lows df["eql"] + # Fib df["bullish_fib382"] + df["bullish_fib500"] + df["bullish_fib618"] ) score *= (df["killzone"].isin([1, 2, 3])).astype(int) score *= (df["adx"] >= 20).astype(int) return score def SellScore(df): score = ( # Trend filter (df["EMA7"] < df["EMA21"]).astype(int) + (df["EMA721_DIFF"] < 0).astype(int) + (df["EMA7_Slope"] < 0).astype(int) + (df["EMA21"] < df["EMA50"]).astype(int) + (df["EMA2150_DIFF"] < 0).astype(int) + (df["EMA21_Slope"] < 0).astype(int) + (df["EMA50"] < df["EMA200"]).astype(int) + (df["EMA50200_DIFF"] < 0).astype(int) + (df["EMA50_Slope"] < 0).astype(int) + (df["EMA200_Slope"] < 0).astype(int) + (df["-di"] > df["+di"]).astype(int) + (df["adx"] > 20).astype(int) + (df["k"] < df["k_smooth"]).astype(int) + # OB mitigation df["bearish_ob_mitigation"] + # Breaker Block df["bearish_bb"] + # MSS (df["bearish_mss"] & (df["bearish_trend"] == 0) ).astype(int) + # MSS Retest df["bearish_mss_retest"] + df["bearish_choch"] + # FVG + MB/RB confluence ( df["bearish_fvg_retracement"] & (df["bearish_mb"] | df["bearish_rb"]) ).astype(int) + # Liquidity ((df["asia_high_dist"] >= -3) & (df["asia_high_dist"] <= 0)).astype(int) + ((df["asia_low_dist"] >= 0) & (df["asia_low_dist"] <= 3)).astype(int) + ((df["pdh_dist"] >= -3) & (df["pdh_dist"] <= 0)).astype(int) + ((df["pdl_dist"] >= 0) & (df["pdl_dist"] <= 3)).astype(int) + # Equal highs df["eqh"] + # Fib df["bearish_fib382"] + df["bearish_fib500"] + df["bearish_fib618"] ) score *= (df["killzone"].isin([1, 2, 3])).astype(int) score *= (df["adx"] >= 20).astype(int) return score def add_indicators(df): # print("Loading indicators") df['adx'], df['+di'], df['-di'] = ADX(df) df['k'], df['k_smooth'] = STOCH(df) df['EMA7'] = EMA(df, 7) # df['EMA1'] = EMA(df, 1) # df['EMA1_Slope'] = df['EMA1'].diff() df['EMA7_Slope'] = df['EMA7'].diff() df['EMA21'] = EMA(df, 21) df['EMA21_Slope'] = df['EMA21'].diff() df['EMA721_DIFF'] = df['EMA7'] - df['EMA21'] df['EMA50'] = EMA(df, 50) df['EMA2150_DIFF'] = df['EMA21'] - df['EMA50'] df['EMA50_Slope'] = df['EMA50'].diff() df['EMA200'] = EMA(df, 200) df['EMA50200_DIFF'] = df['EMA50'] - df['EMA200'] df['EMA200_Slope'] = df['EMA200'].diff() # df['EMA1'] = EMA(df, 1) # df['EMA71_DIFF'] = df['EMA1'] - df['EMA7'] df["vwap"], df["vwap_upper"], df["vwap_lower"], df["above_vwap"], df["below_vwap"], df["vwap_above_upper"], df["vwap_below_lower"], df["vwap_slope"] = VWAP(df) df["range"] = GetRange(df) df["range_ma"] = RangeMA(df) df["indecision"] = Indecision(df) df["bullish_ob"] = BullishOB(df) df["bearish_ob"] = BearishOB(df) df["bullish_ob_mitigation"], df["bearish_ob_mitigation"] = OBMitigation(df) df["bullish_fvg"] = BullishFVG(df) df["bearish_fvg"] = BearishFVG(df) df["bullish_fvg_retracement"], df["bearish_fvg_retracement"] = FVGRetracement(df) df["bullish_ifvg"] = BullishIFVG(df) df["bearish_ifvg"] = BearishIFVG(df) df["bullish"], df["bearish"] = BullishBearish(df) df["eqh"], df["eql"] = EQH_EQL(df) df["eqh_retest"], df["eql_retest"] = EQHEQLRetest(df) df["bearish_mb"] = BearishMB(df) df["bullish_mb"] = BullishMB(df) df["bullish_rb"], df["bearish_rb"] = RejectionBlocks(df) df["swing_high"], df["swing_low"] = Swings(df) df["bullish_mss"], df["bearish_mss"] = MSS(df) df["bullish_mss_retest"], df["bearish_mss_retest"] = MSS_Retest(df) df["bullish_bos"], df["bearish_bos"] = BoS(df) df["bullish_trend"], df["bearish_trend"] = Trend(df) df["bullish_choch"], df["bearish_choch"] = CHoCH(df) df["bullish_bb"] = BullishBB(df) df["bearish_bb"] = BearishBB(df) df["exhaustion"] = ExhaustionCandle(df) df["above_support"], df["below_resistance"] = TrendLines(df) df["bullish_fib382"], df["bullish_fib500"], df["bullish_fib618"], df["bullish_fib786"], df["bearish_fib382"], df["bearish_fib500"], df["bearish_fib618"], df["bearish_fib786"] = Fibonacci(df) df = add_irl_erl(df) df["killzone"] = GetKillzone(df) """ if "Volume" in df.columns: df.rename(columns={ 'Volume': 'volume', }, inplace=True) elif "tick_volume" in df.columns: df.rename(columns={ 'tick_volume': 'volume', }, inplace=True) """ df["asia_high_dist"] = AsiaHighDistance(df) df["asia_low_dist"] = AsiaLowDistance(df) df["pdh_dist"] = PDHDistance(df) df["pdl_dist"] = PDLDistance(df) df["pd_poc_dist"] = PDPOCDistance(df) df["sell_score"] = SellScore(df) df["buy_score"] = BuyScore(df) df = df[["Open", "High", "Low", "Close", "k", "k_smooth", "adx", "+di", "-di", "EMA7", "EMA21", "EMA721_DIFF", "EMA7_Slope", "EMA21_Slope", "EMA50", "EMA200", "EMA2150_DIFF", "EMA50_Slope", "EMA50200_DIFF", "EMA200_Slope", "indecision", "bullish_ob", "bearish_ob", "bullish_fvg", "bearish_fvg", "bullish_ifvg", "bearish_ifvg", "eqh", "eql", "bearish_mb", "bullish_mb","bullish_rb", "bearish_rb", "bullish_bb", "bearish_bb", "bullish_ob_mitigation", "bearish_ob_mitigation", "bullish_fvg_retracement", "bearish_fvg_retracement", "eqh_retest", "eql_retest", "swing_high", "swing_low", "bullish_mss", "bearish_mss", "bullish_mss_retest", "bearish_mss_retest", "bullish_bos", "bearish_bos", "bullish_trend", "bearish_trend", "bullish_choch", "bearish_choch", "range_high", "range_low", "erl_high", "erl_low", "irl", "exhaustion", "above_support", "below_resistance", "bullish_fib382", "bullish_fib500", "bullish_fib618", "bullish_fib786", "bearish_fib382", "bearish_fib500", "bearish_fib618", "bearish_fib786", "vwap", "vwap_upper", "vwap_lower", "above_vwap", "below_vwap", "vwap_above_upper", "vwap_below_lower", "vwap_slope", "range_ma", "range", "bullish", "bearish", "asia_high_dist", "asia_low_dist", "pdh_dist", "pdl_dist", "pd_poc_dist", "killzone", "sell_score", "buy_score", ]].copy() # print("Loaded indicators") df.dropna(inplace=True) return df class PPOxLSTMNetwork(nn.Module): def __init__( self, state_size, hidden_size=128, action_size=3, seq_len=32, ): super().__init__() self.embedding = nn.Linear(state_size, hidden_size) cfg = xLSTMBlockStackConfig( slstm_block=sLSTMBlockConfig( slstm=sLSTMLayerConfig( backend="vanilla", num_heads=4, ) ), slstm_at=[0, 1], context_length=seq_len, num_blocks=2, embedding_dim=hidden_size, ) self.backbone = xLSTMBlockStack(cfg) # Buy / Sell / Hold self.policy = nn.Sequential( nn.Linear(hidden_size, 64), nn.ReLU(), nn.Linear(64, action_size) ) # RR multiplier self.rr_head = nn.Sequential( nn.Linear(hidden_size, 32), nn.ReLU(), nn.Linear(32, 1) ) # SL multiplier self.sl_head = nn.Sequential( nn.Linear(hidden_size, 32), nn.ReLU(), nn.Linear(32, 1) ) # PPO critic self.value = nn.Sequential( nn.Linear(hidden_size, 64), nn.ReLU(), nn.Linear(64, 1) ) def forward(self, x): # (batch, seq, features) x = self.embedding(x) x = self.backbone(x) # Last timestep h = x[:, -1] # Action logits logits = self.policy(h) # RR between 1 and 10 rr_mult = 1.0 + torch.sigmoid( self.rr_head(h) ) * 9.0 # SL multiplier between 0.75x and 2.0x sl_mult = 0.75 + torch.sigmoid( self.sl_head(h) ) * 1.25 # PPO critic value = self.value(h).squeeze(-1) return logits, rr_mult, sl_mult, value class xLSTMPPOAgent: def __init__( self, state_size, hidden_size, action_size, lr=3e-4, gamma=0.95, clip_ratio=0.2, gae_lambda=0.95 ): self.state_size = state_size self.hidden_size = hidden_size self.action_size = action_size self.gamma = gamma self.clip_ratio = clip_ratio self.gae_lambda = gae_lambda self.train_epochs = 10 self.batch_size = 64 self.entropy_coef = 0.01 self.value_coef = 0.5 self.device = torch.device( "cuda" if torch.cuda.is_available() else "cpu" ) self.model = PPOxLSTMNetwork( state_size, hidden_size, action_size ).to(self.device) self.optimizer = torch.optim.Adam( self.model.parameters(), lr=lr ) self.trajectory = [] def _state_tensor(self, state_seq): return torch.tensor( state_seq, dtype=torch.float32, device=self.device ).unsqueeze(0) def select_action(self, state_seq, in_position=False, training=False): state = self._state_tensor(state_seq) with torch.no_grad(): logits, rr_mult, sl_mult, value = self.model(state) logits = logits.squeeze(0) rr_mult = rr_mult.squeeze() sl_mult = sl_mult.squeeze() valid_actions = [0] if in_position else [0,1,2] masked_logits = logits.clone() for i in range(self.action_size): if i not in valid_actions: masked_logits[i] = -1e9 probs = torch.softmax(masked_logits, dim=-1) dist = Categorical(probs) action = dist.sample() if training else torch.argmax(probs) logprob = dist.log_prob(action) return ( int(action.item()), float(logprob.item()), float(value.item()), float(rr_mult.item()), float(sl_mult.item()) ) def store_transition( self, state_seq, action, logprob, value, rr_mult, sl_mult, reward, done ): self.trajectory.append( ( np.array(state_seq, dtype=np.float32), action, logprob, value, rr_mult, sl_mult, reward, done ) ) def compute_gae(self, rewards, values, dones): advantages = [] gae = 0 values = np.append(values, 0.0) for t in reversed(range(len(rewards))): delta = ( rewards[t] + self.gamma * values[t + 1] * (1 - dones[t]) - values[t] ) gae = ( delta + self.gamma * self.gae_lambda * (1 - dones[t]) * gae ) advantages.insert(0, gae) return np.array(advantages, dtype=np.float32) def train(self): if len(self.trajectory) < 32: return ( states, actions, old_logprobs, values, rr_mults, sl_mults, rewards, dones ) = zip(*self.trajectory) states = torch.tensor(np.array(states), dtype=torch.float32, device=self.device) actions = torch.tensor(actions, dtype=torch.long, device=self.device) old_logprobs = torch.tensor(old_logprobs, dtype=torch.float32, device=self.device) values = np.array(values, dtype=np.float32) rewards = np.array(rewards, dtype=np.float32) dones = np.array(dones, dtype=np.float32) advantages = self.compute_gae(rewards, values, dones) returns = advantages + values advantages = (advantages - advantages.mean()) / (advantages.std() + 1e-8) returns = torch.tensor(returns, dtype=torch.float32, device=self.device) advantages = torch.tensor(advantages, dtype=torch.float32, device=self.device) n = len(states) for _ in range(self.train_epochs): idx = torch.randperm(n, device=self.device) for start in range(0, n, self.batch_size): batch = idx[start:start+self.batch_size] logits, rr_pred, sl_pred, value_pred = self.model(states[batch]) dist = Categorical(logits=logits) new_logprob = dist.log_prob(actions[batch]) ratio = torch.exp(new_logprob - old_logprobs[batch]) surr1 = ratio * advantages[batch] surr2 = torch.clamp( ratio, 1-self.clip_ratio, 1+self.clip_ratio ) * advantages[batch] policy_loss = -torch.min(surr1, surr2).mean() value_loss = F.mse_loss( value_pred, returns[batch] ) entropy = dist.entropy().mean() loss = ( policy_loss + self.value_coef * value_loss - self.entropy_coef * entropy ) self.optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(self.model.parameters(), 1.0) self.optimizer.step() self.trajectory.clear() def savecheckpoint(self, symbol): os.makedirs("LSTM-PPO-saves", exist_ok=True) filename = ( f"LSTM-PPO-saves/" f"{datetime.now().strftime('%Y-%m-%d')}-" f"{symbol}.checkpoint.pt" ) torch.save( { "model": self.model.state_dict(), "optimizer": self.optimizer.state_dict() }, filename ) def loadcheckpoint(self, symbol): if not os.path.exists("LSTM-PPO-saves"): return files = [ os.path.join("LSTM-PPO-saves", f) for f in os.listdir("LSTM-PPO-saves") if f.endswith(".checkpoint.pt") and symbol in f ] if not files: return latest = max(files, key=os.path.getmtime) checkpoint = torch.load( latest, map_location=self.device ) self.model.load_state_dict(checkpoint["model"]) if "optimizer" in checkpoint: self.optimizer.load_state_dict( checkpoint["optimizer"] ) print(f"Loaded checkpoint: {latest}") class PPOLSTMNetwork(nn.Module): def __init__(self, state_size=12, hidden_size=64, action_size=3): super().__init__() # print("PPOLSTMNetwork state_size =", state_size) self.lstm = nn.LSTM( input_size=state_size, hidden_size=hidden_size, batch_first=True ) # print("LSTM input_size =", self.lstm.input_size) self.policy = nn.Sequential( nn.Linear(hidden_size, 64), nn.ReLU(), nn.Linear(64, action_size) ) self.value = nn.Sequential( nn.Linear(hidden_size, 64), nn.ReLU(), nn.Linear(64, 1) ) # self.debug = False def forward(self, x): """ if self.debug: print("x shape before lstm:", x.shape) if x.shape[1] == 0: print("ERROR: zero sequence length") print("x shape:", x.shape) raise ValueError("Zero sequence length") """ out, _ = self.lstm(x) h = out[:, -1, :] logits = self.policy(h) value = self.value(h).squeeze(-1) return logits, value class LSTMPPOAgent: def __init__( self, state_size, hidden_size, action_size, lr=3e-4, gamma=0.95, clip_ratio=0.2, gae_lambda=0.95 ): self.state_size = state_size self.hidden_size = hidden_size self.action_size = action_size self.gamma = gamma self.clip_ratio = clip_ratio self.gae_lambda = gae_lambda self.train_epochs = 10 self.batch_size = 64 self.entropy_coef = 0.01 self.value_coef = 0.5 self.device = torch.device( "cuda" if torch.cuda.is_available() else "cpu" ) self.model = PPOLSTMNetwork( state_size, hidden_size, action_size ).to(self.device) self.optimizer = torch.optim.Adam( self.model.parameters(), lr=lr ) self.trajectory = [] # print("Agent state_size =", state_size) def _state_tensor(self, state_seq): return torch.tensor( state_seq, dtype=torch.float32, device=self.device ).unsqueeze(0) def select_action(self, state_seq, in_position=False, training=False): state = self._state_tensor(state_seq) # if training is False: # print("state type:", type(state)) # print("state shape:", state.shape if hasattr(state, "shape") else "no shape") with torch.no_grad(): logits, value = self.model(state) logits = logits.squeeze(0) if in_position: valid_actions = [0] else: valid_actions = [0, 1, 2] masked_logits = logits.clone() for i in range(self.action_size): if i not in valid_actions: masked_logits[i] = -1e9 probs = torch.softmax(masked_logits, dim=-1) dist = Categorical(probs) if training: action = dist.sample() else: action = torch.argmax(probs) logprob = dist.log_prob(action) """ print( f"H={probs[0]:.2f} " f"B={probs[1]:.2f} " f"S={probs[2]:.2f}" ) """ return ( int(action.item()), float(logprob.item()), float(value.item()) ) def store_transition( self, state_seq, action, logprob, value, reward, done ): self.trajectory.append( ( np.array(state_seq, dtype=np.float32), action, logprob, value, reward, done ) ) def compute_gae(self, rewards, values, dones): advantages = [] gae = 0 values = np.append(values, 0.0) for t in reversed(range(len(rewards))): delta = ( rewards[t] + self.gamma * values[t + 1] * (1 - dones[t]) - values[t] ) gae = ( delta + self.gamma * self.gae_lambda * (1 - dones[t]) * gae ) advantages.insert(0, gae) return np.array(advantages, dtype=np.float32) def train(self): if len(self.trajectory) < 32: return states, actions, old_logprobs, values, rewards, dones = zip( *self.trajectory ) states = np.array(states, dtype=np.float32) actions = np.array(actions) old_logprobs = np.array(old_logprobs, dtype=np.float32) values = np.array(values, dtype=np.float32) rewards = np.array(rewards, dtype=np.float32) dones = np.array(dones, dtype=np.float32) advantages = self.compute_gae( rewards, values, dones ) returns = advantages + values advantages = ( advantages - advantages.mean() ) / (advantages.std() + 1e-8) states = torch.tensor( states, dtype=torch.float32, device=self.device ) actions = torch.tensor( actions, dtype=torch.long, device=self.device ) old_logprobs = torch.tensor( old_logprobs, dtype=torch.float32, device=self.device ) returns = torch.tensor( returns, dtype=torch.float32, device=self.device ) advantages = torch.tensor( advantages, dtype=torch.float32, device=self.device ) n = len(states) for _ in range(self.train_epochs): idx = torch.randperm(n, device=self.device) for start in range(0, n, self.batch_size): batch_idx = idx[start:start+self.batch_size] b_states = states[batch_idx] b_actions = actions[batch_idx] b_old_logprobs = old_logprobs[batch_idx] b_returns = returns[batch_idx] b_advantages = advantages[batch_idx] # print("b_states.shape =", b_states.shape) # print("model input_size =", self.model.lstm.input_size) logits, values_pred = self.model(b_states) dist = Categorical(logits=logits) new_logprobs = dist.log_prob( b_actions ) entropy = dist.entropy().mean() ratio = torch.exp( new_logprobs - b_old_logprobs ) surr1 = ratio * b_advantages surr2 = torch.clamp( ratio, 1 - self.clip_ratio, 1 + self.clip_ratio ) * b_advantages policy_loss = -torch.min( surr1, surr2 ).mean() value_loss = F.mse_loss( values_pred, b_returns ) loss = ( policy_loss + self.value_coef * value_loss - self.entropy_coef * entropy ) self.optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_( self.model.parameters(), 1.0 ) self.optimizer.step() self.trajectory.clear() def savecheckpoint(self, symbol): os.makedirs( "LSTM-PPO-saves", exist_ok=True ) filename = ( f"LSTM-PPO-saves/" f"{datetime.now().strftime('%Y-%m-%d')}-" f"{symbol}.checkpoint.pt" ) torch.save( { "model": self.model.state_dict(), "optimizer": self.optimizer.state_dict() }, filename ) def loadcheckpoint(self, symbol): if not os.path.exists("LSTM-PPO-saves"): return files = [ os.path.join("LSTM-PPO-saves", f) for f in os.listdir("LSTM-PPO-saves") if f.endswith(".checkpoint.pt") and symbol in f ] if not files: return latest = max(files, key=os.path.getmtime) checkpoint = torch.load( latest, map_location=self.device ) self.model.load_state_dict( checkpoint["model"] ) if "optimizer" in checkpoint: self.optimizer.load_state_dict( checkpoint["optimizer"] ) print(f"Loaded checkpoint: {latest}") class WinRateKNN: def __init__(self, symbol, k=10): self.k = k self.symbol = symbol self.states = [] self.labels = [] # 1 = win, 0 = loss self.model = None def add(self, state, is_win): try: state = np.array(state, dtype=np.float32).flatten() # Force all elements to float except Exception as e: # print("❌ Could not convert state to float:", state, "| Error:", e) return if not np.all(np.isfinite(state)): # print("⚠️ Skipping state with NaN or Inf:", state) return self.states.append(state) self.labels.append(1 if is_win else 0) if len(self.states) >= 100: self._remove_redundant_neighbor() # self.states.pop(0) # self.labels.pop(0) if len(self.states) >= self.k: self._fit() def _remove_redundant_neighbor(self): if len(self.states) < 2: return # Nothing to remove X = np.array(self.states) # Compute pairwise similarity (cosine, or use euclidean if you prefer) sim_matrix = cosine_similarity(X) # Zero out diagonal (self-similarity) np.fill_diagonal(sim_matrix, 0) # Compute average similarity for each row (how redundant each entry is) redundancy_scores = sim_matrix.mean(axis=1) # Remove the most redundant (highest avg similarity) idx_to_remove = np.argmax(redundancy_scores) del self.states[idx_to_remove] del self.labels[idx_to_remove] def _fit(self): """ Fit the KNN model with stored data. """ if len(self.states) < 1: # print("⚠️ Not enough data to fit KNN.") return # Safety check k_neighbors = max(1, min(self.k, len(self.states))) self.model = NearestNeighbors(n_neighbors=k_neighbors, algorithm="kd_tree") self.model.fit(self.states) def predict_win_rate(self, state_seq, k_near=5, k_far=5): """ Return the win rate based on k nearest neighbors of the input state. """ # if not self.model or len(self.states) < self.k: if len(self.states) < 1000: # return True # Not enough data return 1 # Not enough data # Find the 100 nearest neighbors distances, indices = self.model.kneighbors(state.reshape(1, -1), n_neighbors=50) distances = distances[0] indices = indices[0] # Split into nearest and farthest groups nearest_idx = indices[:k_near] nearest_dist = distances[:k_near] farthest_idx = indices[-k_far:] farthest_dist = distances[-k_far:] # Combine indices and distances combined_idx = np.concatenate([nearest_idx, farthest_idx]) combined_dist = np.concatenate([nearest_dist, farthest_dist]) # Get win/loss labels for selected neighbors selected_labels = np.array([self.labels[i] for i in combined_idx]) # Calculate weights (closer gets higher weight) weights = 1 / (combined_dist + 1e-6) # Add epsilon to avoid div-by-zero # Normalize weights weights /= weights.sum() # Compute weighted win rate win_rate = np.dot(selected_labels, weights) return win_rate def save(self): """ Save the KNN model to disk. """ path = f"LSTM-PPO-saves/{datetime.now().strftime('%Y-%m-%d')}-{self.symbol}.win_rate_knn.pkl" os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, "wb") as f: pickle.dump({ "states": self.states, "labels": self.labels, "model": self.model }, f) def load(self): """ Load the KNN model from disk. """ # path = f"LSTM-PPO-saves/win_rate_knn-{self.symbol}.pkl" files = sorted(os.listdir("LSTM-PPO-saves")) files = [f for f in files if f.endswith(".win_rate_knn.pkl") and self.symbol in f] if not files: print(f"[!] No checkpoint found for {self.symbol}") return latest = os.path.join("LSTM-PPO-saves", files[-1]) try: with open(latest, "rb") as f: data = pickle.load(f) self.states = data["states"] self.labels = data["labels"] self.model = data["model"] # print(f"✅ Loaded WinRateKNN from {latest}") except FileNotFoundError: print(f"⚠️ No saved KNN found at {path}. Starting fresh.") def sharpe_ratio(returns, risk_free_rate=0.0): mean_ret = np.mean(returns) std_ret = np.std(returns) if std_ret == 0: return 0 return (mean_ret - risk_free_rate) / std_ret def sortino_ratio(returns, risk_free_rate=0.0): mean_ret = np.mean(returns) # Downside deviation: only consider returns below risk-free rate, and their square differences downside_diff = [(r - risk_free_rate)**2 for r in returns if r < risk_free_rate] if len(downside_diff) == 0: return 0 # Or float('inf') if you'd rather signal perfect performance downside_std = np.sqrt(np.mean(downside_diff)) if downside_std == 0: return 0 return (mean_ret - risk_free_rate) / downside_std def rolling_sharpe(trade_returns_50): if len(trade_returns_50) < 10: return 0.0 r = np.asarray(trade_returns_50, dtype=np.float32) std = r.std() if std < 1e-8: return 0.0 return r.mean() / std def max_drawdown(returns): if len(returns) == 0: return 0 equity = np.cumsum(returns) peak = equity[0] max_dd = 0 for value in equity: peak = max(peak, value) dd = peak - value max_dd = max(max_dd, dd) return max_dd def SelectAction(df, i): # Session filter if df["killzone"].iloc[i] not in (1, 2, 3): # Asia, London, NY return 0 # Trend strength if df["adx"].iloc[i] < 20: return 0 buy = 0 sell = 0 # ------------------------- # OB mitigation # ------------------------- buy += int(df["bullish_ob_mitigation"].iloc[i]) sell += int(df["bearish_ob_mitigation"].iloc[i]) # Breaker block buy += int(df["bullish_bb"].iloc[i]) sell += int(df["bearish_bb"].iloc[i]) # MSS buy += int( df["bullish_mss"].iloc[i] and not df["bullish_trend"].iloc[i] ) sell += int( df["bearish_mss"].iloc[i] and not df["bearish_trend"].iloc[i] ) # MSS Retest buy += int(df["bullish_mss_retest"].iloc[i]) sell += int(df["bearish_mss_retest"].iloc[i]) # FVG + MB/RB buy += int( df["bullish_fvg_retracement"].iloc[i] and (df["bullish_mb"].iloc[i] or df["bullish_rb"].iloc[i]) ) sell += int( df["bearish_fvg_retracement"].iloc[i] and (df["bearish_mb"].iloc[i] or df["bearish_rb"].iloc[i]) ) # Asia High buy += int(0 <= df["asia_high_dist"].iloc[i] <= 3) sell += int(-3 <= df["asia_high_dist"].iloc[i] <= 0) # Asia Low buy += int(-3 <= df["asia_low_dist"].iloc[i] <= 0) sell += int(0 <= df["asia_low_dist"].iloc[i] <= 3) # PDH buy += int(0 <= df["pdh_dist"].iloc[i] <= 3) sell += int(-3 <= df["pdh_dist"].iloc[i] <= 0) # PDL buy += int(-3 <= df["pdl_dist"].iloc[i] <= 0) sell += int(0 <= df["pdl_dist"].iloc[i] <= 3) # Equal High / Equal Low buy += int(df["eql"].iloc[i]) sell += int(df["eqh"].iloc[i]) # 0.382, 0.5, 0.618 retracement buy += int(df["bullish_fib382"].iloc[i]) sell += int(df["bearish_fib382"].iloc[i]) buy += int(df["bullish_fib500"].iloc[i]) sell += int(df["bearish_fib500"].iloc[i]) buy += int(df["bullish_fib618"].iloc[i]) sell += int(df["bearish_fib618"].iloc[i]) # Final decision if buy >= 2 and buy > sell: return 1 if sell >= 2 and sell > buy: return 2 return 0 def train_bot(symbol="XAUUSD"): print("Training bot") start = time.perf_counter() df = load_last_mb_xauusd() print(f"Loading indicators... ({time.strftime('%H:%M')})") df = add_indicators(df) start = time.perf_counter() # elapsed = int((time.perf_counter() - start) // 60) elapsed = int((time.perf_counter() - start)) print(f"Loaded indicators (Elapsed: {elapsed}m)") print(f"Loading checkpoint....") SEQ_LEN = 12 * 3 save_count = 1440 FEATURES = [ "k", "k_smooth", "adx", "+di", "-di", "EMA7", "EMA21", "EMA721_DIFF", "EMA7_Slope", "EMA21_Slope", "EMA50", "EMA200", "EMA2150_DIFF", "EMA50_Slope", "EMA50200_DIFF", "EMA200_Slope", "indecision", "bullish_ob", "bearish_ob", "bullish_fvg", "bearish_fvg", "bullish_ifvg", "bearish_ifvg", "eqh", "eql", "bearish_mb", "bullish_mb","bullish_rb", "bearish_rb", "bullish_bb", "bearish_bb", "bullish_ob_mitigation", "bearish_ob_mitigation", "bullish_fvg_retracement", "bearish_fvg_retracement", "eqh_retest", "eql_retest", "swing_high", "swing_low", "bullish_mss", "bearish_mss", "bullish_mss_retest", "bearish_mss_retest", "bullish_bos", "bearish_bos", "bullish_trend", "bearish_trend", "bullish_choch", "bearish_choch", "range_high", "range_low", "erl_high", "erl_low", "irl", "exhaustion", "above_support", "below_resistance", "bullish_fib382", "bullish_fib500", "bullish_fib618", "bullish_fib786", "bearish_fib382", "bearish_fib500", "bearish_fib618", "bearish_fib786", "above_vwap", "below_vwap", "vwap_above_upper", "vwap_below_lower", "vwap_slope", "range_ma", "range", "bullish", "bearish", "asia_high_dist", "asia_low_dist", "pdh_dist", "pdl_dist", "pd_poc_dist", "killzone", "sell_score", "buy_score" ] agent = xLSTMPPOAgent( state_size=len(FEATURES), hidden_size=64, action_size=3 ) agent.loadcheckpoint(symbol) print(f"Loaded checkpoint... ({time.strftime('%H:%M')})") save_counter = 0 in_position = False position_type = None entry_price = 0 sl_price = 0 tp1_price = 0.0 tp2_price = 0.0 trade1_open = False trade2_open = False be_moved = False trade_returns = [] rr_list = [] SPREAD_AND_COMMISSION = 6 sl_pips = 0 sl_list = [] PIP_VALUE = 0.1 training_start_2 = time.time() training_start_3 = time.time() state_buffer = [] for i in range(len(df)): row = df.iloc[i][FEATURES].values.astype(np.float32) state_buffer.append(row) # ------------------------------------------------------------------ # Week / month / recovery-factor training config # ------------------------------------------------------------------ WEEK_SIZE = save_count # 1440 M5 candles ≈ 1 trading week WEEKS_PER_MONTH = 4 # agent.trajectory is reset every 4 weeks RF_TARGET = 1.5 # keep retraining a week until RF exceeds this MAX_ATTEMPTS_PER_WEEK = 20 # safety cap so a "dead" week can't hang forever LOOP_FOREVER = True # True: wrap back to the start of df and repeat # False: stop once len(df) has been covered total_weeks = max(1, math.ceil((len(df) - SEQ_LEN) / WEEK_SIZE)) week_idx = 0 week_start = SEQ_LEN while True: # Wrap back to the beginning of df once we've covered it all if week_start >= len(df): if not LOOP_FOREVER: break week_start = SEQ_LEN week_idx = 0 week_end = min(week_start + WEEK_SIZE, len(df)) # Reset the trajectory at the start of every new "month" if week_idx % WEEKS_PER_MONTH == 0: agent.trajectory.clear() rf = 0.0 attempt = 0 # ============================================================== # Keep retraining THIS week until its recovery factor beats the # target, then fall through and move on to the next week. # ============================================================== while rf <= RF_TARGET and attempt < MAX_ATTEMPTS_PER_WEEK: attempt += 1 # Per-attempt trading state resets (replaying the same week) in_position = False position_type = None entry_price = 0 sl_price = 0 tp1_price = 0.0 tp2_price = 0.0 trade1_open = False trade2_open = False be_moved = False trade_returns = [] rr_list = [] sl_list = [] sl_pips = 0 pending_entry = None # caches the decision that opened the current trade training_start_3 = time.time() for i in range(week_start, week_end): current = df.iloc[i] current_price = current["Close"] high = current["High"] low = current["Low"] base_sl = 50 state_seq = np.array(df.iloc[i - SEQ_LEN:i][FEATURES], dtype=np.float32) result = agent.select_action(state_seq, in_position, training=True) if result is None: continue action, logprob, value, rr_mult, sl_mult = result rr_list.append(rr_mult) pnl = 0.0 done = False if in_position: action = 0 # ========================================================== # OPEN LONG # ========================================================== if action == 1: in_position = True trade1_open = True trade2_open = True position_type = "long" be_moved = False entry_price = current_price sl_pips = np.clip(base_sl * sl_mult, 20, 50) sl_list.append(sl_pips) sl_price = entry_price - sl_pips * PIP_VALUE full_tp_distance = sl_pips * rr_mult * PIP_VALUE tp1_price = entry_price + full_tp_distance * 0.5 tp2_price = entry_price + full_tp_distance # Remember what produced this trade so the eventual # outcome is credited to THIS decision, not to a later # unrelated one. pending_entry = dict( state_seq=state_seq, action=action, logprob=logprob, value=value, rr_mult=rr_mult, sl_mult=sl_mult, ) # ========================================================== # OPEN SHORT # ========================================================== elif action == 2: in_position = True trade1_open = True trade2_open = True position_type = "short" be_moved = False entry_price = current_price sl_pips = np.clip(base_sl * sl_mult, 20, 50) sl_list.append(sl_pips) sl_price = entry_price + sl_pips * PIP_VALUE full_tp_distance = sl_pips * rr_mult * PIP_VALUE tp1_price = entry_price - full_tp_distance * 0.5 tp2_price = entry_price - full_tp_distance pending_entry = dict( state_seq=state_seq, action=action, logprob=logprob, value=value, rr_mult=rr_mult, sl_mult=sl_mult, ) # ========================================================== # MANAGE POSITION # ========================================================== if in_position: trade_closed = False if position_type == "long": if trade1_open and high >= tp1_price: pnl += sl_pips * rr_mult * 0.5 - SPREAD_AND_COMMISSION trade1_open = False if not be_moved: sl_price = entry_price be_moved = True if trade2_open and high >= tp2_price: pnl += sl_pips * rr_mult - SPREAD_AND_COMMISSION trade2_open = False trade_closed = True if not trade_closed and low <= sl_price: if sl_price != entry_price: pnl -= sl_pips * 2 + SPREAD_AND_COMMISSION * 2 trade1_open = False trade2_open = False trade_closed = True elif position_type == "short": if trade1_open and low <= tp1_price: pnl += sl_pips * rr_mult * 0.5 - SPREAD_AND_COMMISSION trade1_open = False if not be_moved: sl_price = entry_price be_moved = True if trade2_open and low <= tp2_price: pnl += sl_pips * rr_mult - SPREAD_AND_COMMISSION trade2_open = False trade_closed = True if not trade_closed and high >= sl_price: if sl_price != entry_price: pnl -= sl_pips * 2 + SPREAD_AND_COMMISSION * 2 trade1_open = False trade2_open = False trade_closed = True # Any close — win via TP2 or loss via SL — ends the episode. if trade_closed: in_position = False done = True trade_returns.append(pnl) if pending_entry is not None: reward = pnl / 50.0 # ~R-multiple, stable scale agent.store_transition( pending_entry["state_seq"], pending_entry["action"], pending_entry["logprob"], pending_entry["value"], rr_mult=pending_entry["rr_mult"], sl_mult=pending_entry["sl_mult"], reward=reward, done=True, ) pending_entry = None pnl = 0 # ========================================================== # END OF THIS PASS OVER THE WEEK: compute stats + RF # ========================================================== if len(trade_returns) > 5: wins = [r for r in trade_returns if r > 0] losses = [r for r in trade_returns if r < 0] weekly_pnl = np.sum(trade_returns) winrate = ( len(wins) / len(trade_returns) if len(trade_returns) > 0 else 0 ) mean_win = ( np.mean(wins) if len(wins) > 0 else 0 ) mean_loss = ( np.mean(losses) if len(losses) > 0 else 0 ) sharpe = sharpe_ratio(trade_returns) sortino = sortino_ratio(trade_returns) gross_profit = sum(wins) gross_loss = abs(sum(losses)) profit_factor = ( gross_profit / gross_loss if gross_loss > 0 else float("inf") ) avg_rr = sum(rr_list) / len(rr_list) if rr_list else 0 avg_sl = sum(sl_list) / len(sl_list) if sl_list else 0 max_dd = max_drawdown(trade_returns) R_pnl = weekly_pnl / avg_sl if avg_sl else 0 if max_dd > 0 and avg_sl > 0: rf = R_pnl / (max_dd / ((avg_sl / 10) * 2)) else: rf = 0.0 print() print("================================================") print(f"[{symbol}] WEEK {week_idx + 1}/{total_weeks} " f"(attempt {attempt}) PPO TRAINING") print("================================================") print(f"Trades: {len(trade_returns)}") print(f"Weekly PnL: {weekly_pnl:.0f} pips") print(f"Winrate: {winrate*100:.2f}%") print(f"Mean Win: {mean_win:.0f} pips") print(f"Mean Loss: {mean_loss:.0f} pips") if avg_sl: print(f"Max DD: {max_dd/(avg_sl*2):.2f}R") print(f"PF: {profit_factor:.2f}") print(f"RF: {rf:.2f}") if avg_sl: print(f"Weekly R PnL: {R_pnl/2:.2f}R") print(f"Sharpe: {sharpe:.2f}") print(f"Sortino: {sortino:.2f}") print("================================================") print( f"[{symbol}] [INFO] Trained on week " f"(Elapsed: {timedelta(seconds=int(time.time() - training_start_3))})" ) print() else: # Not enough trades to trust the RF number — force a retry rf = 0.0 print( f"[{symbol}] Week {week_idx + 1}/{total_weeks} " f"attempt {attempt}: only {len(trade_returns)} trades, retrying..." ) training_start = time.time() print(f"[{symbol}] [INFO] Training PPO on week {week_idx + 1} " f"(attempt {attempt})...") agent.train() print( f"[{symbol}] " f"[INFO] Finished training PPO " f"(Elapsed: {timedelta(seconds=int(time.time() - training_start))})" ) agent.savecheckpoint(symbol) if attempt >= MAX_ATTEMPTS_PER_WEEK: print( f"[{symbol}] [WARN] Week {week_idx + 1} hit " f"MAX_ATTEMPTS_PER_WEEK ({MAX_ATTEMPTS_PER_WEEK}) " f"without RF > {RF_TARGET} (last RF={rf:.2f}); moving on anyway." ) elapsed_total = time.time() - training_start_2 print( f"[{symbol}] [INFO] Week {week_idx + 1}/{total_weeks} done " f"(RF={rf:.2f} > {RF_TARGET} after {attempt} attempt(s)) | " f"Total elapsed: {timedelta(seconds=int(elapsed_total))}" ) # Advance to the next week week_start = week_end week_idx += 1 agent.train() agent.savecheckpoint(symbol) print( f"[{symbol}] " f"[INFO] Finished training " ) return agent def open_long(symbol, lot_size, sl_pips, rr_mult): tick = mt5.symbol_info_tick(symbol) entry = tick.ask sl = entry - sl_pips / 10 tp1 = entry + sl_pips / 10 * rr_mult tps = [tp1] for tp in tps: request = { "action": mt5.TRADE_ACTION_DEAL, "symbol": symbol, "volume": lot_size, "type": mt5.ORDER_TYPE_BUY, "price": entry, "sl": sl, "tp": tp, "deviation": 20, "magic": 123456, "comment": "Project Helios", "type_time": mt5.ORDER_TIME_GTC, "type_filling": mt5.ORDER_FILLING_IOC } result = mt5.order_send(request) if result.retcode != mt5.TRADE_RETCODE_DONE: print(f"Order failed: {result.retcode}") return None # return result.position # print(result) def open_short(symbol, lot_size, sl_pips, rr_mult): tick = mt5.symbol_info_tick(symbol) entry = tick.bid sl = entry + sl_pips / 10 tp1 = entry - sl_pips / 10 * rr_mult, tps = [tp1] for tp in tps: request = { "action": mt5.TRADE_ACTION_DEAL, "symbol": symbol, "volume": lot_size, "type": mt5.ORDER_TYPE_SELL, "price": entry, "sl": sl, "tp": tp, "deviation": 20, "magic": 123456, "comment": "Project Helios", "type_time": mt5.ORDER_TIME_GTC, "type_filling": mt5.ORDER_FILLING_IOC } result = mt5.order_send(request) if result.retcode != mt5.TRADE_RETCODE_DONE: print(f"Order failed: {result.retcode}") return None # return result.position # print(result) def open_positions(symbol): positions = mt5.positions_get(symbol=symbol) positions = [ p for p in positions if p.magic == 123456 ] return len(positions) def close_trades(magic=123456): positions = mt5.positions_get() if positions is None: print("Failed to get positions:", mt5.last_error()) return for pos in positions: if pos.magic != magic: continue tick = mt5.symbol_info_tick(pos.symbol) if tick is None: continue if pos.type == mt5.POSITION_TYPE_BUY: order_type = mt5.ORDER_TYPE_SELL price = tick.bid else: order_type = mt5.ORDER_TYPE_BUY price = tick.ask request = { "action": mt5.TRADE_ACTION_DEAL, "symbol": pos.symbol, "volume": pos.volume, "type": order_type, "position": pos.ticket, "price": price, "deviation": 20, "magic": magic, "comment": "End of Day", "type_time": mt5.ORDER_TIME_GTC, "type_filling": mt5.ORDER_FILLING_IOC, } result = mt5.order_send(request) if result.retcode != mt5.TRADE_RETCODE_DONE: print(f"Failed to close {pos.ticket}: {result.retcode}") else: print(f"Closed {pos.ticket}") def modify_sl(): positions = mt5.positions_get() if positions is None: return False # Only our EA's positions positions = [p for p in positions if p.magic == 123456] # Only modify if exactly one position remains if len(positions) != 1: return False position = positions[0] # Already at breakeven? if abs(position.sl - position.price_open) < 0.01: return True request = { "action": mt5.TRADE_ACTION_SLTP, "position": position.ticket, "symbol": position.symbol, "sl": position.price_open, "tp": position.tp, } result = mt5.order_send(request) return result.retcode == mt5.TRADE_RETCODE_DONE def test_bot(symbol="XAUUSD"): SEQ_LEN = 12 * 3 mt5.initialize() account = mt5.account_info() balance = account.balance RISK = 0.0005 FEATURES = [ # "Open", "High", "Low", "Close", "k", "k_smooth", "adx", "+di", "-di", "EMA7", "EMA21", "EMA721_DIFF", "EMA7_Slope", "EMA21_Slope", "EMA50", "EMA200", "EMA2150_DIFF", "EMA50_Slope", "EMA50200_DIFF", "EMA200_Slope", "indecision", "bullish_ob", "bearish_ob", "bullish_fvg", "bearish_fvg", "bullish_ifvg", "bearish_ifvg", "eqh", "eql", "bearish_mb", "bullish_mb","bullish_rb", "bearish_rb", "bullish_bb", "bearish_bb", "bullish_ob_mitigation", "bearish_ob_mitigation", "bullish_fvg_retracement", "bearish_fvg_retracement", "eqh_retest", "eql_retest", "swing_high", "swing_low", "bullish_mss", "bearish_mss", "bullish_mss_retest", "bearish_mss_retest", "bullish_bos", "bearish_bos", "bullish_trend", "bearish_trend", "bullish_choch", "bearish_choch", "range_high", "range_low", "erl_high", "erl_low", "irl", "exhaustion", "above_support", "below_resistance", "bullish_fib382", "bullish_fib500", "bullish_fib618", "bullish_fib786", "bearish_fib382", "bearish_fib500", "bearish_fib618", "bearish_fib786", #"vwap", "vwap_upper", "vwap_lower", "above_vwap", "below_vwap", "vwap_above_upper", "vwap_below_lower", "vwap_slope", "range_ma", "range", "bullish", "bearish", "asia_high_dist", "asia_low_dist", "pdh_dist", "pdl_dist", "pd_poc_dist", "killzone", "sell_score", "buy_score" ] # last_m15 = None last_m5 = None agent = xLSTMPPOAgent( state_size=len(FEATURES), hidden_size=64, action_size=3 ) # agent.model.debug = True # try: agent.loadcheckpoint("XAUUSD") # except: # print("No file for prior training, cancelling test.") # return # ========================================================== # INITIAL LOAD # ========================================================== rates_m5 = mt5.copy_rates_from_pos( symbol, mt5.TIMEFRAME_M5, 0, 600 ) df = pd.DataFrame(rates_m5) df.rename(columns={ 'open': 'Open', 'high': 'High', 'low': 'Low', 'close': 'Close', 'time': 'Date', "tick_volume": "Volume" }, inplace=True) # print(f"columns: {df.columns}") # df["Date"] = pd.to_datetime(df["Date"], unit="s") df["Date"] = ( pd.to_datetime(df["Date"], unit="s", utc=True) # .dt.tz_convert("Europe/Helsinki") ) df.set_index("Date", inplace=True) raw_df = df df = add_indicators(df) last_m5 = df.index[-1] # trade = [] # ========================================================== # MAIN LOOP # ========================================================== # print("entering main loop in test function") while True: # print("in main loop") now = datetime.now() position_multiplier = 1 seconds_until_next_5m = ( (5 - now.minute % 5) * 60 - now.second - now.microsecond / 1_000_000 ) # print(f"sleeping {seconds_until_next_5m:.0f} seconds, current time: {datetime.now()}") if seconds_until_next_5m <= 0: seconds_until_next_5m += 30 time.sleep(seconds_until_next_5m) # print(f"slept {seconds_until_next_5m:.0f} seconds, current time: {datetime.now()}") tick = mt5.symbol_info_tick(symbol) # SL_PIPS = round(tick.bid * 0.00125 * 10, 0) # SL_PIPS = 40 # ====================================================== # CHECK FOR NEW M15 CANDLE # ====================================================== new_m5 = mt5.copy_rates_from_pos( symbol, mt5.TIMEFRAME_M5, 0, 1 ) current_m5 = new_m5[0]["time"] while True: if current_m5 != last_m5: break else: time.sleep(0.25) new_m5 = mt5.copy_rates_from_pos( symbol, mt5.TIMEFRAME_M5, 0, 1 ) current_m5 = new_m5[0]["time"] # current_m5 = new_m5[0]["time"] if current_m5 != last_m5: last_m5 = current_m5 # ================================================== # APPEND NEW CANDLE # ================================================== new_row = pd.DataFrame(new_m5) new_row.rename(columns={ 'open': 'Open', 'high': 'High', 'low': 'Low', 'close': 'Close', 'time': 'Date', "tick_volume": "Volume" }, inplace=True) new_row["Date"] = ( pd.to_datetime(new_row["Date"], unit="s", utc=True) # .dt.tz_convert("Europe/Helsinki") ) new_row.set_index("Date", inplace=True) if new_row.index[-1] != df.index[-1]: raw_df = pd.concat( [raw_df, new_row] ) raw_df = raw_df.tail(600) df = add_indicators(raw_df.copy()) # ================================================== # BUILD STATE SEQUENCE # ================================================== state_seq = ( df[FEATURES] .tail(SEQ_LEN) .values .astype(np.float32) ) # ================================================== # POSITION CHECK # ================================================== open_pos = open_positions(symbol) # ================================================== # PPO DECISION # ================================================== if state_seq.shape[0] != SEQ_LEN: print(f"Bad state shape: {state_seq.shape}") continue result = agent.select_action(state_seq, open_pos > 0, training=True) if result is None: continue action, logprob, value, rr_mult, sl_mult = result # if value < 0: # value = value * -1 current_time = df.index[-1] # or however you store timestamps # print(f"time: {current_time.hour}h{current_time.minute}m") base_sl = max(10, df["Volume"].iloc[-1] / 100) if current_time.hour == 23 and current_time.minute >= 54: # Force close any open position if open_pos != 0: close_trades() # Force HOLD action = 0 # print(f"adx: {df['adx'].iloc[-1]}") # print(f"+di: {df['+di'].iloc[-1]}") # print(f"-di: {df['-di'].iloc[-1]}") # print(f"stoch k: {df['k'].iloc[-1]}") # print(f"stock d: {df['k_smooth'].iloc[-1]}") # if df["adx"].iloc[-1] < 20: # action = 0 # print(f"Test action: {action}") # ================================================== # OPEN NEW TRADE # ================================================== if open_pos == 0: # trade = None account = mt5.account_info() balance = account.balance # risk_per_position = max( # balance * RISK / 500 / 4, # 0.01 # ) # if action == 1 and df["adx"].iloc[-1] > 20 and df["+di"].iloc[-1] > df["-di"].iloc[-1] and df["EMA_DIFF"].iloc[-1] > 0 and df["k"].iloc[-1] < 80: # if action == 1 and df["buy_score"].iloc[-1] > df["sell_score"].iloc[-1]: # if action == 1 and df["EMA7"].iloc[-1] > df["EMA21"].iloc[-1] and df["k"].iloc[-1] < 80: if action == 1: """ if df["above_vwap"].iloc[-1] == 1: position_multiplier = 2 if df["vwap_above_upper"].iloc[-1] == 1: position_multiplier = 3 """ # print( # f"[{symbol}] PPO BUY" # ) sl_pips = np.clip( base_sl * sl_mult, 20, 50 ) risk_per_position = min( max((balance * RISK) / (sl_pips* 10), 0.01), 100.0 ) risk_per_position = round(risk_per_position, 2) # sl_price = entry_price - (sl_pips * PIP_VALUE) # if value < 0: # value = value * -1 # tp_price = entry_price + ( # base_sl * rr_mult * PIP_VALUE # # 2 # ) half_lot = max(round(risk_per_position / 2, 2), 0.01) open_long( symbol, half_lot, sl_pips, rr_mult * 0.5 ) open_long( symbol, half_lot, sl_pips, rr_mult ) # trade.append({ # "tp1_ticket": ticket1, # "tp2_ticket": ticket2, # "entry": df["Close"].iloc[-1], # "sl": df["Close"].iloc[-1] - sl_pips / 10, # "breakeven": False, # }) # elif action == 2 and df["adx"].iloc[-1] > 20 and df["-di"].iloc[-1] > df["+di"].iloc[-1] and df["EMA_DIFF"].iloc[-1] < 0 and df["k"].iloc[-1] > 20: # elif action == 2 and df["buy_score"].iloc[-1] < df["sell_score"].iloc[-1]: # elif action == 2 and df["EMA7"].iloc[-1] < df["EMA21"].iloc[-1] and df["k"].iloc[-1] > 20: elif action == 2: """ if df["below_vwap"].iloc[-1] == 1: position_multiplier = 2 if df["vwap_below_lower"].iloc[-1] == 1: position_multiplier = 3 """ # print( # f"[{symbol}] PPO SELL" # ) sl_pips = np.clip( base_sl * sl_mult, 20, 50 ) risk_per_position = min( max((balance * RISK) / (sl_pips* 10), 0.01), 100.0 ) risk_per_position = round(risk_per_position, 2) # sl_price = entry_price + (sl_pips * PIP_VALUE) # if value < 0: # value = value * -1 # tp_price = entry_price - ( # base_sl * rr_mult * PIP_VALUE # # 2 # ) half_lot = max(round(risk_per_position / 2, 2), 0.01) open_short( symbol, half_lot, sl_pips, rr_mult * 0.5 ) open_short( symbol, half_lot, sl_pips, rr_mult ) # trade.append({ # "tp1_ticket": ticket1, # "tp2_ticket": ticket2, # "entry": df["Close"].iloc[-1], # "sl": df["Close"].iloc[-1] + sl_pips / 10, # "breakeven": False, # }) # else: # print( # f"[{symbol}] PPO HOLD" # ) else: positions = mt5.positions_get() if positions is not None: # Only this EA's positions positions = [p for p in positions if p.magic == 123456] # TP1 has closed, only TP2 remains if len(positions) == 1: modify_sl() def get_last_date(): if not os.path.exists(CSV_FILE): return None df = pd.read_csv( CSV_FILE, sep=";" ) if df.empty: return None return pd.to_datetime( df["Date"].iloc[-1] ) def download_xauusd_data(): last_date = get_last_date() if ( last_date is not None and ( datetime.now().date() - last_date.date() ).days <= 90 ): print( "Data already up to date." ) return None if last_date is None: start_date = ( datetime.now() - timedelta(days=365 * 3) ).strftime( "%Y-%m-%d" ) else: start_date = ( last_date - timedelta(days=1) ).strftime( "%Y-%m-%d" ) end_date = ( datetime.now() - timedelta(days=1) ).strftime( "%Y-%m-%d" ) print( f"Downloading " f"{start_date} -> {end_date}" ) subprocess.run( [ # "npx", "dukascopy-node", "-i", "xauusd", "-from", start_date, "-to", end_date, "-t", "m5", "-f", "csv" ], check=True ) files = [ f for f in os.listdir(".") if f.startswith("xauusd") and f.endswith(".csv") ] if not files: raise FileNotFoundError( "No Dukascopy CSV was downloaded." ) return max( files, key=os.path.getmtime ) def append_xauusd_data(downloaded_file): if downloaded_file is None: return new_df = pd.read_csv( downloaded_file ) new_df.rename( columns={ "timestamp": "Date", "open": "Open", "high": "High", "low": "Low", "close": "Close", "volume": "Volume" }, inplace=True ) if os.path.exists(CSV_FILE): old_df = pd.read_csv( CSV_FILE, sep=";" ) df = pd.concat( [ old_df, new_df ], ignore_index=True ) else: df = new_df df.drop_duplicates( subset=["Date"], keep="last", inplace=True ) df.sort_values( "Date", inplace=True ) df.to_csv( CSV_FILE, sep=";", index=False ) os.remove( downloaded_file ) print( f"Saved " f"{len(df)} candles " f"to {CSV_FILE}" ) def update_xauusd_data(): downloaded_file = ( download_xauusd_data() ) append_xauusd_data( downloaded_file ) def main(): parser = argparse.ArgumentParser() parser.add_argument("--train", action="store_true") parser.add_argument("--test", action="store_true") parser.add_argument("--symbol", default="XAUUSD-VIP") args = parser.parse_args() threads = [] if args.train: t = threading.Thread( target=train_bot, # args=(args.symbol), daemon=True ) t.start() threads.append(t) if args.test: t = threading.Thread( target=test_bot, args=(args.symbol,), daemon=True ) t.start() threads.append(t) for t in threads: t.join() main()