From f02566e8ae962c47d5ceec07b44223c9cf06330c Mon Sep 17 00:00:00 2001 From: Vittus Mikiassen Date: Fri, 3 Jul 2026 18:41:56 +0200 Subject: [PATCH] Delete bot.py --- bot.py | 3757 -------------------------------------------------------- 1 file changed, 3757 deletions(-) delete mode 100644 bot.py diff --git a/bot.py b/bot.py deleted file mode 100644 index 6126c5b..0000000 --- a/bot.py +++ /dev/null @@ -1,3757 +0,0 @@ -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) - 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()